@hupan56/wlkj 2.7.12 → 3.1.2
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 +1435 -1107
- package/package.json +29 -29
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +161 -161
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +6 -6
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +43 -17
- package/templates/qoder/config.yaml +52 -14
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +94 -23
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +85 -49
- package/templates/qoder/scripts/README.md +102 -0
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
- package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
- package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
- package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
- package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
- package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
- package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
- package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
- package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
- package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
- package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
- package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
- package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
- package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
- package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
- package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
- package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
- package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
- package/templates/qoder/scripts/domain/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
- package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
- package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/domain/report/role.py +39 -0
- package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
- package/templates/qoder/scripts/domain/requirement/req.py +228 -0
- package/templates/qoder/scripts/domain/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
- package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
- package/templates/qoder/scripts/domain/task/task.py +229 -0
- package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
- package/templates/qoder/scripts/domain/task/task_query.py +162 -0
- package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
- package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
- package/templates/qoder/scripts/foundation/core/config.py +187 -0
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/data/contract.py +317 -0
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/foundation/data/result.py +223 -0
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
- package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
- package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
- package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
- package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
- package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
- package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
- package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
- package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
- package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +23 -2
- package/templates/qoder/skills/design-import/SKILL.md +235 -226
- package/templates/qoder/skills/design-review/SKILL.md +91 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
- package/templates/qoder/skills/prd-review/SKILL.md +8 -6
- package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
- package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
- package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
- package/templates/qoder/skills/test-generator/SKILL.md +9 -7
- package/templates/qoder/skills/wl-code/SKILL.md +25 -13
- package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
- package/templates/qoder/skills/wl-design/SKILL.md +7 -5
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
- package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
- package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
- package/templates/qoder/skills/wl-report/SKILL.md +27 -25
- package/templates/qoder/skills/wl-search/SKILL.md +151 -80
- package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
- package/templates/qoder/skills/wl-status/SKILL.md +50 -18
- package/templates/qoder/skills/wl-task/SKILL.md +94 -12
- package/templates/qoder/skills/wl-test/SKILL.md +154 -45
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -237
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp.py +0 -298
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_api_params.py +0 -246
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/kg_mcp_server.py +0 -801
- package/templates/qoder/scripts/kg_semantic.py +0 -150
- package/templates/qoder/scripts/mcp_launcher.py +0 -414
- package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
zentao_client.py —— 禅道 HTTP 客户端(唯一真相源)。
|
|
4
|
+
|
|
5
|
+
消除重复: zentao_sync.py (CLI 批量同步) 和 zentao_mcp_server.py (MCP server)
|
|
6
|
+
原本各自实现了一份禅道 HTTP 层(内网探测/登录/token缓存/API/401重登),
|
|
7
|
+
现统一收敛到本类。
|
|
8
|
+
|
|
9
|
+
设计:
|
|
10
|
+
- ZentaoClient(url, user, password) —— 显式构造
|
|
11
|
+
- ZentaoClient.from_env() —— 从 ZENTAO_URL/USER/PASSWORD 环境变量 (mcp_launcher 注入)
|
|
12
|
+
- ZentaoClient.from_config() —— 从 .qoder/config.yaml 的 zentao 段 (CLI 用)
|
|
13
|
+
- 凭据不全时工厂返回 None(调用方自行降级/报错)
|
|
14
|
+
|
|
15
|
+
client.check_intranet() -> (ok, msg) 带缓存,避免连续 ping
|
|
16
|
+
client.api(method, path, data) -> Response 内含 401 自动重登
|
|
17
|
+
|
|
18
|
+
与 nexus 的关系:
|
|
19
|
+
禅道 MCP server (zentao_mcp_server.py) 是 nexus L1 层的 MCP 资产。
|
|
20
|
+
本 client 是其 HTTP 底座;未来 McpCap 实现后,工作流统一走
|
|
21
|
+
nexus.resolve().mcp.call("list_execution_tasks"),本 client 仍是底层实现。
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import socket
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ZentaoClient:
|
|
31
|
+
"""禅道 REST API 客户端(登录/token/401重登/内网探测)。
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
url: 禅道根地址(构造时已去尾斜杠)。
|
|
35
|
+
user/password: 账号密码。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, url: str, user: str, password: str, intranet_ttl: int = 60):
|
|
39
|
+
self.url = (url or '').rstrip('/')
|
|
40
|
+
self.user = (user or '').strip()
|
|
41
|
+
self.password = password or ''
|
|
42
|
+
self._token: Optional[str] = None
|
|
43
|
+
# 内网可达性缓存 [timestamp, ok, msg],list 原地写避免 global 声明
|
|
44
|
+
self._intranet_cache: list = []
|
|
45
|
+
self._intranet_ttl = intranet_ttl
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# 工厂
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_env(cls) -> Optional['ZentaoClient']:
|
|
53
|
+
"""从环境变量构造(mcp_launcher 注入)。凭据不全返回 None。"""
|
|
54
|
+
url = os.environ.get('ZENTAO_URL', '').strip()
|
|
55
|
+
user = os.environ.get('ZENTAO_USER', '').strip()
|
|
56
|
+
pwd = os.environ.get('ZENTAO_PASSWORD', '')
|
|
57
|
+
if not url or not user or not pwd:
|
|
58
|
+
return None
|
|
59
|
+
return cls(url, user, pwd)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_config(cls) -> Optional['ZentaoClient']:
|
|
63
|
+
"""从 .qoder/config.yaml 的 zentao 段构造(CLI 用)。凭据不全返回 None。"""
|
|
64
|
+
try:
|
|
65
|
+
from foundation.core.config import load_config_section, SECTIONS
|
|
66
|
+
cfg = load_config_section(SECTIONS.ZENTAO)
|
|
67
|
+
except Exception:
|
|
68
|
+
return None
|
|
69
|
+
url = (cfg.get('url') or '').strip()
|
|
70
|
+
user = (cfg.get('user') or '').strip()
|
|
71
|
+
pwd = (cfg.get('password') or '')
|
|
72
|
+
if not url or not user or not pwd:
|
|
73
|
+
return None
|
|
74
|
+
return cls(url, user, pwd)
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# 校验
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def credentials_ok(self) -> tuple[bool, str]:
|
|
81
|
+
"""凭据完整性校验。返回 (ok, msg)。"""
|
|
82
|
+
missing = []
|
|
83
|
+
if not self.url:
|
|
84
|
+
missing.append('url')
|
|
85
|
+
if not self.user:
|
|
86
|
+
missing.append('user')
|
|
87
|
+
if not self.password:
|
|
88
|
+
missing.append('password')
|
|
89
|
+
if missing:
|
|
90
|
+
return False, '禅道凭据未配置 (缺 %s)。请在 .qoder/config.yaml 的 zentao: 段填写。' % '/'.join(missing)
|
|
91
|
+
return True, ''
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# 内网探测
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def _ping_target(self) -> tuple[str, int]:
|
|
98
|
+
"""从 url 解析 (host, port)。"""
|
|
99
|
+
host = self.url.replace('http://', '').replace('https://', '').split('/')[0]
|
|
100
|
+
try:
|
|
101
|
+
port = 80
|
|
102
|
+
if self.url.count(':') >= 2:
|
|
103
|
+
port = int(self.url.split(':')[2].split('/')[0])
|
|
104
|
+
except (ValueError, IndexError):
|
|
105
|
+
port = 80
|
|
106
|
+
return host, port
|
|
107
|
+
|
|
108
|
+
def check_intranet(self) -> tuple[bool, str]:
|
|
109
|
+
"""内网连通性(带 TTL 缓存,失败不缓存)。
|
|
110
|
+
|
|
111
|
+
多工具连续调用时只 ping 一次。失败不缓存,便于网络恢复后立即重试。
|
|
112
|
+
"""
|
|
113
|
+
import time
|
|
114
|
+
now = time.time()
|
|
115
|
+
cached = self._intranet_cache
|
|
116
|
+
if cached and now - cached[0] < self._intranet_ttl and cached[1]:
|
|
117
|
+
return cached[1], cached[2]
|
|
118
|
+
host, port = self._ping_target()
|
|
119
|
+
try:
|
|
120
|
+
with socket.create_connection((host, port), timeout=3):
|
|
121
|
+
self._intranet_cache[:] = [now, True, '']
|
|
122
|
+
return True, ''
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return False, '无法连接禅道 (%s): %s。请确认已连内网。' % (self.url, str(e)[:60])
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# HTTP
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _ensure_requests():
|
|
132
|
+
"""延迟导入 requests(未装时报清晰错误,而非启动即崩)。"""
|
|
133
|
+
try:
|
|
134
|
+
import requests
|
|
135
|
+
return requests
|
|
136
|
+
except ImportError:
|
|
137
|
+
raise RuntimeError('缺少 requests 库。请运行: pip install requests')
|
|
138
|
+
|
|
139
|
+
def login(self) -> str:
|
|
140
|
+
"""登录拿 token(进程内缓存)。"""
|
|
141
|
+
if self._token:
|
|
142
|
+
return self._token
|
|
143
|
+
requests = self._ensure_requests()
|
|
144
|
+
r = requests.post(self.url + '/api.php/v1/tokens',
|
|
145
|
+
json={'account': self.user, 'password': self.password},
|
|
146
|
+
timeout=10)
|
|
147
|
+
if r.status_code in (200, 201):
|
|
148
|
+
self._token = r.json().get('token')
|
|
149
|
+
return self._token
|
|
150
|
+
raise RuntimeError('禅道登录失败: %s %s' % (r.status_code, r.text[:100]))
|
|
151
|
+
|
|
152
|
+
def api(self, method: str, path: str, data: dict | None = None):
|
|
153
|
+
"""调禅道 REST API。401 自动重登一次。
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
method: GET/POST/PUT/DELETE
|
|
157
|
+
path: /api.php/v1 之后的路径(如 /executions/12/tasks)
|
|
158
|
+
data: GET 时作 query params,POST/PUT 时作 json body
|
|
159
|
+
Returns:
|
|
160
|
+
requests.Response
|
|
161
|
+
"""
|
|
162
|
+
requests = self._ensure_requests()
|
|
163
|
+
token = self.login()
|
|
164
|
+
full = self.url + '/api.php/v1' + path
|
|
165
|
+
kwargs = dict(
|
|
166
|
+
headers={'Token': token},
|
|
167
|
+
json=data if method in ('POST', 'PUT') else None,
|
|
168
|
+
params=data if method == 'GET' else None,
|
|
169
|
+
timeout=15,
|
|
170
|
+
)
|
|
171
|
+
r = requests.request(method, full, **kwargs)
|
|
172
|
+
if r.status_code == 401:
|
|
173
|
+
self._token = None
|
|
174
|
+
token = self.login()
|
|
175
|
+
kwargs['headers'] = {'Token': token}
|
|
176
|
+
r = requests.request(method, full, **kwargs)
|
|
177
|
+
return r
|
|
178
|
+
|
|
179
|
+
def upload_file(self, file_path: str, title: str = '', extra_fields: dict | None = None):
|
|
180
|
+
"""上传文件到禅道 (multipart/form-data, POST /api.php/v1/files)。
|
|
181
|
+
|
|
182
|
+
禅道文件接口需 multipart 而非 JSON, api() 不支持, 故单独实现。
|
|
183
|
+
用于: 上传原型图 HTML / PRD 附件到需求。
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
file_path: 本地文件绝对路径
|
|
187
|
+
title: 文件标题 (可选, 默认取文件名)
|
|
188
|
+
extra_fields: 额外表单字段 (如 {'objectType':'story','objectID':812} 关联到对象)
|
|
189
|
+
Returns:
|
|
190
|
+
requests.Response
|
|
191
|
+
"""
|
|
192
|
+
import os
|
|
193
|
+
requests = self._ensure_requests()
|
|
194
|
+
token = self.login()
|
|
195
|
+
full = self.url + '/api.php/v1/files'
|
|
196
|
+
fname = os.path.basename(file_path)
|
|
197
|
+
title = title or fname
|
|
198
|
+
fields = {'title': title}
|
|
199
|
+
if extra_fields:
|
|
200
|
+
fields.update(extra_fields)
|
|
201
|
+
with open(file_path, 'rb') as f:
|
|
202
|
+
files = {'file': (fname, f, 'application/octet-stream')}
|
|
203
|
+
r = requests.post(full, headers={'Token': token},
|
|
204
|
+
data=fields, files=files, timeout=30)
|
|
205
|
+
if r.status_code == 401:
|
|
206
|
+
self._token = None
|
|
207
|
+
token = self.login()
|
|
208
|
+
with open(file_path, 'rb') as f:
|
|
209
|
+
files = {'file': (fname, f, 'application/octet-stream')}
|
|
210
|
+
r = requests.post(full, headers={'Token': token},
|
|
211
|
+
data=fields, files=files, timeout=30)
|
|
212
|
+
return r
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def err_text(r) -> str:
|
|
216
|
+
"""从 response 提取错误文本(容错)。"""
|
|
217
|
+
try:
|
|
218
|
+
return r.json().get('error', r.text[:120])
|
|
219
|
+
except Exception:
|
|
220
|
+
return r.text[:120]
|
|
File without changes
|
|
@@ -36,7 +36,7 @@ if _THIS_DIR not in sys.path:
|
|
|
36
36
|
try:
|
|
37
37
|
from .atomicio import atomic_write_json, safe_read_json
|
|
38
38
|
from .filelock import FileLock, LockTimeoutError
|
|
39
|
-
from .paths import get_repo_root
|
|
39
|
+
from foundation.core.paths import get_repo_root
|
|
40
40
|
except ImportError:
|
|
41
41
|
# 被当作脚本直接运行时
|
|
42
42
|
from atomicio import atomic_write_json, safe_read_json
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
duckdb_conn —— DuckDB 连接基础设施(L0 基础设施层)。
|
|
4
|
+
|
|
5
|
+
设计模式: Utility(纯连接管理,无领域语义)
|
|
6
|
+
|
|
7
|
+
职责:打开/关闭 kg.duckdb 连接。不知道表结构,不知道业务逻辑。
|
|
8
|
+
被 L2(nexus)和 L3(kg/learning)共用,避免 L2 反向依赖 L3。
|
|
9
|
+
|
|
10
|
+
init_schema 等领域 DDL 留在各域(kg_duckdb.py 定义图谱表,
|
|
11
|
+
nexus/adapters/mcp.py 定义自己的 learning_events 表)。
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from foundation.core.paths import DATA_INDEX_DIR
|
|
16
|
+
|
|
17
|
+
DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_db():
|
|
21
|
+
"""Open DuckDB read-write connection (creates file if not exists)."""
|
|
22
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
23
|
+
import duckdb
|
|
24
|
+
return duckdb.connect(DB_PATH)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_db_readonly():
|
|
28
|
+
"""Open read-only DuckDB connection. Returns None if file missing.
|
|
29
|
+
|
|
30
|
+
DuckDB 不允许对同一文件混用 read_only 和 read_write 连接,
|
|
31
|
+
所以只读路径必须全程用 read_only。
|
|
32
|
+
"""
|
|
33
|
+
import duckdb
|
|
34
|
+
if not os.path.isfile(DB_PATH):
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
return duckdb.connect(DB_PATH, read_only=True)
|
|
38
|
+
except Exception:
|
|
39
|
+
return None
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
mcp_base.py - MCP Server 基类 (Template Method 模式)
|
|
4
|
+
|
|
5
|
+
治"3个MCP server协议层逐行复制~255行"。三者(kg/mysql/zentao)的:
|
|
6
|
+
- _send (JSON-RPC写)
|
|
7
|
+
- main (readline循环 + 每请求新event_loop)
|
|
8
|
+
- _handle 五分支骨架 (initialize/notifications/initialized/tools·list/tools·call/ping/error)
|
|
9
|
+
- stdout重定向 (stdout→stderr, 避免污染stdio协议)
|
|
10
|
+
全部逐字一致, 仅差异点:
|
|
11
|
+
- serverInfo.name/version
|
|
12
|
+
- initialize 是否调 check_intranet 塞 warning
|
|
13
|
+
- tools/call 的异常分类 (mysql 多 ValueError→只读拒绝)
|
|
14
|
+
- error 分支文案
|
|
15
|
+
|
|
16
|
+
本基类把骨架固化为 final 方法, 子类填钩子:
|
|
17
|
+
- SERVER_NAME / SERVER_VERSION: 类属性
|
|
18
|
+
- TOOLS: 工具元数据列表
|
|
19
|
+
- handle_tool(name, args) -> str: 工具执行 (Strategy 钩子)
|
|
20
|
+
- on_initialize_hook(info) -> None: 塞 warning 等 (默认空)
|
|
21
|
+
- on_tool_error(e) -> (text, is_error): 自定义异常分类 (默认通用)
|
|
22
|
+
|
|
23
|
+
Usage (子类示例):
|
|
24
|
+
from foundation.protocol.mcp_base import BaseMCPServer
|
|
25
|
+
|
|
26
|
+
class ZentaoMCPServer(BaseMCPServer):
|
|
27
|
+
SERVER_NAME = "qoder-zentao"
|
|
28
|
+
SERVER_VERSION = "1.0.0"
|
|
29
|
+
TOOLS = [...] # 工具元数据
|
|
30
|
+
def handle_tool(self, name, args):
|
|
31
|
+
if name == 'list_stories': return self._list_stories(args)
|
|
32
|
+
return '未知工具: %s' % name
|
|
33
|
+
def on_initialize_hook(self, info):
|
|
34
|
+
ok, msg = check_intranet()
|
|
35
|
+
if not ok: info['warning'] = msg
|
|
36
|
+
|
|
37
|
+
if __name__ == '__main__':
|
|
38
|
+
ZentaoMCPServer().run()
|
|
39
|
+
|
|
40
|
+
收益: 加第4个MCP server从复制~255行降到写~30行(只填tools+handle_tool)。
|
|
41
|
+
|
|
42
|
+
v3.0+ 增强: ToolRegistry + @tool 装饰器 (Command Registry 模式)。
|
|
43
|
+
消除子类 _execute_tool 的长 if-elif 链 + TOOLS 元数据两处维护的漂移。
|
|
44
|
+
用法见底部示例。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import asyncio
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
52
|
+
|
|
53
|
+
__all__ = ["BaseMCPServer", "tool", "ToolRegistry"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ============================================================
|
|
57
|
+
# ToolRegistry: @tool 装饰器收集工具定义 (Command Registry 模式)
|
|
58
|
+
# ============================================================
|
|
59
|
+
|
|
60
|
+
def tool(name: str, description: str = "", input_schema: Optional[Dict] = None):
|
|
61
|
+
"""装饰器: 把一个方法注册为 MCP 工具。
|
|
62
|
+
|
|
63
|
+
替代旧式的 "TOOLS 列表 + _execute_tool if 链 两处维护"。
|
|
64
|
+
装饰后, BaseMCPServer.TOOLS 自动收集, handle_tool 自动分发。
|
|
65
|
+
|
|
66
|
+
Usage (子类内):
|
|
67
|
+
@tool("search_code", "搜代码", {"type":"object","properties":{...}})
|
|
68
|
+
def search_code(self, args):
|
|
69
|
+
return "结果文本"
|
|
70
|
+
"""
|
|
71
|
+
def decorator(method):
|
|
72
|
+
method._tool_meta = {
|
|
73
|
+
"name": name,
|
|
74
|
+
"description": description or method.__doc__ or "",
|
|
75
|
+
"inputSchema": input_schema or {"type": "object", "properties": {}},
|
|
76
|
+
}
|
|
77
|
+
return method
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _ToolRegistryMeta(type):
|
|
82
|
+
"""元类: 子类定义完成后, 扫描 @tool 装饰的方法, 自动填充 TOOLS + _tool_map。
|
|
83
|
+
|
|
84
|
+
这样子类只需用 @tool 装饰, 不用再手写 TOOLS 列表和 handle_tool if 链。
|
|
85
|
+
"""
|
|
86
|
+
def __new__(mcs, name, bases, namespace):
|
|
87
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
88
|
+
# 收集本类 + 父类的 @tool 方法
|
|
89
|
+
tool_map = {}
|
|
90
|
+
# 先继承父类的
|
|
91
|
+
for base in bases:
|
|
92
|
+
if hasattr(base, "_tool_map"):
|
|
93
|
+
tool_map.update(base._tool_map)
|
|
94
|
+
# 再收集本类的
|
|
95
|
+
for attr_name, attr_val in namespace.items():
|
|
96
|
+
meta = getattr(attr_val, "_tool_meta", None)
|
|
97
|
+
if meta:
|
|
98
|
+
tool_map[meta["name"]] = {
|
|
99
|
+
"meta": meta,
|
|
100
|
+
"handler": attr_name, # 方法名, 运行时 getattr 取
|
|
101
|
+
}
|
|
102
|
+
cls._tool_map = tool_map
|
|
103
|
+
# 如果子类没显式定义 TOOLS (用装饰器模式), 自动生成
|
|
104
|
+
if tool_map and not namespace.get("TOOLS"):
|
|
105
|
+
cls.TOOLS = [v["meta"] for v in tool_map.values()]
|
|
106
|
+
return cls
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class BaseMCPServer(metaclass=_ToolRegistryMeta):
|
|
110
|
+
"""MCP stdio server 基类 (Template Method + ToolRegistry)。
|
|
111
|
+
|
|
112
|
+
两种用法 (二选一):
|
|
113
|
+
A) 装饰器模式 (推荐, 消除 if 链):
|
|
114
|
+
class MyServer(BaseMCPServer):
|
|
115
|
+
SERVER_NAME = "my"
|
|
116
|
+
@tool("foo", "做foo", {...})
|
|
117
|
+
def foo(self, args): return "结果"
|
|
118
|
+
# TOOLS 自动收集, handle_tool 自动分发, 无需覆写
|
|
119
|
+
B) 传统模式 (兼容旧代码):
|
|
120
|
+
class MyServer(BaseMCPServer):
|
|
121
|
+
SERVER_NAME = "my"
|
|
122
|
+
TOOLS = [...] # 手写
|
|
123
|
+
def handle_tool(self, name, args): ... # 手写 if 链
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# ---- 子类必填类属性 ----
|
|
127
|
+
SERVER_NAME: str = "qoder-mcp"
|
|
128
|
+
SERVER_VERSION: str = "0.1.0"
|
|
129
|
+
TOOLS: List[Dict[str, Any]] = []
|
|
130
|
+
_tool_map: Dict[str, Dict] = {} # 由元类自动填充
|
|
131
|
+
|
|
132
|
+
# ---- 协议常量 (可覆写) ----
|
|
133
|
+
PROTOCOL_VERSION: str = "2024-11-05"
|
|
134
|
+
|
|
135
|
+
# ============================================================
|
|
136
|
+
# 内部: stdout 重定向 (MCP 用 stdout 传 JSON-RPC, 不能被 print 污染)
|
|
137
|
+
# ============================================================
|
|
138
|
+
_real_stdout = None
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def _setup_streams(cls):
|
|
142
|
+
"""把 sys.stdout 存起来, 重定向 sys.stdout→stderr (子进程的 print 进 stderr)。"""
|
|
143
|
+
if cls._real_stdout is None:
|
|
144
|
+
cls._real_stdout = sys.stdout
|
|
145
|
+
sys.stdout = sys.stderr
|
|
146
|
+
|
|
147
|
+
# ============================================================
|
|
148
|
+
# final: 协议发送
|
|
149
|
+
# ============================================================
|
|
150
|
+
@classmethod
|
|
151
|
+
def _send(cls, msg: Dict[str, Any]) -> None:
|
|
152
|
+
"""发送 JSON-RPC 消息到真实 stdout (跨3个server逐字一致)。"""
|
|
153
|
+
cls._real_stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
154
|
+
cls._real_stdout.flush()
|
|
155
|
+
|
|
156
|
+
# ============================================================
|
|
157
|
+
# 默认 handle_tool: 从 _tool_map 自动分发 (装饰器模式)
|
|
158
|
+
# ============================================================
|
|
159
|
+
def handle_tool(self, name: str, args: Dict[str, Any]) -> str:
|
|
160
|
+
"""[默认实现] 从 ToolRegistry 取 handler 执行。
|
|
161
|
+
|
|
162
|
+
- 若用了 @tool 装饰器: 自动分发, 子类不覆写
|
|
163
|
+
- 若传统模式 (子类覆写了 handle_tool): 走子类的 if 链
|
|
164
|
+
"""
|
|
165
|
+
entry = self._tool_map.get(name)
|
|
166
|
+
if entry:
|
|
167
|
+
handler = getattr(self, entry["handler"])
|
|
168
|
+
return handler(args)
|
|
169
|
+
return "未知工具: %s" % name
|
|
170
|
+
|
|
171
|
+
def on_initialize_hook(self, info: Dict[str, Any]) -> None:
|
|
172
|
+
"""[可选覆写] initialize 时往 serverInfo 塞额外信息 (如 warning)。
|
|
173
|
+
|
|
174
|
+
默认空。mysql/zentao 在此调 check_intranet 塞 warning。
|
|
175
|
+
"""
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def on_tool_error(self, e: Exception) -> Tuple[str, bool]:
|
|
179
|
+
"""[可选覆写] tools/call 抛异常时的分类。
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
(text, is_error): 返回给客户端的文本 + 是否标记为错误
|
|
183
|
+
默认: 通用异常 → "[error] xxx" + is_error=True。
|
|
184
|
+
mysql 覆写此方法把 ValueError(只读拒绝)单独分类。
|
|
185
|
+
"""
|
|
186
|
+
return ("[error] %s" % str(e)[:200], True)
|
|
187
|
+
|
|
188
|
+
# ============================================================
|
|
189
|
+
# final: 请求处理骨架 (Template Method)
|
|
190
|
+
# ============================================================
|
|
191
|
+
async def _handle(self, req: Dict[str, Any]) -> None:
|
|
192
|
+
"""处理一个 JSON-RPC 请求 (五分支骨架, 子类不覆写)。"""
|
|
193
|
+
method = req.get("method", "")
|
|
194
|
+
req_id = req.get("id")
|
|
195
|
+
params = req.get("params", {}) or {}
|
|
196
|
+
|
|
197
|
+
if method == "initialize":
|
|
198
|
+
info = {"name": self.SERVER_NAME, "version": self.SERVER_VERSION}
|
|
199
|
+
self.on_initialize_hook(info) # 钩子: 子类塞 warning
|
|
200
|
+
self._send({
|
|
201
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
202
|
+
"result": {
|
|
203
|
+
"protocolVersion": self.PROTOCOL_VERSION,
|
|
204
|
+
"capabilities": {"tools": {}},
|
|
205
|
+
"serverInfo": info,
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
elif method == "notifications/initialized":
|
|
209
|
+
pass
|
|
210
|
+
elif method == "tools/list":
|
|
211
|
+
self._send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": self.TOOLS}})
|
|
212
|
+
elif method == "tools/call":
|
|
213
|
+
tool_name = params.get("name", "")
|
|
214
|
+
arguments = params.get("arguments", {}) or {}
|
|
215
|
+
try:
|
|
216
|
+
result_text = await asyncio.get_event_loop().run_in_executor(
|
|
217
|
+
None, self.handle_tool, tool_name, arguments
|
|
218
|
+
)
|
|
219
|
+
self._send({
|
|
220
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
221
|
+
"result": {
|
|
222
|
+
"content": [{"type": "text", "text": result_text}],
|
|
223
|
+
"isError": False,
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
except Exception as e:
|
|
227
|
+
text, is_error = self.on_tool_error(e)
|
|
228
|
+
self._send({
|
|
229
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
230
|
+
"result": {
|
|
231
|
+
"content": [{"type": "text", "text": text}],
|
|
232
|
+
"isError": is_error,
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
elif method == "ping":
|
|
236
|
+
self._send({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
237
|
+
elif req_id is not None:
|
|
238
|
+
self._send({
|
|
239
|
+
"jsonrpc": "2.0", "id": req_id,
|
|
240
|
+
"error": {"code": -32601, "message": "Method not found: %s" % method},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# ============================================================
|
|
244
|
+
# final: 主循环
|
|
245
|
+
# ============================================================
|
|
246
|
+
def run(self) -> None:
|
|
247
|
+
"""启动 stdio 主循环 (readline → 解析 → _handle)。子类不覆写。"""
|
|
248
|
+
self._setup_streams()
|
|
249
|
+
tag = "[%s]" % self.SERVER_NAME
|
|
250
|
+
while True:
|
|
251
|
+
try:
|
|
252
|
+
line = sys.stdin.buffer.readline()
|
|
253
|
+
except (EOFError, OSError):
|
|
254
|
+
break
|
|
255
|
+
if not line:
|
|
256
|
+
break
|
|
257
|
+
line = line.strip()
|
|
258
|
+
if not line:
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
req = json.loads(line.decode("utf-8"))
|
|
262
|
+
loop = asyncio.new_event_loop()
|
|
263
|
+
loop.run_until_complete(self._handle(req))
|
|
264
|
+
loop.close()
|
|
265
|
+
except json.JSONDecodeError:
|
|
266
|
+
pass
|
|
267
|
+
except Exception as e:
|
|
268
|
+
print("%s error: %s" % (tag, str(e)[:80]), file=sys.stderr)
|
|
File without changes
|
|
@@ -26,6 +26,7 @@ eval_prd.py 的 main() 也改为调用本模块。
|
|
|
26
26
|
|
|
27
27
|
import os
|
|
28
28
|
import sys
|
|
29
|
+
import re
|
|
29
30
|
|
|
30
31
|
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
31
32
|
_SCRIPTS_DIR = os.path.dirname(_THIS_DIR)
|
|
@@ -33,7 +34,7 @@ if _SCRIPTS_DIR not in sys.path:
|
|
|
33
34
|
sys.path.insert(0, _SCRIPTS_DIR)
|
|
34
35
|
|
|
35
36
|
# 复用 eval_prd.py 的评分函数 (不重复实现)
|
|
36
|
-
from eval_prd import (
|
|
37
|
+
from validation.metrics.eval_prd import (
|
|
37
38
|
score_reality, score_style, score_template, read_text,
|
|
38
39
|
FULL_SECTIONS, QUICK_SECTIONS,
|
|
39
40
|
)
|
|
@@ -75,6 +76,16 @@ def evaluate(prd_path, html_path=None):
|
|
|
75
76
|
try:
|
|
76
77
|
r2, msg2, miss2 = score_style(read_text(html_path))
|
|
77
78
|
a2 = round(r2 * 30)
|
|
79
|
+
# v3.1 来源锚定检查: HTML 必须含 @prototype-source 或 x-source-pages 注释
|
|
80
|
+
# 没有 = AI 没做 Step 2 真实页面锚定, 直接从模板手抄 → 扣 5 分
|
|
81
|
+
html_text = read_text(html_path)
|
|
82
|
+
has_source = bool(re.search(r'@prototype-source:|x-source-pages', html_text))
|
|
83
|
+
if not has_source:
|
|
84
|
+
a2 = max(0, a2 - 5)
|
|
85
|
+
msg2 = (msg2 or '') + '; 缺来源锚定(@prototype-source 注释), 扣 5 分'
|
|
86
|
+
if miss2 is None:
|
|
87
|
+
miss2 = []
|
|
88
|
+
miss2 = list(miss2) + ['缺 @prototype-source 注释']
|
|
78
89
|
except Exception as e:
|
|
79
90
|
r2, msg2, miss2 = 0.0, "评估异常: %s" % e, []
|
|
80
91
|
a2 = 0
|
|
@@ -35,17 +35,34 @@ def _run_pip(args, index_url=None, timeout=MIRROR_TIMEOUT):
|
|
|
35
35
|
"""跑一次 pip, 返回 (returncode, stdout, stderr)。
|
|
36
36
|
index_url 非 None 时加 -i 指定镜像源。
|
|
37
37
|
用 trusted-host 避免某些镜像的 SSL 证书警告阻断。
|
|
38
|
+
|
|
39
|
+
⚠ 不用 subprocess.run / communicate:
|
|
40
|
+
Anaconda Python 3.9 + Windows 下, subprocess 的 communicate() 内部 join 读子进程
|
|
41
|
+
输出的线程时, 会触发 "PyEval_SaveThread: GIL released" 致命崩溃
|
|
42
|
+
(pip 跑 tree-sitter/playwright/ddddocr 这类大包时尤其稳定复现)。
|
|
43
|
+
50 人不同环境都要跑这步, 必须跨版本稳。
|
|
44
|
+
|
|
45
|
+
解法: Popen + wait(), stdout/stderr 全丢 DEVNULL。
|
|
46
|
+
pip 的啰嗦输出对我们无用 (成功与否以 _verify_packages 的 import 验证为准),
|
|
47
|
+
丢了反而避免大输出撑爆管道 + 触发 GIL 崩溃。超时用独立的计时线程判, 不走 communicate。
|
|
38
48
|
"""
|
|
39
|
-
cmd = [sys.executable, '-m', 'pip', 'install'
|
|
49
|
+
cmd = [sys.executable, '-m', 'pip', 'install', '--no-input',
|
|
50
|
+
'--progress-bar', 'off'] + args
|
|
40
51
|
if index_url:
|
|
41
52
|
cmd += ['-i', index_url,
|
|
42
53
|
'--trusted-host', _host_of(index_url)]
|
|
43
54
|
try:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL,
|
|
56
|
+
stderr=subprocess.DEVNULL)
|
|
57
|
+
try:
|
|
58
|
+
rc = proc.wait(timeout=timeout)
|
|
59
|
+
return rc, '', ''
|
|
60
|
+
except subprocess.TimeoutExpired:
|
|
61
|
+
try:
|
|
62
|
+
proc.kill()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return 1, '', '安装超时 (%ds)' % timeout
|
|
49
66
|
except Exception as e:
|
|
50
67
|
return 1, '', str(e)
|
|
51
68
|
|
|
@@ -26,7 +26,7 @@ def _platform_explicitly_passed(argv=None):
|
|
|
26
26
|
return '--platform' in argv
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def assert_platform_selected(skill='wl-prd
|
|
29
|
+
def assert_platform_selected(skill='wl-prd', argv=None):
|
|
30
30
|
"""硬墙: 没显式传 --platform 就调用 PRD 链路脚本 = 违规。
|
|
31
31
|
|
|
32
32
|
1. best-effort 上报 rule_violation 事件 (不阻塞, 失败静默)
|
|
File without changes
|