@hupan56/wlkj 3.2.0 → 3.3.0
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/bin/cli.js +117 -0
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +276 -276
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +13 -1
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +76 -4
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +69 -5
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +145 -4
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +613 -613
- package/templates/qoder/commands/wl-test.md +16 -1
- package/templates/qoder/contracts/spec.md +4 -0
- package/templates/qoder/hooks/post-tool-use.py +41 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/registry.py +21 -23
- package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- package/templates/root/AGENTS.md +9 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
wlkj_panel.py - 开发工作台数据接口(禅道任务 + 平台 spec/impact;供 VS Code 扩展 zentao-panel 调用)
|
|
5
5
|
|
|
6
6
|
目的: 给侧边栏面板一个稳定的 JSON 数据源, 一次返回"指派给我的【任务+需求+Bug】",
|
|
7
7
|
跨所有执行/产品全量聚合。逻辑与 my_workbench 工具一致 (只读), 但输出 JSON 而非文本。
|
|
@@ -13,9 +13,9 @@ zentao_panel.py - 禅道任务面板数据接口 (供 VS Code 扩展 zentao-pane
|
|
|
13
13
|
收集循环则与 zentao_mcp_server.py:_collect_my_* 逐字一致 (DRY 同款), 保证数据口径统一。
|
|
14
14
|
|
|
15
15
|
用法:
|
|
16
|
-
python .qoder/scripts/domain/task/
|
|
17
|
-
python .qoder/scripts/domain/task/
|
|
18
|
-
python .qoder/scripts/domain/task/
|
|
16
|
+
python .qoder/scripts/domain/task/wlkj_panel.py --json # 三域活动项 JSON
|
|
17
|
+
python .qoder/scripts/domain/task/wlkj_panel.py --json --include-closed # 含已关闭历史项
|
|
18
|
+
python .qoder/scripts/domain/task/wlkj_panel.py --json --kind tasks # 只看任务
|
|
19
19
|
|
|
20
20
|
退出码 (对齐项目规范):
|
|
21
21
|
0 成功 (含空结果); 3 配置/归属错误; 5 网络不通。
|
|
@@ -436,6 +436,7 @@ def fetch_workbench(include_closed=False, kind=None):
|
|
|
436
436
|
norm_stories = [_norm_story(s) for s in stories]
|
|
437
437
|
_add_progress('task', norm_tasks, tasks)
|
|
438
438
|
_add_progress('bug', norm_bugs, bugs)
|
|
439
|
+
_add_progress('story', norm_stories, stories)
|
|
439
440
|
|
|
440
441
|
# 当前任务 (工作流的 .current-task 机制)
|
|
441
442
|
current_task = None
|
|
@@ -490,6 +491,139 @@ def _check_owner(client, my_account, kind, item_id):
|
|
|
490
491
|
return None
|
|
491
492
|
|
|
492
493
|
|
|
494
|
+
def _do_confirm_spec(client, my_account, kind, item_id, params):
|
|
495
|
+
"""确认并上传 spec 到平台(禅道面板「确认 spec」按钮触发)。
|
|
496
|
+
|
|
497
|
+
流程: 定位 spec.md → 解析 zentao story id → 调 spec_upload 回流平台。
|
|
498
|
+
spec 挂在 story 上(zentao_id = story 号),与 commit_pipeline 同套关联键。
|
|
499
|
+
幂等: spec.md 契约头有 platform_spec_id → update_spec;无 → create+confirm+回写。
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
client: ZentaoClient 或 None(显式给 zentao_id 时不需禅道连接)。
|
|
503
|
+
kind: task/bug/story(item_id 的类型,用于反查 story)。
|
|
504
|
+
item_id: 禅道 task/bug/story id(仅展示用 + 反查 story)。
|
|
505
|
+
params: spec_path / zentao_id / code_targets(均可选,缺则自动解析)。
|
|
506
|
+
"""
|
|
507
|
+
# ① 定位 spec.md: 优先 --spec-path;否则 find_spec() 找当前任务目录的 spec
|
|
508
|
+
spec_path = params.get('spec_path')
|
|
509
|
+
if not spec_path:
|
|
510
|
+
try:
|
|
511
|
+
from foundation.core.paths import find_spec
|
|
512
|
+
hits = find_spec() or []
|
|
513
|
+
if hits:
|
|
514
|
+
spec_path = hits[0].get('path')
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
if not spec_path:
|
|
518
|
+
return {'ok': False, 'error': '没找到 spec.md。先用 /wl-spec 生成,或传 --spec-path <路径>。'}, 3
|
|
519
|
+
|
|
520
|
+
# ② 解析 zentao story id: 优先 --zentao-id;否则从 item 反查(task/bug 读 story 字段, story 用自身 id)
|
|
521
|
+
zentao_id = params.get('zentao_id')
|
|
522
|
+
if not zentao_id and client:
|
|
523
|
+
try:
|
|
524
|
+
r = client.api('GET', '/%s/%s' % ({'task': 'tasks', 'bug': 'bugs', 'story': 'stories'}.get(kind, kind + 's'), item_id))
|
|
525
|
+
if r.status_code == 200:
|
|
526
|
+
d = r.json()
|
|
527
|
+
zentao_id = str(d.get('id') if kind == 'story' else (d.get('story') or '') or '')
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
if not zentao_id:
|
|
531
|
+
return {'ok': False, 'error': '未能从禅道 %s #%s 解析 story id(spec 挂 story 上)。传 --zentao-id 显式指定。' % (kind, item_id)}, 3
|
|
532
|
+
|
|
533
|
+
# ③ 调 spec_upload(复用回流客户端)
|
|
534
|
+
try:
|
|
535
|
+
from domain.integration.spec_upload import upload_spec
|
|
536
|
+
except Exception as e:
|
|
537
|
+
return {'ok': False, 'error': 'spec_upload 客户端加载失败: %s' % e}, 3
|
|
538
|
+
ct_raw = params.get('code_targets')
|
|
539
|
+
code_targets = [c.strip() for c in ct_raw.split(',') if c.strip()] if ct_raw else None
|
|
540
|
+
r = upload_spec(spec_path, zentao_id_override=zentao_id, code_targets=code_targets)
|
|
541
|
+
if r.get('ok'):
|
|
542
|
+
return {'ok': True, 'data': {
|
|
543
|
+
'action': 'confirm_spec', 'id': item_id, 'spec_id': r['platform_spec_id'],
|
|
544
|
+
'via': r.get('via'), 'status': r.get('status'), 'version': r.get('version'),
|
|
545
|
+
'message': '%s:%s(story=%s → platform spec %s, %s)' % (
|
|
546
|
+
'spec 已上传' if r.get('via') == 'create_spec' else 'spec 已更新',
|
|
547
|
+
spec_path, zentao_id, r['platform_spec_id'], r.get('status'))}}, 0
|
|
548
|
+
return {'ok': False, 'error': 'spec 上传失败: %s' % r.get('message')}, 5
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _do_link_version(client, my_account, kind, item_id, params):
|
|
552
|
+
"""把 story/bug 关联到 build(禅道「关联版本」按钮触发)。
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
kind: story/bug(决定挂到 /builds/{id}/stories 还是 /bugs)
|
|
556
|
+
item_id: 禅道 story/bug id
|
|
557
|
+
params: build_id 必填
|
|
558
|
+
"""
|
|
559
|
+
build_id = params.get('build_id')
|
|
560
|
+
if not build_id:
|
|
561
|
+
return {'ok': False, 'error': '关联版本必须指定 --build-id'}, 3
|
|
562
|
+
obj_key = 'stories' if kind == 'story' else 'bugs'
|
|
563
|
+
r = client.api('POST', '/builds/%s/%s' % (build_id, obj_key), {obj_key: [item_id]})
|
|
564
|
+
if r.status_code in (200, 201):
|
|
565
|
+
return {'ok': True, 'data': {
|
|
566
|
+
'action': 'link_version', 'id': item_id,
|
|
567
|
+
'message': '%s #%s 已关联到 build %s' % (kind, item_id, build_id)}}, 0
|
|
568
|
+
return {'ok': False, 'error': '关联版本失败 (HTTP %s): %s' % (
|
|
569
|
+
r.status_code, ZentaoClient.err_text(r))}, 5
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _do_publish_story(client, my_account, kind, item_id, params):
|
|
573
|
+
"""建禅道 story(禅道「发布需求」按钮触发)。
|
|
574
|
+
|
|
575
|
+
新建需求挂产品;可选关联 plan / build。
|
|
576
|
+
item_id 仅展示用(新建不依赖已有对象)。
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
params: product_id 必填;title/spec/pri/category/plan_id/build_id 可选
|
|
580
|
+
"""
|
|
581
|
+
pid = params.get('product_id')
|
|
582
|
+
if not pid:
|
|
583
|
+
return {'ok': False, 'error': '建需求必须指定 --product-id'}, 3
|
|
584
|
+
try:
|
|
585
|
+
payload = {
|
|
586
|
+
'product': int(pid),
|
|
587
|
+
'title': params.get('title') or '',
|
|
588
|
+
'spec': params.get('spec', ''),
|
|
589
|
+
'pri': int(params.get('pri', 3)),
|
|
590
|
+
'category': params.get('category', 'feature'),
|
|
591
|
+
}
|
|
592
|
+
except (ValueError, TypeError):
|
|
593
|
+
return {'ok': False, 'error': 'product-id/pri 必须是数字。'}, 3
|
|
594
|
+
if not payload['title']:
|
|
595
|
+
return {'ok': False, 'error': '建需求必须指定 --title'}, 3
|
|
596
|
+
|
|
597
|
+
r = client.api('POST', '/stories', payload)
|
|
598
|
+
if r.status_code not in (200, 201):
|
|
599
|
+
return {'ok': False, 'error': '建需求失败 (HTTP %s): %s' % (
|
|
600
|
+
r.status_code, ZentaoClient.err_text(r))}, 5
|
|
601
|
+
try:
|
|
602
|
+
story_id = r.json().get('id')
|
|
603
|
+
except Exception:
|
|
604
|
+
story_id = None
|
|
605
|
+
if not story_id:
|
|
606
|
+
return {'ok': False, 'error': '建需求响应无 id: %s' % ZentaoClient.err_text(r)}, 5
|
|
607
|
+
|
|
608
|
+
# 可选关联 plan
|
|
609
|
+
plan_id = params.get('plan_id')
|
|
610
|
+
if plan_id:
|
|
611
|
+
try:
|
|
612
|
+
client.api('POST', '/plans/%s/stories' % plan_id, {'stories': [story_id]})
|
|
613
|
+
except Exception:
|
|
614
|
+
pass # 关联失败不阻塞主流程(需求已建好)
|
|
615
|
+
|
|
616
|
+
# 可选关联 build
|
|
617
|
+
build_id = params.get('build_id')
|
|
618
|
+
if build_id:
|
|
619
|
+
_do_link_version(client, my_account, 'story', story_id, {'build_id': build_id})
|
|
620
|
+
|
|
621
|
+
return {'ok': True, 'data': {
|
|
622
|
+
'action': 'publish_story', 'story_id': story_id,
|
|
623
|
+
'message': '需求 #%s 已创建(产品 %s)%s' % (
|
|
624
|
+
story_id, pid, (' 并关联 build %s' % build_id) if build_id else '')}}, 0
|
|
625
|
+
|
|
626
|
+
|
|
493
627
|
# 禅道写操作的 HTTP 调度表: action -> (method, path_tpl, payload_builder, ok_msg)
|
|
494
628
|
# payload_builder(args_dict) -> payload_dict
|
|
495
629
|
def _do_action(client, my_account, action, item_id, params):
|
|
@@ -499,6 +633,20 @@ def _do_action(client, my_account, action, item_id, params):
|
|
|
499
633
|
if not my_account:
|
|
500
634
|
return {'ok': False, 'error': '未识别禅道身份, 无法校验归属。请配 member.json 的 zentao_account。'}, 3
|
|
501
635
|
|
|
636
|
+
# spec 确认上传(不走禅道 HTTP 写,调 spec_upload 回流平台)
|
|
637
|
+
if action == 'confirm_spec':
|
|
638
|
+
_k = params.get('kind') or 'task'
|
|
639
|
+
_k = {'tasks': 'task', 'bugs': 'bug', 'stories': 'story'}.get(_k, _k)
|
|
640
|
+
return _do_confirm_spec(client, my_account, _k, item_id, params)
|
|
641
|
+
|
|
642
|
+
# story 域写操作(自定义逻辑,独立函数分支,同 confirm_spec)
|
|
643
|
+
if action == 'publish_story':
|
|
644
|
+
return _do_publish_story(client, my_account, 'story', item_id, params)
|
|
645
|
+
if action == 'link_version':
|
|
646
|
+
_k = params.get('kind') or 'story'
|
|
647
|
+
_k = {'tasks': 'task', 'bugs': 'bug', 'stories': 'story'}.get(_k, _k)
|
|
648
|
+
return _do_link_version(client, my_account, _k, item_id, params)
|
|
649
|
+
|
|
502
650
|
# action 分派
|
|
503
651
|
ACTIONS = {
|
|
504
652
|
# 任务类 (kind=task)
|
|
@@ -575,6 +723,11 @@ def _do_action(client, my_account, action, item_id, params):
|
|
|
575
723
|
def perform_action(action, item_id, params=None):
|
|
576
724
|
"""写操作入口。返回 (result_dict, exit_code)。"""
|
|
577
725
|
params = params or {}
|
|
726
|
+
# confirm_spec: 已显式给 spec_path + zentao_id 时不依赖禅道连接(直接回流平台)
|
|
727
|
+
if action == 'confirm_spec' and params.get('spec_path') and params.get('zentao_id'):
|
|
728
|
+
_k = params.get('kind') or 'task'
|
|
729
|
+
_k = {'tasks': 'task', 'bugs': 'bug', 'stories': 'story'}.get(_k, _k)
|
|
730
|
+
return _do_confirm_spec(None, None, _k, item_id, params)
|
|
578
731
|
client = _zentao_creds()
|
|
579
732
|
if client is None:
|
|
580
733
|
return {'ok': False, 'error': '禅道凭据未配置。'}, 3
|
|
@@ -763,6 +916,65 @@ def _parse_prefetch(text):
|
|
|
763
916
|
return _parse_context_pack(text, 'prefetch')
|
|
764
917
|
|
|
765
918
|
|
|
919
|
+
def _query_kg_semantic(query, top_k=5):
|
|
920
|
+
"""调 kg.py semantic 语义搜索 (概念检索, 理解业务含义)。
|
|
921
|
+
|
|
922
|
+
比 prefetch 的关键词匹配更强: 查"数据权限"能命中 permission/auth 等概念相关代码。
|
|
923
|
+
用于补充 prefetch 可能漏掉的语义相关文件。带超时, daemon 未启动时冷启动~27s。
|
|
924
|
+
"""
|
|
925
|
+
import subprocess
|
|
926
|
+
script = os.path.join(_SCRIPTS, 'domain', 'kg', 'kg.py')
|
|
927
|
+
if not os.path.isfile(script):
|
|
928
|
+
return []
|
|
929
|
+
try:
|
|
930
|
+
r = subprocess.run(
|
|
931
|
+
['python', script, 'semantic', query, '--top', str(top_k)],
|
|
932
|
+
capture_output=True, text=True, timeout=35, encoding='utf-8', errors='replace')
|
|
933
|
+
if r.returncode != 0 or '[error]' in r.stdout:
|
|
934
|
+
return []
|
|
935
|
+
# 解析输出: " 0.849 数据权限 handlePermission"
|
|
936
|
+
import re
|
|
937
|
+
results = []
|
|
938
|
+
for line in r.stdout.split('\n'):
|
|
939
|
+
m = re.match(r'\s*([\d.]+)\s+(.+)', line)
|
|
940
|
+
if m:
|
|
941
|
+
results.append(m.group(2).strip())
|
|
942
|
+
return results[:top_k]
|
|
943
|
+
except Exception:
|
|
944
|
+
return []
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _query_kg_impact(keyword):
|
|
948
|
+
"""调 kg.py impact 影响分析 (改某模块影响哪些端点/Controller)。
|
|
949
|
+
|
|
950
|
+
用于 bug 提示词的"影响范围"——告诉 AI 这个改动会波及哪些接口。
|
|
951
|
+
"""
|
|
952
|
+
import subprocess
|
|
953
|
+
script = os.path.join(_SCRIPTS, 'domain', 'kg', 'kg.py')
|
|
954
|
+
if not os.path.isfile(script):
|
|
955
|
+
return []
|
|
956
|
+
try:
|
|
957
|
+
r = subprocess.run(
|
|
958
|
+
['python', script, 'impact', keyword],
|
|
959
|
+
capture_output=True, text=True, timeout=15, encoding='utf-8', errors='replace')
|
|
960
|
+
if r.returncode != 0 or '未找到' in r.stdout:
|
|
961
|
+
return []
|
|
962
|
+
# 解析: 提取 Controller 名和端点
|
|
963
|
+
import re
|
|
964
|
+
controllers = set()
|
|
965
|
+
endpoints = set()
|
|
966
|
+
for line in r.stdout.split('\n'):
|
|
967
|
+
m = re.match(r'\s+后端:\s*(\S+)', line)
|
|
968
|
+
if m:
|
|
969
|
+
controllers.add(m.group(1))
|
|
970
|
+
m2 = re.match(r'^###\s+(\S+)', line)
|
|
971
|
+
if m2 and m2.group(1).startswith('/'):
|
|
972
|
+
endpoints.add(m2.group(1))
|
|
973
|
+
return {'controllers': list(controllers)[:6], 'endpoints': list(endpoints)[:6]}
|
|
974
|
+
except Exception:
|
|
975
|
+
return {}
|
|
976
|
+
|
|
977
|
+
|
|
766
978
|
def _clean_html(s):
|
|
767
979
|
"""去 HTML 标签 + 实体, 压缩空白。禅道详情字段是富文本 HTML。"""
|
|
768
980
|
if not s:
|
|
@@ -792,8 +1004,9 @@ def _fetch_zentao_detail(client, kind, item_id, max_len=600):
|
|
|
792
1004
|
import re
|
|
793
1005
|
out = {'detail': '', 'story_id': 0, 'story_title': '', 'story_spec': '', 'scene': ''}
|
|
794
1006
|
|
|
795
|
-
# 自身详情
|
|
796
|
-
|
|
1007
|
+
# 自身详情 (zentao 路径: task→tasks, bug→bugs, story→stories — story +es 不是 +s)
|
|
1008
|
+
_kind_path = {'task': 'tasks', 'bug': 'bugs', 'story': 'stories'}.get(kind, kind + 's')
|
|
1009
|
+
path = '/%s/%s' % (_kind_path, item_id)
|
|
797
1010
|
r = client.api('GET', path)
|
|
798
1011
|
if r.status_code != 200:
|
|
799
1012
|
return out
|
|
@@ -803,7 +1016,7 @@ def _fetch_zentao_detail(client, kind, item_id, max_len=600):
|
|
|
803
1016
|
if kind == 'bug':
|
|
804
1017
|
raw = data.get('steps', '')
|
|
805
1018
|
elif kind == 'story':
|
|
806
|
-
raw = data.get('spec', '')
|
|
1019
|
+
raw = data.get('spec', '') # 故事详情=spec;spec 空则无详情(title 已在关联需求段,不重复)
|
|
807
1020
|
else: # task
|
|
808
1021
|
raw = data.get('desc', '')
|
|
809
1022
|
detail = _clean_html(raw)
|
|
@@ -812,7 +1025,11 @@ def _fetch_zentao_detail(client, kind, item_id, max_len=600):
|
|
|
812
1025
|
out['detail'] = detail
|
|
813
1026
|
|
|
814
1027
|
# 场景名: execution 名 (任务) / product 名 (bug/story)
|
|
815
|
-
|
|
1028
|
+
# kind=story: 故事本身就是需求,story_id 用自身 id(不找父 story,story 的 data.story 是父/None)
|
|
1029
|
+
if kind == 'story':
|
|
1030
|
+
story_id = item_id
|
|
1031
|
+
else:
|
|
1032
|
+
story_id = data.get('story') or 0
|
|
816
1033
|
|
|
817
1034
|
if kind == 'task':
|
|
818
1035
|
eid = data.get('execution')
|
|
@@ -830,14 +1047,19 @@ def _fetch_zentao_detail(client, kind, item_id, max_len=600):
|
|
|
830
1047
|
# 关联需求 (业务背景, 最有价值的上下文)
|
|
831
1048
|
if story_id:
|
|
832
1049
|
out['story_id'] = story_id
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
out['
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1050
|
+
if kind == 'story':
|
|
1051
|
+
# 故事自身即需求,用已取的 data(不重复 fetch /stories/{自身})
|
|
1052
|
+
out['story_title'] = str(data.get('title', ''))
|
|
1053
|
+
out['story_spec'] = detail # 已 _clean_html 过(kind=story 时 raw=spec or title)
|
|
1054
|
+
else:
|
|
1055
|
+
sr = client.api('GET', '/stories/%s' % story_id)
|
|
1056
|
+
if sr.status_code == 200:
|
|
1057
|
+
sd = sr.json()
|
|
1058
|
+
out['story_title'] = str(sd.get('title', ''))
|
|
1059
|
+
spec = _clean_html(sd.get('spec', ''))
|
|
1060
|
+
if spec and len(spec) > max_len:
|
|
1061
|
+
spec = spec[:max_len] + '…'
|
|
1062
|
+
out['story_spec'] = spec
|
|
841
1063
|
|
|
842
1064
|
return out
|
|
843
1065
|
|
|
@@ -908,24 +1130,41 @@ def enrich_prompt(kind, item_id, title, client=None):
|
|
|
908
1130
|
lines.append('')
|
|
909
1131
|
lines.extend(detail_block)
|
|
910
1132
|
|
|
911
|
-
#
|
|
1133
|
+
# 平台上下文(按禅道 story 反查需求→spec + 代码变更史)
|
|
1134
|
+
try:
|
|
1135
|
+
if d.get('story_id'):
|
|
1136
|
+
from domain.integration.spec_upload import _resolve_endpoint, _mcp_call
|
|
1137
|
+
_base, _tok, _pid = _resolve_endpoint()
|
|
1138
|
+
if _tok:
|
|
1139
|
+
_sid = str(d['story_id'])
|
|
1140
|
+
_ps = _mcp_call(_base, _tok, "find_specs_by_zentao",
|
|
1141
|
+
{"zentao_id": _sid, "project_id": _pid})
|
|
1142
|
+
_cc = _mcp_call(_base, _tok, "code_changes",
|
|
1143
|
+
{"project_id": _pid, "req_key": "XQ" + _sid, "mode": "summary"})
|
|
1144
|
+
if _ps.get("specs") or _cc.get("commits") or _cc.get("functions"):
|
|
1145
|
+
lines.append('')
|
|
1146
|
+
lines.append('## 平台上下文(禅道 #%s → %s)' % (
|
|
1147
|
+
_sid, _ps.get("requirement_title") or ("XQ" + _sid)))
|
|
1148
|
+
for _s in (_ps.get("specs") or []):
|
|
1149
|
+
lines.append('- SPEC [%s] %s v%s:%s' % (
|
|
1150
|
+
_s.get("status"), _s.get("title"), _s.get("current_version"),
|
|
1151
|
+
str(_s.get("content_excerpt") or '')[:150].replace("\n", " ")))
|
|
1152
|
+
if _cc.get("commits") or _cc.get("functions"):
|
|
1153
|
+
_tf = ', '.join(f.get('fn', '') for f in (_cc.get('top_functions') or [])[:3])
|
|
1154
|
+
lines.append('- 代码变更史:%s 函数 / %s 提交(top: %s)' % (
|
|
1155
|
+
_cc.get('functions'), _cc.get('commits'), _tf or '—'))
|
|
1156
|
+
except Exception:
|
|
1157
|
+
pass # 平台上下文拉不到(无 token/无 story_id)不阻塞 enrich
|
|
1158
|
+
|
|
1159
|
+
# 要做的事(任务化一句,替代通用 4 点 dump)
|
|
912
1160
|
lines.append('')
|
|
913
|
-
lines.append('##
|
|
1161
|
+
lines.append('## 要做的事')
|
|
914
1162
|
if zt_kind == 'bug':
|
|
915
|
-
lines.append('
|
|
916
|
-
lines.append('2. **影响范围**:列出受影响的接口、表、页面(用下面的知识图谱代码定位)')
|
|
917
|
-
lines.append('3. **修复方案**:具体改哪些文件、改什么,给出关键代码片段')
|
|
918
|
-
lines.append('4. **回归点**:修复后需要验证哪些场景,避免引入新问题')
|
|
1163
|
+
lines.append('定位根因(数据/逻辑/接口/权限?)→ 修复方案(改哪个文件改什么)→ 回归点')
|
|
919
1164
|
elif zt_kind == 'story':
|
|
920
|
-
lines.append('
|
|
921
|
-
lines.append('2. **技术方案**:涉及哪些接口/表/页面,新增还是改造(参考下方代码)')
|
|
922
|
-
lines.append('3. **实现步骤**:按依赖顺序列出开发步骤')
|
|
923
|
-
lines.append('4. **风险点**:可能影响现有功能的地方')
|
|
1165
|
+
lines.append('拆子功能 → 技术方案(接口/表/页面,新建还是改造)→ 实现步骤 → 风险点')
|
|
924
1166
|
else:
|
|
925
|
-
lines.append('
|
|
926
|
-
lines.append('2. **代码定位**:在下方知识图谱命中的文件里,具体改哪个类/方法')
|
|
927
|
-
lines.append('3. **实现步骤**:按顺序的开发步骤')
|
|
928
|
-
lines.append('4. **自检点**:完成后要验证什么')
|
|
1167
|
+
lines.append('实现思路(用哪些现有代码)→ 改哪个类/方法 → 开发步骤 → 自检点')
|
|
929
1168
|
|
|
930
1169
|
# ── ② 知识图谱增强 (代码定位) ──
|
|
931
1170
|
# v0.18: 用 prefetch 替代多次 context_pack —— 一次多词合并查询, 更快更准。
|
|
@@ -967,31 +1206,25 @@ def enrich_prompt(kind, item_id, title, client=None):
|
|
|
967
1206
|
if not (code_list or api_list or field_list):
|
|
968
1207
|
return {'ok': True, 'data': {'prompt': '\n'.join(lines), 'enriched': detail_added, 'keyword': ','.join(kws[:3])}}
|
|
969
1208
|
|
|
970
|
-
#
|
|
971
|
-
lines.append('')
|
|
972
|
-
lines.append('## 知识图谱参考 (%s方向, 关键词: %s)' % (platform_label, ','.join(kws[:3])))
|
|
973
|
-
lines.append('> 以下是代码库中与该任务相关的文件/接口/字段,用于快速定位实现位置。')
|
|
1209
|
+
# 代码定位:top-3 文件指针(不 dump API/字段/语义,让 agent 用 rag_search/get_impact/context_360 工具自取深入)
|
|
974
1210
|
if code_list:
|
|
975
1211
|
lines.append('')
|
|
976
|
-
lines.append('
|
|
977
|
-
for f, cnt in ranked_code:
|
|
1212
|
+
lines.append('## 代码定位(top %d;细节用 rag_search / get_impact / context_360 工具取)' % min(3, len(ranked_code)))
|
|
1213
|
+
for f, cnt in ranked_code[:3]:
|
|
978
1214
|
mark = ' ←多词命中' if cnt >= 2 else ''
|
|
979
1215
|
lines.append('- `%s`%s' % (f, mark))
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
lines.append('**相关字段**:')
|
|
988
|
-
for fld in field_list:
|
|
989
|
-
lines.append('- %s' % fld)
|
|
1216
|
+
# bug 影响范围(1 行,不展开)
|
|
1217
|
+
if zt_kind == 'bug' and kws:
|
|
1218
|
+
impact = _query_kg_impact(kws[0])
|
|
1219
|
+
if impact and (impact.get('controllers') or impact.get('endpoints')):
|
|
1220
|
+
hit = (impact.get('controllers') or [])[:3] + (impact.get('endpoints') or [])[:3]
|
|
1221
|
+
lines.append('')
|
|
1222
|
+
lines.append('**影响**:改「%s」波及 %s' % (kws[0], ', '.join(hit)))
|
|
990
1223
|
lines.append('')
|
|
991
|
-
lines.append('
|
|
1224
|
+
lines.append('先读上述文件理解现状再动手;细节走工具,别脱离现有架构另起炉灶。')
|
|
992
1225
|
used_kws = ','.join(kws[:3])
|
|
993
1226
|
return {'ok': True, 'data': {'prompt': '\n'.join(lines), 'enriched': True, 'keyword': used_kws,
|
|
994
|
-
'counts': {'code':
|
|
1227
|
+
'counts': {'code': min(3, len(ranked_code)) if code_list else 0}}}
|
|
995
1228
|
|
|
996
1229
|
|
|
997
1230
|
def main():
|
|
@@ -1001,13 +1234,26 @@ def main():
|
|
|
1001
1234
|
parser.add_argument('--pretty', action='store_true', help='JSON 美化缩进 (默认紧凑, 面板解析更快)')
|
|
1002
1235
|
# 写操作子命令 (v0.3)
|
|
1003
1236
|
parser.add_argument('--action', choices=['start_task', 'pause_task', 'finish_task',
|
|
1004
|
-
'log_effort', 'confirm_bug', 'resolve_bug'
|
|
1005
|
-
|
|
1237
|
+
'log_effort', 'confirm_bug', 'resolve_bug', 'confirm_spec',
|
|
1238
|
+
'publish_story', 'link_version'],
|
|
1239
|
+
help='执行禅道写操作 (面板按钮触发); confirm_spec=确认并上传 spec 到平台; '
|
|
1240
|
+
'publish_story=建需求; link_version=关联 build')
|
|
1006
1241
|
parser.add_argument('--id', type=int, help='写操作的目标 task/bug ID')
|
|
1007
1242
|
parser.add_argument('--left', help='剩余工时 (start/finish/log_effort)')
|
|
1008
1243
|
parser.add_argument('--consumed', help='消耗工时 (finish/log_effort)')
|
|
1009
1244
|
parser.add_argument('--resolution', help='Bug 解决方式 (resolve_bug): fixed/bydesign/duplicate/notrepro/postpone')
|
|
1010
1245
|
parser.add_argument('--comment', help='Bug 解决备注 (resolve_bug)')
|
|
1246
|
+
# spec 确认上传 (confirm_spec)
|
|
1247
|
+
parser.add_argument('--spec-path', help='本地 spec.md 路径 (confirm_spec; 不传则用当前任务 spec)')
|
|
1248
|
+
parser.add_argument('--code-targets', help='代码目标实体逗号分隔 (confirm_spec, 可选)')
|
|
1249
|
+
parser.add_argument('--zentao-id', help='禅道 story 号 (confirm_spec; 不传则从 --id 反查 story)')
|
|
1250
|
+
# story 域写操作 (publish_story / link_version)
|
|
1251
|
+
parser.add_argument('--product-id', help='产品 ID (publish_story 必填;需求挂产品)')
|
|
1252
|
+
parser.add_argument('--spec', help='需求规格正文 (publish_story)')
|
|
1253
|
+
parser.add_argument('--spec-file', help='需求规格正文文件路径 (publish_story; 优先于 --spec, 规避 shell 长度限制)')
|
|
1254
|
+
parser.add_argument('--pri', type=int, default=3, help='需求优先级 1-4 (publish_story; 默认 3)')
|
|
1255
|
+
parser.add_argument('--plan-id', help='关联计划 ID (publish_story 可选)')
|
|
1256
|
+
parser.add_argument('--build-id', help='关联 build/版本 ID (publish_story 可选; link_version 必填)')
|
|
1011
1257
|
# 提示词增强 (v0.6)
|
|
1012
1258
|
parser.add_argument('--enrich', action='store_true', help='构建带知识图谱上下文的提示词 (发AI用)')
|
|
1013
1259
|
parser.add_argument('--title', help='任务/Bug 标题 (enrich 用)')
|
|
@@ -1057,9 +1303,25 @@ def main():
|
|
|
1057
1303
|
result = {'ok': False, 'error': '写操作必须提供 --id'}
|
|
1058
1304
|
code = 3
|
|
1059
1305
|
else:
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1306
|
+
# publish_story 的 spec 正文: --spec-file 优先于 --spec (读文件, 规避 shell 长度限制)
|
|
1307
|
+
spec_text = args.spec or ''
|
|
1308
|
+
if args.spec_file:
|
|
1309
|
+
try:
|
|
1310
|
+
with open(args.spec_file, encoding='utf-8') as fh:
|
|
1311
|
+
spec_text = fh.read()
|
|
1312
|
+
except Exception as e:
|
|
1313
|
+
result = {'ok': False, 'error': '读 --spec-file 失败: %s' % e}
|
|
1314
|
+
code = 3
|
|
1315
|
+
spec_text = None
|
|
1316
|
+
if spec_text is not None:
|
|
1317
|
+
params = {'left': args.left, 'consumed': args.consumed,
|
|
1318
|
+
'resolution': args.resolution, 'comment': args.comment,
|
|
1319
|
+
'spec_path': args.spec_path, 'code_targets': args.code_targets,
|
|
1320
|
+
'zentao_id': args.zentao_id, 'kind': args.kind,
|
|
1321
|
+
'product_id': args.product_id, 'title': args.title,
|
|
1322
|
+
'spec': spec_text, 'pri': args.pri, 'plan_id': args.plan_id,
|
|
1323
|
+
'build_id': args.build_id}
|
|
1324
|
+
result, code = perform_action(args.action, args.id, params)
|
|
1063
1325
|
else:
|
|
1064
1326
|
# workbench: kind 期望 tasks/bugs/stories (单数形式转复数)
|
|
1065
1327
|
wb_kind = args.kind if args.kind in ('tasks', 'bugs', 'stories') else None
|