@hupan56/wlkj 3.1.32 → 3.3.0
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 +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +4 -4
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +23 -3
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +138 -7
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +70 -6
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +230 -15
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +3 -3
- package/templates/qoder/commands/wl-test.md +17 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +116 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +61 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- package/templates/qoder/scripts/capability/__pycache__/__init__.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 +1 -1
- 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/registry.py +21 -23
- package/templates/qoder/scripts/capability/registry_mcp.py +69 -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/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -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 +392 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- 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/extract.py.bak +430 -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/extract/ts_extract.py +111 -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/storage/kg_duckdb.py +43 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
- 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/core/paths.py +102 -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 +24 -11
- 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/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +41 -14
- package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
|
@@ -107,6 +107,42 @@ def _dev():
|
|
|
107
107
|
return None
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def _auto_learn_from_prd(prd_path):
|
|
111
|
+
"""PRD 写完后自动调 MCP learn 沉淀业务规则(宿主能力接入)。
|
|
112
|
+
|
|
113
|
+
读 PRD 标题 → 调工作台 MCP learn → 知识层自动积累。
|
|
114
|
+
失败静默(铁律:hook 崩溃不阻塞会话)。
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
title = os.path.basename(prd_path).replace('.md', '')
|
|
118
|
+
# 读 mcp_config 拿 token
|
|
119
|
+
cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
|
|
120
|
+
if not os.path.isfile(cfg_path):
|
|
121
|
+
return
|
|
122
|
+
import json as _j
|
|
123
|
+
cfg = _j.load(open(cfg_path, encoding='utf-8'))
|
|
124
|
+
srv = cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
|
|
125
|
+
token = srv.get('token', '')
|
|
126
|
+
url = srv.get('url', '').replace('/mcp', '/mcp') # 保持 /mcp
|
|
127
|
+
pid = srv.get('env', {}).get('WLKJ_PROJECT_ID', '')
|
|
128
|
+
if not token or not url or not pid:
|
|
129
|
+
return
|
|
130
|
+
# 调 MCP learn(POST /mcp JSON-RPC)
|
|
131
|
+
import urllib.request
|
|
132
|
+
rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
133
|
+
"params": {"name": "learn",
|
|
134
|
+
"arguments": {"project_id": pid, "category": "decision",
|
|
135
|
+
"pattern": "PRD更新: " + title,
|
|
136
|
+
"source": "qoder-hook-auto"}}}
|
|
137
|
+
req = urllib.request.Request(
|
|
138
|
+
url, data=_j.dumps(rpc).encode(),
|
|
139
|
+
headers={"Content-Type": "application/json", "X-MCP-Token": token},
|
|
140
|
+
method="POST")
|
|
141
|
+
urllib.request.urlopen(req, timeout=5).read() # 5s 超时,不阻塞
|
|
142
|
+
except Exception:
|
|
143
|
+
pass # 铁律:静默
|
|
144
|
+
|
|
145
|
+
|
|
110
146
|
def _append_event(event, data):
|
|
111
147
|
"""best-effort append 一条事件到当前 dev 的 feedback.jsonl。避开 learn import 断链, 直接写文件。"""
|
|
112
148
|
try:
|
|
@@ -128,10 +164,30 @@ def _append_event(event, data):
|
|
|
128
164
|
pass # 铁律: 埋点失败绝不阻塞
|
|
129
165
|
|
|
130
166
|
|
|
167
|
+
def _log_tool_call(tool_name):
|
|
168
|
+
"""轻量记录每次工具调用到独立日志 tool-calls.jsonl(不混入 feedback.jsonl)。
|
|
169
|
+
|
|
170
|
+
为'思考链过长/调用过多'校准积累数据:事后按时间窗口聚合算每 turn 调用次数分布。
|
|
171
|
+
纯统计,不阻断不提示。turn 内累计留给分析层算(避免跨进程状态机复杂化)。
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
dev = _dev() or 'unknown'
|
|
175
|
+
log_file = os.path.join(BASE, 'workspace', 'members', dev, 'journal', 'tool-calls.jsonl')
|
|
176
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
177
|
+
with open(log_file, 'a', encoding='utf-8') as f:
|
|
178
|
+
f.write(json.dumps({
|
|
179
|
+
'ts': datetime.now().isoformat(timespec='seconds'),
|
|
180
|
+
'tool': (tool_name or 'unknown').lower(),
|
|
181
|
+
}, ensure_ascii=False) + '\n')
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
131
186
|
def main():
|
|
132
187
|
try:
|
|
133
188
|
tool_input, tool_name = _read_tool_info()
|
|
134
189
|
tn = (tool_name or '').lower()
|
|
190
|
+
_log_tool_call(tool_name) # 轻量调用计数(独立日志,不阻断), 为'思考链过长'校准积累数据
|
|
135
191
|
|
|
136
192
|
# 1. Write/Edit 落到 PRD / task.json → 埋点
|
|
137
193
|
if tn in WRITE_TOOLS or tn == 'bash':
|
|
@@ -140,8 +196,13 @@ def main():
|
|
|
140
196
|
plow = p.lower().replace('\\', '/')
|
|
141
197
|
if 'prd' in os.path.basename(plow).lower() and plow.endswith('.md'):
|
|
142
198
|
_append_event('prd_written', {'prd': os.path.basename(p), 'tool': tool_name})
|
|
199
|
+
# ★ 宿主能力接入:PRD 写完 → 自动调 MCP learn 沉淀业务规则
|
|
200
|
+
_auto_learn_from_prd(p)
|
|
143
201
|
if plow.endswith('task.json'):
|
|
144
202
|
_append_event('task_touched', {'task_json': os.path.basename(os.path.dirname(p)), 'tool': tool_name})
|
|
203
|
+
# ★ 宿主能力接入:代码文件改完 → 自动记 code_changed(知识层下次检测过时用)
|
|
204
|
+
if any(plow.endswith(ext) for ext in ('.java', '.vue', '.ts', '.js', '.py')):
|
|
205
|
+
_append_event('code_changed', {'file': os.path.basename(p), 'path': plow[:200]})
|
|
145
206
|
# v3.1 软提示: prototype HTML 写完但缺来源锚定 → stderr 提醒(不阻断)
|
|
146
207
|
if 'prototype' in os.path.basename(plow) and plow.endswith('.html'):
|
|
147
208
|
try:
|
|
@@ -134,7 +134,7 @@ def get_index_info():
|
|
|
134
134
|
pass
|
|
135
135
|
|
|
136
136
|
lines.append('## Knowledge Graph')
|
|
137
|
-
lines.append('MCP工具 (QoderWork自动调用): search_code / get_impact /
|
|
137
|
+
lines.append('MCP工具 (Qoder/QoderWork自动调用): search_code / rag_search / get_impact / project_brief / graphrag_ask 等43个')
|
|
138
138
|
lines.append(' python .qoder/scripts/orchestration/wlkj.py search <keyword> [--platform web|app]')
|
|
139
139
|
lines.append(' python .qoder/scripts/orchestration/wlkj.py context <keyword> # 一次打包全部上下文')
|
|
140
140
|
|
|
@@ -149,72 +149,40 @@ def get_index_info():
|
|
|
149
149
|
role = _line.split('=', 1)[1].strip()
|
|
150
150
|
except Exception:
|
|
151
151
|
pass
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# 管理员/开发: 能构建
|
|
184
|
-
if code_newer or db_age_hours > 168: # 7天 = 168小时
|
|
185
|
-
lines.append(' [WARN] 知识图谱过期! 代码有新变更.')
|
|
186
|
-
lines.append(' → 管理员请运行: python .qoder/scripts/kg/kg_build.py')
|
|
187
|
-
lines.append(' → 完成后: python .qoder/scripts/orchestration/wlkj.py sync push (共享给团队)')
|
|
188
|
-
else:
|
|
189
|
-
lines.append(' ✓ 知识图谱最新')
|
|
190
|
-
else:
|
|
191
|
-
# 非管理员: 只拉取
|
|
192
|
-
if code_newer or db_age_hours > 168:
|
|
193
|
-
lines.append(' [WARN] 知识图谱可能过期.')
|
|
194
|
-
lines.append(' → 运行: python .qoder/scripts/orchestration/wlkj.py sync pull (获取管理员最新构建)')
|
|
195
|
-
else:
|
|
196
|
-
lines.append(' ✓ 知识图谱最新')
|
|
197
|
-
|
|
198
|
-
# 检查是否有未标注的模块 (提示AI做语义标注)
|
|
199
|
-
try:
|
|
200
|
-
import subprocess as _sp
|
|
201
|
-
# v3.x: kg_semantic.py 已迁移到 domain/kg/graph/ (旧扁平路径不存在)
|
|
202
|
-
r = _sp.run([sys.executable, os.path.join(BASE, '.qoder', 'scripts', 'domain', 'kg', 'graph', 'kg_semantic.py'), '--check'],
|
|
203
|
-
capture_output=True, text=True, encoding='utf-8', timeout=5)
|
|
204
|
-
if r.stdout and 'semantic_labeling_needed' in r.stdout:
|
|
205
|
-
import json as _json
|
|
206
|
-
data = _json.loads(r.stdout)
|
|
207
|
-
unlabeled = len(data.get('modules', []))
|
|
208
|
-
if unlabeled > 0:
|
|
209
|
-
lines.append(' [提示] %d个模块未标注中文名, 运行 kg_semantic.py --check 查看' % unlabeled)
|
|
210
|
-
except Exception:
|
|
211
|
-
pass
|
|
152
|
+
# ★ 知识层健康检查:全迁 PG 后不再检查本地 kg.duckdb(已删除),改探活 MCP 知识层。
|
|
153
|
+
# 复用 wlinkj-workflow/mcp_config.json 的 token+url,调 get_knowledge_health 拿真实健康分。
|
|
154
|
+
# best-effort:任何失败都不阻塞会话启动(与其它 hook 铁律一致)。
|
|
155
|
+
kg_info = None
|
|
156
|
+
try:
|
|
157
|
+
import json as _json
|
|
158
|
+
import urllib.request as _url
|
|
159
|
+
cfg_path = os.path.join(BASE, 'wlinkj-workflow', 'mcp_config.json')
|
|
160
|
+
if os.path.isfile(cfg_path):
|
|
161
|
+
_cfg = _json.load(open(cfg_path, encoding='utf-8'))
|
|
162
|
+
_srv = _cfg.get('mcpServers', {}).get('wlinkj-knowledge', {})
|
|
163
|
+
_url_s = _srv.get('url', '')
|
|
164
|
+
_tok = _srv.get('token', '')
|
|
165
|
+
if _url_s and _tok and _url_s != 'WILL_BE_FILLED_BY_WL_INIT':
|
|
166
|
+
_rpc = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
167
|
+
"params": {"name": "get_knowledge_health", "arguments": {}}}
|
|
168
|
+
_req = _url.Request(_url_s, data=_json.dumps(_rpc).encode(),
|
|
169
|
+
headers={"Content-Type": "application/json",
|
|
170
|
+
"X-MCP-Token": _tok}, method="POST")
|
|
171
|
+
_resp = _url.urlopen(_req, timeout=4).read()
|
|
172
|
+
kg_info = _json.loads(_json.loads(_resp)["result"]["content"][0]["text"])
|
|
173
|
+
except Exception:
|
|
174
|
+
kg_info = None
|
|
175
|
+
|
|
176
|
+
if kg_info and isinstance(kg_info, dict) and "error" not in kg_info:
|
|
177
|
+
lines.append(' ✓ 知识层在线 (PG@5432): 健康分 %s / 覆盖 %s / 新鲜 %s / 完整 %s' % (
|
|
178
|
+
kg_info.get("score", "?"), kg_info.get("coverage", "?"),
|
|
179
|
+
kg_info.get("freshness", "?"), kg_info.get("completeness", "?")))
|
|
180
|
+
issues = kg_info.get("issues", []) or []
|
|
181
|
+
if issues and role in ('admin', 'dev'):
|
|
182
|
+
lines.append(' [提示] 健康问题: ' + '; '.join(str(i)[:60] for i in issues[:2]))
|
|
212
183
|
else:
|
|
213
|
-
lines.append(' [
|
|
214
|
-
|
|
215
|
-
lines.append(' → 运行: python .qoder/scripts/kg/kg_build.py')
|
|
216
|
-
else:
|
|
217
|
-
lines.append(' → 运行: python .qoder/scripts/orchestration/wlkj.py sync pull')
|
|
184
|
+
lines.append(' [WARN] 知识层未响应(MCP 后端未启动?)')
|
|
185
|
+
lines.append(' → 启动: cd wlinkj-workspace && start_pg.bat(后端 :10010)')
|
|
218
186
|
|
|
219
187
|
return NL.join(lines)
|
|
220
188
|
|
|
@@ -105,6 +105,35 @@ def _recent_prd_files():
|
|
|
105
105
|
return candidates
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
# 快速模式 6 章必需标题 (与 prd-quick-template.md 对齐)
|
|
109
|
+
QUICK_REQUIRED_SECTIONS = [
|
|
110
|
+
'功能入口', '需求背景', '需求说明', '影响范围', '验收标准', '不在本次范围',
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check_quick_prd_complete(prd_path):
|
|
115
|
+
"""检测 quick 模式 PRD 是否缺章。返回缺失章节列表 (空=完整)。
|
|
116
|
+
|
|
117
|
+
判定 quick 模式: @contract 头含 mode: quick。
|
|
118
|
+
必需 6 章: 功能入口/需求背景/需求说明/影响范围/验收标准/不在本次范围。
|
|
119
|
+
治"残废品" (真实案例: 海外考勤 PRD 只写"功能入口"一段, 6 章缺 5 章)。
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
text = open(prd_path, encoding='utf-8', errors='replace').read()
|
|
123
|
+
except Exception:
|
|
124
|
+
return [] # 读不到不拦 (误拦比放过更糟)
|
|
125
|
+
# 只检 quick 模式 (完整档 13 章另算, 不在此 hook 范围)
|
|
126
|
+
if 'mode: quick' not in text[:500]:
|
|
127
|
+
return []
|
|
128
|
+
missing = []
|
|
129
|
+
for sec in QUICK_REQUIRED_SECTIONS:
|
|
130
|
+
# 章节标题: ## 功能入口 / ## 需求背景 等 (允许前后空格)
|
|
131
|
+
import re
|
|
132
|
+
if not re.search(r'^#{1,3}\s*' + sec, text, re.MULTILINE):
|
|
133
|
+
missing.append(sec)
|
|
134
|
+
return missing
|
|
135
|
+
|
|
136
|
+
|
|
108
137
|
def _evald_prds():
|
|
109
138
|
"""读 eval-history.jsonl, 返回【已通过门禁】的 prd 文件名集合。
|
|
110
139
|
|
|
@@ -161,6 +190,24 @@ def main():
|
|
|
161
190
|
recent = _recent_prd_files()
|
|
162
191
|
if not recent:
|
|
163
192
|
sys.exit(0) # 本轮没产 PRD, 放行
|
|
193
|
+
|
|
194
|
+
# ── 先查 quick 模式 PRD 缺章 (残废品门禁, 比 eval 更优先) ──
|
|
195
|
+
incomplete = []
|
|
196
|
+
for p in recent:
|
|
197
|
+
miss = _check_quick_prd_complete(p)
|
|
198
|
+
if miss:
|
|
199
|
+
incomplete.append((p, miss))
|
|
200
|
+
if incomplete:
|
|
201
|
+
lines = []
|
|
202
|
+
for p, miss in incomplete[:3]:
|
|
203
|
+
lines.append(' %s 缺: %s' % (os.path.basename(p), '、'.join(miss)))
|
|
204
|
+
sys.stderr.write(
|
|
205
|
+
'【PRD 残废品门禁】检测到快速模式 PRD 缺章 (6 章缺一不可):\n'
|
|
206
|
+
+ '\n'.join(lines) + '\n'
|
|
207
|
+
'修复: 补齐缺失章节的标题和内容 (不涉及的写一句话说明, 标题必须保留),\n'
|
|
208
|
+
' 章节顺序: 功能入口/需求背景/需求说明/影响范围/验收标准/不在本次范围。\n'
|
|
209
|
+
' 模板: .qoder/templates/prd-quick-template.md\n')
|
|
210
|
+
sys.exit(2)
|
|
164
211
|
evald = _evald_prds()
|
|
165
212
|
last_records = _last_record_by_prd()
|
|
166
213
|
# 区分两种未过门禁: 从没跑过 eval / 跑了但没过线
|
|
@@ -53,6 +53,19 @@ test -f .qoder/.developer && echo "已初始化" || echo "未初始化"
|
|
|
53
53
|
注意:用户说"同步/保存产出"通常指 team_sync(push),不是 git commit;
|
|
54
54
|
用户说"提交/commit 代码"才是 wl-commit。
|
|
55
55
|
|
|
56
|
+
## 规则 0.3:命令路由是最终决定(最高优先级,记忆/偏好不可覆盖)
|
|
57
|
+
|
|
58
|
+
**用户明确打了 `/wl-X` → 就走 X,绝不改道。** 这一条优先级高于一切(包括"问平台"),
|
|
59
|
+
任何"用户偏好""历史记忆""上下文暗示"都不可覆盖当前明确的命令。命令名 = 最终路由。
|
|
60
|
+
|
|
61
|
+
- 用户打 `/wl-prd`(哪怕描述像 bug:"无法提交,时间报错")→ **必须走 PRD**(为修复写需求),**不准**跑去 `/wl-test` / `/wl-code` / 排查
|
|
62
|
+
- 用户打 `/wl-code` → 走代码;`/wl-test` → 走测试;`/wl-task` → 走任务;其它命令同理
|
|
63
|
+
- 判断不了路由 → **问用户一句**,不要凭"偏好/记忆"自己猜
|
|
64
|
+
|
|
65
|
+
> 🚫 反面教材(禁止,真实案例):用户打 `/wl-prd 快速,园林绿化养护计划无法提交,时间有问题报错`,
|
|
66
|
+
> AI 因"记忆里用户偏好报bug走test"擅自改道 `/wl-test` → **违规**。命令是 `/wl-prd`,描述像 bug 也走 PRD
|
|
67
|
+
> (`wl-prd.md` 命令优先铁律连这个例子都举了)。"偏好/记忆"绝不可覆盖用户当前明确的命令。
|
|
68
|
+
|
|
56
69
|
## 规则 0.5:数据库查询必须先问环境(安全红线,问了就停)
|
|
57
70
|
|
|
58
71
|
AI 检测到需要查询**真实数据库**(调用 `mcp__qoder-mysql` 的 query_schema / query_data /
|
|
@@ -132,6 +145,30 @@ kg.py context(= context_pack)一条命令返回:相关代码文件 + 同
|
|
|
132
145
|
> 这是工作流比原生慢的根因——原生 AI 本能地并发,工作流却被"逐步执行"的文档节奏带成了串行。
|
|
133
146
|
> 把串行变并发,同样工作量耗时可砍到 1/3。MCP server(kg/mysql/zentao)都支持 JSON-RPC 并发请求。
|
|
134
147
|
|
|
148
|
+
## 规则 2.6:思考链收敛——先取证再推理(治"超长思考链")
|
|
149
|
+
|
|
150
|
+
**痛点:AI 取证了(读了代码)却没抓住矛盾,在假设上连续空转,15+ 步才定位 3 步能找到的根因。**
|
|
151
|
+
> ⚠️ 本规则**只约束排查/修码/取证类推理**;**探索/规划/写PRD** 类(用 feature/workflow/coverage/context 撒网理解业务)不受约束,反而要多用 kg。
|
|
152
|
+
|
|
153
|
+
### 2.6.1 先取证再推理(铁律)
|
|
154
|
+
每个推断必须立刻验证,**不许"假设A → 假设B → 假设C"连环空转**;验证不了就换方向。
|
|
155
|
+
**一轮工具调用 = 一轮推理**,不许"无工具调用的纯思考"超过 1 轮。
|
|
156
|
+
|
|
157
|
+
### 2.6.2 取证优先用知识层(kg 比 grep 快 10 倍,与规则 2 一脉相承)
|
|
158
|
+
- 相关代码文件 → `context_pack` 列文件(**别** grep 全库找)
|
|
159
|
+
- 字段真实类型/命名 → `query_schema` / `field_map` / `db_columns`(**别** Read 整个 Entity 翻)
|
|
160
|
+
- 改动波及面 → `table_impact` / `get_impact`(**别** 手动追踪调用链)
|
|
161
|
+
- API 路由/参数 → `code-api` 索引(**别** grep Controller)
|
|
162
|
+
→ **kg 给了文件才 Read 那几个;kg 给了类型就不读全文。kg 用得更准,不是闲置。**
|
|
163
|
+
|
|
164
|
+
### 2.6.3 排查/修码类先对齐链路(1-2 步对齐,再动手;kg 一次对齐多环)
|
|
165
|
+
- **后端报错/类型异常**:`query_schema`(DB列类型) + `context_pack`(BO/Entity文件) → Read 对齐 → 找不一致点
|
|
166
|
+
- **接口不通/400**:`code-api`(路由/参数) + `table_impact`(影响面)
|
|
167
|
+
- **前端白屏/NaN**:`context_pack`(组件/API) + `field_map`(字段映射)
|
|
168
|
+
|
|
169
|
+
> 正例:`query_schema` 得 DB列=Date + `context_pack` 得 BO=String → "类型不一致,改BO"(2步定位)
|
|
170
|
+
> 反例:假设null → 假设Invalid Date → 假设时区 → 假设disabledDate → …(15步空转,养护计划实录)
|
|
171
|
+
|
|
135
172
|
## 规则 3:原型风格必须来自真源(含图标)
|
|
136
173
|
|
|
137
174
|
- 起点永远是平台模板:.qoder/templates/prototype-web.html / prototype-app.html
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,7 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
from typing import Any, Optional
|
|
25
25
|
|
|
26
|
-
from ..registry_mcp import McpRegistry
|
|
26
|
+
from ..registry_mcp import McpRegistry, _load_servers
|
|
27
27
|
from protocol.transports import ToolResult
|
|
28
28
|
|
|
29
29
|
|
|
@@ -42,7 +42,7 @@ class McpCap:
|
|
|
42
42
|
"""L1 MCP 能力统一入口 —— 复用已有 MCP server(禅道/知识图谱/MySQL/蓝湖)。
|
|
43
43
|
|
|
44
44
|
工作流调用:
|
|
45
|
-
cap =
|
|
45
|
+
cap = resolve()
|
|
46
46
|
result = cap.mcp.call("search_code", {"keyword": "保险"})
|
|
47
47
|
# 自动路由:MCP 可用走 MCP,不可用走 wlkj.py(CLI 降级)
|
|
48
48
|
"""
|
|
@@ -54,8 +54,9 @@ class McpCap:
|
|
|
54
54
|
# spawn 子进程, 不依赖宿主是否原生支持 MCP —— CLI / Codex / Claude 同样能起 server。
|
|
55
55
|
# 故: 宿主未声明 mcp_servers 或声明为空 = "没有宿主原生限制, 用全部本地可起的"。
|
|
56
56
|
if mcp_servers: # 非空列表才过滤
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
# 用 _load_servers() 而非硬编码 SERVERS —— 这样 mcp_config.json 改配置才生效
|
|
58
|
+
all_servers = _load_servers()
|
|
59
|
+
filtered = {k: v for k, v in all_servers.items() if k in mcp_servers}
|
|
59
60
|
self._registry = McpRegistry(servers=filtered)
|
|
60
61
|
else:
|
|
61
62
|
self._registry = _get_registry()
|
|
@@ -117,6 +118,12 @@ class McpCap:
|
|
|
117
118
|
"search_wiki": ("kg", ["wiki"]),
|
|
118
119
|
"fill_prototype": ("kg", ["fill-prototype"]),
|
|
119
120
|
"get_design_system": ("kg", ["design-system"]),
|
|
121
|
+
# RAG/GraphRAG 降级映射(MCP 挂时走本地 wlkj.py kg search,语义降级为关键词)
|
|
122
|
+
"rag_search": ("kg", ["search"]),
|
|
123
|
+
"ask_corpus": ("kg", ["search"]),
|
|
124
|
+
# 学习工具降级(MCP 挂时本地无 learning 表,返回提示)
|
|
125
|
+
"learn": ("noop", []),
|
|
126
|
+
"get_learnings": ("noop", []),
|
|
120
127
|
"list_tasks": ("task", ["list"]),
|
|
121
128
|
"create_task": ("task", ["create"]),
|
|
122
129
|
"start_task": ("task", ["start"]),
|
|
@@ -129,10 +136,16 @@ class McpCap:
|
|
|
129
136
|
if entry is None:
|
|
130
137
|
return None
|
|
131
138
|
cmd, base_args = entry
|
|
139
|
+
# noop 类工具(learn/get_learnings 等):本地无等价 CLI,返回提示不崩
|
|
140
|
+
if cmd == "noop":
|
|
141
|
+
return json.dumps({"degraded": True, "message": "MCP不可用,该工具(learning)降级跳过"}, ensure_ascii=False)
|
|
132
142
|
cli_args = list(base_args)
|
|
133
|
-
# 从 MCP args
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
# 从 MCP args 构建命令行参数。
|
|
144
|
+
# 参数名对齐: 平台 MCP search 工具入参是 query, 引擎命令文档用 keyword。
|
|
145
|
+
# 这里两边都取值, 确保无论上游传哪个都能落到 CLI 查询词上。
|
|
146
|
+
kw = args.get("keyword") or args.get("query")
|
|
147
|
+
if kw is not None:
|
|
148
|
+
cli_args.append(str(kw))
|
|
136
149
|
if "platform" in args and args["platform"]:
|
|
137
150
|
cli_args.extend(["--platform", str(args["platform"])])
|
|
138
151
|
if "role" in args and args["role"]:
|
|
@@ -211,128 +224,91 @@ class McpCap:
|
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
class McpMemoryProvider:
|
|
214
|
-
"""memory 职责链的 MCP provider ——
|
|
227
|
+
"""memory 职责链的 MCP provider —— 接通平台 PG.learning_patterns。
|
|
215
228
|
|
|
216
|
-
设计模式: Adapter
|
|
229
|
+
设计模式: Adapter(把平台 MCP get_learnings/learn 适配成 MemoryCap 契约)
|
|
217
230
|
|
|
218
|
-
职责链位置:
|
|
219
|
-
|
|
231
|
+
职责链位置: 链头优先。平台 MCP 可用(has_tool get_learnings)时抢占,
|
|
232
|
+
复用项目级学习表(PG.learning_patterns,按 token 解析 pid 隔离,跨会话积累)。
|
|
220
233
|
|
|
221
|
-
数据源:
|
|
222
|
-
复用 learn_aggregate.py 的写入逻辑,保证 CLI 和 MCP 行为一致。
|
|
234
|
+
数据源: 平台 MCP 工具 get_learnings / learn(后端 services.knowledge.learn)。
|
|
223
235
|
"""
|
|
224
236
|
|
|
225
237
|
def __init__(self):
|
|
226
|
-
self.
|
|
238
|
+
self._registry = None # 懒取 MCP 单例(避免 import 时即连后端)
|
|
239
|
+
|
|
240
|
+
def _reg(self):
|
|
241
|
+
"""复用 McpCap 同款模块级单例(_get_registry),零重复 spawn。"""
|
|
242
|
+
if self._registry is None:
|
|
243
|
+
self._registry = _get_registry()
|
|
244
|
+
return self._registry
|
|
227
245
|
|
|
228
246
|
@property
|
|
229
247
|
def available(self) -> bool:
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
event TEXT NOT NULL, data JSON)""")
|
|
238
|
-
con.close()
|
|
239
|
-
self._available = True
|
|
240
|
-
except Exception:
|
|
241
|
-
self._available = False
|
|
242
|
-
return self._available
|
|
248
|
+
"""平台 MCP 可用且 get_learnings 工具在索引中即为可用。
|
|
249
|
+
全迁 PG 后记忆走平台 get_learnings(PG.learning_patterns,按项目隔离)。
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
return self._reg().has_tool("get_learnings")
|
|
253
|
+
except Exception:
|
|
254
|
+
return False
|
|
243
255
|
|
|
244
256
|
def recall(self, query: str) -> list[dict]:
|
|
245
|
-
"""
|
|
257
|
+
"""检索记忆: 取平台学习记录,按 query 模糊过滤 pattern/category。"""
|
|
246
258
|
if not self.available:
|
|
247
259
|
return []
|
|
248
260
|
try:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
import json as _json
|
|
262
|
+
result = self._reg().call("get_learnings", {"limit": 50})
|
|
263
|
+
text = getattr(result, "text", "") or ""
|
|
264
|
+
data = _json.loads(text) if text else {}
|
|
265
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
266
|
+
q = (query or "").lower()
|
|
267
|
+
out = []
|
|
268
|
+
for it in items:
|
|
269
|
+
hay = "%s %s" % (it.get("category", ""), it.get("pattern", ""))
|
|
270
|
+
if not q or q in hay.lower():
|
|
271
|
+
out.append({
|
|
272
|
+
"event": it.get("category", ""),
|
|
273
|
+
"data": it.get("pattern", ""),
|
|
274
|
+
"ts": it.get("last_seen", ""),
|
|
275
|
+
"dev": ",".join(it.get("source_devs", []) or []),
|
|
276
|
+
})
|
|
277
|
+
return out[:20]
|
|
262
278
|
except Exception:
|
|
263
279
|
return []
|
|
264
280
|
|
|
265
281
|
def remember(self, key: str, content: str) -> None:
|
|
266
|
-
"""写入一条记忆:
|
|
282
|
+
"""写入一条记忆: 调平台 learn(PG.learning_patterns 累加)。"""
|
|
267
283
|
if not self.available:
|
|
268
284
|
return
|
|
269
285
|
try:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# → INSERT 永不执行 → remember() 静默丢全部写入。
|
|
276
|
-
# learning_events 表已由 self.available 的 CREATE TABLE IF NOT EXISTS 保证存在,
|
|
277
|
-
# 这里直接 INSERT 即可。
|
|
278
|
-
from foundation.protocol.duckdb_conn import get_db
|
|
279
|
-
con = get_db()
|
|
280
|
-
eid = hashlib.md5(("%s:%s:%s" % (key, content, _time.time())).encode()).hexdigest()[:16]
|
|
281
|
-
ts = _time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
282
|
-
data = json.dumps({"content": content}, ensure_ascii=False)
|
|
283
|
-
con.execute(
|
|
284
|
-
"INSERT OR IGNORE INTO learning_events(id, ts, dev, event, data) VALUES (?,?,?,?,?)",
|
|
285
|
-
[eid, ts, "", key, data],
|
|
286
|
-
)
|
|
287
|
-
con.close()
|
|
286
|
+
self._reg().call("learn", {
|
|
287
|
+
"category": "rule",
|
|
288
|
+
"pattern": "%s: %s" % (key, content[:500]),
|
|
289
|
+
"source": "engine/memory",
|
|
290
|
+
})
|
|
288
291
|
except Exception:
|
|
289
|
-
pass
|
|
292
|
+
pass # 写入失败不阻塞
|
|
290
293
|
|
|
291
294
|
def get_fact(self, key: str) -> str | None:
|
|
292
|
-
"""按
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try:
|
|
296
|
-
from foundation.protocol.duckdb_conn import get_db
|
|
297
|
-
con = get_db()
|
|
298
|
-
row = con.execute(
|
|
299
|
-
"SELECT data FROM learning_events WHERE event = ? ORDER BY ts DESC LIMIT 1",
|
|
300
|
-
[key],
|
|
301
|
-
).fetchone()
|
|
302
|
-
con.close()
|
|
303
|
-
if not row:
|
|
304
|
-
return None
|
|
305
|
-
data = row[0]
|
|
306
|
-
if isinstance(data, str):
|
|
307
|
-
return data
|
|
308
|
-
import json as _json
|
|
309
|
-
return _json.dumps(data, ensure_ascii=False) if data else None
|
|
310
|
-
except Exception:
|
|
311
|
-
return None
|
|
295
|
+
"""按 key 取一条事实,无则 None。"""
|
|
296
|
+
results = self.recall(key)
|
|
297
|
+
return results[0].get("data") if results else None
|
|
312
298
|
|
|
313
299
|
def list_recent(self, limit: int = 10) -> list[dict]:
|
|
314
|
-
"""最近 N
|
|
300
|
+
"""最近 N 条记忆(平台按 frequency 倒序返回)。"""
|
|
315
301
|
if not self.available:
|
|
316
302
|
return []
|
|
317
303
|
try:
|
|
318
304
|
import json as _json
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
results = []
|
|
327
|
-
for r in rows:
|
|
328
|
-
data = r[4]
|
|
329
|
-
# DuckDB JSON 列可能返回字符串,需解析
|
|
330
|
-
if isinstance(data, str):
|
|
331
|
-
try:
|
|
332
|
-
data = _json.loads(data)
|
|
333
|
-
except (_json.JSONDecodeError, TypeError):
|
|
334
|
-
data = {"raw": data}
|
|
335
|
-
results.append({"id": r[0], "ts": r[1], "dev": r[2], "event": r[3], "data": data or {}})
|
|
336
|
-
return results
|
|
305
|
+
result = self._reg().call("get_learnings", {"limit": limit})
|
|
306
|
+
text = getattr(result, "text", "") or ""
|
|
307
|
+
data = _json.loads(text) if text else {}
|
|
308
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
309
|
+
return [{"event": it.get("category", ""), "data": it.get("pattern", ""),
|
|
310
|
+
"ts": it.get("last_seen", ""), "dev": ",".join(it.get("source_devs", []) or [])}
|
|
311
|
+
for it in items[:limit]]
|
|
337
312
|
except Exception:
|
|
338
313
|
return []
|
|
314
|
+
|