@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.
- package/bin/cli.js +117 -0
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +4 -4
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +23 -3
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +138 -7
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +70 -6
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +230 -15
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +3 -3
- package/templates/qoder/commands/wl-test.md +17 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +116 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +61 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- 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/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -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 +268 -25
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- 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/extract/ts_extract.py +111 -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/storage/kg_duckdb.py +43 -0
- 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/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
- 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/core/paths.py +102 -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 +24 -11
- 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/qoder/scripts/validation/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +41 -14
- package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- 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
|
|
168
|
-
"""
|
|
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
|
-
#
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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("✗
|
|
205
|
-
return
|
|
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
|
-
|
|
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":
|