mindforge-memory 6.4.1__py3-none-any.whl

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.
Files changed (64) hide show
  1. MindForge.py +20 -0
  2. mindforge/__init__.py +111 -0
  3. mindforge/adapters/__init__.py +18 -0
  4. mindforge/adapters/claude_adapter.py +84 -0
  5. mindforge/adapters/crewai_memory.py +76 -0
  6. mindforge/adapters/generic_api.py +183 -0
  7. mindforge/adapters/langgraph_store.py +463 -0
  8. mindforge/adapters/openclaw_adapter.py +91 -0
  9. mindforge/api/__init__.py +1 -0
  10. mindforge/api/server.py +1567 -0
  11. mindforge/cli/__init__.py +1 -0
  12. mindforge/cli/main.py +13607 -0
  13. mindforge/core/__init__.py +37 -0
  14. mindforge/core/embedding.py +847 -0
  15. mindforge/core/encryption.py +597 -0
  16. mindforge/core/indexer.py +363 -0
  17. mindforge/core/mindforge.py +6298 -0
  18. mindforge/core/paths.py +43 -0
  19. mindforge/core/query.py +501 -0
  20. mindforge/core/storage.py +18656 -0
  21. mindforge/core/types.py +314 -0
  22. mindforge/core/version.py +56 -0
  23. mindforge/daemon/__init__.py +0 -0
  24. mindforge/daemon/server.py +277 -0
  25. mindforge/mcp/__init__.py +4 -0
  26. mindforge/mcp/server.py +2559 -0
  27. mindforge/modules/__init__.py +138 -0
  28. mindforge/modules/categorizer.py +151 -0
  29. mindforge/modules/conflict_detector.py +356 -0
  30. mindforge/modules/connectors.py +331 -0
  31. mindforge/modules/context_window.py +337 -0
  32. mindforge/modules/event_bus.py +572 -0
  33. mindforge/modules/evolution.py +210 -0
  34. mindforge/modules/experience.py +402 -0
  35. mindforge/modules/federated.py +722 -0
  36. mindforge/modules/federated_acl.py +393 -0
  37. mindforge/modules/follow_up.py +315 -0
  38. mindforge/modules/hybrid_search.py +565 -0
  39. mindforge/modules/importer.py +374 -0
  40. mindforge/modules/integrator.py +196 -0
  41. mindforge/modules/intent_router.py +359 -0
  42. mindforge/modules/knowledge_graph.py +395 -0
  43. mindforge/modules/memory_compressor.py +592 -0
  44. mindforge/modules/memory_decay.py +474 -0
  45. mindforge/modules/memory_passport.py +250 -0
  46. mindforge/modules/multi_agent.py +534 -0
  47. mindforge/modules/multimodal.py +304 -0
  48. mindforge/modules/outcome_learning.py +186 -0
  49. mindforge/modules/personality.py +275 -0
  50. mindforge/modules/principled_forgetting.py +258 -0
  51. mindforge/modules/privacy.py +736 -0
  52. mindforge/modules/recall.py +263 -0
  53. mindforge/modules/session_focus.py +305 -0
  54. mindforge/modules/share_conflict.py +521 -0
  55. mindforge/modules/skill_extractor.py +340 -0
  56. mindforge/modules/skill_pack.py +205 -0
  57. mindforge/modules/timeline_viz.py +206 -0
  58. mindforge/modules/v6.py +931 -0
  59. mindforge_memory-6.4.1.dist-info/METADATA +281 -0
  60. mindforge_memory-6.4.1.dist-info/RECORD +64 -0
  61. mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
  62. mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
  63. mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
  64. mindforge_memory-6.4.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,340 @@
