myagent-ai 1.12.0 → 1.12.3

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.
@@ -49,7 +49,7 @@ class MainAgent(BaseAgent):
49
49
  <tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名</toolname><parms>JSON格式的参数对象,例如: {"query": "搜索关键词", "num": 5}</parms><timeout>预估超时时限(秒)</timeout><callback>true/false,要求解析器在该工具执行完后是否要回调llm大模型,将所有工具输出结果+新构造的"context"输入给llm</callback></tool>
50
50
  </toolstocal>
51
51
  <remember><type>global或session</type><content>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得记忆的信息(如用户偏好、重要结论、错误经验等)。type=global表示跨会话全局记忆,type=session表示仅当前会话可用的记忆。如果本轮没有新信息需要记忆,则<content>为空、<type>不填。</content></remember>
52
- <recall>下一轮执行需要调取的记忆,这里要设计接上记忆库</recall>
52
+ <recall>下一轮需要主动召回的记忆描述。填写需要从记忆库中检索的关键字或描述。如果不填写则为空。如果需要更多记忆支持当前任务,填写相关关键词(可包含时间参考,如"2025年1月的项目"),系统将在下一轮搜索top5相关记忆并通过<recall_memory>注入上下文。你也可以直接调用recall_memory工具即时搜索。</recall>
53
53
  <knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等。这些知识将被持久化存储,未来可通过 <get_knowledge> 检索复用。如果本轮没有需要保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
54
54
  <get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
55
55
  <askuser>需要询问用户的内容,如无,则为空</askuser>
@@ -69,7 +69,7 @@ class MainAgent(BaseAgent):
69
69
  7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
70
70
  8. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
71
71
  9. <remember>: 包含 <type> 和 <content> 子标签。type 填 "global"(跨会话全局记忆)或 "session"(仅当前会话)。content 填从最新用户输入中提炼的值得记忆的关键信息。如果本轮无需记忆,content 为空且不填 type。注意:用户个人偏好、重要结论、通用经验用 global;当前任务的临时上下文、过程信息用 session
72
- 10. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
72
+ 10. <recall>: 填写下一轮需要从记忆库中主动召回的内容描述和关键字(可包含时间参考)。系统将根据这些信息搜索top5相关记忆,在下一轮通过 <recall_memory> 标签注入上下文。如果当前 <automemory> 中的记忆已足够完成任务,<recall> 为空;如果需要更多历史记忆支撑,则填写。你也可以直接使用 recall_memory 工具在当前轮即时搜索
73
73
  11. <knowledge>: 从本轮对话或工具执行结果中提炼值得长期保存的专业知识、事实、经验法则、技术要点等。这些知识会被持久化到知识库文件,未来可通过 get_knowledge 检索复用。如果没有需要保存的知识,则为空。格式:简洁明确,每条知识一行
74
74
  12. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
75
75
  13. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
@@ -78,6 +78,11 @@ class MainAgent(BaseAgent):
78
78
  16. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
79
79
  17. 使用中文输出所有内容
80
80
 
81
+ ## 上下文中的记忆系统说明
82
+ - <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
83
+ - <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
84
+ - 两种记忆互补:automemory 是自动匹配的,recall_memory 是你主动指定搜索的。如果 automemory 不足,使用 <recall> 请求更多。
85
+
81
86
  ## 工具选择指南
82
87
  - **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
83
88
  - **读取网页内容**: 用 `web_read`(传入URL,提取正文)
@@ -85,6 +90,7 @@ class MainAgent(BaseAgent):
85
90
  - **执行代码**: 用 `code` 工具(language: python/javascript/shell)
86
91
  - **执行命令**: 用 `command` 或 `command_run` 工具
87
92
  - **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
