@hupan56/wlkj 3.3.14 → 3.3.15
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 +16 -1
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +4 -2
- package/templates/qoder/commands/wl-commit.md +3 -1
- package/templates/qoder/commands/wl-init.md +2 -2
- package/templates/qoder/commands/wl-prd.md +24 -0
- package/templates/qoder/commands/wl-search.md +18 -11
- package/templates/qoder/commands/wl-task.md +3 -1
- package/templates/qoder/config.yaml +0 -6
- package/templates/qoder/contracts/insight.md +55 -0
- package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
- package/templates/qoder/hooks/session-start.py +20 -1
- package/templates/qoder/hooks/stop-eval.py +129 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +9 -1
- package/templates/qoder/scripts/capability/registry.py +0 -3
- package/templates/qoder/scripts/capability/registry_mcp.py +10 -4
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +6 -2
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +7 -7
- package/templates/qoder/scripts/deployment/setup/setup.py +5 -2
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +4 -2
- package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +41 -5
- package/templates/qoder/scripts/domain/kg/kg.py +14 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +169 -14
- package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +106 -0
- package/templates/qoder/scripts/tool_guide.md +70 -0
- package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
- package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
- package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
- package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
- package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
- package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
- package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
- package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
- package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
- package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
- package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
- package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
- package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
- package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
- package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
- package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
- package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
- package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
- package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
- package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
- package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
- package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
- package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
- package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
- package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
- package/templates/qoder/settings.json +8 -0
- package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
- package/templates/qoder/agents/design-agent.md +0 -20
- package/templates/qoder/agents/spec-generator.md +0 -21
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +0 -94
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +0 -10
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +0 -57
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +0 -62
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +0 -146
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +0 -64
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +0 -52
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +0 -104
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +0 -55
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +0 -95
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +0 -8
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +0 -22
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +0 -55
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +0 -90
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +0 -9
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +0 -206
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +0 -132
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +0 -80
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +0 -59
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +0 -93
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +0 -421
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +0 -9
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +0 -79
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +0 -99
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +0 -69
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +0 -78
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +0 -8
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +0 -105
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +0 -153
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +0 -120
- package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
- package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +0 -109
- package/templates/qoder/scripts/engine/poller.py +0 -219
|
@@ -834,6 +834,7 @@ def check_lanhu():
|
|
|
834
834
|
def main():
|
|
835
835
|
args = [a for a in sys.argv[1:]]
|
|
836
836
|
fix = '--fix' in args
|
|
837
|
+
skip_code = '--skip-code' in args or '--cloud' in args
|
|
837
838
|
pos = [a for a in args if not a.startswith('--')]
|
|
838
839
|
name = pos[0] if pos else None
|
|
839
840
|
role = pos[1] if len(pos) > 1 else None
|
|
@@ -848,8 +849,11 @@ def main():
|
|
|
848
849
|
check_dependencies(fix)
|
|
849
850
|
check_developer(fix, name, role)
|
|
850
851
|
check_team_sync(fix)
|
|
851
|
-
|
|
852
|
-
|
|
852
|
+
if skip_code:
|
|
853
|
+
print('\n--- 4. 源码/索引: 云模式跳过(KG 在云平台,引擎调云 MCP)---')
|
|
854
|
+
else:
|
|
855
|
+
check_source_repos(fix, config)
|
|
856
|
+
check_index(fix)
|
|
853
857
|
check_style(fix)
|
|
854
858
|
check_prd_templates()
|
|
855
859
|
check_weekly(fix)
|
|
@@ -438,20 +438,20 @@ def rewrite_mcp_json_for_launcher():
|
|
|
438
438
|
}
|
|
439
439
|
changed = False
|
|
440
440
|
for srv, sub in name_map.items():
|
|
441
|
-
# 共识:kg
|
|
441
|
+
# 共识:kg 走云平台(默认 10.89.7.120:10010/mcp,config platform_mcp.url 可覆盖)
|
|
442
442
|
if srv == "qoder-knowledge-graph":
|
|
443
|
+
_pm = "http://10.89.7.120:10010/mcp"
|
|
443
444
|
try:
|
|
444
445
|
import yaml as _y
|
|
445
446
|
_cfg_p = get_repo_root() / ".qoder" / "config.yaml"
|
|
446
447
|
if _cfg_p.is_file():
|
|
447
448
|
_cfg = _y.safe_load(_cfg_p.read_text(encoding="utf-8")) or {}
|
|
448
|
-
_pm = (_cfg.get("platform_mcp") or {}).get("url", "")
|
|
449
|
-
if _pm:
|
|
450
|
-
servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
|
|
451
|
-
changed = True
|
|
452
|
-
continue
|
|
449
|
+
_pm = (_cfg.get("platform_mcp") or {}).get("url", "") or _pm
|
|
453
450
|
except Exception:
|
|
454
|
-
pass
|
|
451
|
+
pass
|
|
452
|
+
servers[srv] = {"url": _pm, "type": "sse", "enabled": True}
|
|
453
|
+
changed = True
|
|
454
|
+
continue
|
|
455
455
|
entry = servers.get(srv)
|
|
456
456
|
# 全新机器: kg/mysql/lanhu 条目可能不存在 → 创建它 (launcher 模式)
|
|
457
457
|
if not entry:
|
|
@@ -242,9 +242,11 @@ def detect_name(explicit=None):
|
|
|
242
242
|
# Step 2: 调 init_doctor --fix (主体)
|
|
243
243
|
# ============================================================
|
|
244
244
|
|
|
245
|
-
def run_doctor(name, role):
|
|
245
|
+
def run_doctor(name, role, skip_code=False):
|
|
246
246
|
print('\n--- 初始化 (init_doctor) ---')
|
|
247
247
|
cmd = [sys.executable, str(THIS_DIR / 'init_doctor.py'), '--fix', name, role]
|
|
248
|
+
if skip_code:
|
|
249
|
+
cmd.append('--skip-code')
|
|
248
250
|
# 透传 stdio (用户能看到实时输出) + 强制 UTF-8 环境 (解决 cmd GBK 乱码)
|
|
249
251
|
env = dict(os.environ)
|
|
250
252
|
env['PYTHONIOENCODING'] = 'utf-8'
|
|
@@ -911,6 +913,7 @@ def main():
|
|
|
911
913
|
parser.add_argument('--skip-cron', action='store_true', help='跳过 cron 注册')
|
|
912
914
|
parser.add_argument('--skip-qoderwork', action='store_true', help='跳过 QoderWork 安装')
|
|
913
915
|
parser.add_argument('--skip-lanhu', action='store_true', help='跳过蓝湖 MCP 引导')
|
|
916
|
+
parser.add_argument('--skip-code', '--cloud', action='store_true', help='云模式:跳过本地源码 clone/索引(KG 在云平台)')
|
|
914
917
|
args = parser.parse_args()
|
|
915
918
|
|
|
916
919
|
print('=' * 56)
|
|
@@ -931,7 +934,7 @@ def main():
|
|
|
931
934
|
configure_git_identity(name)
|
|
932
935
|
|
|
933
936
|
# Step 2
|
|
934
|
-
if not run_doctor(name, args.role):
|
|
937
|
+
if not run_doctor(name, args.role, args.skip_code):
|
|
935
938
|
warn('init_doctor 未完全成功, 继续 setup 后续步骤...')
|
|
936
939
|
|
|
937
940
|
# Step 3
|
|
@@ -20,6 +20,7 @@ integration.return_to_platform —— 引擎产出回流平台的统一通道。
|
|
|
20
20
|
"""
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import os
|
|
23
24
|
from typing import Optional
|
|
24
25
|
|
|
25
26
|
|
|
@@ -34,6 +35,7 @@ def _load_mcp_config() -> dict:
|
|
|
34
35
|
except Exception:
|
|
35
36
|
repo = Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
36
37
|
for cand in [
|
|
38
|
+
repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先查(否则读不到 token → 回流误报"无 X-Engine-Token")
|
|
37
39
|
repo / "mcp_config.json",
|
|
38
40
|
]:
|
|
39
41
|
try:
|
|
@@ -121,7 +123,7 @@ def _via_http(payload: dict) -> tuple[bool, str]:
|
|
|
121
123
|
|
|
122
124
|
|
|
123
125
|
def _platform_base() -> str:
|
|
124
|
-
"""平台 base url
|
|
126
|
+
"""平台 base url(默认 http://10.89.7.120 云),从 mcp_config server url 推导。"""
|
|
125
127
|
from urllib.parse import urlparse
|
|
126
128
|
cfg = _load_mcp_config()
|
|
127
129
|
for srv in (cfg.get("mcpServers") or {}).values():
|
|
@@ -133,7 +135,7 @@ def _platform_base() -> str:
|
|
|
133
135
|
if ep:
|
|
134
136
|
p = urlparse(ep)
|
|
135
137
|
return "%s://%s" % (p.scheme, p.netloc)
|
|
136
|
-
return "http://
|
|
138
|
+
return os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
|
|
137
139
|
|
|
138
140
|
|
|
139
141
|
def _engine_token() -> Optional[str]:
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
契约: wlinkj-workspace/docs/SPEC-MCP-CONTRACT.md v1(create/confirm/update/get_spec)
|
|
6
6
|
|
|
7
7
|
流程: 解析契约头 → 有platform_spec_id走update/无走create → confirm → 回写幂等锚
|
|
8
|
-
传输: POST /mcp direct JSON-RPC + X-MCP-Token
|
|
8
|
+
传输: POST /mcp direct JSON-RPC + X-MCP-Token 头
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
import json, os, re, sys, urllib.request, urllib.error
|
|
@@ -24,8 +24,8 @@ def _load_mcp_config() -> dict:
|
|
|
24
24
|
repo = get_repo_root()
|
|
25
25
|
except Exception:
|
|
26
26
|
repo = Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
27
|
-
for cand in [repo / "mcp_config.json",
|
|
28
|
-
repo / "
|
|
27
|
+
for cand in [repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先(同 return_to_platform,否则可能读到 repo/ 旧文件)
|
|
28
|
+
repo / "mcp_config.json"]:
|
|
29
29
|
try:
|
|
30
30
|
if cand.is_file():
|
|
31
31
|
with open(cand, encoding="utf-8") as f:
|
|
@@ -48,7 +48,7 @@ def _resolve_endpoint() -> Tuple[str, str, str]:
|
|
|
48
48
|
u = srv.get("url", "")
|
|
49
49
|
if "/mcp" in u:
|
|
50
50
|
base = u.split("/mcp")[0]; break
|
|
51
|
-
base = (base or "http://
|
|
51
|
+
base = (base or os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")).rstrip("/")
|
|
52
52
|
token = os.environ.get("WLKJ_MCP_TOKEN", "")
|
|
53
53
|
if not token:
|
|
54
54
|
for srv in (cfg.get("mcpServers") or {}).values():
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
|
|
3
3
|
|
|
4
|
-
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
5
|
-
import os as _o, sys as _s
|
|
6
|
-
_f = _o.path.abspath(__file__)
|
|
7
|
-
for _ in range(10):
|
|
8
|
-
_f = _o.path.dirname(_f)
|
|
9
|
-
_cp = _o.path.join(_f, 'foundation')
|
|
10
|
-
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
11
|
-
break
|
|
12
|
-
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
13
|
-
from bootstrap import setup; setup()
|
|
14
|
-
|
|
15
4
|
产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
|
|
16
5
|
让 kg.py feature <功能名> 能返回功能画像。
|
|
17
6
|
|
|
@@ -34,6 +23,18 @@ from bootstrap import setup; setup()
|
|
|
34
23
|
业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
|
|
35
24
|
那些脚本未实现时不影响本脚本的 entity 基础层。
|
|
36
25
|
"""
|
|
26
|
+
# v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
|
|
27
|
+
# ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
|
|
28
|
+
import os as _o, sys as _s
|
|
29
|
+
_f = _o.path.abspath(__file__)
|
|
30
|
+
for _ in range(10):
|
|
31
|
+
_f = _o.path.dirname(_f)
|
|
32
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
33
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
34
|
+
break
|
|
35
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
36
|
+
from bootstrap import setup; setup()
|
|
37
|
+
|
|
37
38
|
import json
|
|
38
39
|
import os
|
|
39
40
|
import sys
|
|
@@ -41,7 +42,6 @@ from datetime import datetime
|
|
|
41
42
|
|
|
42
43
|
# 路径自适应 (和兄弟脚本一致)
|
|
43
44
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
44
|
-
sys.path.insert(0, os.path.join(THIS_DIR, '..'))
|
|
45
45
|
from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
|
|
46
46
|
|
|
47
47
|
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
|
|
3
3
|
|
|
4
|
-
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
5
|
-
import os as _o, sys as _s
|
|
6
|
-
_f = _o.path.abspath(__file__)
|
|
7
|
-
for _ in range(10):
|
|
8
|
-
_f = _o.path.dirname(_f)
|
|
9
|
-
_cp = _o.path.join(_f, 'foundation')
|
|
10
|
-
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
11
|
-
break
|
|
12
|
-
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
13
|
-
from bootstrap import setup; setup()
|
|
14
|
-
|
|
15
4
|
产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
|
|
16
5
|
让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
|
|
17
6
|
|
|
@@ -32,13 +21,24 @@ from bootstrap import setup; setup()
|
|
|
32
21
|
- code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
|
|
33
22
|
- test-assertions.json: tests (controller_to_tests 推导)
|
|
34
23
|
"""
|
|
24
|
+
# v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
|
|
25
|
+
# ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
|
|
26
|
+
import os as _o, sys as _s
|
|
27
|
+
_f = _o.path.abspath(__file__)
|
|
28
|
+
for _ in range(10):
|
|
29
|
+
_f = _o.path.dirname(_f)
|
|
30
|
+
_cp = _o.path.join(_f, 'foundation')
|
|
31
|
+
if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
|
|
32
|
+
break
|
|
33
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
34
|
+
from bootstrap import setup; setup()
|
|
35
|
+
|
|
35
36
|
import json
|
|
36
37
|
import os
|
|
37
38
|
import sys
|
|
38
39
|
from datetime import datetime
|
|
39
40
|
|
|
40
41
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
41
|
-
sys.path.insert(0, os.path.join(THIS_DIR, '..'))
|
|
42
42
|
from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
|
|
43
43
|
|
|
44
44
|
|
|
@@ -278,6 +278,31 @@ def build_embeddings(batch_size=64):
|
|
|
278
278
|
con.close()
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
def _cloud_rag_search(query, top_k=10):
|
|
282
|
+
"""云优先语义搜索:调工作台 MCP rag_search(本地无 KG/embedding,全迁云)。
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
[(entity_id, text, score), ...] 或 None(云不可用时返回 None,让调用方退本地)。
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
from domain.kg.search._remote import call_remote
|
|
289
|
+
data = call_remote("rag_search", {"query": query, "top_k": top_k})
|
|
290
|
+
except Exception:
|
|
291
|
+
return None
|
|
292
|
+
items = data.get("items") or []
|
|
293
|
+
out = []
|
|
294
|
+
for it in items[:top_k]:
|
|
295
|
+
eid = it.get("entity_id") or it.get("id") or ""
|
|
296
|
+
text = it.get("text") or it.get("name") or ""
|
|
297
|
+
score = it.get("score")
|
|
298
|
+
try:
|
|
299
|
+
score = float(score) if score is not None else 0.0
|
|
300
|
+
except Exception:
|
|
301
|
+
score = 0.0
|
|
302
|
+
out.append((eid, text, score))
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
281
306
|
def semantic_search(query, top_k=10):
|
|
282
307
|
"""语义搜索: query→向量→余弦相似度→top_k 实体。
|
|
283
308
|
|
|
@@ -656,13 +681,24 @@ if __name__ == '__main__':
|
|
|
656
681
|
n = build_embeddings()
|
|
657
682
|
print('Built %d embeddings' % n)
|
|
658
683
|
|
|
659
|
-
elif len(sys.argv) > 2 and sys.argv[1] == 'search'
|
|
660
|
-
|
|
684
|
+
elif (len(sys.argv) > 2 and sys.argv[1] == 'search') or \
|
|
685
|
+
(len(sys.argv) > 1 and sys.argv[1] not in ('--check', '--label', 'build-embeddings', '--status', '--backfill-cn')):
|
|
686
|
+
# 兼容两种姿势: `search <词>` 和裸词 `<词>`(wlkj.py semantic 保险异常 透传过来)。
|
|
687
|
+
# ★ 云优先:本地无 KG/embedding(全迁云),先调云 rag_search;云不可用才退本地 semantic_search。
|
|
688
|
+
query = sys.argv[2] if sys.argv[1] == 'search' else sys.argv[1]
|
|
689
|
+
results = _cloud_rag_search(query)
|
|
690
|
+
if results is None:
|
|
691
|
+
# 云不可用 → 退本地(开发期有本地 embedding 时仍可用)
|
|
692
|
+
results = semantic_search(query)
|
|
661
693
|
if results:
|
|
662
|
-
for
|
|
663
|
-
|
|
694
|
+
for row in results:
|
|
695
|
+
if len(row) == 3:
|
|
696
|
+
eid, text, score = row
|
|
697
|
+
print(' %.3f %s' % (score, text[:70]))
|
|
698
|
+
else:
|
|
699
|
+
print(' %s' % row)
|
|
664
700
|
else:
|
|
665
|
-
print('(
|
|
701
|
+
print('(无结果或云 rag_search 不可用 + 本地 embedding 未构建)')
|
|
666
702
|
|
|
667
703
|
elif len(sys.argv) > 1 and sys.argv[1] == '--status':
|
|
668
704
|
st = embedding_status()
|
|
@@ -872,6 +872,20 @@ def main(argv=None):
|
|
|
872
872
|
return 0
|
|
873
873
|
cmd = argv[0]
|
|
874
874
|
rest = argv[1:]
|
|
875
|
+
# ★ MCP 名兼容:AI 常把 MCP 工具名(search_code)当 kg.py 子命令调 → "未知子命令"。
|
|
876
|
+
# 在此统一归一化,AI 猜哪个名(kg.py 子命令 / MCP 工具名)都能命中(治工作流混名)。
|
|
877
|
+
_MCP_ALIASES = {
|
|
878
|
+
'search_code': 'search', 'search_api': 'api', 'search_prds': 'prd',
|
|
879
|
+
'search_field': 'search', 'search_style': 'search', 'search_wiki': 'wiki',
|
|
880
|
+
'rag_search': 'semantic', 'ask_corpus': 'semantic',
|
|
881
|
+
'context_pack': 'context', 'context_360': 'context360',
|
|
882
|
+
'get_impact': 'impact', 'multi_hop': 'hop',
|
|
883
|
+
'coverage_matrix': 'coverage', 'feature_overview': 'feature',
|
|
884
|
+
'get_workflow': 'workflow', 'get_design_system': 'design-system',
|
|
885
|
+
'fill_prototype': 'fill-prototype',
|
|
886
|
+
}
|
|
887
|
+
if cmd in _MCP_ALIASES:
|
|
888
|
+
cmd = _MCP_ALIASES[cmd]
|
|
875
889
|
# 查找子命令
|
|
876
890
|
handler = None
|
|
877
891
|
for name, fn, _ in COMMANDS:
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
python switch_project.py --email a@b.c --pw xxx # 非交互:直接指定
|
|
7
7
|
python switch_project.py --project-uuid <UUID> # 非交互:直接指定项目
|
|
8
8
|
|
|
9
|
-
需工作台后端在跑(默认 http://
|
|
9
|
+
需工作台后端在跑(默认 http://10.89.7.120 云)。
|
|
10
10
|
"""
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
@@ -33,7 +33,7 @@ def _find_config_path():
|
|
|
33
33
|
CONFIG_PATH = _find_config_path()
|
|
34
34
|
|
|
35
35
|
# 平台地址(默认 10010,可 env 覆盖)
|
|
36
|
-
BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://
|
|
36
|
+
BASE_URL = os.environ.get("WLKJ_BASE_URL", "http://10.89.7.120")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def bind(email, password, base_url=BASE_URL):
|
|
@@ -51,9 +51,118 @@ from foundation.integrations.zentao_client import ZentaoClient
|
|
|
51
51
|
# ============================================================
|
|
52
52
|
|
|
53
53
|
def _zentao_creds():
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
"""构建禅道客户端。返回 (client, preset_account)。
|
|
55
|
+
|
|
56
|
+
凭据源优先级:
|
|
57
|
+
① 云平台 per-user 凭据(Option A:get_zentao_creds)—— account 已知(preset)
|
|
58
|
+
② 本地 .qoder/config.yaml + secrets/zentao.env —— account 走 member.json
|
|
59
|
+
云凭据优先:demo 机器在内网可达禅道,平台凭据比本地文件更可能是最新配的。
|
|
60
|
+
凭据全无 → (None, '')。
|
|
61
|
+
"""
|
|
62
|
+
c = _cloud_zentao_creds()
|
|
63
|
+
if c and c.get('ok') and c.get('url') and c.get('account') and c.get('password'):
|
|
64
|
+
try:
|
|
65
|
+
return ZentaoClient(c['url'], c['account'], c['password']), c['account']
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return ZentaoClient.from_config(), ''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cloud_my_panel():
|
|
72
|
+
"""云优先:调平台 get_my_zentao_panel(凭据走平台 per-user 配置,不出平台)。
|
|
73
|
+
|
|
74
|
+
直连云 /mcp JSON-RPC(读 .qoder/mcp_config.json 的 token+url)。
|
|
75
|
+
Returns: {ok:True, tasks, bugs, stories, account} 或 None(云不可达/工具失败)。
|
|
76
|
+
云返 {ok:False,error}(未配置/登录失败)→ 返该 dict(调用方据此提示+回退本地)。
|
|
77
|
+
"""
|
|
78
|
+
import json as _json
|
|
79
|
+
import urllib.request as _u
|
|
80
|
+
import urllib.error as _ue
|
|
81
|
+
# 定位 mcp_config.json(switch_project 写在 .qoder/)
|
|
82
|
+
cfg_candidates = []
|
|
83
|
+
try:
|
|
84
|
+
from foundation.core.paths import get_repo_root
|
|
85
|
+
cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
|
|
89
|
+
token, url = '', ''
|
|
90
|
+
for cp in cfg_candidates:
|
|
91
|
+
try:
|
|
92
|
+
with open(cp, encoding='utf-8') as f:
|
|
93
|
+
cfg = _json.load(f)
|
|
94
|
+
srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
|
|
95
|
+
token = srv.get('token', '')
|
|
96
|
+
url = (srv.get('url') or '').rstrip('/')
|
|
97
|
+
if token and url:
|
|
98
|
+
break
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
if not token or not url:
|
|
102
|
+
return None # 云未绑定(switch_project 没跑)→ 回退本地
|
|
103
|
+
endpoint = url if url.endswith('/mcp') else url + '/mcp'
|
|
104
|
+
body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
105
|
+
"params": {"name": "get_my_zentao_panel", "arguments": {}}}).encode('utf-8')
|
|
106
|
+
req = _u.Request(endpoint, data=body, method='POST')
|
|
107
|
+
req.add_header('Content-Type', 'application/json')
|
|
108
|
+
req.add_header('X-MCP-Token', token)
|
|
109
|
+
try:
|
|
110
|
+
with _u.urlopen(req, timeout=30) as resp:
|
|
111
|
+
r = _json.loads(resp.read().decode('utf-8', errors='replace'))
|
|
112
|
+
except Exception:
|
|
113
|
+
return None # 网络不通 → 回退本地
|
|
114
|
+
try:
|
|
115
|
+
text = r.get('result', {}).get('content', [{}])[0].get('text', '')
|
|
116
|
+
data = _json.loads(text) if text else {}
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
return data # {ok:True/False, ...}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cloud_zentao_creds():
|
|
123
|
+
"""Option A:从平台取禅道凭据(url/account/password 解密)。
|
|
124
|
+
|
|
125
|
+
用途:云到禅道被防火墙挡(my-panel 登录失败)时,本机(demo 机器,内网可达)
|
|
126
|
+
用平台凭据直连禅道拉真实数据。凭据随绑定 token 取本人配置(同信任级)。
|
|
127
|
+
Returns: {ok:True, url, account, password} 或 None(云不可达/未配置)。
|
|
128
|
+
"""
|
|
129
|
+
import json as _json
|
|
130
|
+
import urllib.request as _u
|
|
131
|
+
cfg_candidates = []
|
|
132
|
+
try:
|
|
133
|
+
from foundation.core.paths import get_repo_root
|
|
134
|
+
cfg_candidates.append(os.path.join(str(get_repo_root()), '.qoder', 'mcp_config.json'))
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
cfg_candidates.append(os.path.join(_SCRIPTS, '..', 'mcp_config.json'))
|
|
138
|
+
token, url = '', ''
|
|
139
|
+
for cp in cfg_candidates:
|
|
140
|
+
try:
|
|
141
|
+
with open(cp, encoding='utf-8') as f:
|
|
142
|
+
cfg = _json.load(f)
|
|
143
|
+
srv = (cfg.get('mcpServers') or {}).get('wlinkj-knowledge') or {}
|
|
144
|
+
token = srv.get('token', '')
|
|
145
|
+
url = (srv.get('url') or '').rstrip('/')
|
|
146
|
+
if token and url:
|
|
147
|
+
break
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
if not token or not url:
|
|
151
|
+
return None
|
|
152
|
+
endpoint = url if url.endswith('/mcp') else url + '/mcp'
|
|
153
|
+
body = _json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
154
|
+
"params": {"name": "get_zentao_creds", "arguments": {}}}).encode('utf-8')
|
|
155
|
+
req = _u.Request(endpoint, data=body, method='POST')
|
|
156
|
+
req.add_header('Content-Type', 'application/json')
|
|
157
|
+
req.add_header('X-MCP-Token', token)
|
|
158
|
+
try:
|
|
159
|
+
with _u.urlopen(req, timeout=15) as resp:
|
|
160
|
+
r = _json.loads(resp.read().decode('utf-8', errors='replace'))
|
|
161
|
+
text = r.get('result', {}).get('content', [{}])[0].get('text', '')
|
|
162
|
+
data = _json.loads(text) if text else {}
|
|
163
|
+
return data if data.get('ok') else None
|
|
164
|
+
except Exception:
|
|
165
|
+
return None
|
|
57
166
|
|
|
58
167
|
|
|
59
168
|
def _resolve_my_account(developer):
|
|
@@ -359,26 +468,72 @@ def fetch_workbench(include_closed=False, kind=None):
|
|
|
359
468
|
"""
|
|
360
469
|
developer = get_developer()
|
|
361
470
|
|
|
362
|
-
#
|
|
363
|
-
|
|
471
|
+
# ⓪ 云优先:调平台 get_my_zentao_panel(凭据走平台 per-user,不出平台)。
|
|
472
|
+
# 成功 → 直接用云数据,跳过本地凭据/连通/归属/收集(① ~ ④)。
|
|
473
|
+
# 未配置/登录失败 → 返回 {ok:False} 提示用户去平台配置,并回退本地(本地配了仍可用)。
|
|
474
|
+
# 云不可达(返回 None) → 静默回退本地全流程。
|
|
475
|
+
cloud = _cloud_my_panel()
|
|
476
|
+
if isinstance(cloud, dict):
|
|
477
|
+
# 云可达 → 云的判断权威(平台是凭据源)。
|
|
478
|
+
if cloud.get('ok'):
|
|
479
|
+
tasks = cloud.get('tasks') or []
|
|
480
|
+
bugs = cloud.get('bugs') or []
|
|
481
|
+
stories = cloud.get('stories') or []
|
|
482
|
+
# 按 kind 裁剪(与本地一致)
|
|
483
|
+
if kind == 'tasks':
|
|
484
|
+
bugs, stories = [], []
|
|
485
|
+
elif kind == 'bugs':
|
|
486
|
+
tasks, stories = [], []
|
|
487
|
+
elif kind == 'stories':
|
|
488
|
+
tasks, bugs = [], []
|
|
489
|
+
tasks, bugs, stories = _filter_live(tasks, bugs, stories, include_closed)
|
|
490
|
+
# 云路径:数据已平台聚合,归一化即可(进度检测需本地 client,云模式跳过给默认值)
|
|
491
|
+
norm_tasks = [_norm_task(t) for t in tasks]
|
|
492
|
+
norm_bugs = [_norm_bug(b) for b in bugs]
|
|
493
|
+
norm_stories = [_norm_story(s) for s in stories]
|
|
494
|
+
for it in norm_tasks + norm_bugs + norm_stories:
|
|
495
|
+
it['progress'] = {'prd': False, 'spec': 'none', 'code': False}
|
|
496
|
+
data = {'account': cloud.get('account', ''), 'developer': developer,
|
|
497
|
+
'current_task': None, 'tasks': norm_tasks,
|
|
498
|
+
'bugs': norm_bugs, 'stories': norm_stories}
|
|
499
|
+
meta = {'fetched_at': datetime.now().isoformat(timespec='seconds'),
|
|
500
|
+
'source': 'cloud',
|
|
501
|
+
'counts': {'tasks': len(norm_tasks), 'bugs': len(norm_bugs),
|
|
502
|
+
'stories': len(norm_stories)},
|
|
503
|
+
'include_closed': include_closed}
|
|
504
|
+
return {'ok': True, 'data': data, 'meta': meta}, 0
|
|
505
|
+
# 云可达但失败(未配置/登录失败/防火墙)→ 记下云诊断,回退本地。
|
|
506
|
+
# 回退价值:云到禅道常被防火墙挡,但本机(demo 机器,内网)可能直连可达。
|
|
507
|
+
# 本地也失败时,把云诊断一起带出(平台有凭据但云连不上 + 本地无凭据)。
|
|
508
|
+
cloud_error = cloud.get('error', '禅道不可用')
|
|
509
|
+
else:
|
|
510
|
+
cloud_error = None # 云不可达(switch_project 没跑/平台宕机)→ 静默回退本地
|
|
511
|
+
|
|
512
|
+
# ① 凭据(云平台优先 Option A,回退本地文件)
|
|
513
|
+
client, preset_account = _zentao_creds()
|
|
364
514
|
if client is None:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
515
|
+
base_msg = ('禅道凭据未配置。推荐在【平台设置 → 禅道】配置(panel 自动走云取凭据);'
|
|
516
|
+
'或本地 .qoder/config.yaml 的 zentao.url + secrets/zentao.env 账号密码。')
|
|
517
|
+
if cloud_error:
|
|
518
|
+
base_msg += '(云 my-panel 失败:%s)' % cloud_error
|
|
519
|
+
return {'ok': False, 'error': base_msg}, 3
|
|
368
520
|
|
|
369
521
|
ok, msg = client.credentials_ok()
|
|
370
522
|
if not ok:
|
|
371
523
|
return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
|
|
372
524
|
|
|
373
|
-
# ②
|
|
525
|
+
# ② 内网连通(本机能否直连禅道——demo 机器在内网可达)
|
|
374
526
|
ok, msg = client.check_intranet()
|
|
375
527
|
if not ok:
|
|
376
|
-
return {'ok': False, 'error': '
|
|
528
|
+
return {'ok': False, 'error': '本机连不上禅道内网: %s' % msg}, 5
|
|
377
529
|
|
|
378
|
-
# ③
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
530
|
+
# ③ 归属(云凭据带的 account 优先;否则走 member.json)
|
|
531
|
+
if preset_account:
|
|
532
|
+
my_account = preset_account
|
|
533
|
+
else:
|
|
534
|
+
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
|
|
535
|
+
if not my_account:
|
|
536
|
+
return {'ok': False, 'error': err}, 3
|
|
382
537
|
|
|
383
538
|
# ④ 收集 (三域并行, 按 kind 决定取哪几域)
|
|
384
539
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
|
|
2
|
+
|
|
3
|
+
PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
|
|
4
|
+
|
|
5
|
+
存储: workspace/specs/{REQ}/.context-cache.json
|
|
6
|
+
结构: {query_hash: {"query":..., "text":..., "ts":...}}
|
|
7
|
+
TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
|
|
8
|
+
|
|
9
|
+
纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
DEFAULT_TTL = 24 * 3600 # 24h
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _repo_root():
|
|
20
|
+
"""从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
|
|
21
|
+
d = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
for _ in range(7):
|
|
23
|
+
if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
|
|
24
|
+
return d
|
|
25
|
+
parent = os.path.dirname(d)
|
|
26
|
+
if parent == d:
|
|
27
|
+
break
|
|
28
|
+
d = parent
|
|
29
|
+
return os.getcwd()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sanitize(req_id):
|
|
33
|
+
return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cache_path(req_id, base=None):
|
|
37
|
+
base = base or _repo_root()
|
|
38
|
+
return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load(path):
|
|
42
|
+
try:
|
|
43
|
+
with open(path, encoding="utf-8") as f:
|
|
44
|
+
data = json.load(f)
|
|
45
|
+
return data if isinstance(data, dict) else {}
|
|
46
|
+
except Exception:
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save(path, data):
|
|
51
|
+
try:
|
|
52
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
53
|
+
tmp = path + ".tmp"
|
|
54
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
56
|
+
os.replace(tmp, path) # 原子写
|
|
57
|
+
return True
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _qhash(query):
|
|
63
|
+
return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get(req_id, query, ttl=DEFAULT_TTL, base=None):
|
|
67
|
+
"""命中且未过期 → 返回 text;否则 None。"""
|
|
68
|
+
data = _load(cache_path(req_id, base))
|
|
69
|
+
entry = data.get(_qhash(query))
|
|
70
|
+
if not entry:
|
|
71
|
+
return None
|
|
72
|
+
if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
|
|
73
|
+
return None
|
|
74
|
+
return entry.get("text")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def put(req_id, query, text, base=None):
|
|
78
|
+
"""写一条缓存(覆盖同 query 旧值)。"""
|
|
79
|
+
path = cache_path(req_id, base)
|
|
80
|
+
data = _load(path)
|
|
81
|
+
data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
|
|
82
|
+
return _save(path, data)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def invalidate(req_id, base=None):
|
|
86
|
+
"""清整个 REQ 的缓存(PRD/spec 改动后调)。"""
|
|
87
|
+
return _save(cache_path(req_id, base), {})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def stats(req_id, base=None):
|
|
91
|
+
"""缓存条数 + query 摘要(调试/观测命中率用)。"""
|
|
92
|
+
data = _load(cache_path(req_id, base))
|
|
93
|
+
return {"entries": len(data),
|
|
94
|
+
"queries": [str(e.get("query", ""))[:40] for e in data.values()]}
|