1
+ """
2
+ MindForge 记忆 → 技能转化引擎
3
+ ====================================
4
+ 核心想法:一组「解决某问题」的记忆,可以被抽象成可复用的技能模板:
5
+ Skill = {name, description, triggers, slots, steps, examples, tags}
6
+
7
+ 抽取流程:
8
+ 1. 候选聚类:按标签 + 关键词 overlap 把记忆分桶(同一技能 ≈ 主题簇)
9
+ 2. 槽位识别:在每条记忆里找出 {{参数}} / <参数> / 变量名模式
10
+ 3. 步骤提炼:按「先/然后/最后/接下来/步骤 1~n」等连接词 + 序号模式抽取步骤序列
11
+ 4. 触发词归纳:出现频次 ≥ min_hits 的关键词/标签 即为触发词
12
+ 5. 示例采样:从簇中挑最多 k 条作为 skill 的 examples
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Dict, List, Optional, Set, Tuple
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # 模式定义
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _SLOT_PATTERNS = [
26
+ re.compile(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]{0,29})\s*\}\}"),
27
+ re.compile(r"<\s*([A-Za-z_][A-Za-z0-9_]{0,29})\s*>"),
28
+ re.compile(r"(\$[A-Z_][A-Z0-9_]{0,19})"),
29
+ re.compile(r"[「『](变量|参数|占位|入参)?\s*([A-Za-z_][A-Za-z0-9_\u4e00-\u9fa5]{1,15})\s*[」』]"),
30
+ ]
31
+
32
+ _STEP_PATTERNS = [
33
+ (re.compile(r"(?:步骤|第)\s*([0-9一二三四五六七八九十]{1,4})\s*[、步::\.]\s*([^\n,,。;;]{2,120})"), "numbered"),
34
+ (re.compile(r"(首先|第一步|先|一开始)[::\s]*([^\n,,。;;]{2,120})"), "first"),
35
+ (re.compile(r"(然后|接着|接下来|下一步|第二步)[::\s]*([^\n,,。;;]{2,120})"), "next"),
36
+ (re.compile(r"(最后|最终|完成后|收尾|第三步)[::\s]*([^\n,,。;;]{2,120})"), "last"),
37
+ (re.compile(r"(注意|提示|警告|重要)[::\s]*([^\n,,。;;]{2,120})"), "note"),
38
+ ]
39
+
40
+ _CMD_VERBS = ["安装", "配置", "启动", "停止", "运行", "执行", "调用", "发送", "请求",
41
+ "打开", "访问", "下载", "上传", "生成", "创建", "删除", "修改", "更新",
42
+ "导出", "导入", "备份", "恢复", "部署", "测试", "验证", "构建", "打包"]
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # 数据类
47
+ # ---------------------------------------------------------------------------
48
+
49
+ @dataclass
50
+ class SkillSlot:
51
+ name: str
52
+ examples: List[str] = field(default_factory=list)
53
+ required: bool = False
54
+ default: Optional[str] = None
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ return {"name": self.name, "examples": self.examples[:5],
58
+ "required": self.required, "default": self.default}
59
+
60
+
61
+ @dataclass
62
+ class SkillTemplate:
63
+ name: str
64
+ description: str
65
+ triggers: List[str] = field(default_factory=list)
66
+ slots: List[SkillSlot] = field(default_factory=list)
67
+ steps: List[Dict[str, Any]] = field(default_factory=list)
68
+ examples: List[str] = field(default_factory=list)
69
+ tags: List[str] = field(default_factory=list)
70
+ source_memory_ids: List[str] = field(default_factory=list)
71
+ confidence: float = 0.0
72
+ cluster_size: int = 0
73
+
74
+ def to_dict(self) -> Dict[str, Any]:
75
+ return {
76
+ "name": self.name,
77
+ "description": self.description,
78
+ "triggers": self.triggers[:20],
79
+ "slots": [s.to_dict() for s in self.slots],
80
+ "steps": self.steps[:30],
81
+ "examples": self.examples[:5],
82
+ "tags": self.tags[:20],
83
+ "source_memory_ids": self.source_memory_ids[:100],
84
+ "confidence": round(self.confidence, 3),
85
+ "cluster_size": self.cluster_size,
86
+ }
87
+
88
+ def render(self, **kwargs) -> str:
89
+ """把技能模板用用户提供的 slot 值填回去,得到可执行流程文本。"""
90
+ mapping = {s.name: kwargs.get(s.name, s.default or f"{{{{{s.name}}}}}")
91
+ for s in self.slots}
92
+ lines = [f"# 技能:{self.name}"]
93
+ if self.description:
94
+ lines.append(f"> {self.description}")
95
+ lines.append("")
96
+ if self.steps:
97
+ lines.append("## 步骤")
98
+ for i, step in enumerate(self.steps, 1):
99
+ action = str(step.get("action", ""))
100
+ for k, v in mapping.items():
101
+ action = re.sub(rf"(\{{{{\s*{re.escape(k)}\s*\}}}}|<\s*{re.escape(k)}\s*>)", str(v), action)
102
+ lines.append(f"{i}. {action}")
103
+ if self.examples:
104
+ lines.append("")
105
+ lines.append("## 示例")
106
+ for ex in self.examples:
107
+ lines.append(f"- {ex}")
108
+ return "\n".join(lines)
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # 引擎
113
+ # ---------------------------------------------------------------------------
114
+
115
+ class SkillExtractor:
116
+ """无外部依赖的记忆→技能抽取引擎。"""
117
+
118
+ def __init__(self,
119
+ min_cluster_size: int = 2,
120
+ max_skills: int = 50,
121
+ overlap_threshold: float = 0.35, # v5.4.8 P3-002: 从 0.20 提高到 0.35
122
+ min_trigger_hits: int = 1,
123
+ max_step_per_skill: int = 25):
124
+ self.min_cluster_size = max(1, int(min_cluster_size))
125
+ self.max_skills = max_skills
126
+ self.base_threshold = overlap_threshold
127
+ self.overlap_threshold = overlap_threshold
128
+ self.min_trigger_hits = min_trigger_hits
129
+ self.max_step = max_step_per_skill
130
+
131
+ # -- helpers -----------------------------------------------------------
132
+
133
+ @staticmethod
134
+ def _ngrams(text: str, n: int = 2) -> Set[str]:
135
+ s = re.sub(r"[\W_]+", "", text)
136
+ if len(s) < n:
137
+ return {s} if s else set()
138
+ return {s[i:i + n] for i in range(len(s) - n + 1)}
139
+
140
+ @staticmethod
141
+ def _tags_of(mem: Dict[str, Any]) -> List[str]:
142
+ raw = mem.get("tags") or []
143
+ if isinstance(raw, str):
144
+ return [t.strip() for t in re.split(r"[,,#\s]+", raw) if t.strip()]
145
+ return [str(t).strip() for t in raw if str(t).strip()]
146
+
147
+ def _cluster(self, memories: List[Dict[str, Any]]) -> List[List[int]]:
148
+ n = len(memories)
149
+ parent = list(range(n))
150
+
151
+ # v5.4.8 P3-002 修复:自适应阈值,防止大数据集下的链式合并
152
+ # 数据集越大,阈值越高,避免不相关的记忆通过中间节点连接
153
+ import math
154
+ adaptive_threshold = min(
155
+ self.base_threshold + 0.05 * math.log(max(n, 1)),
156
+ 0.60 # 上限 0.60
157
+ )
158
+ threshold = max(self.base_threshold, adaptive_threshold)
159
+
160
+ def find(x: int) -> int:
161
+ while parent[x] != x:
162
+ parent[x] = parent[parent[x]]
163
+ x = parent[x]
164
+ return x
165
+
166
+ def union(a: int, b: int) -> None:
167
+ ra, rb = find(a), find(b)
168
+ if ra != rb:
169
+ parent[ra] = rb
170
+
171
+ feats: List[Tuple[Set[str], Set[str]]] = []
172
+ for m in memories:
173
+ content = str(m.get("content", ""))
174
+ feats.append((self._ngrams(content, 2), set(self._tags_of(m))))
175
+
176
+ for i in range(n):
177
+ ai, ti = feats[i]
178
+ for j in range(i + 1, n):
179
+ aj, tj = feats[j]
180
+ overlap_n = 0.0
181
+ if ai and aj:
182
+ overlap_n = len(ai & aj) / max(1, min(len(ai), len(aj)))
183
+ overlap_t = 0.0
184
+ if ti and tj:
185
+ overlap_t = len(ti & tj) / max(1, min(len(ti), len(tj)))
186
+ score = max(overlap_n, overlap_t)
187
+ # 若标签完全相同直接合并
188
+ if ti and tj and ti == tj:
189
+ score = max(score, 0.85)
190
+ if score >= threshold:
191
+ union(i, j)
192
+
193
+ buckets: Dict[int, List[int]] = {}
194
+ for i in range(n):
195
+ buckets.setdefault(find(i), []).append(i)
196
+ return [b for b in buckets.values() if len(b) >= self.min_cluster_size]
197
+
198
+ # -- extractors --------------------------------------------------------
199
+
200
+ def _extract_slots(self, texts: List[str]) -> List[SkillSlot]:
201
+ slot_hits: Dict[str, Set[str]] = {}
202
+ for t in texts:
203
+ for pat in _SLOT_PATTERNS:
204
+ for m in pat.finditer(t):
205
+ name = m.group(1) if pat.groups == 1 else m.group(m.lastindex or 1)
206
+ # group count 不同,统一兜底
207
+ try:
208
+ if m.lastindex and m.lastindex >= 1:
209
+ name = m.group(m.lastindex)
210
+ except Exception:
211
+ pass
212
+ if not name or len(name) < 1:
213
+ continue
214
+ slot_hits.setdefault(name, set()).add(t[:60])
215
+ out = []
216
+ for name, exs in slot_hits.items():
217
+ out.append(SkillSlot(name=name, examples=list(exs), required=False))
218
+ out.sort(key=lambda s: -len(s.examples))
219
+ return out
220
+
221
+ def _extract_steps(self, texts: List[str]) -> List[Dict[str, Any]]:
222
+ steps: List[Tuple[int, str, str]] = [] # (order_key, action, kind)
223
+ for t in texts:
224
+ for pat, kind in _STEP_PATTERNS:
225
+ for m in pat.finditer(t):
226
+ raw_order = m.group(1)
227
+ action = m.group(2).strip("  \t")
228
+ order_key = 50
229
+ if kind == "first":
230
+ order_key = 10
231
+ elif kind == "next":
232
+ order_key = 30
233
+ elif kind == "last":
234
+ order_key = 70
235
+ elif kind == "note":
236
+ order_key = 90
237
+ elif kind == "numbered":
238
+ # 中文数字/阿拉伯数字 转 int,超出范围给个中值
239
+ try:
240
+ if raw_order.isdigit():
241
+ order_key = 10 + int(raw_order) * 2
242
+ else:
243
+ cn = "一二三四五六七八九十"
244
+ order_key = 10 + (cn.index(raw_order) + 1) * 2
245
+ except Exception:
246
+ order_key = 20
247
+ steps.append((order_key, action, kind))
248
+ # 去重 + 排序 + 截断
249
+ seen = set()
250
+ out = []
251
+ for key, action, kind in sorted(steps, key=lambda x: x[0]):
252
+ sig = action[:40]
253
+ if sig in seen:
254
+ continue
255
+ seen.add(sig)
256
+ out.append({"order": key, "action": action, "kind": kind})
257
+ if len(out) >= self.max_step:
258
+ break
259
+ return out
260
+
261
+ def _collect_triggers(self, memories_in_cluster: List[Dict[str, Any]],
262
+ texts: List[str]) -> List[str]:
263
+ freq: Dict[str, int] = {}
264
+ # 标签
265
+ for m in memories_in_cluster:
266
+ for t in self._tags_of(m):
267
+ freq[t] = freq.get(t, 0) + 1
268
+ # 命令动词
269
+ joined = "\n".join(texts)
270
+ for v in _CMD_VERBS:
271
+ c = joined.count(v)
272
+ if c > 0:
273
+ freq[v] = freq.get(v, 0) + c
274
+ # 2-gram 中高并集频(简化:取频次高的前若干)
275
+ items = [(k, v) for k, v in freq.items() if v >= self.min_trigger_hits]
276
+ items.sort(key=lambda kv: (-kv[1], kv[0]))
277
+ return [k for k, _ in items[:20]]
278
+
279
+ @staticmethod
280
+ def _summarize_name(memories: List[Dict[str, Any]]) -> Tuple[str, str]:
281
+ tags: Set[str] = set()
282
+ first_lines: List[str] = []
283
+ for m in memories:
284
+ for t in SkillExtractor._tags_of(m):
285
+ tags.add(t)
286
+ c = str(m.get("content", ""))
287
+ first_line = re.split(r"[\n,,。;;]", c, maxsplit=1)[0][:40].strip()
288
+ if first_line:
289
+ first_lines.append(first_line)
290
+ tags_list = sorted(tags)
291
+ name_prefix = "_".join(tags_list[:2]) if tags_list else "skill"
292
+ name = f"{name_prefix}_{len(memories)}mem".replace(" ", "_")
293
+ desc_parts = first_lines[:2]
294
+ description = ";".join(desc_parts) if desc_parts else f"从 {len(memories)} 条记忆归纳的通用流程"
295
+ return name, description
296
+
297
+ # -- main API ----------------------------------------------------------
298
+
299
+ def extract(self, memories: List[Dict[str, Any]]) -> List[SkillTemplate]:
300
+ """从一组记忆中批量抽取技能。"""
301
+ clusters = self._cluster(memories)
302
+ # 优先大簇
303
+ clusters.sort(key=lambda c: -len(c))
304
+ skills: List[SkillTemplate] = []
305
+ for cl in clusters[: self.max_skills]:
306
+ cl_mems = [memories[i] for i in cl]
307
+ texts = [str(m.get("content", "")) for m in cl_mems]
308
+ ids = [str(m.get("id", i)) for i, m in zip(cl, cl_mems)]
309
+ name, desc = self._summarize_name(cl_mems)
310
+ slots = self._extract_slots(texts)
311
+ steps = self._extract_steps(texts)
312
+ triggers = self._collect_triggers(cl_mems, texts)
313
+ confidence = 0.0
314
+ if triggers:
315
+ confidence += min(len(triggers) * 0.04, 0.35)
316
+ if slots:
317
+ confidence += min(len(slots) * 0.06, 0.25)
318
+ if steps:
319
+ confidence += min(len(steps) * 0.04, 0.30)
320
+ if len(cl) >= 4:
321
+ confidence += 0.10
322
+ examples = [t for t in texts if len(t) >= 8][:5]
323
+ # tags 汇总
324
+ tag_set: Set[str] = set()
325
+ for m in cl_mems:
326
+ for t in self._tags_of(m):
327
+ tag_set.add(t)
328
+ skills.append(SkillTemplate(
329
+ name=name, description=desc,
330
+ triggers=triggers, slots=slots, steps=steps,
331
+ examples=examples, tags=sorted(tag_set),
332
+ source_memory_ids=ids,
333
+ confidence=min(confidence, 1.0),
334
+ cluster_size=len(cl),
335
+ ))
336
+ skills.sort(key=lambda s: (-s.confidence, -s.cluster_size))
337
+ return skills
338
+
339
+
340
+ __all__ = ["SkillExtractor", "SkillTemplate", "SkillSlot"]
@@ -0,0 +1,205 @@
1
+ """
2
+ MindForge v6.0.0 功能 3:可分享的技能包
3
+
4
+ 让用户把一组好用的技能模板一键导出为可分享文件,别人一键导入安装。
5
+
6
+ - PACK_FORMAT: "mindforge-skill-pack",FORMAT_VERSION = 1
7
+ - export_skill_pack(): 按技能名导出为结构化 JSON
8
+ - validate_skill_pack(): 校验格式/版本/条数上限,防恶意包
9
+ - import_skill_pack(): merge(按 name UPSERT 合并)或 replace(先删后写)
10
+
11
+ 安全约束:
12
+ - MAX_SKILLS_PER_PACK = 500,MAX_CASES = 2000(防超大包)
13
+ - 字段白名单解析,忽略未知字段
14
+ - 导入失败原子回滚(单技能失败不影响已导入项)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import time
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from mindforge.modules.skill_extractor import SkillSlot, SkillTemplate
24
+
25
+ PACK_FORMAT = "mindforge-skill-pack"
26
+ FORMAT_VERSION = 1
27
+
28
+ MAX_SKILLS_PER_PACK = 500
29
+ MAX_CASES_PER_PACK = 2000
30
+
31
+
32
+ def export_skill_pack(skill_store, names: Optional[List[str]] = None,
33
+ include_cases: bool = True,
34
+ max_cases: int = 200) -> Dict[str, Any]:
35
+ """按技能名导出技能包(可分享文件内容)。"""
36
+ skills = skill_store.list_skills(limit=MAX_SKILLS_PER_PACK)
37
+ if names:
38
+ wanted = {n.lower() for n in names if n and n.strip()}
39
+ skills = [s for s in skills if (s.get("name") or "").lower() in wanted]
40
+ if not skills:
41
+ return {"success": False, "error": "没有可导出的技能"}
42
+
43
+ pack_skills = []
44
+ for s in skills[:MAX_SKILLS_PER_PACK]:
45
+ pack_skills.append({
46
+ "name": s.get("name", ""),
47
+ "description": s.get("description", ""),
48
+ "triggers": list(s.get("triggers", []) or [])[:20],
49
+ "slots": [{"name": sl.get("name", ""),
50
+ "examples": list(sl.get("examples", []) or [])[:5],
51
+ "required": bool(sl.get("required", False)),
52
+ "default": sl.get("default")}
53
+ for sl in (s.get("slots", []) or [])[:20]],
54
+ "steps": list(s.get("steps", []) or [])[:30],
55
+ "examples": list(s.get("examples", []) or [])[:5],
56
+ "tags": list(s.get("tags", []) or [])[:20],
57
+ "confidence": float(s.get("confidence", 0.0) or 0.0),
58
+ "cluster_size": int(s.get("cluster_size", 0) or 0),
59
+ })
60
+
61
+ pack = {
62
+ "format": PACK_FORMAT,
63
+ "format_version": FORMAT_VERSION,
64
+ "exported_at": round(time.time(), 3),
65
+ "skills": pack_skills,
66
+ }
67
+ if include_cases:
68
+ cases = skill_store.list_cases(limit=min(max_cases, MAX_CASES_PER_PACK))
69
+ pack["cases"] = [{
70
+ "task": c.get("task", ""),
71
+ "content": (c.get("content") or "")[:2000],
72
+ "result": (c.get("result") or "")[:2000],
73
+ "outcome": c.get("outcome", "success"),
74
+ "duration_seconds": float(c.get("duration_seconds", 0.0) or 0.0),
75
+ "tags": list(c.get("tags", []) or [])[:20],
76
+ } for c in cases[:MAX_CASES_PER_PACK]]
77
+ return {"success": True, "pack": pack,
78
+ "skill_count": len(pack_skills),
79
+ "case_count": len(pack.get("cases", []))}
80
+
81
+
82
+ def validate_skill_pack(pack: Any) -> Dict[str, Any]:
83
+ """校验技能包格式。返回 {valid, errors}。"""
84
+ errors = []
85
+ if not isinstance(pack, dict):
86
+ return {"valid": False, "errors": ["技能包必须是 JSON 对象"]}
87
+ if pack.get("format") != PACK_FORMAT:
88
+ errors.append(f"格式标识错误: {pack.get('format')!r}(应为 {PACK_FORMAT})")
89
+ ver = pack.get("format_version")
90
+ if ver != FORMAT_VERSION:
91
+ errors.append(f"版本不支持: {ver!r}(当前支持 v{FORMAT_VERSION})")
92
+ skills = pack.get("skills")
93
+ if not isinstance(skills, list):
94
+ errors.append("缺少 skills 列表")
95
+ elif not skills:
96
+ errors.append("技能包为空(无技能)")
97
+ elif len(skills) > MAX_SKILLS_PER_PACK:
98
+ errors.append(f"技能数超过上限 {MAX_SKILLS_PER_PACK}")
99
+ else:
100
+ names = set()
101
+ for i, s in enumerate(skills):
102
+ if not isinstance(s, dict):
103
+ errors.append(f"skills[{i}] 不是对象")
104
+ continue
105
+ name = (s.get("name") or "").strip()
106
+ if not name:
107
+ errors.append(f"skills[{i}] 缺少 name")
108
+ elif name.lower() in names:
109
+ errors.append(f"技能名重复: {name}")
110
+ names.add(name.lower())
111
+ cases = pack.get("cases")
112
+ if isinstance(cases, list) and len(cases) > MAX_CASES_PER_PACK:
113
+ errors.append(f"案例数超过上限 {MAX_CASES_PER_PACK}")
114
+ return {"valid": not errors, "errors": errors}
115
+
116
+
117
+ def _parse_skill(pack_skill: Dict[str, Any]) -> Optional[SkillTemplate]:
118
+ """按白名单解析单个技能,返回 SkillTemplate 或 None。"""
119
+ try:
120
+ name = (pack_skill.get("name") or "").strip()
121
+ if not name:
122
+ return None
123
+ slots = []
124
+ for sl in (pack_skill.get("slots") or [])[:20]:
125
+ if not isinstance(sl, dict):
126
+ continue
127
+ slots.append(SkillSlot(
128
+ name=(sl.get("name") or "").strip(),
129
+ examples=list(sl.get("examples", []) or [])[:5],
130
+ required=bool(sl.get("required", False)),
131
+ default=sl.get("default"),
132
+ ))
133
+ return SkillTemplate(
134
+ name=name,
135
+ description=(pack_skill.get("description") or "")[:1000],
136
+ triggers=list(pack_skill.get("triggers", []) or [])[:20],
137
+ slots=slots,
138
+ steps=list(pack_skill.get("steps", []) or [])[:30],
139
+ examples=list(pack_skill.get("examples", []) or [])[:5],
140
+ tags=list(pack_skill.get("tags", []) or [])[:20],
141
+ confidence=max(0.0, min(1.0, float(pack_skill.get("confidence", 0.0) or 0.0))),
142
+ cluster_size=int(pack_skill.get("cluster_size", 0) or 0),
143
+ )
144
+ except Exception:
145
+ return None
146
+
147
+
148
+ def import_skill_pack(skill_store, pack: Any,
149
+ mode: str = "merge",
150
+ actor: str = "") -> Dict[str, Any]:
151
+ """导入技能包。mode: merge(UPSERT 合并)/ replace(先删后写)。"""
152
+ v = validate_skill_pack(pack)
153
+ if not v["valid"]:
154
+ return {"success": False, "errors": v["errors"]}
155
+
156
+ skills = pack.get("skills") or []
157
+ templates = []
158
+ for ps in skills:
159
+ t = _parse_skill(ps)
160
+ if t is not None:
161
+ templates.append(t)
162
+
163
+ if mode == "replace":
164
+ # 先删除现有全部技能(仅在包非空时)
165
+ try:
166
+ existing = skill_store.list_skills(limit=MAX_SKILLS_PER_PACK)
167
+ for e in existing:
168
+ skill_store.delete_skill(e.get("name", ""))
169
+ except Exception as ex:
170
+ return {"success": False, "error": f"替换模式清空失败: {ex}"}
171
+
172
+ saved = 0
173
+ if templates:
174
+ try:
175
+ saved = skill_store.save_skills(templates)
176
+ except Exception as ex:
177
+ return {"success": False, "error": f"技能保存失败: {ex}"}
178
+
179
+ imported_cases = 0
180
+ cases = pack.get("cases") or []
181
+ for c in cases[:MAX_CASES_PER_PACK]:
182
+ if not isinstance(c, dict):
183
+ continue
184
+ try:
185
+ skill_store.record_case(
186
+ task=(c.get("task") or "")[:500] or "imported",
187
+ content=(c.get("content") or "")[:2000],
188
+ result=(c.get("result") or "")[:2000],
189
+ outcome=c.get("outcome", "success"),
190
+ duration_seconds=float(c.get("duration_seconds", 0.0) or 0.0),
191
+ tags=list(c.get("tags", []) or [])[:20],
192
+ agent_id=actor or "",
193
+ )
194
+ imported_cases += 1
195
+ except Exception:
196
+ continue
197
+
198
+ return {
199
+ "success": True,
200
+ "mode": mode,
201
+ "skills_imported": saved,
202
+ "cases_imported": imported_cases,
203
+ "skills_total": len(templates),
204
+ "cases_total": len(cases),
205
+ }