93
+ - **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
88
94
  """
89
95
 
90
96
  def __init__(self, tool_agent=None, memory_agent=None, **kwargs):
@@ -553,7 +559,7 @@ class MainAgent(BaseAgent):
553
559
  llm_raw = response.content
554
560
  logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
555
561
 
556
- # 保存 LLM 原始输出到会话记忆(用于回溯和审计)
562
+ # 保存 LLM 原始输出到会话记忆(用于回溯和审计,key=llm_output 不出现在对话历史中)
557
563
  if self.memory:
558
564
  self.memory.add_session(
559
565
  session_id=context.session_id,
@@ -752,31 +758,20 @@ class MainAgent(BaseAgent):
752
758
 
753
759
  # Step 10: 执行工具调用(无论 finish 值如何,先执行工具)
754
760
  if not parsed.tools_to_call:
755
- # 无工具调用: 直接根据 finish 判断是否结束
756
- if parsed.finish:
757
- logger.info(f"[{task_id}] finish=true 且无工具调用,结束循环")
758
- before, after = extract_surrounding_text(llm_raw)
759
- final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "任务已完成。"
760
- context.working_memory["final_response"] = final_text
761
- await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
762
- if self.memory:
763
- self.memory.add_session(
764
- session_id=context.session_id,
765
- role="assistant",
766
- content=final_text,
767
- )
761
+ # 无工具调用: 优先使用已收集的 reasoning 文本(包含 parsed.response),避免丢失第一轮输出
762
+ if _v2_reasoning_collected:
763
+ final_text = "\n".join(_v2_reasoning_collected)
768
764
  else:
769
- logger.info(f"[{task_id}] 无工具调用且 finish=false,结束")
770
765
  before, after = extract_surrounding_text(llm_raw)
771
- final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "处理完毕。"
772
- context.working_memory["final_response"] = final_text
773
- await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
774
- if self.memory:
775
- self.memory.add_session(
776
- session_id=context.session_id,
777
- role="assistant",
778
- content=final_text,
779
- )
766
+ final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "任务已完成。"
767
+ context.working_memory["final_response"] = final_text
768
+ await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
769
+ if self.memory:
770
+ self.memory.add_session(
771
+ session_id=context.session_id,
772
+ role="assistant",
773
+ content=final_text,
774
+ )
780
775
  break
781
776
 
782
777
  # Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
@@ -981,15 +976,8 @@ class MainAgent(BaseAgent):
981
976
  # 数据密集型工具允许更长的输出
982
977
  _HEAVY_TOOLS = ("web_search", "web_read", "url_read", "file_list",
983
978
  "file_search", "browser_open", "process_list")
984
- # OpenClaw prompt-only 技能也允许较长输出(SKILL.md 指令)
985
- _is_openclaw = (
986
- isinstance(tool_result.get("data"), dict)
987
- and tool_result.get("data", {}).get("skill_type") == "openclaw"
988
- )
989
979
  if tool_name in _HEAVY_TOOLS:
990
980
  _max_output = 6000
991
- elif _is_openclaw:
992
- _max_output = 8000
993
981
  else:
994
982
  _max_output = 3000
995
983
  tool_outputs_parts.append(
@@ -1168,6 +1156,34 @@ class MainAgent(BaseAgent):
1168
1156
  else:
1169
1157
  result["error"] = "执行引擎未初始化"
1170
1158
 
1159
+ elif tool_name == "recall_memory":
1160
+ # === 主动召回记忆工具 ===
1161
+ # 根据 memory_agent.recall_memory() 搜索历史记忆
1162
+ try:
1163
+ if self.memory_agent:
1164
+ recall_results = await self.memory_agent.recall_memory(
1165
+ keyword=params.get("keyword", ""),
1166
+ time_point=params.get("time_point", ""),
1167
+ session_id=params.get("session_id", ""),
1168
+ limit=params.get("limit", 5),
1169
+ )
1170
+ if recall_results:
1171
+ output_lines = [f"找到 {len(recall_results)} 条相关记忆:"]
1172
+ for i, mem in enumerate(recall_results, 1):
1173
+ output_lines.append(
1174
+ f"{i}. [{mem.get('created_at', '')}] "
1175
+ f"[{mem.get('category', '')}] "
1176
+ f"{mem.get('content', '')}"
1177
+ )
1178
+ result = {"success": True, "output": "\n".join(output_lines), "data": recall_results}
1179
+ else:
1180
+ result = {"success": True, "output": "未找到相关记忆", "data": []}
1181
+ else:
1182
+ result = {"success": False, "error": "记忆系统未初始化"}
1183
+ except Exception as re_err:
1184
+ result = {"success": False, "error": f"记忆召回失败: {re_err}"}
1185
+ logger.warning(f"[{task_id}] recall_memory 工具异常: {re_err}")
1186
+
1171
1187
  elif self.skills:
1172
1188
  exec_result = await self.skills.execute(tool_name, **params)
1173
1189
  result = exec_result.to_dict()
@@ -333,3 +333,91 @@ class MemoryAgent(BaseAgent):
333
333
 
334
334
  if context_parts:
335
335
  context.working_memory["memory_context_prompt"] = "\n".join(context_parts)
336
+
337
+ async def recall_memory(
338
+ self,
339
+ keyword: str = "",
340
+ time_point: str = "",
341
+ session_id: str = "",
342
+ limit: int = 5,
343
+ ) -> list:
344
+ """
345
+ 主动召回记忆 —— 根据关键字和时间点搜索历史记忆。
346
+
347
+ 这是供大模型通过 <recall> 标签调用的工具方法。
348
+ 搜索范围包括全局记忆和会话记忆。
349
+
350
+ Args:
351
+ keyword: 搜索关键字(必填,用于模糊匹配和语义搜索)
352
+ time_point: 时间参考点(可选),格式如 "2025-01" 或 "2025-01-15"
353
+ 系统会将其转换为 start_time 进行时间范围过滤
354
+ session_id: 会话 ID(可选,为空则跨会话搜索)
355
+ limit: 返回数量(默认 5)
356
+
357
+ Returns:
358
+ 匹配的记忆列表,每项包含 content, created_at, key, category
359
+ """
360
+ if not self.memory:
361
+ return []
362
+
363
+ # 解析 time_point 为 start_time
364
+ start_time = ""
365
+ if time_point:
366
+ # 支持多种格式: "2025-01", "2025-01-15", "2025年1月"
367
+ import re as _re
368
+ # 尝试 "YYYY年MM月" 格式
369
+ m = _re.match(r"(\d{4})年(\d{1,2})月", time_point)
370
+ if m:
371
+ start_time = f"{m.group(1)}-{int(m.group(2)):02d}-01 00:00:00"
372
+ else:
373
+ # 尝试 "YYYY-MM" 或 "YYYY-MM-DD"
374
+ m = _re.match(r"(\d{4})-(\d{1,2})(?:-(\d{1,2}))?", time_point)
375
+ if m:
376
+ year = m.group(1)
377
+ month = int(m.group(2))
378
+ day = m.group(3)
379
+ if day:
380
+ start_time = f"{year}-{month:02d}-{int(day):02d} 00:00:00"
381
+ else:
382
+ start_time = f"{year}-{month:02d}-01 00:00:00"
383
+
384
+ # 使用 search_by_time_range 进行精确搜索
385
+ if start_time or keyword:
386
+ results = self.memory.search_by_time_range(
387
+ session_id=session_id,
388
+ start_time=start_time,
389
+ keyword=keyword,
390
+ limit=limit,
391
+ )
392
+ if results:
393
+ return [
394
+ {
395
+ "content": e.content,
396
+ "created_at": e.created_at,
397
+ "key": e.key,
398
+ "category": e.category,
399
+ "summary": e.summary,
400
+ }
401
+ for e in results
402
+ ]
403
+
404
+ # 回退到普通搜索
405
+ if keyword:
406
+ results = self.memory.search(
407
+ query=keyword,
408
+ session_id=session_id,
409
+ limit=limit,
410
+ mode="hybrid",
411
+ )
412
+ return [
413
+ {
414
+ "content": e.content,
415
+ "created_at": e.created_at,
416
+ "key": e.key,
417
+ "category": e.category,
418
+ "summary": e.summary,
419
+ }
420
+ for e in results
421
+ ]
422
+
423
+ return []
@@ -4,14 +4,15 @@ core/context_builder.py - 上下文构建器
4
4
  为 LLM 系统提示词构建结构化的 XML <context> 块。
5
5
 
6
6
  <context> 包含以下段落:
7
- <whomi> - Agent 身份信息(名称、描述)
8
- <memory> - 记忆检索结果(Top 10 相关记忆)
9
- <knowledge> - 知识库 RAG 检索结果(根据 get_knowledge 关键词搜索)
10
- <resentdialog> - 近期对话历史(截断至 ~20K 字符)
11
- <userprint> - 用户键盘输入文本(语音输入时为空)
12
- <usersays> - 用户语音转文本输入(键盘输入时为空)
13
- <task_plan> - 当前任务计划(Markdown 格式,上轮已完成时为空)
14
- <tools> - 可用工具列表(名称、描述、参数格式)
7
+ <whomi> - Agent 身份信息(名称、描述)
8
+ <automemory> - 自动记忆检索结果(Top 10 相关记忆,由 <remember> 产生)
9
+ <recall_memory> - 主动召回记忆(上一轮 <recall> 触发的 Top 5 记忆)
10
+ <knowledge> - 知识库 RAG 检索结果(根据 get_knowledge 关键词搜索)
11
+ <resentdialog> - 近期对话历史(截断至 ~20K 字符)
12
+ <userprint> - 用户键盘输入文本(语音输入时为空)
13
+ <usersays> - 用户语音转文本输入(键盘输入时为空)
14
+ <task_plan> - 当前任务计划(Markdown 格式,上轮已完成时为空)
15
+ <tools> - 可用工具列表(名称、描述、参数格式)
15
16
 
16
17
  使用示例:
17
18
  builder = ContextBuilder(memory_manager=mm, skill_registry=sr)
@@ -51,6 +52,12 @@ class ContextBuilder:
51
52
 
52
53
  该 XML 块会注入到 LLM 系统提示词中,为模型提供完整的上下文感知能力。
53
54
 
55
+ 上下文中的记忆分为两层:
56
+ - <automemory>: 自动记忆 —— 由大模型 <remember> 产生并持久化的记忆,
57
+ 根据用户当前输入自动搜索 top10 相关记忆供参考。
58
+ - <recall_memory>: 主动召回记忆 —— 大模型在上一轮通过 <recall> 标签
59
+ 指定要召回的记忆,系统根据关键字+时间搜索 top5 注入。
60
+
54
61
  Args:
55
62
  memory_manager: 记忆管理器实例(可选,传入后可检索相关记忆)
56
63
  skill_registry: 技能注册表实例(可选,传入后可列出可用工具)
@@ -200,51 +207,102 @@ class ContextBuilder:
200
207
 
201
208
  def _build_memory(self, query: str, session_id: str, recall: str = "") -> str:
202
209
  """
