@hupan56/wlkj 3.1.9 → 3.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hupan56/wlkj",
3
- "version": "3.1.9",
3
+ "version": "3.1.11",
4
4
  "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
5
  "bin": {
6
6
  "wlkj": "bin/cli.js"
@@ -16,22 +16,24 @@ developer_instructions = """
16
16
  如果没有,扫描 `workspace/specs/` 目录找到最新的未生成 Spec 的 PRD。
17
17
 
18
18
  ### Step 2: 读取上下文
19
- 必须读取以下文件:
20
- 1. PRD 文件 — 需求来源
21
- 2. `.qoder/rules/spec-template.md` — Spec 结构模板
22
- 3. `.qoder/rules/code-style.md` — 命名规范
23
- 4. `.qoder/context/architecture.md` 项目架构
24
- 5. `.qoder/context/data-dictionary.md` — 数据字典(如存在)
19
+ 必须读取以下文件(**注意:这些是真实存在的路径,旧版引用的 rules/spec-template.md 等 4 个文件已不存在**):
20
+ 1. PRD 文件 — 需求来源(drafts 里的 `REQ-*.md` 或 `data/docs/prd/REQ-*.md`)
21
+ 2. `.qoder/skills/spec-generator/SKILL.md` — Spec 结构模板与生成流程(权威源)
22
+ 3. `.qoder/contracts/spec.md` — Spec 模块契约(字段/格式/贯穿检索要求)
23
+ 4. `data/docs/architecture/` 下的架构文档(如 `kg-layers.md`)— 项目架构
24
+ 5. `data/docs/constitution/eva-metrics.md` — 质量度量(Spec 要对齐验收标准)
25
25
 
26
26
  ### Step 3: 生成 Spec
27
- 按 spec-template.md 的结构,从 PRD 提取:
27
+ SKILL.md + spec.md 契约的结构,从 PRD 提取:
28
28
  - 每个用户故事 → API 接口
29
29
  - 验收标准 → 接口输入/输出/异常
30
30
  - 数据需求 → 数据库表设计
31
31
  - 非功能需求 → 约束章节
32
32
 
33
33
  ### Step 4: 输出
34
- 写入 `workspace/specs/REQ-{ID}-{模块名}.md`
34
+ 写入 `workspace/specs/REQ-{ID}-{module}.spec.md`(**文件名必须带 `.spec.md` 后缀**,
35
+ 与 `.qoder/contracts/spec.md` 契约一致 —— 旧版写成 `REQ-{ID}-{模块名}.md` 会导致
36
+ spec→code→test 贯穿检索断链)。
35
37
  完成后汇报:文件列表、与 PRD 的映射关系、需要开发关注的点。
36
38
 
37
39
  ## 规则
@@ -10,12 +10,12 @@ You are the spec-generator. Do NOT spawn another spec-generator.
10
10
  Generate technical Spec from PRD + Design.
11
11
 
12
12
  ## Workflow
13
- 1. Read PRD from workspace/specs/prd/ or task dir
14
- 2. Read design artifacts from workspace/tasks/{id}/design/
15
- 3. Read team rules from workspace/constitution/
16
- 4. Generate spec.md covering:
13
+ 1. Read PRD 先找 `workspace/members/{dev}/drafts/REQ-*.md`,再找 `data/docs/prd/REQ-*.md`,最后 `workspace/specs/prd/`
14
+ 2. Read design artifacts from `workspace/tasks/{id}/design/` (若存在)
15
+ 3. Read team rules from `.qoder/skills/spec-generator/SKILL.md` + `.qoder/contracts/spec.md` (权威源);`workspace/constitution/` 有则参考
16
+ 4. Generate spec covering:
17
17
  - Architecture changes
18
18
  - Data model changes
19
19
  - API changes
20
20
  - Implementation plan
21
- 5. Save to workspace/specs/ or workspace/tasks/{id}/spec.md
21
+ 5. Save to `workspace/specs/REQ-{ID}-{module}.spec.md` (文件名带 `.spec.md` 后缀,与 contracts/spec.md 契约一致,保证 spec→code→test 贯穿检索)
@@ -1,271 +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
- def __init__(self):
109
- self._env = is_qoderwork()
110
-
111
- @property
112
- def available(self) -> bool:
113
- """文件系统可写即可用。"""
114
- try:
115
- from pathlib import Path
116
- repo_root = Path(__file__).resolve().parent.parent.parent.parent
117
- cron_dir = repo_root / self.CRON_DIR
118
- cron_dir.mkdir(parents=True, exist_ok=True)
119
- return cron_dir.is_dir()
120
- except Exception:
121
- return False
122
-
123
- def add(self, cron_expr: str, command: str, label: str = "") -> str:
124
- """添加定时任务,返回 job_id。"""
125
- import json
126
- import time
127
- from pathlib import Path
128
-
129
- try:
130
- repo_root = Path(__file__).resolve().parent.parent.parent.parent
131
- cron_dir = repo_root / self.CRON_DIR
132
- cron_dir.mkdir(parents=True, exist_ok=True)
133
-
134
- job_id = "%s-%s" % (label or "job", int(time.time()))
135
- job = {
136
- "id": job_id,
137
- "cron": cron_expr,
138
- "command": command,
139
- "label": label,
140
- "enabled": True,
141
- }
142
- job_file = cron_dir / ("%s.json" % job_id)
143
- with open(job_file, "w", encoding="utf-8") as f:
144
- json.dump(job, f, ensure_ascii=False, indent=2)
145
- return job_id
146
- except Exception as e:
147
- return "error: %s" % str(e)[:80]
148
-
149
- def list(self) -> list[dict]:
150
- """列出所有定时任务。"""
151
- import json
152
- from pathlib import Path
153
-
154
- try:
155
- repo_root = Path(__file__).resolve().parent.parent.parent.parent
156
- cron_dir = repo_root / self.CRON_DIR
157
- if not cron_dir.is_dir():
158
- return []
159
- jobs = []
160
- for f in cron_dir.glob("*.json"):
161
- try:
162
- with open(f, encoding="utf-8") as fh:
163
- jobs.append(json.load(fh))
164
- except Exception:
165
- continue
166
- return sorted(jobs, key=lambda j: j.get("id", ""))
167
- except Exception:
168
- return []
169
-
170
- def remove(self, job_id: str) -> bool:
171
- """删除定时任务。"""
172
- from pathlib import Path
173
-
174
- try:
175
- repo_root = Path(__file__).resolve().parent.parent.parent.parent
176
- job_file = repo_root / self.CRON_DIR / ("%s.json" % job_id)
177
- if job_file.is_file():
178
- job_file.unlink()
179
- return True
180
- except Exception:
181
- pass
182
- return False
183
-
184
-
185
- class QwPresentCap:
186
- """QoderWork 富展示能力适配器。
187
-
188
- 设计模式: Adapter(QoderWork qoder_show_widget → PresentCap 契约)
189
-
190
- 把"渲染交互 HTML 到对话流"从宿主原生工具适配到 PresentCap.widget()
191
- 降级链: QoderWork 原生 qoder_show_widget → 落盘 HTML 文件(纯 CLI 兜底)。
192
- """
193
-
194
- def __init__(self):
195
- self._env = is_qoderwork()
196
-
197
- @property
198
- def available(self) -> bool:
199
- """QoderWork 环境且 qoder_show_widget 工具可达时可用。"""
200
- if not self._env:
201
- return False
202
- # 探测宿主工具是否可达 (走 McpCap, 失败则降级)
203
- try:
204
- from .mcp import _get_registry
205
- reg = _get_registry()
206
- return reg.has_tool("qoder_show_widget")
207
- except Exception:
208
- return False
209
-
210
- def _try_host_widget(self, html: str) -> bool:
211
- """尝试调宿主 qoder_show_widget 把 HTML 嵌入对话流。成功返回 True。"""
212
- try:
213
- from .mcp import _get_registry
214
- reg = _get_registry()
215
- result = reg.call("qoder_show_widget", {"html": html})
216
- return not result.is_error
217
- except Exception:
218
- return False
219
-
220
- def widget(self, html: str) -> bool:
221
- """展示交互 widget。优先嵌入对话流, 降级落盘 HTML 文件。"""
222
- if self._try_host_widget(html):
223
- return True
224
- # 降级: 落盘到 journal (纯 CLI / 工具不可达时)
225
- try:
226
- from datetime import datetime
227
- from pathlib import Path
228
- repo_root = Path(__file__).resolve().parent.parent.parent.parent
229
- # 找当前 dev
230
- dev = "default"
231
- dev_file = repo_root / ".qoder" / ".developer"
232
- if dev_file.is_file():
233
- for line in dev_file.read_text(encoding="utf-8").splitlines():
234
- if "name=" in line:
235
- dev = line.split("name=", 1)[1].strip().split()[0]
236
- break
237
- out_dir = repo_root / "workspace" / "members" / dev / "journal"
238
- out_dir.mkdir(parents=True, exist_ok=True)
239
- out = out_dir / ("dashboard-%s.html" % datetime.now().strftime("%Y%m%d-%H%M%S"))
240
- out.write_text(html, encoding="utf-8")
241
- print("[PresentCap] qoder_show_widget 不可达, 看板已落盘: %s" % out)
242
- return False # 通知调用方: 未嵌入对话流(但已落盘)
243
- except Exception:
244
- return False
245
-
246
- def table(self, rows: list, headers: list | None = None) -> str:
247
- """渲染 markdown 表格 (兜底, 所有宿主都能看)。"""
248
- if not rows:
249
- return ""
250
- headers = headers or (list(rows[0].keys()) if rows and isinstance(rows[0], dict) else [])
251
- lines = ["| " + " | ".join(str(h) for h in headers) + " |",
252
- "| " + " | ".join("---" for _ in headers) + " |"]
253
- for r in rows:
254
- if isinstance(r, dict):
255
- lines.append("| " + " | ".join(str(r.get(h, "")) for h in headers) + " |")
256
- else:
257
- lines.append("| " + " | ".join(str(x) for x in r) + " |")
258
- return "\n".join(lines)
259
-
260
- def files(self, paths: list) -> bool:
261
- """以文件卡片展示。降级: 打印路径列表。"""
262
- try:
263
- from .mcp import _get_registry
264
- reg = _get_registry()
265
- if reg.has_tool("present_files"):
266
- result = reg.call("present_files", {"paths": paths})
267
- return not result.is_error
268
- except Exception:
269
- pass
270
- print("[PresentCap] present_files 不可达, 文件列表: " + ", ".join(str(p) for p in paths))
271
- 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
@@ -198,7 +198,7 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
198
198
  }, "required": ["module"]})
199
199
  def _t_get_workflow(self, args):
200
200
  from domain.kg.kg_capabilities import workflow_query
201
- from domain.kg.storage.kg_duckdb import get_db
201
+ from domain.kg.storage.kg_duckdb import get_db_readonly
202
202
  q = (args.get("module") or '').strip()
203
203
  if not q:
204
204
  return "请提供模块名。可选: assets/cost/hr/maintenance/operation/qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe"
@@ -207,7 +207,9 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
207
207
  _hint = _require_kgdb()
208
208
  if _hint:
209
209
  return _hint
210
- con = get_db()
210
+ con = get_db_readonly()
211
+ if con is None:
212
+ return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
211
213
  try:
212
214
  return workflow_query(con, q)
213
215
  finally:
@@ -215,14 +217,14 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
215
217
  except Exception: pass
216
218
 
217
219
  @tool("multi_hop",
218
- "知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳, 找出全部可达实体(页面→接口→控制器→测试)。知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转entity_id)。例: multi_hop(symbol='资产管理') 或 multi_hop(symbol='/asset/list', max_depth=3)",
220
+ "知识图谱多跳遍历: 从一个符号出发, 沿edges递归N跳, 找出全部可达实体(页面→接口→控制器→测试)。知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转entity_id)。例: multi_hop(symbol='资产管理') 或 multi_hop(symbol='handleExport', max_depth=3)",
219
221
  {"type": "object", "properties": {
220
222
  "symbol": {"type": "string", "description": "起点符号: 可传entity_id、别名(资产管理)、端点(/asset/list)或函数名"},
221
223
  "max_depth": {"type": "integer", "default": 3, "description": "最大跳数(1-5), 默认3。越大越全但越慢"},
222
224
  }, "required": ["symbol"]})
223
225
  def _t_multi_hop(self, args):
224
226
  from domain.kg.kg_capabilities import hop_query
225
- from domain.kg.storage.kg_duckdb import get_db, multi_hop
227
+ from domain.kg.storage.kg_duckdb import get_db_readonly, multi_hop
226
228
  sym = (args.get("symbol") or '').strip()
227
229
  if not sym:
228
230
  return "请提供起点符号 (entity_id / 端点 / 函数名 / 中文别名)"
@@ -234,7 +236,9 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
234
236
  _hint = _require_kgdb()
235
237
  if _hint:
236
238
  return _hint
237
- con = get_db()
239
+ con = get_db_readonly()
240
+ if con is None:
241
+ return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
238
242
  try:
239
243
  return hop_query(con, sym, depth, multi_hop_fn=multi_hop)
240
244
  finally:
@@ -259,12 +263,14 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
259
263
  "field": {"type": "string", "description": "字段名(英文field或中文标题)"},
260
264
  }, "required": ["field"]})
261
265
  def _t_search_field(self, args):
262
- from domain.kg.storage.kg_duckdb import get_db
266
+ from domain.kg.storage.kg_duckdb import get_db_readonly
263
267
  field = (args.get("field") or '').strip()
264
268
  _hint = _require_kgdb()
265
269
  if _hint:
266
270
  return _hint
267
- con = get_db()
271
+ con = get_db_readonly()
272
+ if con is None:
273
+ return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
268
274
  try:
269
275
  rows = con.execute(
270
276
  "SELECT field, title, count, sample_files FROM fields "
@@ -286,12 +292,14 @@ class KnowledgeGraphMCPServer(BaseMCPServer):
286
292
  "platform": {"type": "string", "enum": ["web", "app"], "description": "平台过滤。可选"},
287
293
  }})
288
294
  def _t_list_modules(self, args):
289
- from domain.kg.storage.kg_duckdb import get_db
295
+ from domain.kg.storage.kg_duckdb import get_db_readonly
290
296
  platform = args.get("platform", "")
291
297
  _hint = _require_kgdb()
292
298
  if _hint:
293
299
  return _hint
294
- con = get_db()
300
+ con = get_db_readonly()
301
+ if con is None:
302
+ return "知识图谱暂不可用 (kg.duckdb 连接失败), 稍后重试或联系管理员。"
295
303
  try:
296
304
  if platform == 'web':
297
305
  rows = con.execute("SELECT dir, cn, project FROM modules WHERE project LIKE '%fywl%' ORDER BY dir").fetchall()