@hupan56/wlkj 3.2.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +117 -0
- package/package.json +2 -2
- package/templates/.qoder/.runtime/hook-errors.log +4 -0
- package/templates/qoder/commands/optional/wl-insight.md +276 -276
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +13 -1
- package/templates/qoder/commands/optional/wl-status.md +14 -1
- package/templates/qoder/commands/wl-code.md +111 -4
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +69 -5
- package/templates/qoder/commands/wl-init.md +129 -102
- package/templates/qoder/commands/wl-prd.md +162 -6
- package/templates/qoder/commands/wl-search.md +106 -20
- package/templates/qoder/commands/wl-task.md +617 -613
- package/templates/qoder/commands/wl-test.md +20 -1
- package/templates/qoder/contracts/spec.md +4 -0
- package/templates/qoder/hooks/post-tool-use.py +71 -0
- package/templates/qoder/hooks/pre-tool-use.py +136 -0
- package/templates/qoder/hooks/session-start.py +365 -397
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/present_html.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +5 -5
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/caps/notify.py +64 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +38 -0
- package/templates/qoder/scripts/capability/present_html.py +68 -0
- package/templates/qoder/scripts/capability/registry.py +27 -25
- package/templates/qoder/scripts/capability/registry_mcp.py +67 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +395 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +208 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +27 -3
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/switch_project.py +158 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/{zentao_panel.py → wlkj_panel.py} +315 -53
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1 -1
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- package/templates/qoder/scripts/validation/metrics/__pycache__/present_board.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/validation/metrics/present_board.py +180 -0
- package/templates/qoder/settings.json +10 -0
- package/templates/root/AGENTS.md +9 -10
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
adapters.qw —— QoderWork 原生能力 Adapter。
|
|
4
|
-
|
|
5
|
-
设计模式: Adapter(适配 QoderWork 原生工具到 Cap 契约)
|
|
6
|
-
|
|
7
|
-
实现 QoderWork 提供的 L2 宿主原生能力。其他宿主(ZCode/Codex)不适用,
|
|
8
|
-
未实现的 Cap 由 NoOp 兜底。
|
|
9
|
-
|
|
10
|
-
QoderWork 提供的相关工具(参考):
|
|
11
|
-
memory / memory_search / memory_get → QwMemoryCap(转发到 MCP 层)
|
|
12
|
-
qoder_cron → QwCronCap(文件 + 系统 crontab)
|
|
13
|
-
qw_mcp_list/get/call → 走 McpCap(L1)
|
|
14
|
-
delegate_to_im → QwNotifyCap(后续补)
|
|
15
|
-
qoder_show_widget / present_files → QwPresentCap(后续补)
|
|
16
|
-
|
|
17
|
-
本模块实现 QwMemoryCap + QwCronCap,其余后续补。
|
|
18
|
-
"""
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import os
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def is_qoderwork() -> bool:
|
|
25
|
-
"""探测当前是否运行在 QoderWork 宿主内。
|
|
26
|
-
|
|
27
|
-
QoderWork 注入 QODER_SESSION_ID(会话)或 QODER_MCP_AVAILABLE(MCP 网关)。
|
|
28
|
-
"""
|
|
29
|
-
if os.environ.get("QODER_SESSION_ID"):
|
|
30
|
-
return True
|
|
31
|
-
return os.environ.get("QODER_MCP_AVAILABLE", "").lower() in ("1", "true", "yes")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class QwMemoryCap:
|
|
35
|
-
"""QoderWork memory 工具适配器。
|
|
36
|
-
|
|
37
|
-
设计模式: Adapter(QoderWork → MemoryCap 契约)
|
|
38
|
-
|
|
39
|
-
策略: 优先使用 QoderWork 原生 memory 工具,降级到 MCP MemoryProvider。
|
|
40
|
-
当前实现: 转发到 MCP 层的 McpMemoryProvider(接通 kg.duckdb)。
|
|
41
|
-
未来: QoderWork 原生 memory 工具可用时,优先用原生。
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def __init__(self):
|
|
45
|
-
self._env = is_qoderwork()
|
|
46
|
-
self._mcp_provider = None
|
|
47
|
-
|
|
48
|
-
def _get_mcp_provider(self):
|
|
49
|
-
"""延迟获取 MCP MemoryProvider(避免循环导入)。"""
|
|
50
|
-
if self._mcp_provider is None:
|
|
51
|
-
try:
|
|
52
|
-
from .mcp import McpMemoryProvider
|
|
53
|
-
self._mcp_provider = McpMemoryProvider()
|
|
54
|
-
except Exception:
|
|
55
|
-
pass
|
|
56
|
-
return self._mcp_provider
|
|
57
|
-
|
|
58
|
-
@property
|
|
59
|
-
def available(self) -> bool:
|
|
60
|
-
"""QoderWork 环境下可用(降级到 MCP 层)。"""
|
|
61
|
-
if self._env:
|
|
62
|
-
return True
|
|
63
|
-
# 非 QoderWork 环境,看 MCP provider 是否可用
|
|
64
|
-
provider = self._get_mcp_provider()
|
|
65
|
-
return bool(provider and provider.available)
|
|
66
|
-
|
|
67
|
-
def recall(self, query: str) -> list[dict]:
|
|
68
|
-
"""语义检索记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
69
|
-
# TODO: QoderWork 原生 memory_search 工具可用时优先调用
|
|
70
|
-
provider = self._get_mcp_provider()
|
|
71
|
-
if provider and provider.available:
|
|
72
|
-
return provider.recall(query)
|
|
73
|
-
return []
|
|
74
|
-
|
|
75
|
-
def remember(self, key: str, content: str) -> None:
|
|
76
|
-
"""写入记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
77
|
-
# TODO: QoderWork 原生 memory 工具可用时优先调用
|
|
78
|
-
provider = self._get_mcp_provider()
|
|
79
|
-
if provider and provider.available:
|
|
80
|
-
provider.remember(key, content)
|
|
81
|
-
|
|
82
|
-
def get_fact(self, key: str) -> str | None:
|
|
83
|
-
"""按 key 取事实:优先 QoderWork 原生,降级 MCP。"""
|
|
84
|
-
provider = self._get_mcp_provider()
|
|
85
|
-
if provider and provider.available:
|
|
86
|
-
return provider.get_fact(key)
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
def list_recent(self, limit: int = 10) -> list[dict]:
|
|
90
|
-
"""最近 N 条记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
91
|
-
provider = self._get_mcp_provider()
|
|
92
|
-
if provider and provider.available:
|
|
93
|
-
return provider.list_recent(limit)
|
|
94
|
-
return []
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class QwCronCap:
|
|
98
|
-
"""QoderWork cron 工具适配器。
|
|
99
|
-
|
|
100
|
-
设计模式: Adapter(QoderWork → CronCap 契约)
|
|
101
|
-
|
|
102
|
-
当前实现: 使用 common.events 的 cron 注册文件(.qoder/data/cron/)。
|
|
103
|
-
未来: QoderWork 原生 qoder_cron 工具可用时优先用原生。
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
CRON_DIR = ".qoder" + os.sep + "data" + os.sep + "cron"
|
|
107
|
-
|
|
108
|
-
@staticmethod
|
|
109
|
-
def _repo_root():
|
|
110
|
-
"""定位仓库根。
|
|
111
|
-
|
|
112
|
-
⚠ 不能用 Path(__file__).parent^4! qw.py 在 scripts/capability/adapters/ 下
|
|
113
|
-
(scripts 下 3 级), 4 次 parent 解析到 .qoder/ 目录本身 (非仓库根),
|
|
114
|
-
再拼 CRON_DIR('.qoder/data/cron') → .qoder/.qoder/data/cron/ (双重 .qoder)。
|
|
115
|
-
用 foundation.core.paths.get_repo_root() 才对。失败回退向上找 .qoder/scripts。
|
|
116
|
-
"""
|
|
117
|
-
try:
|
|
118
|
-
from foundation.core.paths import get_repo_root
|
|
119
|
-
return get_repo_root()
|
|
120
|
-
except Exception:
|
|
121
|
-
from pathlib import Path
|
|
122
|
-
here = Path(__file__).resolve()
|
|
123
|
-
for _ in range(10):
|
|
124
|
-
if (here / '.qoder' / 'scripts').is_dir():
|
|
125
|
-
return here
|
|
126
|
-
parent = here.parent
|
|
127
|
-
if parent == here:
|
|
128
|
-
break
|
|
129
|
-
here = parent
|
|
130
|
-
return Path(__file__).resolve().parent.parent.parent
|
|
131
|
-
|
|
132
|
-
def __init__(self):
|
|
133
|
-
self._env = is_qoderwork()
|
|
134
|
-
|
|
135
|
-
@property
|
|
136
|
-
def available(self) -> bool:
|
|
137
|
-
"""文件系统可写即可用。"""
|
|
138
|
-
try:
|
|
139
|
-
from pathlib import Path
|
|
140
|
-
repo_root = self._repo_root()
|
|
141
|
-
cron_dir = repo_root / self.CRON_DIR
|
|
142
|
-
cron_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
-
return cron_dir.is_dir()
|
|
144
|
-
except Exception:
|
|
145
|
-
return False
|
|
146
|
-
|
|
147
|
-
def add(self, cron_expr: str, command: str, label: str = "") -> str:
|
|
148
|
-
"""添加定时任务,返回 job_id。"""
|
|
149
|
-
import json
|
|
150
|
-
import time
|
|
151
|
-
from pathlib import Path
|
|
152
|
-
|
|
153
|
-
try:
|
|
154
|
-
repo_root = self._repo_root()
|
|
155
|
-
cron_dir = repo_root / self.CRON_DIR
|
|
156
|
-
cron_dir.mkdir(parents=True, exist_ok=True)
|
|
157
|
-
|
|
158
|
-
job_id = "%s-%s" % (label or "job", int(time.time()))
|
|
159
|
-
job = {
|
|
160
|
-
"id": job_id,
|
|
161
|
-
"cron": cron_expr,
|
|
162
|
-
"command": command,
|
|
163
|
-
"label": label,
|
|
164
|
-
"enabled": True,
|
|
165
|
-
}
|
|
166
|
-
job_file = cron_dir / ("%s.json" % job_id)
|
|
167
|
-
with open(job_file, "w", encoding="utf-8") as f:
|
|
168
|
-
json.dump(job, f, ensure_ascii=False, indent=2)
|
|
169
|
-
return job_id
|
|
170
|
-
except Exception as e:
|
|
171
|
-
return "error: %s" % str(e)[:80]
|
|
172
|
-
|
|
173
|
-
def list(self) -> list[dict]:
|
|
174
|
-
"""列出所有定时任务。"""
|
|
175
|
-
import json
|
|
176
|
-
from pathlib import Path
|
|
177
|
-
|
|
178
|
-
try:
|
|
179
|
-
repo_root = self._repo_root()
|
|
180
|
-
cron_dir = repo_root / self.CRON_DIR
|
|
181
|
-
if not cron_dir.is_dir():
|
|
182
|
-
return []
|
|
183
|
-
jobs = []
|
|
184
|
-
for f in cron_dir.glob("*.json"):
|
|
185
|
-
try:
|
|
186
|
-
with open(f, encoding="utf-8") as fh:
|
|
187
|
-
jobs.append(json.load(fh))
|
|
188
|
-
except Exception:
|
|
189
|
-
continue
|
|
190
|
-
return sorted(jobs, key=lambda j: j.get("id", ""))
|
|
191
|
-
except Exception:
|
|
192
|
-
return []
|
|
193
|
-
|
|
194
|
-
def remove(self, job_id: str) -> bool:
|
|
195
|
-
"""删除定时任务。"""
|
|
196
|
-
from pathlib import Path
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
repo_root = self._repo_root()
|
|
200
|
-
job_file = repo_root / self.CRON_DIR / ("%s.json" % job_id)
|
|
201
|
-
if job_file.is_file():
|
|
202
|
-
job_file.unlink()
|
|
203
|
-
return True
|
|
204
|
-
except Exception:
|
|
205
|
-
pass
|
|
206
|
-
return False
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
class QwPresentCap:
|
|
210
|
-
"""QoderWork 富展示能力适配器。
|
|
211
|
-
|
|
212
|
-
设计模式: Adapter(QoderWork qoder_show_widget → PresentCap 契约)
|
|
213
|
-
|
|
214
|
-
把"渲染交互 HTML 到对话流"从宿主原生工具适配到 PresentCap.widget()。
|
|
215
|
-
降级链: QoderWork 原生 qoder_show_widget → 落盘 HTML 文件(纯 CLI 兜底)。
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
def __init__(self):
|
|
219
|
-
self._env = is_qoderwork()
|
|
220
|
-
|
|
221
|
-
@property
|
|
222
|
-
def available(self) -> bool:
|
|
223
|
-
"""QoderWork 环境且 qoder_show_widget 工具可达时可用。"""
|
|
224
|
-
if not self._env:
|
|
225
|
-
return False
|
|
226
|
-
# 探测宿主工具是否可达 (走 McpCap, 失败则降级)
|
|
227
|
-
try:
|
|
228
|
-
from .mcp import _get_registry
|
|
229
|
-
reg = _get_registry()
|
|
230
|
-
return reg.has_tool("qoder_show_widget")
|
|
231
|
-
except Exception:
|
|
232
|
-
return False
|
|
233
|
-
|
|
234
|
-
def _try_host_widget(self, html: str) -> bool:
|
|
235
|
-
"""尝试调宿主 qoder_show_widget 把 HTML 嵌入对话流。成功返回 True。"""
|
|
236
|
-
try:
|
|
237
|
-
from .mcp import _get_registry
|
|
238
|
-
reg = _get_registry()
|
|
239
|
-
result = reg.call("qoder_show_widget", {"html": html})
|
|
240
|
-
return not result.is_error
|
|
241
|
-
except Exception:
|
|
242
|
-
return False
|
|
243
|
-
|
|
244
|
-
def widget(self, html: str) -> bool:
|
|
245
|
-
"""展示交互 widget。优先嵌入对话流, 降级落盘 HTML 文件。"""
|
|
246
|
-
if self._try_host_widget(html):
|
|
247
|
-
return True
|
|
248
|
-
# 降级: 落盘到 journal (纯 CLI / 工具不可达时)
|
|
249
|
-
try:
|
|
250
|
-
from datetime import datetime
|
|
251
|
-
from pathlib import Path
|
|
252
|
-
repo_root = self._repo_root()
|
|
253
|
-
# 找当前 dev
|
|
254
|
-
dev = "default"
|
|
255
|
-
dev_file = repo_root / ".qoder" / ".developer"
|
|
256
|
-
if dev_file.is_file():
|
|
257
|
-
for line in dev_file.read_text(encoding="utf-8").splitlines():
|
|
258
|
-
if "name=" in line:
|
|
259
|
-
dev = line.split("name=", 1)[1].strip().split()[0]
|
|
260
|
-
break
|
|
261
|
-
out_dir = repo_root / "workspace" / "members" / dev / "journal"
|
|
262
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
-
out = out_dir / ("dashboard-%s.html" % datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
264
|
-
out.write_text(html, encoding="utf-8")
|
|
265
|
-
print("[PresentCap] qoder_show_widget 不可达, 看板已落盘: %s" % out)
|
|
266
|
-
return False # 通知调用方: 未嵌入对话流(但已落盘)
|
|
267
|
-
except Exception:
|
|
268
|
-
return False
|
|
269
|
-
|
|
270
|
-
def table(self, rows: list, headers: list | None = None) -> str:
|
|
271
|
-
"""渲染 markdown 表格 (兜底, 所有宿主都能看)。"""
|
|
272
|
-
if not rows:
|
|
273
|
-
return ""
|
|
274
|
-
headers = headers or (list(rows[0].keys()) if rows and isinstance(rows[0], dict) else [])
|
|
275
|
-
lines = ["| " + " | ".join(str(h) for h in headers) + " |",
|
|
276
|
-
"| " + " | ".join("---" for _ in headers) + " |"]
|
|
277
|
-
for r in rows:
|
|
278
|
-
if isinstance(r, dict):
|
|
279
|
-
lines.append("| " + " | ".join(str(r.get(h, "")) for h in headers) + " |")
|
|
280
|
-
else:
|
|
281
|
-
lines.append("| " + " | ".join(str(x) for x in r) + " |")
|
|
282
|
-
return "\n".join(lines)
|
|
283
|
-
|
|
284
|
-
def files(self, paths: list) -> bool:
|
|
285
|
-
"""以文件卡片展示。降级: 打印路径列表。"""
|
|
286
|
-
try:
|
|
287
|
-
from .mcp import _get_registry
|
|
288
|
-
reg = _get_registry()
|
|
289
|
-
if reg.has_tool("present_files"):
|
|
290
|
-
result = reg.call("present_files", {"paths": paths})
|
|
291
|
-
return not result.is_error
|
|
292
|
-
except Exception:
|
|
293
|
-
pass
|
|
294
|
-
print("[PresentCap] present_files 不可达, 文件列表: " + ", ".join(str(p) for p in paths))
|
|
295
|
-
return False
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
adapters.qw —— QoderWork 原生能力 Adapter。
|
|
4
|
+
|
|
5
|
+
设计模式: Adapter(适配 QoderWork 原生工具到 Cap 契约)
|
|
6
|
+
|
|
7
|
+
实现 QoderWork 提供的 L2 宿主原生能力。其他宿主(ZCode/Codex)不适用,
|
|
8
|
+
未实现的 Cap 由 NoOp 兜底。
|
|
9
|
+
|
|
10
|
+
QoderWork 提供的相关工具(参考):
|
|
11
|
+
memory / memory_search / memory_get → QwMemoryCap(转发到 MCP 层)
|
|
12
|
+
qoder_cron → QwCronCap(文件 + 系统 crontab)
|
|
13
|
+
qw_mcp_list/get/call → 走 McpCap(L1)
|
|
14
|
+
delegate_to_im → QwNotifyCap(后续补)
|
|
15
|
+
qoder_show_widget / present_files → QwPresentCap(后续补)
|
|
16
|
+
|
|
17
|
+
本模块实现 QwMemoryCap + QwCronCap,其余后续补。
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_qoderwork() -> bool:
|
|
25
|
+
"""探测当前是否运行在 QoderWork 宿主内。
|
|
26
|
+
|
|
27
|
+
QoderWork 注入 QODER_SESSION_ID(会话)或 QODER_MCP_AVAILABLE(MCP 网关)。
|
|
28
|
+
"""
|
|
29
|
+
if os.environ.get("QODER_SESSION_ID"):
|
|
30
|
+
return True
|
|
31
|
+
return os.environ.get("QODER_MCP_AVAILABLE", "").lower() in ("1", "true", "yes")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QwMemoryCap:
|
|
35
|
+
"""QoderWork memory 工具适配器。
|
|
36
|
+
|
|
37
|
+
设计模式: Adapter(QoderWork → MemoryCap 契约)
|
|
38
|
+
|
|
39
|
+
策略: 优先使用 QoderWork 原生 memory 工具,降级到 MCP MemoryProvider。
|
|
40
|
+
当前实现: 转发到 MCP 层的 McpMemoryProvider(接通 kg.duckdb)。
|
|
41
|
+
未来: QoderWork 原生 memory 工具可用时,优先用原生。
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self._env = is_qoderwork()
|
|
46
|
+
self._mcp_provider = None
|
|
47
|
+
|
|
48
|
+
def _get_mcp_provider(self):
|
|
49
|
+
"""延迟获取 MCP MemoryProvider(避免循环导入)。"""
|
|
50
|
+
if self._mcp_provider is None:
|
|
51
|
+
try:
|
|
52
|
+
from .mcp import McpMemoryProvider
|
|
53
|
+
self._mcp_provider = McpMemoryProvider()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return self._mcp_provider
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def available(self) -> bool:
|
|
60
|
+
"""QoderWork 环境下可用(降级到 MCP 层)。"""
|
|
61
|
+
if self._env:
|
|
62
|
+
return True
|
|
63
|
+
# 非 QoderWork 环境,看 MCP provider 是否可用
|
|
64
|
+
provider = self._get_mcp_provider()
|
|
65
|
+
return bool(provider and provider.available)
|
|
66
|
+
|
|
67
|
+
def recall(self, query: str) -> list[dict]:
|
|
68
|
+
"""语义检索记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
69
|
+
# TODO: QoderWork 原生 memory_search 工具可用时优先调用
|
|
70
|
+
provider = self._get_mcp_provider()
|
|
71
|
+
if provider and provider.available:
|
|
72
|
+
return provider.recall(query)
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
def remember(self, key: str, content: str) -> None:
|
|
76
|
+
"""写入记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
77
|
+
# TODO: QoderWork 原生 memory 工具可用时优先调用
|
|
78
|
+
provider = self._get_mcp_provider()
|
|
79
|
+
if provider and provider.available:
|
|
80
|
+
provider.remember(key, content)
|
|
81
|
+
|
|
82
|
+
def get_fact(self, key: str) -> str | None:
|
|
83
|
+
"""按 key 取事实:优先 QoderWork 原生,降级 MCP。"""
|
|
84
|
+
provider = self._get_mcp_provider()
|
|
85
|
+
if provider and provider.available:
|
|
86
|
+
return provider.get_fact(key)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def list_recent(self, limit: int = 10) -> list[dict]:
|
|
90
|
+
"""最近 N 条记忆:优先 QoderWork 原生,降级 MCP。"""
|
|
91
|
+
provider = self._get_mcp_provider()
|
|
92
|
+
if provider and provider.available:
|
|
93
|
+
return provider.list_recent(limit)
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class QwCronCap:
|
|
98
|
+
"""QoderWork cron 工具适配器。
|
|
99
|
+
|
|
100
|
+
设计模式: Adapter(QoderWork → CronCap 契约)
|
|
101
|
+
|
|
102
|
+
当前实现: 使用 common.events 的 cron 注册文件(.qoder/data/cron/)。
|
|
103
|
+
未来: QoderWork 原生 qoder_cron 工具可用时优先用原生。
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
CRON_DIR = ".qoder" + os.sep + "data" + os.sep + "cron"
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _repo_root():
|
|
110
|
+
"""定位仓库根。
|
|
111
|
+
|
|
112
|
+
⚠ 不能用 Path(__file__).parent^4! qw.py 在 scripts/capability/adapters/ 下
|
|
113
|
+
(scripts 下 3 级), 4 次 parent 解析到 .qoder/ 目录本身 (非仓库根),
|
|
114
|
+
再拼 CRON_DIR('.qoder/data/cron') → .qoder/.qoder/data/cron/ (双重 .qoder)。
|
|
115
|
+
用 foundation.core.paths.get_repo_root() 才对。失败回退向上找 .qoder/scripts。
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
from foundation.core.paths import get_repo_root
|
|
119
|
+
return get_repo_root()
|
|
120
|
+
except Exception:
|
|
121
|
+
from pathlib import Path
|
|
122
|
+
here = Path(__file__).resolve()
|
|
123
|
+
for _ in range(10):
|
|
124
|
+
if (here / '.qoder' / 'scripts').is_dir():
|
|
125
|
+
return here
|
|
126
|
+
parent = here.parent
|
|
127
|
+
if parent == here:
|
|
128
|
+
break
|
|
129
|
+
here = parent
|
|
130
|
+
return Path(__file__).resolve().parent.parent.parent
|
|
131
|
+
|
|
132
|
+
def __init__(self):
|
|
133
|
+
self._env = is_qoderwork()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def available(self) -> bool:
|
|
137
|
+
"""文件系统可写即可用。"""
|
|
138
|
+
try:
|
|
139
|
+
from pathlib import Path
|
|
140
|
+
repo_root = self._repo_root()
|
|
141
|
+
cron_dir = repo_root / self.CRON_DIR
|
|
142
|
+
cron_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
return cron_dir.is_dir()
|
|
144
|
+
except Exception:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def add(self, cron_expr: str, command: str, label: str = "") -> str:
|
|
148
|
+
"""添加定时任务,返回 job_id。"""
|
|
149
|
+
import json
|
|
150
|
+
import time
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
repo_root = self._repo_root()
|
|
155
|
+
cron_dir = repo_root / self.CRON_DIR
|
|
156
|
+
cron_dir.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
job_id = "%s-%s" % (label or "job", int(time.time()))
|
|
159
|
+
job = {
|
|
160
|
+
"id": job_id,
|
|
161
|
+
"cron": cron_expr,
|
|
162
|
+
"command": command,
|
|
163
|
+
"label": label,
|
|
164
|
+
"enabled": True,
|
|
165
|
+
}
|
|
166
|
+
job_file = cron_dir / ("%s.json" % job_id)
|
|
167
|
+
with open(job_file, "w", encoding="utf-8") as f:
|
|
168
|
+
json.dump(job, f, ensure_ascii=False, indent=2)
|
|
169
|
+
return job_id
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return "error: %s" % str(e)[:80]
|
|
172
|
+
|
|
173
|
+
def list(self) -> list[dict]:
|
|
174
|
+
"""列出所有定时任务。"""
|
|
175
|
+
import json
|
|
176
|
+
from pathlib import Path
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
repo_root = self._repo_root()
|
|
180
|
+
cron_dir = repo_root / self.CRON_DIR
|
|
181
|
+
if not cron_dir.is_dir():
|
|
182
|
+
return []
|
|
183
|
+
jobs = []
|
|
184
|
+
for f in cron_dir.glob("*.json"):
|
|
185
|
+
try:
|
|
186
|
+
with open(f, encoding="utf-8") as fh:
|
|
187
|
+
jobs.append(json.load(fh))
|
|
188
|
+
except Exception:
|
|
189
|
+
continue
|
|
190
|
+
return sorted(jobs, key=lambda j: j.get("id", ""))
|
|
191
|
+
except Exception:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
def remove(self, job_id: str) -> bool:
|
|
195
|
+
"""删除定时任务。"""
|
|
196
|
+
from pathlib import Path
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
repo_root = self._repo_root()
|
|
200
|
+
job_file = repo_root / self.CRON_DIR / ("%s.json" % job_id)
|
|
201
|
+
if job_file.is_file():
|
|
202
|
+
job_file.unlink()
|
|
203
|
+
return True
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class QwPresentCap:
|
|
210
|
+
"""QoderWork 富展示能力适配器。
|
|
211
|
+
|
|
212
|
+
设计模式: Adapter(QoderWork qoder_show_widget → PresentCap 契约)
|
|
213
|
+
|
|
214
|
+
把"渲染交互 HTML 到对话流"从宿主原生工具适配到 PresentCap.widget()。
|
|
215
|
+
降级链: QoderWork 原生 qoder_show_widget → 落盘 HTML 文件(纯 CLI 兜底)。
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(self):
|
|
219
|
+
self._env = is_qoderwork()
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def available(self) -> bool:
|
|
223
|
+
"""QoderWork 环境且 qoder_show_widget 工具可达时可用。"""
|
|
224
|
+
if not self._env:
|
|
225
|
+
return False
|
|
226
|
+
# 探测宿主工具是否可达 (走 McpCap, 失败则降级)
|
|
227
|
+
try:
|
|
228
|
+
from .mcp import _get_registry
|
|
229
|
+
reg = _get_registry()
|
|
230
|
+
return reg.has_tool("qoder_show_widget")
|
|
231
|
+
except Exception:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def _try_host_widget(self, html: str) -> bool:
|
|
235
|
+
"""尝试调宿主 qoder_show_widget 把 HTML 嵌入对话流。成功返回 True。"""
|
|
236
|
+
try:
|
|
237
|
+
from .mcp import _get_registry
|
|
238
|
+
reg = _get_registry()
|
|
239
|
+
result = reg.call("qoder_show_widget", {"html": html})
|
|
240
|
+
return not result.is_error
|
|
241
|
+
except Exception:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def widget(self, html: str) -> bool:
|
|
245
|
+
"""展示交互 widget。优先嵌入对话流, 降级落盘 HTML 文件。"""
|
|
246
|
+
if self._try_host_widget(html):
|
|
247
|
+
return True
|
|
248
|
+
# 降级: 落盘到 journal (纯 CLI / 工具不可达时)
|
|
249
|
+
try:
|
|
250
|
+
from datetime import datetime
|
|
251
|
+
from pathlib import Path
|
|
252
|
+
repo_root = self._repo_root()
|
|
253
|
+
# 找当前 dev
|
|
254
|
+
dev = "default"
|
|
255
|
+
dev_file = repo_root / ".qoder" / ".developer"
|
|
256
|
+
if dev_file.is_file():
|
|
257
|
+
for line in dev_file.read_text(encoding="utf-8").splitlines():
|
|
258
|
+
if "name=" in line:
|
|
259
|
+
dev = line.split("name=", 1)[1].strip().split()[0]
|
|
260
|
+
break
|
|
261
|
+
out_dir = repo_root / "workspace" / "members" / dev / "journal"
|
|
262
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
out = out_dir / ("dashboard-%s.html" % datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
264
|
+
out.write_text(html, encoding="utf-8")
|
|
265
|
+
print("[PresentCap] qoder_show_widget 不可达, 看板已落盘: %s" % out)
|
|
266
|
+
return False # 通知调用方: 未嵌入对话流(但已落盘)
|
|
267
|
+
except Exception:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
def table(self, rows: list, headers: list | None = None) -> str:
|
|
271
|
+
"""渲染 markdown 表格 (兜底, 所有宿主都能看)。"""
|
|
272
|
+
if not rows:
|
|
273
|
+
return ""
|
|
274
|
+
headers = headers or (list(rows[0].keys()) if rows and isinstance(rows[0], dict) else [])
|
|
275
|
+
lines = ["| " + " | ".join(str(h) for h in headers) + " |",
|
|
276
|
+
"| " + " | ".join("---" for _ in headers) + " |"]
|
|
277
|
+
for r in rows:
|
|
278
|
+
if isinstance(r, dict):
|
|
279
|
+
lines.append("| " + " | ".join(str(r.get(h, "")) for h in headers) + " |")
|
|
280
|
+
else:
|
|
281
|
+
lines.append("| " + " | ".join(str(x) for x in r) + " |")
|
|
282
|
+
return "\n".join(lines)
|
|
283
|
+
|
|
284
|
+
def files(self, paths: list) -> bool:
|
|
285
|
+
"""以文件卡片展示。降级: 打印路径列表。"""
|
|
286
|
+
try:
|
|
287
|
+
from .mcp import _get_registry
|
|
288
|
+
reg = _get_registry()
|
|
289
|
+
if reg.has_tool("present_files"):
|
|
290
|
+
result = reg.call("present_files", {"paths": paths})
|
|
291
|
+
return not result.is_error
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
print("[PresentCap] present_files 不可达, 文件列表: " + ", ".join(str(p) for p in paths))
|
|
295
|
+
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
capability.caps —— L2 宿主原生能力契约层。
|
|
4
4
|
|
|
5
5
|
每个能力 = Protocol(契约)+ NoOp(默认空实现)。
|
|
6
6
|
新增能力(扩展场景 B)= 新建一个文件定义 Protocol + NoOp,在此导出,
|
|
@@ -11,9 +11,9 @@ from .repo import RepoCap, NoOpRepoCap
|
|
|
11
11
|
from .identity import IdentityCap, NoOpIdentityCap
|
|
12
12
|
from .context import ContextCap, NoOpContextCap
|
|
13
13
|
from .cron import CronCap, NoOpCronCap
|
|
14
|
-
from .notify import NotifyCap, NoOpNotifyCap
|
|
14
|
+
from .notify import NotifyCap, NoOpNotifyCap, WebhookNotifyCap, default_notify
|
|
15
15
|
from .present import PresentCap, NoOpPresentCap
|
|
16
|
-
from .sandbox import SandboxCap, NoOpSandboxCap
|
|
16
|
+
from .sandbox import SandboxCap, NoOpSandboxCap, SubprocessSandboxCap, default_sandbox
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"MemoryCap", "NoOpMemoryCap",
|
|
@@ -21,7 +21,7 @@ __all__ = [
|
|
|
21
21
|
"IdentityCap", "NoOpIdentityCap",
|
|
22
22
|
"ContextCap", "NoOpContextCap",
|
|
23
23
|
"CronCap", "NoOpCronCap",
|
|
24
|
-
"NotifyCap", "NoOpNotifyCap",
|
|
24
|
+
"NotifyCap", "NoOpNotifyCap", "WebhookNotifyCap", "default_notify",
|
|
25
25
|
"PresentCap", "NoOpPresentCap",
|
|
26
|
-
"SandboxCap", "NoOpSandboxCap",
|
|
26
|
+
"SandboxCap", "NoOpSandboxCap", "SubprocessSandboxCap", "default_sandbox",
|
|
27
27
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|