203
- 构建 <memory> 段落 —— 记忆检索结果。
210
+ 构建 <automemory> 和 <recall_memory> 段落 —— 双层记忆检索结果。
211
+
212
+ <automemory>: 根据用户当前输入自动搜索 top10 相关记忆。
213
+ 这些记忆来自大模型通过 <remember> 标签持久化的内容(包含时间信息)。
214
+ 搜索范围: 全局记忆(global) + 当前会话的 remember 类记忆。
204
215
 
205
- 使用 MemoryManager 的混合搜索 (keyword + TF-IDF) 检索
206
- 与当前查询最相关的 Top 10 条记忆。
216
+ <recall_memory>: 上一轮 LLM 输出的 <recall> 内容触发的主动召回。
217
+ 根据关键字和时间点搜索 top5 历史记忆,注入到本轮上下文中。
218
+ 如果上一轮未输出 <recall>,则此段为空。
207
219
 
208
220
  Args:
209
221
  query: 搜索查询文本(通常为最新用户消息)
210
222
  session_id: 会话 ID
211
- recall: LLM 上一轮输出的 <recall> 内容,
212
- 用于定向检索长期记忆(优先级高于 query)
223
+ recall: LLM 上一轮输出的 <recall> 内容(关键字+时间描述)
213
224
 
