@hupan56/wlkj 3.0.0 → 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 -1373
- package/package.json +1 -1
- package/templates/qoder/commands/optional/wl-insight.md +6 -4
- 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 +5 -5
- 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 +3 -3
- 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 +10 -10
- package/templates/qoder/config.yaml +35 -13
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +82 -1
- 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 +70 -8
- package/templates/qoder/scripts/README.md +78 -115
- 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/{report → deployment/setup}/__init__.py +11 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
- package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
- package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
- package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
- package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
- package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
- package/templates/qoder/scripts/domain/__init__.py +0 -0
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
- package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
- package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
- package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
- package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
- package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
- 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/{common → domain/kg/extract}/test_extract.py +115 -115
- 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 → domain/kg/graph}/kg_link_db.py +7 -2
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
- package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
- package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
- package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
- 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 → domain/kg/storage}/kg_duckdb.py +38 -14
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
- package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
- package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
- package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
- package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
- package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
- 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/{report → domain/requirement}/archive_prd.py +8 -2
- package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
- package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
- package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
- package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
- package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
- package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
- package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
- package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
- package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
- package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
- package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
- 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 +2 -2
- 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/{common → foundation/protocol}/mcp_base.py +1 -1
- 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/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
- 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/{report → validation/metrics}/eval_prd.py +17 -10
- 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/{test → validation/test}/autotest.py +64 -4
- package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
- package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
- package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
- package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
- package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
- package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +30 -0
- package/templates/qoder/skills/design-import/SKILL.md +22 -13
- package/templates/qoder/skills/design-review/SKILL.md +19 -10
- package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
- package/templates/qoder/skills/prd-review/SKILL.md +6 -4
- package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
- package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
- package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
- package/templates/qoder/skills/test-generator/SKILL.md +7 -5
- package/templates/qoder/skills/wl-code/SKILL.md +23 -11
- package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
- package/templates/qoder/skills/wl-design/SKILL.md +5 -3
- package/templates/qoder/skills/wl-init/SKILL.md +6 -6
- package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
- package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
- package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
- package/templates/qoder/skills/wl-report/SKILL.md +25 -23
- package/templates/qoder/skills/wl-search/SKILL.md +149 -78
- package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
- package/templates/qoder/skills/wl-status/SKILL.md +48 -16
- package/templates/qoder/skills/wl-task/SKILL.md +92 -10
- package/templates/qoder/skills/wl-test/SKILL.md +75 -29
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -236
- 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/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
- package/templates/.qoder/learning/eval-history.jsonl +0 -28
- package/templates/data/index/wiki-index.json +0 -8
- package/templates/qoder/nul +0 -4
- package/templates/qoder/scripts/kg/extract.py +0 -398
- package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
- package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
- package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
- package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
- package/templates/qoder/scripts/task/__init__.py +0 -11
- package/templates/qoder/scripts/test/__init__.py +0 -11
- package/templates/qoder/scripts/wlkj.py +0 -116
- /package/templates/qoder/scripts/{common → deployment}/__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/{test → validation/test}/secure-ls.js +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ProviderChain —— 能力 Provider 职责链。
|
|
4
|
+
|
|
5
|
+
设计模式:
|
|
6
|
+
- Chain of Responsibility(职责链): 按优先级遍历,第一个 available 的命中
|
|
7
|
+
- Strategy(策略组合): 每个 provider 是一种策略实现
|
|
8
|
+
- Decorator(装饰器): 可选地对命中的 provider 增强(缓存/日志/统计)
|
|
9
|
+
|
|
10
|
+
透明代理: ProviderChain 把方法调用转发给命中的 provider,
|
|
11
|
+
对调用方而言与直接用 Cap 无异。
|
|
12
|
+
|
|
13
|
+
chain = ProviderChain([McpMemoryProvider(), QwMemoryCap(),
|
|
14
|
+
FileMemoryCap(), NoOpMemoryCap()])
|
|
15
|
+
chain.recall("本周执行") # 自动代理到第一个 available 的 provider
|
|
16
|
+
|
|
17
|
+
链尾永远是 NoOp,保证无能力时不抛错(Null Object 兜底)。
|
|
18
|
+
|
|
19
|
+
演进(扩展场景 C: 能力 MCP 化迁移):
|
|
20
|
+
mem0 MCP server 成熟 → 链头插入 McpMemoryProvider →
|
|
21
|
+
available=True 自动抢占 → 旧的 QwMemoryCap 沉到链尾静默退役 →
|
|
22
|
+
调用方代码零改动。这就是"未来 vs 现实"的机制化。
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Callable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProviderChain:
|
|
30
|
+
"""能力 Provider 职责链。
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
providers: 按优先级排列的 provider 列表,链尾应为 NoOp。
|
|
34
|
+
decorators: 可选装饰器列表,每个形式 func(provider_method, provider) -> wrapped,
|
|
35
|
+
对命中的 provider 的方法做增强(缓存/日志/降级统计),不改原 provider。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, providers: list, decorators: list | None = None):
|
|
39
|
+
# 用 object.__setattr__ 绕过自定义 __getattr__,避免初始化期递归
|
|
40
|
+
object.__setattr__(self, "_providers", list(providers))
|
|
41
|
+
object.__setattr__(self, "_decorators", list(decorators or []))
|
|
42
|
+
object.__setattr__(self, "_active", None)
|
|
43
|
+
object.__setattr__(self, "_probed", False)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def available(self) -> bool:
|
|
47
|
+
"""是否有任一 provider 可用。"""
|
|
48
|
+
return self._probe() is not None
|
|
49
|
+
|
|
50
|
+
def _probe(self) -> Any:
|
|
51
|
+
"""返回第一个 available 的 provider,缓存结果(首次探测后不再变)。"""
|
|
52
|
+
if self._probed:
|
|
53
|
+
return self._active
|
|
54
|
+
for p in self._providers:
|
|
55
|
+
if getattr(p, "available", False):
|
|
56
|
+
object.__setattr__(self, "_active", p)
|
|
57
|
+
break
|
|
58
|
+
object.__setattr__(self, "_probed", True)
|
|
59
|
+
return self._active
|
|
60
|
+
|
|
61
|
+
def providers_status(self) -> list[dict]:
|
|
62
|
+
"""诊断用:每个 provider 的 available 状态 + 是否命中。"""
|
|
63
|
+
active = self._probe()
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
"provider": type(p).__name__,
|
|
67
|
+
"available": bool(getattr(p, "available", False)),
|
|
68
|
+
"hit": p is active,
|
|
69
|
+
}
|
|
70
|
+
for p in self._providers
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def __getattr__(self, name: str) -> Any:
|
|
74
|
+
"""透明代理:把属性/方法调用转发给命中的 provider。
|
|
75
|
+
|
|
76
|
+
调用方写 chain.recall(q) 等价于 active_provider.recall(q)。
|
|
77
|
+
若无任何 provider available,用链尾(NoOp)兜底,绝不抛错。
|
|
78
|
+
"""
|
|
79
|
+
if name.startswith("_"):
|
|
80
|
+
raise AttributeError(name)
|
|
81
|
+
active = self._probe()
|
|
82
|
+
if active is None:
|
|
83
|
+
# 兜底:链尾 NoOp
|
|
84
|
+
active = self._providers[-1] if self._providers else None
|
|
85
|
+
if active is None:
|
|
86
|
+
raise AttributeError(f"ProviderChain 无 provider 可解析 '{name}'")
|
|
87
|
+
attr = getattr(active, name)
|
|
88
|
+
# 应用装饰器(仅对可调用方法)
|
|
89
|
+
if callable(attr) and self._decorators:
|
|
90
|
+
for dec in self._decorators:
|
|
91
|
+
attr = dec(attr, active)
|
|
92
|
+
return attr
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
memory 能力职责链装配。
|
|
4
|
+
|
|
5
|
+
设计模式: Provider Chain + Strategy
|
|
6
|
+
|
|
7
|
+
独立成文件的理由: memory 是最复杂的 L2 能力(4 个 provider),
|
|
8
|
+
未来加 provider(如向量库、mem0 MCP)只改本文件,不动 registry ——
|
|
9
|
+
体现 Plugin/SPI:新实现接入不改核心。
|
|
10
|
+
|
|
11
|
+
优先级(高→低):
|
|
12
|
+
① McpMemoryProvider —— 未来 mem0/Zep MCP server(现状 available=False)
|
|
13
|
+
② QwMemoryCap —— QoderWork 原生(仅 qoderwork 宿主)
|
|
14
|
+
③ FileMemoryCap —— 文件降级(纯 CLI/任意宿主)
|
|
15
|
+
④ NoOpMemoryCap —— 兜底(绝不抛错)
|
|
16
|
+
|
|
17
|
+
演进(扩展场景 C): mem0 MCP 成熟 → 把 McpMemoryProvider 的 available 打开 →
|
|
18
|
+
自动抢占链头 → FileMemoryCap/QwMemoryCap 静默退役 → 调用方零改动。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .chain import ProviderChain
|
|
23
|
+
from .caps import NoOpMemoryCap
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_memory_chain(host: str) -> ProviderChain:
|
|
27
|
+
"""装配 memory 职责链。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
host: detect_host() 返回的宿主名。
|
|
31
|
+
"""
|
|
32
|
+
from .adapters.mcp import McpMemoryProvider
|
|
33
|
+
from .adapters.cli import FileMemoryCap
|
|
34
|
+
|
|
35
|
+
providers: list = [McpMemoryProvider()]
|
|
36
|
+
if host == "qoderwork":
|
|
37
|
+
from .adapters.qw import QwMemoryCap
|
|
38
|
+
providers.append(QwMemoryCap())
|
|
39
|
+
providers.append(FileMemoryCap())
|
|
40
|
+
providers.append(NoOpMemoryCap())
|
|
41
|
+
return ProviderChain(providers)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
nexus.registry —— 宿主发现 + Nexus Facade。
|
|
4
|
+
|
|
5
|
+
设计模式:
|
|
6
|
+
- Registry(HOST_TABLE 单一事实源,泛化 common.active_task._ENV_SESSION_KEYS)
|
|
7
|
+
- Facade(resolve() 一行入口)
|
|
8
|
+
- Bridge(Cap 抽象 × 宿主实现 正交)
|
|
9
|
+
|
|
10
|
+
主轴: 工作流面向能力编程,不面向宿主。
|
|
11
|
+
cap = nexus.resolve()
|
|
12
|
+
cap.memory.recall(...) # 不管在 QoderWork 还是 CLI,同一行代码
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .caps import (
|
|
20
|
+
NoOpCronCap,
|
|
21
|
+
NoOpNotifyCap,
|
|
22
|
+
NoOpPresentCap,
|
|
23
|
+
NoOpSandboxCap,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# HOST_TABLE —— 宿主注册表
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
# 泛化 common/active_task.py:_ENV_SESSION_KEYS 的 7 宿主表:
|
|
31
|
+
# 从"只探测身份"扩展为"身份 + 能力清单"。
|
|
32
|
+
#
|
|
33
|
+
# 现在最小形态: Python dict。
|
|
34
|
+
# 未来完整形态: manifests/*.yaml 声明式文件(每宿主一份)。
|
|
35
|
+
# 迁移路径: dict → yaml,resolve() 接口不变,工作流零改动。
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
HOST_TABLE: dict[str, dict] = {
|
|
39
|
+
# ── Qoder 产品线(第一宿主系列)─────────────────────────────
|
|
40
|
+
"qoder-work": {
|
|
41
|
+
"detect": ("QODER_SESSION_ID",),
|
|
42
|
+
# features = 当前已实现 (非 NoOp) 的能力。notify/present/sandbox 暂无
|
|
43
|
+
# 宿主原生 Adapter,声明即误报可用,故不列。后续补齐 Adapter 再加。
|
|
44
|
+
"features": ("mcp", "memory", "cron"),
|
|
45
|
+
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql", "lanhu"],
|
|
46
|
+
"note": "QoderWork 桌面 Agent,全能力",
|
|
47
|
+
},
|
|
48
|
+
"qoder-ide": {
|
|
49
|
+
"detect": ("QODER_IDE",),
|
|
50
|
+
"features": ("mcp", "memory"),
|
|
51
|
+
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql"],
|
|
52
|
+
"note": "Qoder IDE (VSCode 扩展)",
|
|
53
|
+
},
|
|
54
|
+
"qoder-quest": {
|
|
55
|
+
"detect": ("QODER_QUEST",),
|
|
56
|
+
"features": ("mcp", "memory"),
|
|
57
|
+
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao"],
|
|
58
|
+
"note": "Qoder Quest,自然语言入口",
|
|
59
|
+
},
|
|
60
|
+
# ── 其他宿主(通用 AI 工具)────────────────────────────────
|
|
61
|
+
"codex": {
|
|
62
|
+
"detect": ("CODEX_SESSION_ID", "CODEX_THREAD_ID"),
|
|
63
|
+
"features": ("repo", "identity"),
|
|
64
|
+
"mcp_servers": [],
|
|
65
|
+
"note": "OpenAI Codex CLI",
|
|
66
|
+
},
|
|
67
|
+
"claude": {
|
|
68
|
+
"detect": ("CLAUDE_SESSION_ID", "CLAUDE_CODE_SESSION_ID"),
|
|
69
|
+
"features": ("repo", "identity"),
|
|
70
|
+
"mcp_servers": [],
|
|
71
|
+
"note": "Claude Code",
|
|
72
|
+
},
|
|
73
|
+
"cursor": {
|
|
74
|
+
"detect": ("CURSOR_SESSION_ID",),
|
|
75
|
+
"features": ("repo", "identity"),
|
|
76
|
+
"mcp_servers": [],
|
|
77
|
+
},
|
|
78
|
+
"opencode": {
|
|
79
|
+
"detect": ("OPENCODE_SESSION_ID", "OPENCODE_RUN_ID"),
|
|
80
|
+
"features": ("repo", "identity"),
|
|
81
|
+
"mcp_servers": [],
|
|
82
|
+
},
|
|
83
|
+
"gemini": {
|
|
84
|
+
"detect": ("GEMINI_SESSION_ID",),
|
|
85
|
+
"features": ("repo", "identity"),
|
|
86
|
+
"mcp_servers": [],
|
|
87
|
+
},
|
|
88
|
+
"copilot": {
|
|
89
|
+
"detect": ("COPILOT_SESSION_ID",),
|
|
90
|
+
"features": ("repo", "identity"),
|
|
91
|
+
"mcp_servers": [],
|
|
92
|
+
},
|
|
93
|
+
# ── 兜底 ──────────────────────────────────────────────────
|
|
94
|
+
"cli": {
|
|
95
|
+
"detect": (), # 兜底:无 env 命中即为 CLI / 任意宿主
|
|
96
|
+
"features": ("repo", "identity", "context", "memory", "mcp"),
|
|
97
|
+
# 本地 stdio transport 经 mcp_launcher spawn 子进程, 不依赖宿主原生 MCP。
|
|
98
|
+
# 故 CLI 能起全部本地 server (kg/zentao/mysql/lanhu), 与 QoderWork 等价。
|
|
99
|
+
"mcp_servers": ["qoder-knowledge-graph", "qoder-zentao", "qoder-mysql", "lanhu"],
|
|
100
|
+
"note": "纯 CLI 兜底; MCP 走本地 spawn (非宿主原生), 其余走文件降级",
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def detect_host() -> str:
|
|
106
|
+
"""探测当前宿主名。
|
|
107
|
+
|
|
108
|
+
遍历 HOST_TABLE,第一个 env 命中的返回;无则 'cli'。
|
|
109
|
+
单一事实源 —— 替代散落在 active_task / autotest / mcp_launcher 的多处宿主探测。
|
|
110
|
+
"""
|
|
111
|
+
for name, info in HOST_TABLE.items():
|
|
112
|
+
if name == "cli":
|
|
113
|
+
continue
|
|
114
|
+
for env_var in info.get("detect", ()):
|
|
115
|
+
if os.environ.get(env_var):
|
|
116
|
+
return name
|
|
117
|
+
return "cli"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# =============================================================================
|
|
121
|
+
# Nexus Facade
|
|
122
|
+
# =============================================================================
|
|
123
|
+
|
|
124
|
+
class Nexus:
|
|
125
|
+
"""宿主能力 Facade —— 聚合所有能力,对外统一入口。
|
|
126
|
+
|
|
127
|
+
工作流永远只调 nexus.resolve() 拿到本对象,再访问 .memory/.mcp/...,
|
|
128
|
+
不直接 import 任何 adapter(Facade 模式)。
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, host_name: str, repo, identity, context, memory,
|
|
132
|
+
cron, notify, present, sandbox, mcp):
|
|
133
|
+
self.host_name = host_name
|
|
134
|
+
self.repo = repo
|
|
135
|
+
self.identity = identity
|
|
136
|
+
self.context = context
|
|
137
|
+
self.memory = memory
|
|
138
|
+
self.cron = cron
|
|
139
|
+
self.notify = notify
|
|
140
|
+
self.present = present
|
|
141
|
+
self.sandbox = sandbox
|
|
142
|
+
self.mcp = mcp
|
|
143
|
+
|
|
144
|
+
def describe(self) -> dict:
|
|
145
|
+
"""诊断:当前宿主 + 各能力 available 状态。"""
|
|
146
|
+
return {
|
|
147
|
+
"host": self.host_name,
|
|
148
|
+
"capabilities": {
|
|
149
|
+
"repo": _avail(self.repo),
|
|
150
|
+
"identity": _avail(self.identity),
|
|
151
|
+
"context": _avail(self.context),
|
|
152
|
+
"memory": _avail(self.memory),
|
|
153
|
+
"cron": _avail(self.cron),
|
|
154
|
+
"notify": _avail(self.notify),
|
|
155
|
+
"present": _avail(self.present),
|
|
156
|
+
"sandbox": _avail(self.sandbox),
|
|
157
|
+
"mcp": _avail(self.mcp),
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _avail(cap) -> bool:
|
|
163
|
+
return bool(getattr(cap, "available", False))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# 能力装配(Bridge: Cap 抽象 × 宿主实现 正交)
|
|
168
|
+
# -----------------------------------------------------------------------------
|
|
169
|
+
# 每个 _resolve_xxx 是一个能力的装配点。新增能力(场景 B)= 加一个 _resolve_xxx
|
|
170
|
+
# + Nexus 属性。新增宿主实现 = 在对应 _resolve_xxx 里按 host 名分支。
|
|
171
|
+
# =============================================================================
|
|
172
|
+
|
|
173
|
+
def _resolve_repo(host: str):
|
|
174
|
+
"""核心能力:所有宿主都用文件降级版(无 ~/.qoderwork 依赖)。"""
|
|
175
|
+
from .adapters.cli import FileRepoCap
|
|
176
|
+
return FileRepoCap()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_identity(host: str):
|
|
180
|
+
from .adapters.cli import FileIdentityCap
|
|
181
|
+
return FileIdentityCap()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_context(host: str):
|
|
185
|
+
from .adapters.cli import FileContextCap
|
|
186
|
+
return FileContextCap()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _resolve_memory(host: str):
|
|
190
|
+
"""memory —— 职责链样板(见 memory_chain.py)。"""
|
|
191
|
+
from .memory_chain import build_memory_chain
|
|
192
|
+
return build_memory_chain(host)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _resolve_cron(host: str):
|
|
196
|
+
from .adapters.qw import QwCronCap
|
|
197
|
+
return QwCronCap() # 文件系统 cron,不依赖宿主
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resolve_notify(host: str):
|
|
201
|
+
return NoOpNotifyCap()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _resolve_present(host: str):
|
|
205
|
+
"""L2 富展示 —— QoderWork 调 qoder_show_widget 嵌入对话; 其余 NoOp 降级表格。"""
|
|
206
|
+
if host in ("qoder-work", "qoder-ide"):
|
|
207
|
+
try:
|
|
208
|
+
from .adapters.qw import QwPresentCap
|
|
209
|
+
return QwPresentCap()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
return NoOpPresentCap()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _resolve_sandbox(host: str):
|
|
216
|
+
return NoOpSandboxCap()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _resolve_mcp(host: str):
|
|
220
|
+
"""L1 MCP 统一入口 —— 根据宿主声明的 mcp_servers 决定 spawn 哪些。"""
|
|
221
|
+
from .adapters.mcp import McpCap
|
|
222
|
+
info = HOST_TABLE.get(host, {})
|
|
223
|
+
mcp_servers = info.get("mcp_servers")
|
|
224
|
+
return McpCap(mcp_servers=mcp_servers)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def resolve() -> Nexus:
|
|
228
|
+
"""解析当前环境,返回 Nexus Facade。
|
|
229
|
+
|
|
230
|
+
一行入口: cap = nexus.resolve()
|
|
231
|
+
内部: detect_host → 按宿主装配各 Cap → 注入 NoOp 兜底。
|
|
232
|
+
工作流代码零宿主感知。
|
|
233
|
+
"""
|
|
234
|
+
host = detect_host()
|
|
235
|
+
return Nexus(
|
|
236
|
+
host_name=host,
|
|
237
|
+
repo=_resolve_repo(host),
|
|
238
|
+
identity=_resolve_identity(host),
|
|
239
|
+
context=_resolve_context(host),
|
|
240
|
+
memory=_resolve_memory(host),
|
|
241
|
+
cron=_resolve_cron(host),
|
|
242
|
+
notify=_resolve_notify(host),
|
|
243
|
+
present=_resolve_present(host),
|
|
244
|
+
sandbox=_resolve_sandbox(host),
|
|
245
|
+
mcp=_resolve_mcp(host),
|
|
246
|
+
)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
nexus.registry_mcp —— MCP 工具路由(tool → server → transport)。
|
|
4
|
+
|
|
5
|
+
设计模式: Registry + Strategy(配合 transports 层)
|
|
6
|
+
|
|
7
|
+
职责:
|
|
8
|
+
1. 声明现有 MCP server 清单及其默认 transport(配置驱动,留远程化扩展点)。
|
|
9
|
+
2. 维护 server → Transport 实例池(复用子进程/连接)。
|
|
10
|
+
3. 构建 tool 名 → server 名 的反向索引(懒加载,首次需要时遍历 tools/list)。
|
|
11
|
+
4. resolve_tool(tool_name) → 该 tool 所在 server 的 Transport。
|
|
12
|
+
|
|
13
|
+
扩展点(配置驱动远程化):
|
|
14
|
+
未来知识图谱上服务器,把 SERVERS["qoder-knowledge-graph"]["kind"] 改成 "http"
|
|
15
|
+
+ 填 url,工作流 (cap.mcp.call) 零改动。
|
|
16
|
+
这是"满足现在同时留扩展给未来"的核心机制。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from protocol.transports import get_transport, Transport, NoOpTransport, ToolResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# SERVERS —— MCP server 清单 + 默认 transport 配置(单一事实源)
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
# 每个条目:
|
|
31
|
+
# kind: 传输方式 ("stdio"|"cli"|"http")
|
|
32
|
+
# launcher_arg: mcp_launcher 接受的子名(多数同 key)
|
|
33
|
+
# url: http 时的远程 endpoint(stdio/cli 不用)
|
|
34
|
+
# 未来改这里即可切换部署形态,工作流零改动。
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
SERVERS: dict[str, dict] = {
|
|
38
|
+
"qoder-knowledge-graph": {
|
|
39
|
+
"kind": "stdio",
|
|
40
|
+
"launcher_arg": "kg",
|
|
41
|
+
"note": "知识图谱。本地分发;未来可上服务器 → 改 kind=http + url",
|
|
42
|
+
},
|
|
43
|
+
"qoder-zentao": {
|
|
44
|
+
"kind": "stdio",
|
|
45
|
+
"launcher_arg": "zentao",
|
|
46
|
+
"note": "禅道任务/Bug/需求",
|
|
47
|
+
},
|
|
48
|
+
"qoder-mysql": {
|
|
49
|
+
"kind": "stdio",
|
|
50
|
+
"launcher_arg": "mysql",
|
|
51
|
+
"note": "只读 MySQL 结构/数据",
|
|
52
|
+
},
|
|
53
|
+
"lanhu": {
|
|
54
|
+
"kind": "stdio",
|
|
55
|
+
"launcher_arg": "lanhu",
|
|
56
|
+
"note": "蓝湖设计稿",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_repo_root() -> Path:
|
|
62
|
+
"""从 common.paths 获取仓库根(复用既有工具,不重复造轮子)。"""
|
|
63
|
+
try:
|
|
64
|
+
from foundation.core.paths import get_repo_root
|
|
65
|
+
return get_repo_root()
|
|
66
|
+
except Exception:
|
|
67
|
+
return Path(__file__).resolve().parent.parent.parent.parent
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class McpRegistry:
|
|
71
|
+
"""MCP 工具路由 + Transport 实例池。
|
|
72
|
+
|
|
73
|
+
线程安全说明:实例池按 server 名缓存 Transport,每个 Transport 自带锁。
|
|
74
|
+
反向索引构建期间对多 server 并发(线程池),构建后只读。
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, repo_root: Optional[Path] = None, servers: Optional[dict] = None):
|
|
78
|
+
self._repo = repo_root or _get_repo_root()
|
|
79
|
+
self._servers = servers if servers is not None else SERVERS
|
|
80
|
+
self._pool: dict[str, Transport] = {}
|
|
81
|
+
self._tool_to_server: Optional[dict[str, str]] = None # 懒构建
|
|
82
|
+
self._index_lock_built = False
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Transport 实例池
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def _get_or_create(self, server_name: str) -> Transport:
|
|
89
|
+
"""按 server 名取/建 Transport 实例(复用)。"""
|
|
90
|
+
if server_name not in self._pool:
|
|
91
|
+
cfg = self._servers.get(server_name, {})
|
|
92
|
+
kind = cfg.get("kind", "stdio")
|
|
93
|
+
launcher_arg = cfg.get("launcher_arg", server_name)
|
|
94
|
+
if kind == "stdio":
|
|
95
|
+
self._pool[server_name] = get_transport(
|
|
96
|
+
launcher_arg, kind="stdio", repo_root=self._repo,
|
|
97
|
+
)
|
|
98
|
+
elif kind == "http":
|
|
99
|
+
self._pool[server_name] = get_transport(
|
|
100
|
+
server_name, kind="http",
|
|
101
|
+
url=cfg.get("url", ""), token=cfg.get("token"),
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
self._pool[server_name] = get_transport(
|
|
105
|
+
server_name, kind=kind, repo_root=self._repo, **cfg,
|
|
106
|
+
)
|
|
107
|
+
return self._pool[server_name]
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# 反向索引(tool 名 → server 名)
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _build_index(self) -> None:
|
|
114
|
+
"""对所有 server 发 tools/list,建 tool→server 反向索引。
|
|
115
|
+
|
|
116
|
+
并发构建(各 server 独立 transport,互不干扰)。
|
|
117
|
+
不可用的 server 跳过(其 tool 不进索引)。
|
|
118
|
+
"""
|
|
119
|
+
import concurrent.futures
|
|
120
|
+
|
|
121
|
+
index: dict[str, str] = {}
|
|
122
|
+
|
|
123
|
+
def _probe(server_name: str) -> tuple[str, list[dict]]:
|
|
124
|
+
try:
|
|
125
|
+
t = self._get_or_create(server_name)
|
|
126
|
+
tools = t.list_tools()
|
|
127
|
+
return server_name, tools
|
|
128
|
+
except Exception:
|
|
129
|
+
return server_name, []
|
|
130
|
+
|
|
131
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(self._servers) or 1) as ex:
|
|
132
|
+
for server_name, tools in ex.map(_probe, list(self._servers.keys())):
|
|
133
|
+
for tool in tools:
|
|
134
|
+
tname = tool.get("name", "")
|
|
135
|
+
if tname:
|
|
136
|
+
index[tname] = server_name
|
|
137
|
+
|
|
138
|
+
self._tool_to_server = index
|
|
139
|
+
# ★ 修复: 全空索引不标记为"已建好"。
|
|
140
|
+
# 原版无论结果如何都设 _index_lock_built=True + 赋空 dict,
|
|
141
|
+
# 导致 _ensure_index (判 is None) 永不重建 → 一次冷启动竞态失败就永久 0 工具。
|
|
142
|
+
# 现改为: 空 = 视为未建, 下次调用会重试 (并发冷启动是间歇性的, 重试通常成功)。
|
|
143
|
+
if index:
|
|
144
|
+
self._index_lock_built = True
|
|
145
|
+
else:
|
|
146
|
+
# 保留 None, 下次 _ensure_index 会重 _build_index
|
|
147
|
+
self._tool_to_server = None
|
|
148
|
+
|
|
149
|
+
def _ensure_index(self) -> None:
|
|
150
|
+
if self._tool_to_server is None:
|
|
151
|
+
self._build_index()
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# 公共 API
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def resolve_tool(self, tool_name: str) -> Optional[Transport]:
|
|
158
|
+
"""按 tool 名解析其所在 server 的 Transport。未知返回 None。"""
|
|
159
|
+
self._ensure_index()
|
|
160
|
+
server = (self._tool_to_server or {}).get(tool_name)
|
|
161
|
+
if not server:
|
|
162
|
+
return None
|
|
163
|
+
return self._get_or_create(server)
|
|
164
|
+
|
|
165
|
+
def has_tool(self, tool_name: str) -> bool:
|
|
166
|
+
"""某 tool 是否可用(先查反向索引;未命中尝试解析)。"""
|
|
167
|
+
self._ensure_index()
|
|
168
|
+
if tool_name in (self._tool_to_server or {}):
|
|
169
|
+
return True
|
|
170
|
+
t = self.resolve_tool(tool_name)
|
|
171
|
+
if t is None:
|
|
172
|
+
return False
|
|
173
|
+
try:
|
|
174
|
+
return t.has_tool(tool_name)
|
|
175
|
+
except Exception:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def call(self, tool_name: str, args: Optional[dict] = None,
|
|
179
|
+
server: Optional[str] = None) -> ToolResult:
|
|
180
|
+
"""调用工具。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
tool_name: 工具名(如 list_execution_tasks)。
|
|
184
|
+
args: 工具参数。
|
|
185
|
+
server: 显式指定 server 名(跳过反向索引,避免首次开销)。
|
|
186
|
+
Returns:
|
|
187
|
+
ToolResult。未知工具/不可用返回 is_error=True 的空结果,不抛异常。
|
|
188
|
+
"""
|
|
189
|
+
transport: Transport
|
|
190
|
+
if server:
|
|
191
|
+
if server not in self._servers:
|
|
192
|
+
return ToolResult(text="未知 server: %s" % server, is_error=True)
|
|
193
|
+
transport = self._get_or_create(server)
|
|
194
|
+
else:
|
|
195
|
+
transport = self.resolve_tool(tool_name) or NoOpTransport()
|
|
196
|
+
if not transport.available:
|
|
197
|
+
return ToolResult(
|
|
198
|
+
text="transport 不可用 (server=%s, tool=%s)" % (server or "?", tool_name),
|
|
199
|
+
is_error=True,
|
|
200
|
+
)
|
|
201
|
+
return transport.call_tool(tool_name, args or {})
|
|
202
|
+
|
|
203
|
+
def available(self) -> bool:
|
|
204
|
+
"""任一 server 可用即为 True。"""
|
|
205
|
+
self._ensure_index()
|
|
206
|
+
return bool(self._tool_to_server)
|
|
207
|
+
|
|
208
|
+
def describe(self) -> dict:
|
|
209
|
+
"""诊断:各 server 的 transport 状态 + 已索引工具数 + 延迟 + PID。"""
|
|
210
|
+
import time as _time
|
|
211
|
+
self._ensure_index()
|
|
212
|
+
servers_status = {}
|
|
213
|
+
for name in self._servers:
|
|
214
|
+
t = self._pool.get(name)
|
|
215
|
+
indexed = [tn for tn, sn in (self._tool_to_server or {}).items() if sn == name]
|
|
216
|
+
|
|
217
|
+
# latency: 测量 list_tools 的首次延迟(已在 _build_index 期间完成,此处重新测)
|
|
218
|
+
latency_ms = None
|
|
219
|
+
if t and t.available:
|
|
220
|
+
try:
|
|
221
|
+
t0 = _time.monotonic()
|
|
222
|
+
t.list_tools() # 有缓存,很快
|
|
223
|
+
latency_ms = round((_time.monotonic() - t0) * 1000, 1)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# spawn_pid: stdio transport 的子进程 PID
|
|
228
|
+
spawn_pid = None
|
|
229
|
+
if t and hasattr(t, "_proc") and t._proc is not None:
|
|
230
|
+
spawn_pid = getattr(t._proc, "pid", None)
|
|
231
|
+
|
|
232
|
+
servers_status[name] = {
|
|
233
|
+
"kind": self._servers[name].get("kind", "stdio"),
|
|
234
|
+
"available": bool(t and t.available),
|
|
235
|
+
"tools_indexed": len(indexed),
|
|
236
|
+
"latency_ms": latency_ms,
|
|
237
|
+
"spawn_pid": spawn_pid,
|
|
238
|
+
}
|
|
239
|
+
return {"servers": servers_status, "total_tools": len(self._tool_to_server or {})}
|
|
240
|
+
|
|
241
|
+
def close_all(self) -> None:
|
|
242
|
+
"""关闭所有 Transport(进程退出/连接释放)。"""
|
|
243
|
+
for t in self._pool.values():
|
|
244
|
+
try:
|
|
245
|
+
t.close()
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
self._pool.clear()
|
|
249
|
+
self._tool_to_server = None
|
|
250
|
+
self._index_lock_built = False
|