@hupan56/wlkj 3.1.32 → 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 (178) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -67
  4. package/templates/qoder/agents/prd-reference.md +47 -47
  5. package/templates/qoder/commands/optional/wl-insight.md +4 -4
  6. package/templates/qoder/commands/optional/wl-report.md +1 -1
  7. package/templates/qoder/commands/optional/wl-spec.md +23 -3
  8. package/templates/qoder/commands/optional/wl-status.md +12 -1
  9. package/templates/qoder/commands/wl-code.md +138 -7
  10. package/templates/qoder/commands/wl-commit.md +12 -1
  11. package/templates/qoder/commands/wl-design.md +70 -6
  12. package/templates/qoder/commands/wl-init.md +27 -0
  13. package/templates/qoder/commands/wl-prd.md +230 -15
  14. package/templates/qoder/commands/wl-req.md +10 -3
  15. package/templates/qoder/commands/wl-search.md +74 -20
  16. package/templates/qoder/commands/wl-task.md +3 -3
  17. package/templates/qoder/commands/wl-test.md +17 -2
  18. package/templates/qoder/contracts/CHANGELOG.md +418 -0
  19. package/templates/qoder/contracts/README.md +180 -0
  20. package/templates/qoder/contracts/code.md +82 -0
  21. package/templates/qoder/contracts/commit.md +86 -0
  22. package/templates/qoder/contracts/contract-header.md +76 -0
  23. package/templates/qoder/contracts/design.md +106 -0
  24. package/templates/qoder/contracts/fallback.md +126 -0
  25. package/templates/qoder/contracts/isolation.md +119 -0
  26. package/templates/qoder/contracts/prd.md +118 -0
  27. package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
  28. package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
  29. package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
  30. package/templates/qoder/contracts/spec.md +116 -0
  31. package/templates/qoder/contracts/task.md +125 -0
  32. package/templates/qoder/contracts/test.md +112 -0
  33. package/templates/qoder/hooks/post-tool-use.py +61 -0
  34. package/templates/qoder/hooks/session-start.py +34 -66
  35. package/templates/qoder/hooks/stop-eval.py +47 -0
  36. package/templates/qoder/rules/wl-pipeline.md +37 -0
  37. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  41. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  42. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  43. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  44. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  46. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  47. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  48. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  49. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  50. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  52. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  55. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  56. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  57. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  58. package/templates/qoder/scripts/capability/registry.py +21 -23
  59. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  60. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  61. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  62. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  63. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
  64. package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
  65. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  66. package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
  67. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  68. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  69. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  70. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  71. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  72. package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
  73. package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
  74. package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
  75. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  76. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  77. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  78. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  79. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  80. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  81. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  82. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  83. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  84. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  85. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  86. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  87. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  88. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  89. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  90. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  92. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  93. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  94. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  95. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  96. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  97. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  98. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  99. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  100. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  101. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  102. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  103. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  104. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  105. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  106. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  107. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  108. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  109. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  110. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  111. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  112. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  113. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  114. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  115. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  116. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  117. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  118. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  119. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  120. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  121. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  122. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  123. package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
  124. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  125. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  126. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  127. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  128. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  129. package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
  130. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  131. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  132. package/templates/qoder/scripts/domain/requirement/req.py +134 -28
  133. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
  135. package/templates/qoder/scripts/engine/poller.py +219 -0
  136. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  137. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/core/paths.py +102 -0
  140. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  141. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  142. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  143. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
  144. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  145. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  146. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  147. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  148. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  149. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  150. package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
  151. package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
  152. package/templates/qoder/settings.json +27 -9
  153. package/templates/qoder/skills/design-import/SKILL.md +3 -3
  154. package/templates/qoder/skills/design-review/SKILL.md +1 -1
  155. package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
  156. package/templates/qoder/skills/prd-review/SKILL.md +1 -1
  157. package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
  158. package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
  159. package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
  160. package/templates/qoder/skills/test-generator/SKILL.md +1 -1
  161. package/templates/qoder/skills/wl-code/SKILL.md +13 -1
  162. package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
  163. package/templates/qoder/skills/wl-design/SKILL.md +6 -6
  164. package/templates/qoder/skills/wl-init/SKILL.md +2 -2
  165. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  166. package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
  167. package/templates/qoder/skills/wl-report/SKILL.md +2 -2
  168. package/templates/qoder/skills/wl-search/SKILL.md +1 -1
  169. package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
  170. package/templates/qoder/skills/wl-status/SKILL.md +2 -2
  171. package/templates/qoder/skills/wl-task/SKILL.md +3 -3
  172. package/templates/qoder/skills/wl-test/SKILL.md +2 -2
  173. package/templates/qoder/templates/spec-template.md +124 -0
  174. package/templates/root/AGENTS.md +41 -14
  175. package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
  176. package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
  177. package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
  178. package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python