214
225
  Returns:
215
- <memory> XML 段落字符串
226
+ <automemory> + <recall_memory> XML 段落字符串
216
227
  """
217
228
  if not self.memory_manager:
218
- return "<memory>\n(记忆系统未启用)\n</memory>"
219
-
220
- # 构建搜索查询:优先使用 recall 定向检索,其次用用户消息
221
- search_query = recall.strip() if recall.strip() else query.strip()
222
- if not search_query:
223
- return "<memory>\n(无相关记忆)\n</memory>"
224
-
225
- try:
226
- results = self.memory_manager.search(
227
- query=search_query,
228
- session_id=session_id,
229
- limit=10,
230
- mode="hybrid",
231
- )
232
- except Exception as e:
233
- logger.warning(f"记忆搜索失败: {e}")
234
- return "<memory>\n(记忆搜索不可用)\n</memory>"
235
-
236
- if not results:
237
- return "<memory>\n(无相关记忆)\n</memory>"
238
-
239
- # 每条记忆格式化为单行
240
- lines: List[str] = ["<memory>"]
241
- for i, entry in enumerate(results, 1):
242
- content = entry.content.strip()
243
- if content:
244
- lines.append(f"{i}. {_xml_escape(content)}")
245
-
246
- lines.append("</memory>")
247
- return "\n".join(lines)
229
+ return "<automemory>\n(记忆系统未启用)\n</automemory>\n<recall_memory>\n(记忆系统未启用)\n</recall_memory>"
230
+
231
+ # ═══════════════════════════════════════════
232
+ # Part 1: <automemory> 自动记忆检索
233
+ # ═══════════════════════════════════════════
234
+ search_query = query.strip()
235
+ auto_lines: List[str] = []
236
+
237
+ if search_query:
238
+ try:
239
+ # 搜索全局记忆中由 remember 产生的内容
240
+ global_results = self.memory_manager.search(
241
+ query=search_query,
242
+ session_id="", # 跨会话搜索全局记忆
243
+ category="global",
244
+ limit=10,
245
+ mode="hybrid",
246
+ )
247
+ # 搜索当前会话中 conversation_insight 类记忆
248
+ session_results = self.memory_manager.search(
249
+ query=search_query,
250
+ session_id=session_id,
251
+ category="session",
252
+ limit=5,
253
+ mode="hybrid",
254
+ )
255
+ # 合并去重(全局优先,会话补充)
256
+ seen_ids = set()
257
+ combined = []
258
+ for entry in global_results + session_results:
259
+ if entry.id not in seen_ids:
260
+ seen_ids.add(entry.id)
261
+ combined.append(entry)
262
+
263
+ if combined:
264
+ auto_lines.append("<automemory>")
265
+ for i, entry in enumerate(combined[:10], 1):
266
+ content = entry.content.strip()
267
+ if content:
268
+ auto_lines.append(f"{i}. {_xml_escape(content)}")
269
+ auto_lines.append("</automemory>")
270
+ except Exception as e:
271
+ logger.warning(f"automemory 搜索失败: {e}")
272
+
273
+ if not auto_lines:
274
+ auto_lines = ["<automemory>", "(无相关自动记忆)", "</automemory>"]
275
+
276
+ # ═══════════════════════════════════════════
277
+ # Part 2: <recall_memory> — 主动召回记忆
278
+ # ═══════════════════════════════════════════
279
+ recall_lines: List[str] = []
280
+ recall_text = recall.strip() if recall else ""
281
+
282
+ if recall_text:
283
+ try:
284
+ # 解析 recall 中的关键字和时间描述
285
+ # 格式可能是: "关键字1 关键字2" 或 "关于XX的记忆 2025年1月" 等
286
+ recall_results = self.memory_manager.search(
287
+ query=recall_text,
288
+ session_id="", # 跨会话搜索
289
+ limit=5,
290
+ mode="hybrid",
291
+ )
292
+ if recall_results:
293
+ recall_lines.append("<recall_memory>")
294
+ for i, entry in enumerate(recall_results[:5], 1):
295
+ content = entry.content.strip()
296
+ if content:
297
+ recall_lines.append(f"{i}. {_xml_escape(content)}")
298
+ recall_lines.append("</recall_memory>")
299
+ except Exception as e:
300
+ logger.warning(f"recall_memory 搜索失败: {e}")
301
+
302
+ if not recall_lines:
303
+ recall_lines = ["<recall_memory>", "(无主动召回记忆)", "</recall_memory>"]
304
+
305
+ return "\n".join(auto_lines) + "\n" + "\n".join(recall_lines)
248
306
 
249
307
  def _build_knowledge(self, query: str) -> str:
250
308
  """
