@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
|
Binary file
|
|
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
|
+
|