@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,392 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
integration.return_to_platform —— 引擎产出回流平台的统一通道。
|
|
4
|
+
|
|
5
|
+
设计模式: Facade(封装"产出→平台"的回流细节,供命令/脚本一键调用)
|
|
6
|
+
|
|
7
|
+
历史断点(已修):
|
|
8
|
+
引擎产出 PRD/原型只写本地 workspace/members/{dev}/drafts/,永不回平台,
|
|
9
|
+
平台 AI回流 Tab 永远空。原因: 全仓 grep submit_return/create_prd/create_prototype
|
|
10
|
+
在 wlinkj-workflow 下 0 匹配 —— 引擎根本不知道有回流这回事。
|
|
11
|
+
|
|
12
|
+
本模块提供两条回流路径,任一可用即回流成功:
|
|
13
|
+
1. MCP 写工具(首选): cap.mcp.call("submit_return"|"create_prd"|"create_prototype")
|
|
14
|
+
平台 MCP 已暴露这 4 个写工具(已验证真落库,进 returns/prd/asset/prototype 表)。
|
|
15
|
+
2. HTTP return_endpoint(兜底): 读 mcp_config.json 的 return_endpoint.url,
|
|
16
|
+
直接 POST 回流 JSON(MCP 不可用 / 工具未索引时用)。
|
|
17
|
+
|
|
18
|
+
调用方: 命令脚本(.py)/ 命令文档(.md 里通过 wlkj.py return 子命令)。
|
|
19
|
+
失败兜底: 全程 try/except,回流失败只记日志,绝不影响主流程产出(PRD/原型照常落地)。
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_mcp_config() -> dict:
|
|
27
|
+
"""读 wlinkj-workflow/mcp_config.json,失败返回空 dict。"""
|
|
28
|
+
import json as _json
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from foundation.core.paths import get_repo_root
|
|
33
|
+
repo = get_repo_root()
|
|
34
|
+
except Exception:
|
|
35
|
+
repo = Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
36
|
+
for cand in [
|
|
37
|
+
repo / "mcp_config.json",
|
|
38
|
+
repo / "wlinkj-workflow" / "mcp_config.json",
|
|
39
|
+
]:
|
|
40
|
+
try:
|
|
41
|
+
if cand.is_file():
|
|
42
|
+
with open(cand, encoding="utf-8") as f:
|
|
43
|
+
return _json.load(f)
|
|
44
|
+
except Exception:
|
|
45
|
+
continue
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _truncate(text: str, n: int = 500) -> str:
|
|
50
|
+
"""安全截断,避免超长 content_preview 塞爆 MCP/HTTP。"""
|
|
51
|
+
if not text:
|
|
52
|
+
return ""
|
|
53
|
+
return text if len(text) <= n else (text[:n] + "…")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_file_text(path: str) -> str:
|
|
57
|
+
"""安全读文件全文,失败返回空串。"""
|
|
58
|
+
try:
|
|
59
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
60
|
+
return f.read()
|
|
61
|
+
except Exception:
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _project_id() -> Optional[str]:
|
|
66
|
+
"""从 mcp_config.json 读 WLKJ_PROJECT_ID(平台项目 UUID)。"""
|
|
67
|
+
cfg = _load_mcp_config()
|
|
68
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
69
|
+
env = srv.get("env") or {}
|
|
70
|
+
if env.get("WLKJ_PROJECT_ID"):
|
|
71
|
+
return env["WLKJ_PROJECT_ID"]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _via_mcp(tool: str, args: dict) -> tuple[bool, str]:
|
|
76
|
+
"""通过 cap.mcp.call 走 MCP 写工具回流。
|
|
77
|
+
★ 自动注入 project_id(平台写工具强制要,防写错项目)。
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
(ok, message): ok=True 表示非错误返回。
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# 注入 project_id(写工具必需)
|
|
84
|
+
pid = _project_id()
|
|
85
|
+
if pid and "project_id" not in args:
|
|
86
|
+
args = {**args, "project_id": pid}
|
|
87
|
+
from capability import resolve
|
|
88
|
+
cap = resolve()
|
|
89
|
+
result = cap.mcp.call(tool, args)
|
|
90
|
+
ok = not getattr(result, "is_error", False)
|
|
91
|
+
msg = getattr(result, "text", "") or ("ok" if ok else "error")
|
|
92
|
+
return ok, msg[:200]
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return False, "MCP回流异常: %s" % str(e)[:120]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _via_http(payload: dict) -> tuple[bool, str]:
|
|
98
|
+
"""HTTP 兜底: 读 mcp_config.json 的 return_endpoint 直接 POST。
|
|
99
|
+
★ 带 X-Engine-Token 头(平台项目级鉴权)。
|
|
100
|
+
"""
|
|
101
|
+
import json as _json
|
|
102
|
+
import urllib.request
|
|
103
|
+
import urllib.error
|
|
104
|
+
|
|
105
|
+
cfg = _load_mcp_config()
|
|
106
|
+
ep = (cfg.get("return_endpoint") or {})
|
|
107
|
+
url = ep.get("url")
|
|
108
|
+
if not url:
|
|
109
|
+
return False, "mcp_config.json 无 return_endpoint.url"
|
|
110
|
+
method = ep.get("method", "POST")
|
|
111
|
+
data = _json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
112
|
+
# headers: 配置里的 + Content-Type
|
|
113
|
+
headers = {"Content-Type": "application/json"}
|
|
114
|
+
headers.update(ep.get("headers") or {})
|
|
115
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
116
|
+
try:
|
|
117
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
118
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
119
|
+
return True, body[:200]
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return False, "HTTP回流失败: %s" % str(e)[:120]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _platform_base() -> str:
|
|
125
|
+
"""平台 base url(http://127.0.0.1:10010),从 mcp_config server url 推导。"""
|
|
126
|
+
from urllib.parse import urlparse
|
|
127
|
+
cfg = _load_mcp_config()
|
|
128
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
129
|
+
url = srv.get("url") or ""
|
|
130
|
+
if url:
|
|
131
|
+
p = urlparse(url)
|
|
132
|
+
return "%s://%s" % (p.scheme, p.netloc)
|
|
133
|
+
ep = (cfg.get("return_endpoint") or {}).get("url") or ""
|
|
134
|
+
if ep:
|
|
135
|
+
p = urlparse(ep)
|
|
136
|
+
return "%s://%s" % (p.scheme, p.netloc)
|
|
137
|
+
return "http://127.0.0.1:10010"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _engine_token() -> Optional[str]:
|
|
141
|
+
"""X-Engine-Token(mcp_config return_endpoint.headers 优先,server token 兜底)。"""
|
|
142
|
+
cfg = _load_mcp_config()
|
|
143
|
+
for k, v in ((cfg.get("return_endpoint") or {}).get("headers") or {}).items():
|
|
144
|
+
if k.lower() in ("x-engine-token", "x-return-token"):
|
|
145
|
+
return v
|
|
146
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
147
|
+
if srv.get("token"):
|
|
148
|
+
return srv["token"]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _post_to_platform(path: str, payload: dict, timeout: int = 15) -> tuple[bool, str]:
|
|
153
|
+
"""POST JSON 到平台任意端点(带 X-Engine-Token 鉴权)。
|
|
154
|
+
|
|
155
|
+
path: 相对路径如 /api/git/commits/record;base 从 mcp_config server url 推导。
|
|
156
|
+
供 commit/spec 等"非 return_endpoint"端点回流复用。
|
|
157
|
+
"""
|
|
158
|
+
import json as _json
|
|
159
|
+
import urllib.request
|
|
160
|
+
token = _engine_token()
|
|
161
|
+
if not token:
|
|
162
|
+
return False, "无 X-Engine-Token(mcp_config 未配)"
|
|
163
|
+
url = _platform_base().rstrip("/") + "/" + path.lstrip("/")
|
|
164
|
+
data = _json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
165
|
+
req = urllib.request.Request(url, data=data, headers={
|
|
166
|
+
"Content-Type": "application/json", "X-Engine-Token": token,
|
|
167
|
+
}, method="POST")
|
|
168
|
+
try:
|
|
169
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
170
|
+
return True, resp.read().decode("utf-8", errors="replace")[:200]
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return False, "POST %s 失败: %s" % (path, str(e)[:120])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def submit_prd(prd_path: str, title: str, source: str = "wlkj/wl-prd",
|
|
176
|
+
platform: Optional[str] = None,
|
|
177
|
+
version_id: Optional[str] = None,
|
|
178
|
+
requirement_id: Optional[str] = None) -> dict:
|
|
179
|
+
"""回流一份 PRD 到平台。
|
|
180
|
+
|
|
181
|
+
优先 create_prd(写 PRD 表),失败再 submit_return(写 returns 待审核),
|
|
182
|
+
最后 HTTP 兜底。任一成功即返回 ok=True。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
prd_path: PRD markdown 文件路径。
|
|
186
|
+
title: PRD 标题。
|
|
187
|
+
source: 来源标识。
|
|
188
|
+
platform: 平台(web/app,可选)。
|
|
189
|
+
version_id: 引擎工作流版本ID(关联平台 PRD.version_id)。
|
|
190
|
+
requirement_id: 禅道需求ID(关联平台 PRD.requirement_id)。
|
|
191
|
+
Returns:
|
|
192
|
+
{"ok": bool, "via": "create_prd|submit_return|http|none", "message": str}
|
|
193
|
+
"""
|
|
194
|
+
content_md = _read_file_text(prd_path)
|
|
195
|
+
if not content_md:
|
|
196
|
+
return {"ok": False, "via": "none", "message": "PRD 文件为空或读不到: %s" % prd_path}
|
|
197
|
+
|
|
198
|
+
# 1) MCP submit_return(回流审核队列——默认路径!引擎产出先进 AI回流 Tab,人工确认后才入库)
|
|
199
|
+
return_args = {
|
|
200
|
+
"title": title, "return_type": "prd", "source": source,
|
|
201
|
+
"content_preview": _truncate(content_md),
|
|
202
|
+
"content_full": content_md,
|
|
203
|
+
"target_location": "PRD草稿",
|
|
204
|
+
}
|
|
205
|
+
if version_id:
|
|
206
|
+
return_args["relations"] = "version_id=%s" % version_id
|
|
207
|
+
if requirement_id:
|
|
208
|
+
return_args["relations"] = (return_args.get("relations", "") + " requirement_id=%s" % requirement_id).strip()
|
|
209
|
+
ok, msg = _via_mcp("submit_return", return_args)
|
|
210
|
+
if ok:
|
|
211
|
+
return {"ok": True, "via": "submit_return", "message": "回流成功,待平台审核入库"}
|
|
212
|
+
|
|
213
|
+
# 2) create_prd 兜底(submit_return 失败才直写,标记 publish_status=published)
|
|
214
|
+
create_args = {
|
|
215
|
+
"title": title, "content_md": content_md, "status": "planning",
|
|
216
|
+
"publish_status": "published",
|
|
217
|
+
}
|
|
218
|
+
if version_id:
|
|
219
|
+
create_args["version_id"] = version_id
|
|
220
|
+
if requirement_id:
|
|
221
|
+
create_args["requirement_id"] = requirement_id
|
|
222
|
+
ok, msg = _via_mcp("create_prd", create_args)
|
|
223
|
+
if ok:
|
|
224
|
+
return {"ok": True, "via": "create_prd", "message": msg}
|
|
225
|
+
|
|
226
|
+
# 3) HTTP 兜底
|
|
227
|
+
ok, msg = _via_http({
|
|
228
|
+
"title": title, "return_type": "prd", "source": source,
|
|
229
|
+
"content_preview": _truncate(content_md), "content_full": content_md,
|
|
230
|
+
})
|
|
231
|
+
return {"ok": ok, "via": "http" if ok else "none", "message": msg}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def submit_prototype(proto_path: str, feature: str, platform: str = "web",
|
|
235
|
+
source: str = "wlkj/wl-design") -> dict:
|
|
236
|
+
"""回流一份原型(HTML)到平台。
|
|
237
|
+
|
|
238
|
+
优先 create_prototype(写 prototype 表 + 存文件),失败 submit_return,
|
|
239
|
+
最后 HTTP 兜底。
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
proto_path: 原型 HTML 文件路径。
|
|
243
|
+
feature: 功能名。
|
|
244
|
+
platform: web/app。
|
|
245
|
+
source: 来源标识。
|
|
246
|
+
Returns:
|
|
247
|
+
{"ok": bool, "via": str, "message": str}
|
|
248
|
+
"""
|
|
249
|
+
html = _read_file_text(proto_path)
|
|
250
|
+
if not html:
|
|
251
|
+
return {"ok": False, "via": "none", "message": "原型文件为空或读不到: %s" % proto_path}
|
|
252
|
+
|
|
253
|
+
# 1) MCP create_prototype(写 prototype + 存 HTML 文件,最完整)
|
|
254
|
+
ok, msg = _via_mcp("create_prototype", {
|
|
255
|
+
"feature": feature, "platform": platform, "html_content": html,
|
|
256
|
+
})
|
|
257
|
+
if ok:
|
|
258
|
+
return {"ok": True, "via": "create_prototype", "message": msg}
|
|
259
|
+
|
|
260
|
+
# 2) MCP submit_return
|
|
261
|
+
ok, msg = _via_mcp("submit_return", {
|
|
262
|
+
"title": "原型-%s-%s" % (platform, feature),
|
|
263
|
+
"return_type": "prototype", "source": source,
|
|
264
|
+
"content_preview": _truncate(html),
|
|
265
|
+
"content_full": html,
|
|
266
|
+
"relations": "platform=%s;feature=%s" % (platform, feature),
|
|
267
|
+
})
|
|
268
|
+
if ok:
|
|
269
|
+
return {"ok": True, "via": "submit_return", "message": msg}
|
|
270
|
+
|
|
271
|
+
# 3) HTTP 兜底
|
|
272
|
+
ok, msg = _via_http({
|
|
273
|
+
"title": "原型-%s-%s" % (platform, feature),
|
|
274
|
+
"return_type": "prototype", "source": source,
|
|
275
|
+
"content_preview": _truncate(html), "content_full": html,
|
|
276
|
+
})
|
|
277
|
+
return {"ok": ok, "via": "http" if ok else "none", "message": msg}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def submit_commit(commit_hash: str, message: str, author: str = "",
|
|
281
|
+
repo: str = "", pushed_at: str = "") -> dict:
|
|
282
|
+
"""回流一次 git commit 到平台(git_commits 表 + fixes/implements 边)。
|
|
283
|
+
|
|
284
|
+
平台端点 POST /api/git/commits/record(X-Engine-Token 鉴权,token→project_id)。
|
|
285
|
+
commit→需求关联靠 message 里的 [#REQ-xxx] / [#<禅道号>] 标记,平台 commit_pipeline
|
|
286
|
+
自动解析建边 —— 故 message 必须原样带提交信息(含需求标记),不在此剥离。
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
commit_hash: git commit SHA。
|
|
290
|
+
message: 完整提交信息(含 [#REQ-xxx] 需求标记)。
|
|
291
|
+
author: 提交者。
|
|
292
|
+
repo: 仓库 remote url(可选)。
|
|
293
|
+
pushed_at: ISO 时间(可选)。
|
|
294
|
+
Returns:
|
|
295
|
+
{"ok": bool, "via": "record_commit|none", "message": str}
|
|
296
|
+
"""
|
|
297
|
+
if not commit_hash or not message:
|
|
298
|
+
return {"ok": False, "via": "none", "message": "缺 commit_hash/message"}
|
|
299
|
+
# EngineCallbackIngestor 期望 {sha, message, author, repo, pushed_at}
|
|
300
|
+
payload = {
|
|
301
|
+
"sha": commit_hash, "message": message, "author": author,
|
|
302
|
+
"repo": repo, "pushed_at": pushed_at,
|
|
303
|
+
}
|
|
304
|
+
ok, msg = _post_to_platform("/api/git/commits/record", payload)
|
|
305
|
+
return {"ok": ok, "via": "record_commit" if ok else "none", "message": msg}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _flag_value(args: list, flag: str, default: str = "") -> str:
|
|
309
|
+
"""从 argv 取 --flag value(无则 default)。"""
|
|
310
|
+
if flag in args:
|
|
311
|
+
i = args.index(flag)
|
|
312
|
+
return args[i + 1] if i + 1 < len(args) else default
|
|
313
|
+
return default
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _git_repo_url() -> str:
|
|
317
|
+
"""取当前 git 仓库 remote.origin.url(失败空串)。"""
|
|
318
|
+
import subprocess
|
|
319
|
+
try:
|
|
320
|
+
return subprocess.check_output(
|
|
321
|
+
["git", "config", "--get", "remote.origin.url"], encoding="utf-8"
|
|
322
|
+
).strip()
|
|
323
|
+
except Exception:
|
|
324
|
+
return ""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _cli_commit(rest: list) -> dict:
|
|
328
|
+
"""commit 回流子命令:--last 自动取 HEAD,或显式 <hash> <message>。"""
|
|
329
|
+
if "--last" in rest:
|
|
330
|
+
import subprocess
|
|
331
|
+
try:
|
|
332
|
+
# %B = 完整 message(含 subject + body,[#REQ-xxx] 在 body 也能解析)
|
|
333
|
+
fmt = "%H%x1f%B%x1f%an%x1f%aI"
|
|
334
|
+
out = subprocess.check_output(
|
|
335
|
+
["git", "log", "-1", "--format=" + fmt], encoding="utf-8"
|
|
336
|
+
)
|
|
337
|
+
parts = out.split("\x1f")
|
|
338
|
+
sha = parts[0].strip()
|
|
339
|
+
body = parts[1].strip()
|
|
340
|
+
author = parts[2].strip()
|
|
341
|
+
ts = parts[3].strip()
|
|
342
|
+
return submit_commit(sha, body, author=author,
|
|
343
|
+
repo=_git_repo_url(), pushed_at=ts)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return {"ok": False, "via": "none",
|
|
346
|
+
"message": "取 HEAD commit 失败: %s" % str(e)[:120]}
|
|
347
|
+
if len(rest) < 2:
|
|
348
|
+
return {"ok": False, "via": "none",
|
|
349
|
+
"message": "用法: commit <hash> <message> [--author X] [--repo Y] 或 commit --last"}
|
|
350
|
+
sha, msg = rest[0], rest[1]
|
|
351
|
+
return submit_commit(sha, msg,
|
|
352
|
+
author=_flag_value(rest, "--author"),
|
|
353
|
+
repo=_flag_value(rest, "--repo"),
|
|
354
|
+
pushed_at=_flag_value(rest, "--pushed-at"))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def cli_main() -> int:
|
|
358
|
+
"""命令行入口(供 wlkj.py return 子命令调用)。
|
|
359
|
+
|
|
360
|
+
用法:
|
|
361
|
+
python -m domain.integration.return_to_platform prd <path> <title> [--platform web]
|
|
362
|
+
python -m domain.integration.return_to_platform prototype <path> <feature> [--platform web]
|
|
363
|
+
python -m domain.integration.return_to_platform commit --last # 自动回流 HEAD commit
|
|
364
|
+
python -m domain.integration.return_to_platform commit <hash> <msg> [--author X] [--repo Y]
|
|
365
|
+
退出码: 0=回流成功(含MCP/HTTP), 1=参数错误(主流程不受影响)。
|
|
366
|
+
"""
|
|
367
|
+
import sys
|
|
368
|
+
args = sys.argv[1:]
|
|
369
|
+
if not args or args[0] not in ("prd", "prototype", "commit"):
|
|
370
|
+
print("[return] 用法: return <prd|prototype|commit> ... (commit 支持 --last 自动取 HEAD)")
|
|
371
|
+
return 1
|
|
372
|
+
kind = args[0]
|
|
373
|
+
|
|
374
|
+
if kind == "commit":
|
|
375
|
+
r = _cli_commit(args[1:])
|
|
376
|
+
else:
|
|
377
|
+
if len(args) < 3:
|
|
378
|
+
print("[return] 用法: return %s <文件路径> <标题|功能名> [--platform web]" % kind)
|
|
379
|
+
return 1
|
|
380
|
+
path, name = args[1], args[2]
|
|
381
|
+
platform = _flag_value(args, "--platform", "web")
|
|
382
|
+
r = submit_prd(path, name, platform=platform) if kind == "prd" \
|
|
383
|
+
else submit_prototype(path, name, platform=platform)
|
|
384
|
+
|
|
385
|
+
tag = "✅" if r["ok"] else "⚠️"
|
|
386
|
+
print("%s [回流] via=%s %s" % (tag, r["via"], r["message"]))
|
|
387
|
+
# 回流失败返回 0 —— 主流程产出已落地,回流是增强不是阻塞
|
|
388
|
+
return 0
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
raise SystemExit(cli_main())
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""integration.spec_upload —— 开发 SPEC 制品回流平台(引擎侧客户端)。
|
|
3
|
+
|
|
4
|
+
背景: wlinkj-workspace/docs/ENGINE-SPEC-UPLOAD.md §4.2(wl-spec 第 7-9 步)
|
|
5
|
+
契约: wlinkj-workspace/docs/SPEC-MCP-CONTRACT.md v1(create/confirm/update/get_spec)
|
|
6
|
+
|
|
7
|
+
流程: 解析契约头 → 有platform_spec_id走update/无走create → confirm → 回写幂等锚
|
|
8
|
+
传输: POST /mcp direct JSON-RPC + X-MCP-Token(同 engine/poller.py:_mcp_call)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import json, os, re, sys, urllib.request, urllib.error
|
|
12
|
+
from typing import Optional, Tuple
|
|
13
|
+
|
|
14
|
+
# 直接运行时把 .qoder/scripts 注入 sys.path(让 foundation.* 可 import)
|
|
15
|
+
_SCRIPT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
if _SCRIPT_ROOT not in sys.path:
|
|
17
|
+
sys.path.insert(0, _SCRIPT_ROOT)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_mcp_config() -> dict:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
try:
|
|
23
|
+
from foundation.core.paths import get_repo_root
|
|
24
|
+
repo = get_repo_root()
|
|
25
|
+
except Exception:
|
|
26
|
+
repo = Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
27
|
+
for cand in [repo / "mcp_config.json",
|
|
28
|
+
repo / "wlinkj-workflow" / "mcp_config.json",
|
|
29
|
+
repo / ".qoder" / "mcp_config.json"]:
|
|
30
|
+
try:
|
|
31
|
+
if cand.is_file():
|
|
32
|
+
with open(cand, encoding="utf-8") as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_endpoint() -> Tuple[str, str, str]:
|
|
40
|
+
"""返回 (base, token, pid)。env 覆盖 > mcp_config.json。"""
|
|
41
|
+
cfg = _load_mcp_config()
|
|
42
|
+
base = os.environ.get("WLKJ_MCP_BASE")
|
|
43
|
+
if not base:
|
|
44
|
+
ep = cfg.get("return_endpoint") or {}
|
|
45
|
+
url = ep.get("url", "")
|
|
46
|
+
base = url.split("/api/")[0] if "/api/" in url else ""
|
|
47
|
+
if not base:
|
|
48
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
49
|
+
u = srv.get("url", "")
|
|
50
|
+
if "/mcp" in u:
|
|
51
|
+
base = u.split("/mcp")[0]; break
|
|
52
|
+
base = (base or "http://127.0.0.1:10010").rstrip("/")
|
|
53
|
+
token = os.environ.get("WLKJ_MCP_TOKEN", "")
|
|
54
|
+
if not token:
|
|
55
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
56
|
+
t = srv.get("token", "")
|
|
57
|
+
if t and "WILL_BE_FILLED" not in t:
|
|
58
|
+
token = t; break
|
|
59
|
+
pid = os.environ.get("WLKJ_PROJECT_ID", "")
|
|
60
|
+
if not pid:
|
|
61
|
+
for srv in (cfg.get("mcpServers") or {}).values():
|
|
62
|
+
env = srv.get("env") or {}
|
|
63
|
+
p = env.get("WLKJ_PROJECT_ID", "")
|
|
64
|
+
if p and "WILL_BE_FILLED" not in p:
|
|
65
|
+
pid = p; break
|
|
66
|
+
return base, token, pid
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _mcp_call(base: str, token: str, tool: str, args: dict) -> dict:
|
|
70
|
+
"""POST /mcp JSON-RPC tools/call,解析 result.content[0].text。"""
|
|
71
|
+
body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
72
|
+
"params": {"name": tool, "arguments": args}}
|
|
73
|
+
req = urllib.request.Request(base + "/mcp",
|
|
74
|
+
data=json.dumps(body).encode("utf-8"),
|
|
75
|
+
headers={"Content-Type": "application/json", "X-MCP-Token": token}, method="POST")
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
78
|
+
r = json.loads(resp.read().decode("utf-8", "replace"))
|
|
79
|
+
except urllib.error.HTTPError as e:
|
|
80
|
+
try: return {"error": "HTTP %d: %s" % (e.code, e.read().decode("utf-8","replace")[:200])}
|
|
81
|
+
except Exception: return {"error": "HTTP %d" % e.code}
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return {"error": "网络异常: %s" % str(e)[:150]}
|
|
84
|
+
if r.get("error"):
|
|
85
|
+
return {"error": "JSON-RPC error: %s" % str(r["error"])[:200]}
|
|
86
|
+
text = (r.get("result", {}).get("content") or [{}])[0].get("text", "")
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(text) if text else {}
|
|
89
|
+
except Exception:
|
|
90
|
+
return {"error": "结果非 JSON: %s" % text[:200]}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_CONTRACT_RE = re.compile(r"<!--\s*@contract\s+spec[^\n]*\n(?P<body>.*?)-->", re.DOTALL)
|
|
94
|
+
|
|
95
|
+
def parse_spec_contract(md: str) -> dict:
|
|
96
|
+
fields = {}
|
|
97
|
+
m = _CONTRACT_RE.search(md)
|
|
98
|
+
if m:
|
|
99
|
+
for line in m.group("body").splitlines():
|
|
100
|
+
if ":" in line:
|
|
101
|
+
k, _, v = line.partition(":")
|
|
102
|
+
fields[k.strip()] = v.strip()
|
|
103
|
+
title = ""
|
|
104
|
+
for line in md.splitlines():
|
|
105
|
+
if line.startswith("# "):
|
|
106
|
+
title = line[2:].strip(); break
|
|
107
|
+
zentao_id = fields.get("zentao_id", "")
|
|
108
|
+
if not zentao_id:
|
|
109
|
+
mt = re.search(r"story\s*=\s*(\d+)", fields.get("zentao", ""))
|
|
110
|
+
zentao_id = mt.group(1) if mt else ""
|
|
111
|
+
return {"title": title, "zentao_id": zentao_id,
|
|
112
|
+
"platform_spec_id": fields.get("platform_spec_id", ""),
|
|
113
|
+
"source_prd_id": fields.get("source_prd_id") or fields.get("source-req", ""),
|
|
114
|
+
"content_md": md}
|
|
115
|
+
|
|
116
|
+
def write_back_platform_spec_id(md: str, spec_id: str) -> str:
|
|
117
|
+
m = _CONTRACT_RE.search(md)
|
|
118
|
+
if not m: return md
|
|
119
|
+
body = m.group("body")
|
|
120
|
+
if re.search(r"^\s*platform_spec_id\s*:", body, re.MULTILINE):
|
|
121
|
+
new_body = re.sub(r"(\bplatform_spec_id[ \t]*:[ \t]*)[^\n]*", r"\g<1>%s" % spec_id, body)
|
|
122
|
+
else:
|
|
123
|
+
new_body = body.rstrip() + "\nplatform_spec_id: %s\n" % spec_id
|
|
124
|
+
return md[:m.start("body")] + new_body + md[m.end("body"):]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def upload_spec(spec_path: str, zentao_id_override: Optional[str] = None,
|
|
128
|
+
code_targets: Optional[list] = None) -> dict:
|
|
129
|
+
try:
|
|
130
|
+
with open(spec_path, encoding="utf-8", errors="replace") as f:
|
|
131
|
+
md = f.read()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return {"ok": False, "via": "none", "message": "读不到 spec: %s" % e}
|
|
134
|
+
|
|
135
|
+
c = parse_spec_contract(md)
|
|
136
|
+
title = c["title"] or os.path.splitext(os.path.basename(spec_path))[0]
|
|
137
|
+
zentao_id = zentao_id_override or c["zentao_id"]
|
|
138
|
+
if not zentao_id:
|
|
139
|
+
return {"ok": False, "via": "none",
|
|
140
|
+
"message": "缺 zentao_id(契约头无且未 --zentao-id)—— 取消(无关联键)"}
|
|
141
|
+
|
|
142
|
+
base, token, pid = _resolve_endpoint()
|
|
143
|
+
if not token:
|
|
144
|
+
return {"ok": False, "via": "none",
|
|
145
|
+
"message": "mcp_config.json 未绑定 token(先 /wl-init)或设 WLKJ_MCP_TOKEN"}
|
|
146
|
+
|
|
147
|
+
code_targets = code_targets or []
|
|
148
|
+
existing_id = c["platform_spec_id"]
|
|
149
|
+
|
|
150
|
+
# 1) create 或 update(幂等)
|
|
151
|
+
if existing_id:
|
|
152
|
+
r = _mcp_call(base, token, "update_spec",
|
|
153
|
+
{"spec_id": existing_id, "content_md": c["content_md"]})
|
|
154
|
+
via, spec_id, version = "update_spec", existing_id, r.get("version")
|
|
155
|
+
else:
|
|
156
|
+
args = {"title": title, "zentao_id": zentao_id, "content_md": c["content_md"]}
|
|
157
|
+
if pid: args["project_id"] = pid
|
|
158
|
+
if c["source_prd_id"]: args["source_prd_id"] = c["source_prd_id"]
|
|
159
|
+
r = _mcp_call(base, token, "create_spec", args)
|
|
160
|
+
via, spec_id, version = "create_spec", r.get("id"), r.get("version")
|
|
161
|
+
|
|
162
|
+
if r.get("error") or not spec_id:
|
|
163
|
+
return {"ok": False, "via": via, "message": r.get("error", "无 spec_id 返回")}
|
|
164
|
+
|
|
165
|
+
# 2) confirm(draft→confirmed)
|
|
166
|
+
rc = _mcp_call(base, token, "confirm_spec",
|
|
167
|
+
{"spec_id": spec_id, "code_targets": code_targets})
|
|
168
|
+
status = rc.get("status", "")
|
|
169
|
+
|
|
170
|
+
# 3) 回写幂等锚(仅 create 后)
|
|
171
|
+
if not existing_id:
|
|
172
|
+
try:
|
|
173
|
+
new_md = write_back_platform_spec_id(md, spec_id)
|
|
174
|
+
with open(spec_path, "w", encoding="utf-8") as f:
|
|
175
|
+
f.write(new_md)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
return {"ok": True, "platform_spec_id": spec_id, "via": via,
|
|
180
|
+
"status": status or r.get("status", ""), "version": version,
|
|
181
|
+
"requirement_id": r.get("requirement_id"),
|
|
182
|
+
"created_new": r.get("created_new"),
|
|
183
|
+
"message": "上传成功:%s(zentao_id=%s → requirement_id=%s)" % (
|
|
184
|
+
title, zentao_id, r.get("requirement_id"))}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def cli_main() -> int:
|
|
188
|
+
args = sys.argv[1:]
|
|
189
|
+
if not args or args[0] in ("-h", "--help"):
|
|
190
|
+
print("用法: python -m domain.integration.spec_upload <spec.md> "
|
|
191
|
+
"[--zentao-id N] [--code-targets A,B]"); return 1
|
|
192
|
+
spec_path = args[0]
|
|
193
|
+
zentao_id = None; code_targets = []
|
|
194
|
+
if "--zentao-id" in args:
|
|
195
|
+
i = args.index("--zentao-id")
|
|
196
|
+
zentao_id = args[i+1] if i+1 < len(args) else None
|
|
197
|
+
if "--code-targets" in args:
|
|
198
|
+
i = args.index("--code-targets")
|
|
199
|
+
code_targets = [x.strip() for x in args[i+1].split(",")] if i+1 < len(args) else []
|
|
200
|
+
r = upload_spec(spec_path, zentao_id_override=zentao_id, code_targets=code_targets)
|
|
201
|
+
print("%s [spec-upload] via=%s %s" % ("✅" if r["ok"] else "⚠️", r["via"], r["message"]))
|
|
202
|
+
if r["ok"]:
|
|
203
|
+
print(" platform_spec_id=%s status=%s version=%s" % (
|
|
204
|
+
r["platform_spec_id"], r["status"], r["version"]))
|
|
205
|
+
return 0 # 上传失败返回0:本地spec已落地,上传是增强不阻塞
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
raise SystemExit(cli_main())
|