package/main.py CHANGED
@@ -42,6 +42,36 @@ from core.permissions import PermissionManager
42
42
  from core.deps_checker import check_and_install_deps
43
43
 
44
44
 
45
+ def _get_screen_resolution() -> tuple[int, int]:
46
+ """获取当前屏幕分辨率,跨平台兼容 Windows / macOS / Linux。"""
47
+ try:
48
+ import tkinter as tk
49
+ root = tk.Tk()
50
+ root.withdraw() # 不显示窗口
51
+ w = root.winfo_screenwidth()
52
+ h = root.winfo_screenheight()
53
+ root.destroy()
54
+ return w, h
55
+ except Exception:
56
+ pass
57
+ # Linux 回退方案
58
+ try:
59
+ import subprocess
60
+ result = subprocess.run(
61
+ ["xdpyinfo"], capture_output=True, text=True, timeout=5
62
+ )
63
+ for line in result.stdout.splitlines():
64
+ if "dimensions:" in line:
65
+ parts = line.split()
66
+ for p in parts:
67
+ if "x" in p:
68
+ w, h = p.split("x")
69
+ return int(w), int(h)
70
+ except Exception:
71
+ pass
72
+ return 1920, 1080
73
+
74
+
45
75
  def _open_browser_kiosk(url: str):