2
+ """wlkj 项目切换脚本:选项目 → 拉平台 token → 写 mcp_config.json。
3
+
4
+ 用法:
5
+ python switch_project.py # 交互式:列出项目 → 选 → 写配置
6
+ python switch_project.py --email a@b.c --pw xxx # 非交互:直接指定
7
+ python switch_project.py --project-uuid <UUID> # 非交互:直接指定项目
8
+
9
+ 需工作台后端在跑(默认 http://127.0.0.1:10010)。
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+ import urllib.request
15
+ import urllib.error
16
+
17
+ # 宿主根:脚本部署在 {host}/scripts/domain/kg/,4 级 dirname = {host}(.qoder 或 wlinkj-workflow)
18
+ HOST_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
19
+ REPO_ROOT = os.path.dirname(HOST_DIR) # qoderAll/
20
+
21
+
22
+ def _find_config_path():
23
+ """按宿主探测 mcp_config.json:当前宿主根 → wlinkj-workflow → .qoder(适配多宿主部署)。"""
24
+ for p in (
25
+ os.path.join(HOST_DIR, "mcp_config.json"), # 当前宿主根(.qoder 或 wlinkj-workflow)
26
+ os.path.join(REPO_ROOT, "wlinkj-workflow", "mcp_config.json"),
27
+ os.path.join(REPO_ROOT, ".qoder", "mcp_config.json"),
28
+ ):
29
+ if os.path.isfile(p):
30
+ return p
31
+ return os.path.join(HOST_DIR, "mcp_config.json") # 都没有则写当前宿主根
32
+
33
+
34
+ CONFIG_PATH = _find_config_path()
35
+
36
+ # 平台地址(默认 10010,可 env 覆盖)
37
+ BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://127.0.0.1:10010")
38
+
39
+
40
+ def bind(email, password, base_url=BASE_URL):
41
+ """调平台 /api/auth/bind,返回 {projects:[{id,name,token,mcp_config}], base_url}。"""
42
+ data = json.dumps({"email": email, "password": password, "base_url": base_url}).encode()
43
+ req = urllib.request.Request(
44
+ f"{base_url}/api/auth/bind",
45
+ data=data, method="POST",
46
+ headers={"Content-Type": "application/json"},
47
+ )
48
+ try:
49
+ resp = urllib.request.urlopen(req, timeout=10)
50
+ return json.loads(resp.read())
51
+ except urllib.error.HTTPError as e:
52
+ body = e.read().decode("utf-8", errors="replace")[:200]
53
+ print(f"ERROR: 平台返回 {e.code}: {body}", file=sys.stderr)
54
+ sys.exit(1)
55
+ except urllib.error.URLError as e:
56
+ print(f"ERROR: 连不上平台 {base_url}: {e}", file=sys.stderr)
57
+ print(f"提示: 确保工作台在跑(start_pg.bat),或设 WLKJ_BASE_URL 环境变量", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+
61
+ def write_config(project, base_url):
62
+ """把选中项目的 mcp_config 写入 mcp_config.json(保留 return_endpoint)。"""
63
+ # 读现有配置(保留注释和非 mcpServers 字段)
64
+ existing = {}
65
+ if os.path.isfile(CONFIG_PATH):
66
+ try:
67
+ with open(CONFIG_PATH, encoding="utf-8") as f:
68
+ existing = json.load(f)
69
+ except Exception:
70
+ pass
71
+
72
+ # 用平台的 mcp_config 覆盖
73
+ mcp_cfg = project.get("mcp_config", {})
74
+ if not mcp_cfg:
75
+ # 平台没返回完整 config,手动拼
76
+ mcp_cfg = {
77
+ "mcpServers": {"wlinkj-knowledge": {
78
+ "kind": "http",
79
+ "url": f"{base_url}/mcp",
80
+ "token": project.get("token", ""),
81
+ "env": {"WLKJ_PROJECT_ID": project.get("id", "")},
82
+ }},
83
+ "return_endpoint": {
84
+ "url": f"{base_url}/api/projects/{project.get('id','')}/returns",
85
+ "method": "POST",
86
+ "headers": {"X-Engine-Token": project.get("token", "")},
87
+ },
88
+ }
89
+
90
+ existing.update(mcp_cfg)
91
+ # 确保 return_endpoint 的端口正确
92
+ if "return_endpoint" in existing:
93
+ existing["return_endpoint"]["url"] = f"{base_url}/api/projects/{project['id']}/returns"
94
+
95
+ os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
96
+ with open(CONFIG_PATH, "w", encoding="utf-8") as f:
97
+ json.dump(existing, f, ensure_ascii=False, indent=2)
98
+
99
+ print(f"OK: 已绑定项目「{project.get('name', project.get('id',''))}」")
100
+ print(f" token: {project.get('token','')[:12]}...")
101
+ print(f" project_id: {project.get('id','')}")
102
+ print(f" 配置写入: {CONFIG_PATH}")
103
+ print(f" 平台地址: {base_url}")
104
+
105
+
106
+ def main():
107
+ import argparse
108
+ parser = argparse.ArgumentParser(description="切换 wlkj 项目绑定")
109
+ parser.add_argument("--email", help="平台邮箱")
110
+ parser.add_argument("--pw", "--password", dest="password", help="平台密码")
111
+ parser.add_argument("--project-uuid", help="直接指定项目 UUID(跳过选择)")
112
+ parser.add_argument("--base-url", default=BASE_URL, help=f"平台地址(默认 {BASE_URL})")
113
+ parser.add_argument("--list", action="store_true", help="只列出项目,不写配置")
114
+ args = parser.parse_args()
115
+
116
+ # 1. 收集邮箱密码
117
+ email = args.email or os.environ.get("WLKJ_EMAIL") or input("平台邮箱: ").strip()
118
+ password = args.password or os.environ.get("WLKJ_PASSWORD") or input("平台密码: ").strip()
119
+
120
+ # 2. 拉项目列表
121
+ print(f"连接平台 {args.base_url} ...")
122
+ result = bind(email, password, args.base_url)
123
+ projects = result.get("projects", [])
124
+ if not projects:
125
+ print("该用户没有任何项目,请先在平台创建项目。")
126
+ sys.exit(1)
127
+
128
+ # 3. 列出项目
129
+ print(f"\n找到 {len(projects)} 个项目:")
130
+ for i, p in enumerate(projects):
131
+ print(f" {i+1}. {p.get('name','?')} (id={p.get('id','?')[:8]})")
132
+
133
+ if args.list:
134
+ return
135
+
136
+ # 4. 选择项目
137
+ chosen = None
138
+ if args.project_uuid:
139
+ for p in projects:
140
+ if p.get("id") == args.project_uuid:
141
+ chosen = p
142
+ break
143
+ if not chosen:
144
+ print(f"ERROR: 项目 UUID '{args.project_uuid}' 不在列表中", file=sys.stderr)
145
+ sys.exit(1)
146
+ else:
147
+ try:
148
+ idx = int(input(f"\n选哪个项目?(1-{len(projects)}): ")) - 1
149
+ chosen = projects[idx]
150
+ except (ValueError, IndexError):
151
+ print("ERROR: 无效选择", file=sys.stderr)
152
+ sys.exit(1)
153
+
154
+ # 5. 写配置
155
+ write_config(chosen, args.base_url)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python
2
+ """同步 Qoder Repo Wiki → 平台 PG wiki 表。
3
+
4
+ Qoder 自动为代码仓库生成 Repo Wiki(750+篇业务文档),存在:
5
+ data/code/{repo}/.qoder/repowiki/knowledge/zh/**/*.md
6
+
7
+ 本脚本读这些 md → UPSERT 到 PG wiki 表(repo + keyword + title + description)。
8
+ 这样知识层的 wiki 搜索(graphrag_ask/rag_search)能命中 Qoder 自动生成的高质量文档。
9
+
10
+ 用法:
11
+ python sync_repowiki.py # 同步全部仓库
12
+ python sync_repowiki.py --repo fywl-ics # 只同步指定仓库
13
+ python sync_repowiki.py --dry-run # 只统计不写
14
+ """
15
+ import os
16
+ import sys
17
+ import glob
18
+
19
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
20
+ _up1 = os.path.dirname(_THIS_DIR)
21
+ _up2 = os.path.dirname(_up1)
22
+ SCRIPTS_DIR = os.path.dirname(_up2)
23
+ REPO_ROOT = os.path.dirname(SCRIPTS_DIR)
24
+ CODE_ROOT = os.path.join(REPO_ROOT, "data", "code")
25
+
26
+ PG = dict(host="127.0.0.1", port=5432, user="wlinkj", password="changeme", dbname="wlinkj")
27
+
28
+
29
+ def scan_repowiki(repo):
30
+ """扫描某仓库的 Repo Wiki,返回 [{path, title, content, dir_path}]。"""
31
+ wiki_base = os.path.join(CODE_ROOT, repo, ".qoder", "repowiki")
32
+ if not os.path.isdir(wiki_base):
33
+ return []
34
+ results = []
35
+ for md_path in glob.glob(os.path.join(wiki_base, "**", "*.md"), recursive=True):
36
+ try:
37
+ with open(md_path, encoding="utf-8") as f:
38
+ content = f.read()
39
+ # 标题:md 文件名或第一行 #
40
+ title = os.path.splitext(os.path.basename(md_path))[0]
41
+ for line in content.split("\n")[:5]:
42
+ if line.strip().startswith("#"):
43
+ title = line.strip().lstrip("#").strip()
44
+ break
45
+ # keyword:从目录路径提取业务模块名
46
+ rel = os.path.relpath(md_path, wiki_base)
47
+ parts = rel.replace("\\", "/").split("/")
48
+ keyword = parts[-2] if len(parts) >= 2 else title
49
+ results.append({
50
+ "path": md_path,
51
+ "rel_path": rel,
52
+ "title": title,
53
+ "keyword": keyword,
54
+ "content": content[:2000], # 取前2000字
55
+ })
56
+ except Exception:
57
+ continue
58
+ return results
59
+
60
+
61
+ def sync_to_pg(wiki_items, repo):
62
+ """UPSERT 到 PG wiki 表。"""
63
+ import psycopg2
64
+ from psycopg2.extras import execute_values
65
+
66
+ conn = psycopg2.connect(**PG)
67
+ conn.autocommit = True
68
+ cur = conn.cursor()
69
+
70
+ # ★ 不删旧数据(原 wiki 表有业务文档),只删 repowiki 来源的记录
71
+ # 用 md_path 前缀 'repowiki:' 标识(避免路径分隔符差异)
72
+ cur.execute("DELETE FROM wiki WHERE project = %s AND md_path LIKE 'repowiki:%%'", (repo,))
73
+
74
+ rows = [(w["keyword"], repo, w["title"], "repowiki:" + w["rel_path"], w["content"][:500])
75
+ for w in wiki_items]
76
+ if rows:
77
+ execute_values(
78
+ cur,
79
+ "INSERT INTO wiki(keyword, project, title, md_path, description) VALUES %s",
80
+ rows,
81
+ )
82
+
83
+ print(f"[{repo}] 同步 {len(rows)} 篇 Wiki 到 PG")
84
+ conn.close()
85
+
86
+
87
+ def main():
88
+ import argparse
89
+ parser = argparse.ArgumentParser(description="同步 Qoder Repo Wiki → PG")
90
+ parser.add_argument("--repo", help="指定仓库名")
91
+ parser.add_argument("--dry-run", action="store_true", help="只统计不写")
92
+ args = parser.parse_args()
93
+
94
+ repos = [args.repo] if args.repo else ["fywl-ui", "fywl-ics", "Carmg-H5"]
95
+ total = 0
96
+ for repo in repos:
97
+ items = scan_repowiki(repo)
98
+ print(f" {repo}: 扫到 {len(items)} 篇 Repo Wiki")
99
+ if items:
100
+ for it in items[:3]:
101
+ print(f" [{it['keyword']}] {it['title']}")
102
+ if not args.dry_run:
103
+ sync_to_pg(items, repo)
104
+ total += len(items)
105
+ print(f"\n总计: {total} 篇 Wiki{'(dry-run 未写入)' if args.dry_run else ' 已同步到 PG'}")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -29,7 +29,7 @@ for _ in range(10):
29
29
  break
30
30
  if _cp not in _s.path: _s.path.insert(0, _cp)
31
31
  from bootstrap import setup; setup()
32
- from foundation.core.paths import get_repo_root
32
+ from foundation.core.paths import get_repo_root
33
33
 
34
34
  import os
35
35
  import sys
@@ -164,10 +164,43 @@ def cmd_promote(req_id, dev=None):
164
164
  return 0
165
165
 
166
166
 
167
- def cmd_publish(req_id, dev=None):
168
- """发布 = promote + 发禅道。PM 只用这个。"""
167
+ def _parse_prd_meta(prd_path):
168
+ """ PRD 提取发布所需元信息: 标题(首个#行) + spec(正文) + 可选 product/plan/build。
169
+
170
+ PRD 可在首行用 frontmatter 风格标注禅道目标:
171
+ <!-- zentao: product=1 plan=12 build=8 -->
172
+ 没标则返回 None, 由发布方补。
173
+ """
174
+ text = prd_path.read_text(encoding="utf-8")
175
+ meta = {"product_id": None, "plan_id": None, "build_id": None,
176
+ "title": "", "spec": ""}
177
+ for line in text.splitlines():
178
+ s = line.strip()
179
+ if s.startswith("<!--") and "zentao" in s:
180
+ for kv in s.replace("<!--", "").replace("-->", "").split():
181
+ if "=" in kv:
182
+ k, v = kv.split("=", 1)
183
+ kk = {"product": "product_id", "plan": "plan_id", "build": "build_id"}.get(k)
184
+ if kk and v.isdigit():
185
+ meta[kk] = int(v)
186
+ # 标题: 第一个非空 # 行; spec: 全文(截断到 8000 防 API 截断)
187
+ for line in text.splitlines():
188
+ if line.startswith("# ") and line[2:].strip():
189
+ meta["title"] = line[2:].strip()[:80]
190
+ break
191
+ meta["spec"] = text[:8000]
192
+ return meta
193
+
194
+
195
+ def cmd_publish(req_id, dev=None, product_id=None, plan_id=None, build_id=None, confirm=False):
196
+ """发布 = promote + 发禅道(建需求+关联计划+纳入版本)。
197
+
198
+ 默认 dry-run(只打印不写禅道); --confirm 才真发。
199
+ 产品/计划/版本: 优先用参数 > PRD里<!-- zentao: -->标注 > 缺则停在第该步问PM。
200
+ 用 ZentaoClient 直连 HTTP(不走 MCP), AI 只触发一次, 省 token。
201
+ """
169
202
  dev = dev or get_developer(BASE) or "unknown"
170
- # 先确保在 outputs (不在则 promote)
203
+ # 1. promote (草稿→产出)
171
204
  out_found = [e for e in find_req_dirs(req_id, dev, BASE) if e["area"] == "outputs"]
172
205
  if not out_found:
173
206
  print("草稿 → 产出...")
@@ -179,30 +212,92 @@ def cmd_publish(req_id, dev=None):
179
212
  print("✗ 发布失败: 产出区找不到 %s" % req_id)
180
213
  return 1
181
214
  out_path = out_found[0]["path"]
182
- print()
183
- print("发禅道 (从产出区: %s)" % out_path)
184
- print("-" * 40)
185
- # 找禅道 MCP 创建需求
186
- try:
187
- sys.path.insert(0, str(BASE / ".qoder" / "scripts" / "mcp"))
188
- # 禅道发布: 读 PRD, 调 zentao MCP 建 story
189
- prd_file = None
190
- for f in out_path.iterdir():
191
- if f.is_file() and (f.name.endswith(".md") and "prd" in f.name.lower()):
215
+
216
+ # 2. PRD
217
+ prd_file = None
218
+ for f in out_path.iterdir():
219
+ if f.is_file() and f.suffix == ".md" and not f.name.startswith("_"):
220
+ if "prd" in f.name.lower() or prd_file is None:
192
221
  prd_file = f
193
- break
194
- if f.is_file() and f.suffix == ".md" and not f.name.startswith("_"):
195
- prd_file = f # 第一个 md 当 PRD
196
- if prd_file:
197
- print(" PRD: %s" % prd_file.name)
198
- print()
199
- print("⚠️ 禅道发布需要 QoderWork 的禅道 MCP (mcp__qoder-zentao__create_story)。")
200
- print(" AI 会读产出的 PRD, 调禅道建需求。")
201
- print(" 如果你在 QoderWork 里说'%s 发布', AI 会自动完成这步。" % req_id)
222
+ if not prd_file:
223
+ print("✗ 发布失败: 产出区没有 .md (PRD)。")
224
+ return 1
225
+ meta = _parse_prd_meta(prd_file)
226
+ pid = product_id or meta["product_id"]
227
+ plan = plan_id or meta["plan_id"]
228
+ build = build_id or meta["build_id"]
229
+
230
+ mode = "★真发" if confirm else "DRY-RUN(只预览不写禅道, --confirm 真发)"
231
+ print("发禅道 [%s] 需求: %s PRD: %s" % (mode, req_id, prd_file.name))
232
+ print("-" * 50)
233
+ print(" 标题: %s" % meta["title"] or "(未提取到#标题)")
234
+ print(" 产品: %s" % (pid if pid else "⚠️ 未指定 — 用 --product=N 或 PRD标 <!-- zentao: product=N -->"))
235
+
236
+ if not pid:
237
+ print("\n✗ 缺产品ID, 已停(禅道需求必须挂某产品)。")
238
+ print(" 补: python req.py %s 发布 --product=N [--plan=M --build=K]" % req_id)
239
+ return 2
240
+ if not confirm:
241
+ print("\n [预览] 将: 建 story(product=%s) → %s%s" % (
242
+ pid,
243
+ "关联计划#%s → " % plan if plan else "",
244
+ "纳入版本#%s" % build if build else ""))
245
+ print(" 真发加 --confirm")
202
246
  return 0
247
+
248
+ # 3. 真发: 直连禅道 HTTP
249
+ try:
250
+ from foundation.integrations.zentao_client import ZentaoClient
203
251
  except Exception as e:
204
- print("✗ 发布出错: %s" % e)
205
- return 1
252
+ print("✗ 加载禅道客户端失败: %s" % e)
253
+ return 3
254
+ client = ZentaoClient.from_config()
255
+ if client is None:
256
+ print("✗ 禅道未配置(凭据/内网)。先 /wl-init 或检查 ~/.qoderwork/.secrets/zentao.env")
257
+ return 3
258
+ ok, msg = client.check_intranet()
259
+ if not ok:
260
+ print("✗ 禅道不可达: %s" % msg)
261
+ return 3
262
+
263
+ # 3a. 建 story
264
+ payload = {"title": meta["title"] or req_id, "spec": meta["spec"], "pri": 3, "category": "feature"}
265
+ r = client.api("POST", "/products/%s/stories" % pid, payload)
266
+ if r.status_code not in (200, 201):
267
+ print("✗ 建需求失败: %s %s" % (r.status_code, r.text[:120]))
268
+ return 3
269
+ story_id = r.json().get("id")
270
+ if not story_id:
271
+ print("✗ 建需求异常(无id): %s" % str(r.json())[:150])
272
+ return 3
273
+ print(" ✅ 需求已建 #%s" % story_id)
274
+
275
+ # 3b. 关联计划 (可选)
276
+ if plan:
277
+ r2 = client.api("POST", "/plans/%s/linkStory" % plan, {"stories": [story_id]})
278
+ if r2.status_code in (200, 201):
279
+ print(" ✅ 已关联计划 #%s" % plan)
280
+ else:
281
+ print(" ⚠️ 关联计划#%s 失败: %s (不影响,可手动)" % (plan, r2.status_code))
282
+
283
+ # 3c. 纳入版本 (可选)
284
+ if build:
285
+ r3 = client.api("POST", "/builds/%s/linkStory" % build, {"stories": [story_id]})
286
+ if r3.status_code in (200, 201):
287
+ print(" ✅ 已纳入版本 #%s" % build)
288
+ else:
289
+ print(" ⚠️ 纳入版本#%s 失败: %s (不影响,可手动)" % (build, r3.status_code))
290
+
291
+ # 3d. 回写 story_id 到 PRD (留痕, 防重复建)
292
+ try:
293
+ with open(prd_file, "a", encoding="utf-8") as fh:
294
+ fh.write("\n\n<!-- zentao: product=%s story=%s plan=%s build=%s -->\n" % (
295
+ pid, story_id, plan or 0, build or 0))
296
+ except Exception:
297
+ pass
298
+
299
+ print("\n🎉 发布完成! 禅道需求 #%s" % story_id)
300
+ return 0
206
301
 
207
302
 
208
303
  def main():
@@ -211,10 +306,21 @@ def main():
211
306
  return cmd_list()
212
307
  req_id = args[0]
213
308
  action = args[1] if len(args) > 1 else "show"
214
- if action == "show":
309
+ # 发布支持 --product/--plan/--build/--confirm
310
+ if action in ("发布", "publish", "fabu"):
311
+ opts = {"product_id": None, "plan_id": None, "build_id": None, "confirm": False}
312
+ for a in args[2:]:
313
+ if a == "--confirm":
314
+ opts["confirm"] = True
315
+ elif a.startswith("--product="):
316
+ opts["product_id"] = int(a.split("=", 1)[1])
317
+ elif a.startswith("--plan="):
318
+ opts["plan_id"] = int(a.split("=", 1)[1])
319
+ elif a.startswith("--build="):
320
+ opts["build_id"] = int(a.split("=", 1)[1])
321
+ return cmd_publish(req_id, **opts)
322
+ elif action == "show":
215
323
  return cmd_show(req_id)
216
- elif action in ("发布", "publish", "fabu"):
217
- return cmd_publish(req_id)
218
324
  elif action == "promote":
219
325
  return cmd_promote(req_id)
220
326
  elif action == "list":