myagent-ai 1.12.0 → 1.12.2
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/agents/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/agents/__pycache__/memory_agent.cpython-312.pyc +0 -0
- package/agents/main_agent.py +49 -26
- package/agents/memory_agent.py +88 -0
- package/core/__pycache__/context_builder.cpython-312.pyc +0 -0
- package/core/__pycache__/output_parser.cpython-312.pyc +0 -0
- package/core/context_builder.py +102 -44
- package/main.py +32 -3
- package/memory/__pycache__/manager.cpython-312.pyc +0 -0
- package/memory/manager.py +69 -4
- package/package.json +1 -1
- package/web/api_server.py +3 -1
- package/web/ui/chat/chat.css +13 -8
- package/web/ui/chat/chat_main.js +40 -10
- package/web/ui/chat/flow_engine.js +14 -3
- package/web/ui/chat/left_sessions.html +1 -1
- package/web/ui/chat/right_agents.html +1 -1
- package/web/ui/index.html +6 -5
|
Binary file
|
|
Binary file
|
package/agents/main_agent.py
CHANGED
|
@@ -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
|
|
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
|
-
# 无工具调用:
|
|
756
|
-
if
|
|
757
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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 决定回调
|
|
@@ -1168,6 +1163,34 @@ class MainAgent(BaseAgent):
|
|
|
1168
1163
|
else:
|
|
1169
1164
|
result["error"] = "执行引擎未初始化"
|
|
1170
1165
|
|
|
1166
|
+
elif tool_name == "recall_memory":
|
|
1167
|
+
# === 主动召回记忆工具 ===
|
|
1168
|
+
# 根据 memory_agent.recall_memory() 搜索历史记忆
|
|
1169
|
+
try:
|
|
1170
|
+
if self.memory_agent:
|
|
1171
|
+
recall_results = await self.memory_agent.recall_memory(
|
|
1172
|
+
keyword=params.get("keyword", ""),
|
|
1173
|
+
time_point=params.get("time_point", ""),
|
|
1174
|
+
session_id=params.get("session_id", ""),
|
|
1175
|
+
limit=params.get("limit", 5),
|
|
1176
|
+
)
|
|
1177
|
+
if recall_results:
|
|
1178
|
+
output_lines = [f"找到 {len(recall_results)} 条相关记忆:"]
|
|
1179
|
+
for i, mem in enumerate(recall_results, 1):
|
|
1180
|
+
output_lines.append(
|
|
1181
|
+
f"{i}. [{mem.get('created_at', '')}] "
|
|
1182
|
+
f"[{mem.get('category', '')}] "
|
|
1183
|
+
f"{mem.get('content', '')}"
|
|
1184
|
+
)
|
|
1185
|
+
result = {"success": True, "output": "\n".join(output_lines), "data": recall_results}
|
|
1186
|
+
else:
|
|
1187
|
+
result = {"success": True, "output": "未找到相关记忆", "data": []}
|
|
1188
|
+
else:
|
|
1189
|
+
result = {"success": False, "error": "记忆系统未初始化"}
|
|
1190
|
+
except Exception as re_err:
|
|
1191
|
+
result = {"success": False, "error": f"记忆召回失败: {re_err}"}
|
|
1192
|
+
logger.warning(f"[{task_id}] recall_memory 工具异常: {re_err}")
|
|
1193
|
+
|
|
1171
1194
|
elif self.skills:
|
|
1172
1195
|
exec_result = await self.skills.execute(tool_name, **params)
|
|
1173
1196
|
result = exec_result.to_dict()
|
package/agents/memory_agent.py
CHANGED
|
@@ -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 []
|
|
Binary file
|
|
Binary file
|
package/core/context_builder.py
CHANGED
|
@@ -4,14 +4,15 @@ core/context_builder.py - 上下文构建器
|
|
|
4
4
|
为 LLM 系统提示词构建结构化的 XML <context> 块。
|
|
5
5
|
|
|
6
6
|
<context> 包含以下段落:
|
|
7
|
-
<whomi>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
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
|
-
构建 <
|
|
210
|
+
构建 <automemory> 和 <recall_memory> 段落 —— 双层记忆检索结果。
|
|
211
|
+
|
|
212
|
+
<automemory>: 根据用户当前输入自动搜索 top10 相关记忆。
|
|
213
|
+
这些记忆来自大模型通过 <remember> 标签持久化的内容(包含时间信息)。
|
|
214
|
+
搜索范围: 全局记忆(global) + 当前会话的 remember 类记忆。
|
|
204
215
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
<
|
|
226
|
+
<automemory> + <recall_memory> XML 段落字符串
|
|
216
227
|
"""
|
|
217
228
|
if not self.memory_manager:
|
|
218
|
-
return "<
|
|
219
|
-
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
#
|
|
87
|
+
# 获取实际屏幕分辨率,确保窗口占满屏幕
|
|
58
88
|
# --app 模式下 --start-maximized 不生效,需要手动设置窗口大小
|
|
59
|
-
screen_width =
|
|
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=[
|
|
Binary file
|
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=
|
|
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
package/web/api_server.py
CHANGED
|
@@ -2654,6 +2654,7 @@ class ApiServer:
|
|
|
2654
2654
|
"id": m.id, "name": m.name, "provider": m.provider,
|
|
2655
2655
|
"api_type": m.api_type,
|
|
2656
2656
|
"model": m.model, "base_url": m.base_url,
|
|
2657
|
+
"api_key": m.api_key or "",
|
|
2657
2658
|
"max_tokens": m.max_tokens, "temperature": m.temperature,
|
|
2658
2659
|
"context_window": m.context_window,
|
|
2659
2660
|
"input_modes": m.input_modes,
|
|
@@ -2705,7 +2706,8 @@ class ApiServer:
|
|
|
2705
2706
|
for k in ("name", "provider", "api_type", "model", "base_url", "max_tokens", "temperature", "context_window", "input_modes", "reasoning", "enabled", "is_global_fallback"):
|
|
2706
2707
|
if k in data:
|
|
2707
2708
|
setattr(m, k, data[k])
|
|
2708
|
-
|
|
2709
|
+
# api_key: 如果请求中包含 api_key 字段(即使是空字符串),则更新
|
|
2710
|
+
if "api_key" in data:
|
|
2709
2711
|
m.api_key = data["api_key"]
|
|
2710
2712
|
found = True
|
|
2711
2713
|
break
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -2074,16 +2074,20 @@ input,textarea,select{font:inherit}
|
|
|
2074
2074
|
.message-bubble > .msg-timeline > .inline-exec-event{background:var(--bg);border-left-color:var(--accent)}
|
|
2075
2075
|
[data-theme="dark"] .message-bubble > .msg-timeline > .inline-exec-event{background:var(--bg)}
|
|
2076
2076
|
[data-theme="dark"] .message-bubble > .msg-timeline > .inline-exec-code{background:var(--bg2)}
|
|
2077
|
-
.inline-exec-event{margin:2px 0;padding:8px 12px;background:var(--bg2);border-left:3px solid var(--border);border-radius:6px;font-size:13px;animation:execEventSlide .3s ease-out}
|
|
2078
|
-
.inline-exec-
|
|
2079
|
-
.inline-exec-
|
|
2080
|
-
.inline-exec-
|
|
2081
|
-
.inline-exec-
|
|
2082
|
-
.inline-exec-
|
|
2077
|
+
.inline-exec-event{margin:2px 0;padding:8px 12px;background:var(--bg2);border-left:3px solid var(--border);border-radius:6px;font-size:13px;animation:execEventSlide .3s ease-out;display:flex;flex-direction:column;gap:4px}
|
|
2078
|
+
.inline-exec-event.tool-success{border-left-color:#22c55e}
|
|
2079
|
+
.inline-exec-event.tool-failed{border-left-color:#ef4444}
|
|
2080
|
+
.inline-exec-event.tool-running{border-left-color:#f59e0b}
|
|
2081
|
+
.inline-exec-event.tool-call-pending{border-left-color:#60a5fa}
|
|
2082
|
+
.inline-exec-header{display:flex;align-items:center;gap:6px;min-height:20px;line-height:1}
|
|
2083
|
+
.inline-exec-icon{font-size:14px;flex-shrink:0;width:18px;text-align:center}
|
|
2084
|
+
.inline-exec-title{font-weight:600;color:var(--text);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
|
|
2085
|
+
.inline-exec-meta{color:var(--text3);font-size:11px;margin-left:auto;flex-shrink:0;white-space:nowrap}
|
|
2086
|
+
.inline-exec-code{background:var(--bg);padding:8px 10px;border-radius:4px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:11.5px;color:var(--text2);overflow:hidden;cursor:pointer;transition:var(--transition);white-space:pre-wrap;word-break:break-all;line-height:1.5;max-height:80px}
|
|
2083
2087
|
.inline-exec-code:hover{background:var(--bg3)}
|
|
2084
2088
|
.inline-exec-code.expanded{max-height:none}
|
|
2085
|
-
.inline-exec-summary{color:var(--text2);font-size:12px;
|
|
2086
|
-
.inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;
|
|
2089
|
+
.inline-exec-summary{color:var(--text2);font-size:12px;line-height:1.5;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
2090
|
+
.inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;transition:var(--transition);flex-shrink:0}
|
|
2087
2091
|
.inline-exec-result-btn:hover{background:var(--bg2);border-color:var(--accent);color:var(--accent)}
|
|
2088
2092
|
|
|
2089
2093
|
/* ── Execution Result Modal ── */
|
|
@@ -2123,6 +2127,7 @@ input,textarea,select{font:inherit}
|
|
|
2123
2127
|
[data-theme="dark"] .exec-result-modal-body pre{background:#0a0c10;color:#cdd6f4}
|
|
2124
2128
|
[data-theme="dark"] .exec-result-info-item{background:var(--bg3)}
|
|
2125
2129
|
[data-theme="dark"] .inline-exec-event{background:var(--bg3);border-left-color:var(--border)}
|
|
2130
|
+
[data-theme="dark"] .inline-exec-event.tool-call-pending{border-left-color:rgba(96,165,250,.6)}
|
|
2126
2131
|
[data-theme="dark"] .inline-exec-code{background:var(--bg)}
|
|
2127
2132
|
[data-theme="dark"] .inline-exec-result-btn:hover{background:var(--bg4)}
|
|
2128
2133
|
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1348,7 +1348,7 @@ function showAgentModal(editPath, parentPath, data) {
|
|
|
1348
1348
|
+ '<option value="qq"' + (platform === 'qq' ? ' selected' : '') + '>🐧 QQ</option>'
|
|
1349
1349
|
+ '<option value="wechat"' + (platform === 'wechat' ? ' selected' : '') + '>💬 微信</option>'
|
|
1350
1350
|
+ '</select>'
|
|
1351
|
-
+ '<button class="config-action-btn" style="padding:6px 10px;font-size:11px" onclick="window.
|
|
1351
|
+
+ '<button class="config-action-btn" style="padding:6px 10px;font-size:11px" onclick="window.location.href=\'/ui/index.html?page=platforms\'" title="管理聊天平台">管理平台</button>'
|
|
1352
1352
|
+ '</div>'
|
|
1353
1353
|
+ '<div class="hint">选择 Agent 对接的聊天平台,或点击"管理平台"新增接入</div></div>'
|
|
1354
1354
|
+ '<div class="agent-form-group" id="platformTokenGroup" style="display:' + (platform ? '' : 'none') + '"><label>平台 Token</label>'
|
|
@@ -2060,21 +2060,32 @@ function groupHistoryMessages(messages) {
|
|
|
2060
2060
|
const next = messages[i];
|
|
2061
2061
|
|
|
2062
2062
|
if (next.role === 'tool') {
|
|
2063
|
+
// Parse tool_result content: format is "[tool_name] 成功/失败\n{output}"
|
|
2064
|
+
const firstNewline = next.content.indexOf('\n');
|
|
2065
|
+
const headerLine = firstNewline > 0 ? next.content.substring(0, firstNewline) : next.content.substring(0, 80);
|
|
2066
|
+
const bodyContent = firstNewline > 0 ? next.content.substring(firstNewline + 1) : '';
|
|
2067
|
+
// Extract tool name from header: [tool_name] 成功 or [tool_name] 失败
|
|
2068
|
+
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2063
2069
|
const isOk = !next.content.includes('失败');
|
|
2070
|
+
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2064
2071
|
parts.push({
|
|
2065
2072
|
type: 'exec',
|
|
2066
2073
|
data: {
|
|
2067
2074
|
id: _evtId++,
|
|
2068
2075
|
type: 'tool_result',
|
|
2069
|
-
title:
|
|
2076
|
+
title: displayTitle,
|
|
2077
|
+
tool_name: toolResultName,
|
|
2070
2078
|
success: isOk,
|
|
2071
|
-
summary:
|
|
2072
|
-
result: { output:
|
|
2079
|
+
summary: bodyContent.substring(0, 500).trim(),
|
|
2080
|
+
result: { output: bodyContent.substring(0, 2000) },
|
|
2073
2081
|
}
|
|
2074
2082
|
});
|
|
2075
2083
|
i++;
|
|
2076
2084
|
} else if (next.role === 'assistant' && next.key === 'tool_call') {
|
|
2085
|
+
// Parse tool_call content: format is "调用工具: {name}\n参数: {parms}"
|
|
2077
2086
|
const toolName = (next.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
|
|
2087
|
+
const paramsMatch = next.content.match(/参数:\s*([\s\S]*)/);
|
|
2088
|
+
const toolParams = paramsMatch ? paramsMatch[1].trim() : '';
|
|
2078
2089
|
parts.push({
|
|
2079
2090
|
type: 'exec',
|
|
2080
2091
|
data: {
|
|
@@ -2082,6 +2093,7 @@ function groupHistoryMessages(messages) {
|
|
|
2082
2093
|
type: 'tool_call',
|
|
2083
2094
|
title: toolName ? ('调用工具: ' + toolName) : next.content.substring(0, 100),
|
|
2084
2095
|
tool_name: toolName,
|
|
2096
|
+
params: toolParams || undefined,
|
|
2085
2097
|
status: 'done',
|
|
2086
2098
|
}
|
|
2087
2099
|
});
|
|
@@ -2118,6 +2130,8 @@ function groupHistoryMessages(messages) {
|
|
|
2118
2130
|
|
|
2119
2131
|
if (isCall) {
|
|
2120
2132
|
const toolName = (msg.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
|
|
2133
|
+
const paramsMatch = msg.content.match(/参数:\s*([\s\S]*)/);
|
|
2134
|
+
const toolParams = paramsMatch ? paramsMatch[1].trim() : '';
|
|
2121
2135
|
parts.push({
|
|
2122
2136
|
type: 'exec',
|
|
2123
2137
|
data: {
|
|
@@ -2125,20 +2139,27 @@ function groupHistoryMessages(messages) {
|
|
|
2125
2139
|
type: 'tool_call',
|
|
2126
2140
|
title: toolName ? ('调用工具: ' + toolName) : msg.content.substring(0, 100),
|
|
2127
2141
|
tool_name: toolName,
|
|
2142
|
+
params: toolParams || undefined,
|
|
2128
2143
|
status: 'done',
|
|
2129
2144
|
}
|
|
2130
2145
|
});
|
|
2131
2146
|
} else if (isResult) {
|
|
2147
|
+
const firstNewline = msg.content.indexOf('\n');
|
|
2148
|
+
const headerLine = firstNewline > 0 ? msg.content.substring(0, firstNewline) : msg.content.substring(0, 80);
|
|
2149
|
+
const bodyContent = firstNewline > 0 ? msg.content.substring(firstNewline + 1) : '';
|
|
2150
|
+
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2132
2151
|
const isOk = !msg.content.includes('失败');
|
|
2152
|
+
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2133
2153
|
parts.push({
|
|
2134
2154
|
type: 'exec',
|
|
2135
2155
|
data: {
|
|
2136
2156
|
id: _evtId++,
|
|
2137
2157
|
type: 'tool_result',
|
|
2138
|
-
title:
|
|
2158
|
+
title: displayTitle,
|
|
2159
|
+
tool_name: toolResultName,
|
|
2139
2160
|
success: isOk,
|
|
2140
|
-
summary:
|
|
2141
|
-
result: { output:
|
|
2161
|
+
summary: bodyContent.substring(0, 500).trim(),
|
|
2162
|
+
result: { output: bodyContent.substring(0, 2000) },
|
|
2142
2163
|
}
|
|
2143
2164
|
});
|
|
2144
2165
|
}
|
|
@@ -2148,6 +2169,8 @@ function groupHistoryMessages(messages) {
|
|
|
2148
2169
|
const next = messages[i];
|
|
2149
2170
|
if (next.role === 'assistant' && next.key === 'tool_call') {
|
|
2150
2171
|
const toolName = (next.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
|
|
2172
|
+
const paramsMatch = next.content.match(/参数:\s*([\s\S]*)/);
|
|
2173
|
+
const toolParams = paramsMatch ? paramsMatch[1].trim() : '';
|
|
2151
2174
|
parts.push({
|
|
2152
2175
|
type: 'exec',
|
|
2153
2176
|
data: {
|
|
@@ -2155,6 +2178,7 @@ function groupHistoryMessages(messages) {
|
|
|
2155
2178
|
type: 'tool_call',
|
|
2156
2179
|
title: toolName ? ('调用工具: ' + toolName) : next.content.substring(0, 100),
|
|
2157
2180
|
tool_name: toolName,
|
|
2181
|
+
params: toolParams || undefined,
|
|
2158
2182
|
status: 'done',
|
|
2159
2183
|
}
|
|
2160
2184
|
});
|
|
@@ -2165,16 +2189,22 @@ function groupHistoryMessages(messages) {
|
|
|
2165
2189
|
}
|
|
2166
2190
|
i++;
|
|
2167
2191
|
} else if (next.role === 'tool') {
|
|
2192
|
+
const firstNewline = next.content.indexOf('\n');
|
|
2193
|
+
const headerLine = firstNewline > 0 ? next.content.substring(0, firstNewline) : next.content.substring(0, 80);
|
|
2194
|
+
const bodyContent = firstNewline > 0 ? next.content.substring(firstNewline + 1) : '';
|
|
2195
|
+
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2168
2196
|
const isOk = !next.content.includes('失败');
|
|
2197
|
+
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2169
2198
|
parts.push({
|
|
2170
2199
|
type: 'exec',
|
|
2171
2200
|
data: {
|
|
2172
2201
|
id: _evtId++,
|
|
2173
2202
|
type: 'tool_result',
|
|
2174
|
-
title:
|
|
2203
|
+
title: displayTitle,
|
|
2204
|
+
tool_name: toolResultName,
|
|
2175
2205
|
success: isOk,
|
|
2176
|
-
summary:
|
|
2177
|
-
result: { output:
|
|
2206
|
+
summary: bodyContent.substring(0, 500).trim(),
|
|
2207
|
+
result: { output: bodyContent.substring(0, 2000) },
|
|
2178
2208
|
}
|
|
2179
2209
|
});
|
|
2180
2210
|
i++;
|
|
@@ -907,6 +907,12 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
907
907
|
'</div>';
|
|
908
908
|
}
|
|
909
909
|
|
|
910
|
+
// Determine event state class for legacy events
|
|
911
|
+
let stateClass = '';
|
|
912
|
+
if (data.type === 'tool_call' || data.type === 'skill_call') stateClass = 'tool-call-pending';
|
|
913
|
+
if (data.type === 'tool_result') stateClass = data.success === false ? 'tool-failed' : 'tool-success';
|
|
914
|
+
if (data.type === 'code_result') stateClass = data.timed_out ? 'tool-failed' : (data.success ? 'tool-success' : 'tool-failed');
|
|
915
|
+
|
|
910
916
|
const iconEmoji = getEventIconEmoji(data);
|
|
911
917
|
const title = data.title || (data.tool_name || data.skill_name || '执行事件');
|
|
912
918
|
|
|
@@ -914,13 +920,18 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
914
920
|
let metaParts = [];
|
|
915
921
|
if (data.execution_time !== undefined) metaParts.push('耗时 ' + data.execution_time + 's');
|
|
916
922
|
if (data.language) metaParts.push(escapeHtml(data.language));
|
|
917
|
-
if (data.tool_name || data.skill_name) metaParts.push(escapeHtml(data.tool_name || data.skill_name));
|
|
918
923
|
if (data.timed_out) metaParts.push('超时');
|
|
919
924
|
if (data.exit_code !== undefined) metaParts.push('exit: ' + data.exit_code);
|
|
920
925
|
const metaText = metaParts.join(' · ');
|
|
921
926
|
|
|
922
927
|
// Build body content
|
|
923
928
|
let bodyHtml = '';
|
|
929
|
+
// Params for tool_call/skill_call
|
|
930
|
+
if (data.params && (data.type === 'tool_call' || data.type === 'skill_call')) {
|
|
931
|
+
let paramPreview = typeof data.params === 'string' ? data.params : JSON.stringify(data.params);
|
|
932
|
+
if (paramPreview.length > 300) paramPreview = paramPreview.substring(0, 300) + '...';
|
|
933
|
+
bodyHtml += '<div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div>';
|
|
934
|
+
}
|
|
924
935
|
// Code preview for code_exec/code_result
|
|
925
936
|
if (data.code_preview && (data.type === 'code_exec' || data.type === 'code_result')) {
|
|
926
937
|
bodyHtml += '<div class="inline-exec-code" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')" title="点击查看完整结果">' + escapeHtml(data.code_preview) + '</div>';
|
|
@@ -938,7 +949,7 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
938
949
|
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
|
|
939
950
|
}
|
|
940
951
|
|
|
941
|
-
return '<div class="inline-exec-event">' +
|
|
952
|
+
return '<div class="inline-exec-event ' + stateClass + '">' +
|
|
942
953
|
'<div class="inline-exec-header">' +
|
|
943
954
|
'<span class="inline-exec-icon">' + iconEmoji + '</span>' +
|
|
944
955
|
'<span class="inline-exec-title">' + escapeHtml(title) + '</span>' +
|
|
@@ -1286,7 +1297,7 @@ async function sendMessage() {
|
|
|
1286
1297
|
// This handles cases where v2_context event is delayed or missed
|
|
1287
1298
|
if (!_isV2Mode && currentText.trim().startsWith('<')) {
|
|
1288
1299
|
_isV2Mode = true;
|
|
1289
|
-
|
|
1300
|
+
// V2 mode auto-detected from text_delta
|
|
1290
1301
|
}
|
|
1291
1302
|
if (_isV2Mode) {
|
|
1292
1303
|
// In V2 mode, text_delta contains raw XML — store separately
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<div class="sidebar-footer">
|
|
45
|
-
<button class="sidebar-footer-btn" onclick="window.
|
|
45
|
+
<button class="sidebar-footer-btn" onclick="window.location.href='/ui/index.html'">
|
|
46
46
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
47
47
|
后台管理
|
|
48
48
|
</button>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</div>
|
|
7
7
|
<div class="agent-panel-header">
|
|
8
8
|
<h3><span class="agents-emoji">🤖</span> Agents</h3>
|
|
9
|
-
<button class="agent-manage-btn" onclick="window.
|
|
9
|
+
<button class="agent-manage-btn" onclick="window.location.href='/ui/index.html?page=agents'" title="Agent 管理后台">
|
|
10
10
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
11
11
|
</button>
|
|
12
12
|
<button class="agent-create-btn" onclick="showCreateAgentModal()" title="创建新 Agent">
|
package/web/ui/index.html
CHANGED
|
@@ -224,7 +224,7 @@ tr:hover{background:var(--surface2)}
|
|
|
224
224
|
</div>
|
|
225
225
|
<div class="mobile-overlay" id="adminMobileOverlay" onclick="closeMobileSidebar()"></div>
|
|
226
226
|
<div class="main">
|
|
227
|
-
<div class="header"><div style="display:flex;align-items:center;gap:12px"><button class="header-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" style="display:none" title="菜单"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
|
|
227
|
+
<div class="header"><div style="display:flex;align-items:center;gap:12px"><button class="header-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" style="display:none" title="菜单"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button><button class="header-btn" onclick="window.location.href='/ui/chat/chat_container.html'" title="返回聊天" style="display:flex;align-items:center;gap:4px;font-size:13px;white-space:nowrap"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 18 9 12 15 6"/></svg>返回聊天</button><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
|
|
228
228
|
<div class="content" id="content"></div>
|
|
229
229
|
</div>
|
|
230
230
|
<div id="modalContainer"></div>
|
|
@@ -772,7 +772,7 @@ function confirmDeleteAgent(path,name){
|
|
|
772
772
|
|
|
773
773
|
// 直接以执行模式打开与指定 Agent 的对话
|
|
774
774
|
function chatWithAgent(path){
|
|
775
|
-
window.
|
|
775
|
+
window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(path)+'&mode=exec';
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
// ========== Platforms ==========
|
|
@@ -908,7 +908,7 @@ async function _loadSessionMessages(){
|
|
|
908
908
|
async function clearSession(sid){await api(`/api/sessions/${encodeURIComponent(sid)}`,{method:'DELETE'});renderSessions()}
|
|
909
909
|
// 切入会话: 打开聊天页面并自动加载指定会话
|
|
910
910
|
function enterSession(sid,agentName){
|
|
911
|
-
window.
|
|
911
|
+
window.location.href='/ui/chat/chat_container.html?agent='+encodeURIComponent(agentName)+'&mode=exec&session='+encodeURIComponent(sid);
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
// ========== Memory ==========
|
|
@@ -1188,6 +1188,7 @@ function showAddModelModal(editId){
|
|
|
1188
1188
|
const provider=model?model.provider:'custom';
|
|
1189
1189
|
const apiType=model?model.api_type:'openai-completions';
|
|
1190
1190
|
const baseUrl=model?model.base_url:'';
|
|
1191
|
+
const apiKey=model&&model.api_key?model.api_key:'';
|
|
1191
1192
|
const contextWindow=model?model.context_window:128000;
|
|
1192
1193
|
const temperature=model?model.temperature:0.1;
|
|
1193
1194
|
const inputModesArr=model&&model.input_modes?model.input_modes:['text'];
|
|
@@ -1219,7 +1220,7 @@ function showAddModelModal(editId){
|
|
|
1219
1220
|
<input id="mlApiTypeCustom" value="${isCustomApiType?escHtml(apiType):''}" placeholder="输入自定义 API 类型">
|
|
1220
1221
|
</div>
|
|
1221
1222
|
<div class="form-group"><label>Base URL</label><input id="mlBaseUrl" value="${escHtml(baseUrl)}" placeholder="https://api.example.com/v1"></div>
|
|
1222
|
-
<div class="form-group"><label>API Key
|
|
1223
|
+
<div class="form-group"><label>API Key</label><input id="mlApiKey" type="text" value="${escHtml(apiKey)}" placeholder="sk-..." style="width:100%"></div>
|
|
1223
1224
|
<div class="form-row">
|
|
1224
1225
|
<div class="form-group"><label>上下文窗口</label><input id="mlContextWindow" type="number" value="${contextWindow}" placeholder="128000"></div>
|
|
1225
1226
|
<div class="form-group"><label>最大输出</label><input id="mlMaxTokens" type="number" value="${maxTokens}" placeholder="4096"></div>
|
|
@@ -1262,7 +1263,7 @@ async function doSaveModel(editId){
|
|
|
1262
1263
|
reasoning:$('mlReasoning').checked,
|
|
1263
1264
|
is_global_fallback:$('mlGlobalFallback').checked
|
|
1264
1265
|
};
|
|
1265
|
-
if($('mlApiKey').value)payload.api_key=$('mlApiKey').value;
|
|
1266
|
+
if($('mlApiKey').value!==undefined)payload.api_key=$('mlApiKey').value;
|
|
1266
1267
|
let r;
|
|
1267
1268
|
if(editId){
|
|
1268
1269
|
r=await api(`/api/models/${editId}`,{method:'PUT',body:JSON.stringify(payload)});
|