46
76
  """打开浏览器窗口(无地址栏模式),回退到系统浏览器。
47
77
 
@@ -54,10 +84,9 @@ def _open_browser_kiosk(url: str):
54
84
 
55
85
  async def _launch():
56
86
  pw = await async_playwright().start()
57
- # 使用 window-size 和 window-position 确保窗口最大化
87
+ # 获取实际屏幕分辨率,确保窗口占满屏幕
58
88
  # --app 模式下 --start-maximized 不生效,需要手动设置窗口大小
59
- screen_width = 1920
60
- screen_height = 1080
89
+ screen_width, screen_height = _get_screen_resolution()
61
90
  browser = await pw.chromium.launch(
62
91
  headless=False,
63
92
  args=[
@@ -229,8 +258,6 @@ class MyAgentApp:
229
258
  self.skill_registry = SkillRegistry()
230
259
  # 注册内置技能
231
260
  self._register_builtin_skills()
232
- # 加载外部 OpenClaw 技能
233
- self.skill_registry.load_openclaw_skills()
234
261
  skills = self.skill_registry.list_skills()
235
262
  self.logger.info(f"技能系统: {len(skills)} 个技能已注册 - {skills}")
236
263
 
package/memory/manager.py CHANGED
@@ -152,6 +152,8 @@ class MemoryManager:
152
152
  CREATE INDEX IF NOT EXISTS idx_key ON memories(key);
153
153
  CREATE INDEX IF NOT EXISTS idx_session_category ON memories(session_id, category);
154
154
  CREATE INDEX IF NOT EXISTS idx_importance ON memories(importance DESC);
155
+ CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at);
156
+ CREATE INDEX IF NOT EXISTS idx_session_category_created ON memories(session_id, category, created_at);
155
157
 
156
158
  CREATE TABLE IF NOT EXISTS session_names (
157
159
  session_id TEXT PRIMARY KEY,
@@ -276,19 +278,28 @@ class MemoryManager:
276
278
  # ==========================================================================
277
279
 
278
280
  def add_session(self, session_id, role="", content="", key="", importance=0.5, metadata=None) -> str:
279
- """添加会话记忆。"""
281
+ """添加会话记忆。内容自动注入时间前缀,确保自包含时间信息。"""
282
+ from datetime import datetime as _dt
283
+ _now_str = _dt.now().strftime("%Y-%m-%d %H:%M:%S")
284
+ # 对话类记忆(user/assistant/system/tool)自动加时间前缀
285
+ if role and content and not content.startswith("["):
286
+ timestamped_content = f"[{_now_str}] {content}"
287
+ else:
288
+ timestamped_content = truncate_str(content, 50000)
280
289
  entry = MemoryEntry(
281
290
  session_id=session_id, category="session", role=role,
282
- content=truncate_str(content, 50000), key=key,
283
- importance=importance, metadata=metadata or {},
291
+ content=timestamped_content, key=key,
292
+ importance=importance, metadata={"timestamp": _now_str, **(metadata or {})},
284
293
  )
285
294
  return self._insert(entry)
286
295
 
287
296
  def get_conversation(self, session_id, limit=500, include_roles=None) -> List[MemoryEntry]:
288
- """获取对话历史(session 分类中 role 非空的条目)"""
297
+ """获取对话历史(session 分类中 role 非空的条目),按时间正序排列。"""
289
298
  conn = self._get_conn()
299
+ # 排除内部系统条目(llm_output, tool_call, tool_result, conversation_insight)
290
300
  sql = """SELECT * FROM memories
