@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.
Files changed (122) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +276 -276
  4. package/templates/qoder/commands/optional/wl-report.md +1 -1
  5. package/templates/qoder/commands/optional/wl-spec.md +13 -1
  6. package/templates/qoder/commands/optional/wl-status.md +12 -1
  7. package/templates/qoder/commands/wl-code.md +76 -4
  8. package/templates/qoder/commands/wl-commit.md +12 -1
  9. package/templates/qoder/commands/wl-design.md +69 -5
  10. package/templates/qoder/commands/wl-init.md +27 -0
  11. package/templates/qoder/commands/wl-prd.md +145 -4
  12. package/templates/qoder/commands/wl-search.md +74 -20
  13. package/templates/qoder/commands/wl-task.md +613 -613
  14. package/templates/qoder/commands/wl-test.md +16 -1
  15. package/templates/qoder/contracts/spec.md +4 -0
  16. package/templates/qoder/hooks/post-tool-use.py +41 -0
  17. package/templates/qoder/hooks/session-start.py +34 -66
  18. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  19. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  20. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  21. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  22. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  23. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  24. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  25. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  26. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  27. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  28. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  29. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  39. package/templates/qoder/scripts/capability/registry.py +21 -23
  40. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  42. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  43. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  44. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  46. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  47. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  48. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  49. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  50. package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
  51. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  52. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  53. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  54. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  55. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  56. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  57. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  58. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  59. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  60. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  61. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  62. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  63. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  64. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  65. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  66. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  67. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  68. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  69. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  70. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  71. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  72. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  73. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  74. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  75. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  76. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  77. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  78. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  79. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  80. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  81. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  82. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  83. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  84. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  85. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  86. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  87. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  88. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  89. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  90. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  92. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  93. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  94. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  95. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  96. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  97. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  98. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  99. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  100. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  101. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  102. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  103. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  104. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  105. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  106. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  107. package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
  108. package/templates/qoder/scripts/engine/poller.py +219 -0
  109. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  110. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  111. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  112. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  113. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  114. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
  116. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  117. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  118. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  119. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  120. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  121. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  122. 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
- zentao_panel.py - 禅道任务面板数据接口 (供 VS Code 扩展 zentao-panel 调用)
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/zentao_panel.py --json # 三域活动项 JSON
17
- python .qoder/scripts/domain/task/zentao_panel.py --json --include-closed # 含已关闭历史项
18
- python .qoder/scripts/domain/task/zentao_panel.py --json --kind tasks # 只看任务
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
- path = '/%ss/%s' % (kind, item_id)
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', '') or data.get('title', '')
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
- story_id = data.get('story') or 0
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
- sr = client.api('GET', '/stories/%s' % story_id)
834
- if sr.status_code == 200:
835
- sd = sr.json()
836
- out['story_title'] = str(sd.get('title', ''))
837
- spec = _clean_html(sd.get('spec', ''))
838
- if spec and len(spec) > max_len:
839
- spec = spec[:max_len] + '…'
840
- out['story_spec'] = spec
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
- # 输出要求 (替代啰嗦的"请帮我", 给AI明确的结构化产出指令)
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('1. **根因分析**:结合重现步骤/关联需求,判断是数据问题、逻辑分支、还是接口/权限')
916
- lines.append('2. **影响范围**:列出受影响的接口、表、页面(用下面的知识图谱代码定位)')
917
- lines.append('3. **修复方案**:具体改哪些文件、改什么,给出关键代码片段')
918
- lines.append('4. **回归点**:修复后需要验证哪些场景,避免引入新问题')
1163
+ lines.append('定位根因(数据/逻辑/接口/权限?)→ 修复方案(改哪个文件改什么)→ 回归点')
919
1164
  elif zt_kind == 'story':
920
- lines.append('1. **需求拆解**:把需求拆成可独立实现的子功能点')
921
- lines.append('2. **技术方案**:涉及哪些接口/表/页面,新增还是改造(参考下方代码)')
922
- lines.append('3. **实现步骤**:按依赖顺序列出开发步骤')
923
- lines.append('4. **风险点**:可能影响现有功能的地方')
1165
+ lines.append('拆子功能 → 技术方案(接口/表/页面,新建还是改造)→ 实现步骤 → 风险点')
924
1166
  else:
925
- lines.append('1. **实现思路**:这个任务该怎么实现,用到哪些现有代码')
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
- if api_list:
981
- lines.append('')
982
- lines.append('**相关接口**:')
983
- for a in api_list:
984
- lines.append('- `%s`' % a)
985
- if field_list:
986
- lines.append('')
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': len(code_list), 'api': len(api_list), 'field': len(field_list)}}}
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
- help='执行禅道写操作 (面板按钮触发)')
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
- params = {'left': args.left, 'consumed': args.consumed,
1061
- 'resolution': args.resolution, 'comment': args.comment}
1062
- result, code = perform_action(args.action, args.id, params)
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