291
301
  WHERE session_id = ? AND category = 'session' AND role != ''
302
+ AND key NOT IN ('llm_output', 'tool_call', 'tool_result', 'conversation_insight')
292
303
  ORDER BY created_at ASC LIMIT ?"""
293
304
  rows = conn.execute(sql, (session_id, limit)).fetchall()
294
305
  entries = [MemoryEntry.from_row(row) for row in rows]
@@ -296,6 +307,60 @@ class MemoryManager:
296
307
  entries = [e for e in entries if e.role in include_roles]
297
308
  return entries
298
309
 
310
+ def get_conversation_all(self, session_id, limit=5000) -> List[MemoryEntry]:
311
+ """获取全量对话历史(包含所有内部条目),用于完整回溯。"""
312
+ conn = self._get_conn()
313
+ sql = """SELECT * FROM memories
314
+ WHERE session_id = ? AND category = 'session' AND role != ''
315
+ ORDER BY created_at ASC LIMIT ?"""
316
+ rows = conn.execute(sql, (session_id, limit)).fetchall()
317
+ return [MemoryEntry.from_row(row) for row in rows]
318
+
319
+ def search_by_time_range(
320
+ self,
321
+ session_id: str = "",
322
+ start_time: str = "",
323
+ end_time: str = "",
324
+ keyword: str = "",
325
+ limit: int = 10,
326
+ ) -> List[MemoryEntry]:
327
+ """
328
+ 按时间范围 + 关键词搜索记忆。
329
+
330
+ Args:
331
+ session_id: 会话 ID(空=跨会话)
332
+ start_time: 起始时间 ISO 格式(如 "2025-01-01 00:00:00"),空=不限
333
+ end_time: 截止时间 ISO 格式,空=不限
334
+ keyword: 关键词过滤(LIKE 模糊匹配),空=不限
335
+ limit: 返回数量
336
+
337
+ Returns:
338
+ 匹配的记忆条目列表(按时间倒序)
339
+ """
340
+ conn = self._get_conn()
341
+ conditions = ["category != ''", "(expires_at = '' OR expires_at > ?)"]
342
+ params: list = [timestamp()]
343
+
344
+ if session_id:
345
+ conditions.append("session_id = ?")
346
+ params.append(session_id)
347
+ if start_time:
348
+ conditions.append("created_at >= ?")
349
+ params.append(start_time)
350
+ if end_time:
351
+ conditions.append("created_at <= ?")
352
+ params.append(end_time)
353
+ if keyword:
354
+ conditions.append("(content LIKE ? OR summary LIKE ? OR key LIKE ?)")
355
+ like_pattern = f"%{keyword}%"
356
+ params.extend([like_pattern, like_pattern, like_pattern])
357
+
358
+ where = " AND ".join(conditions)
359
+ sql = f"SELECT * FROM memories WHERE {where} ORDER BY created_at DESC LIMIT ?"
360
+ params.append(limit)
361
+ rows = conn.execute(sql, params).fetchall()
362
+ return [MemoryEntry.from_row(row) for row in rows]
363
+
299
364
  def get_conversation_text(
300
365
  self,
301
366
  session_id: str,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.12.0",
3
+ "version": "1.12.3",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {