myagent-ai 1.15.83 → 1.15.85
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/main_agent.py +21 -5
- package/core/context_builder.py +57 -7
- package/memory/manager.py +36 -18
- package/package.json +1 -1
- package/skills/search_skill.py +66 -21
package/agents/main_agent.py
CHANGED
|
@@ -50,7 +50,7 @@ class MainAgent(BaseAgent):
|
|
|
50
50
|
</toolstocal>
|
|
51
51
|
</response>
|
|
52
52
|
<task_plan>仅复杂任务使用任务计划,如"context"包含非空"task_plan",则更新它。否则,先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过3步,则此处输出为空,不要创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过3步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记。</task_plan>
|
|
53
|
-
<remember><type>填global或session,其中"global"为跨会话全局记忆,"session"为仅当前会话。</type><content>仅从最新用户输入,包括"userprint"或"usersays_correct"
|
|
53
|
+
<remember><type>填global或session,其中"global"为跨会话全局记忆,"session"为仅当前会话。</type><content>仅从最新用户输入,包括"userprint"或"usersays_correct"或工具调用结果,中提炼值得记忆的信息(如用户偏好、重要结论、错误经验、用户个人信息、对话要点、用户诉求、ai回复等)。因为对话默认不自动保存聊天记录,而是从记忆库搜索最相关的最新内容到"automemory"供决策,所以此次必须有所记忆,才能为后续多轮对话提供持续记忆基础。</content></remember>
|
|
54
54
|
<recall>下一轮需要主动召回的记忆描述。填写需要从记忆库中检索的关键字或描述。如果不填写则为空。如果需要更多记忆支持当前任务,填写相关关键词(可包含时间参考,如"2025年1月的项目"),系统将在下一轮搜索top5相关记忆并通过"recall_memory"注入上下文。也可直接调用"recall_memory"工具即时搜索。</recall>
|
|
55
55
|
<knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等,将被持久化存储,未来可通过 "get_knowledge"检索复用。如果本轮无需保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
|
|
56
56
|
<get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的knowledge内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
|
|
@@ -593,6 +593,8 @@ class MainAgent(BaseAgent):
|
|
|
593
593
|
)
|
|
594
594
|
|
|
595
595
|
# Step 1: 构建 Context XML
|
|
596
|
+
# 获取 MemoryAgent 预加载的用户偏好/错误模式(如果有)
|
|
597
|
+
_memory_ctx_prompt = context.working_memory.get("memory_context_prompt", "")
|
|
596
598
|
context_xml = self.context_builder.build_context(
|
|
597
599
|
agent_name=agent_name,
|
|
598
600
|
agent_description=agent_description,
|
|
@@ -604,6 +606,7 @@ class MainAgent(BaseAgent):
|
|
|
604
606
|
agent_override_prompt=agent_override_prompt,
|
|
605
607
|
get_knowledge=get_knowledge_content,
|
|
606
608
|
recall=recall_content,
|
|
609
|
+
memory_context_prompt=_memory_ctx_prompt,
|
|
607
610
|
)
|
|
608
611
|
|
|
609
612
|
await self._emit_v2_event(
|
|
@@ -951,13 +954,26 @@ class MainAgent(BaseAgent):
|
|
|
951
954
|
importance=0.7,
|
|
952
955
|
)
|
|
953
956
|
else:
|
|
954
|
-
# ===
|
|
955
|
-
self.memory.
|
|
957
|
+
# === 会话记忆:查重后存储(避免 "必须记忆" 导致大量重复) ===
|
|
958
|
+
_session_dup = self.memory.find_duplicate_memory(
|
|
959
|
+
content=parsed.remember,
|
|
956
960
|
session_id=context.session_id,
|
|
957
961
|
key="conversation_insight",
|
|
958
|
-
|
|
959
|
-
|
|
962
|
+
similarity_threshold=0.80,
|
|
963
|
+
category="session",
|
|
960
964
|
)
|
|
965
|
+
if _session_dup:
|
|
966
|
+
logger.debug(
|
|
967
|
+
f"[{task_id}] 会话记忆查重: 跳过相似内容 "
|
|
968
|
+
f"(相似度>=0.80, 旧记忆ID={_session_dup.id})"
|
|
969
|
+
)
|
|
970
|
+
else:
|
|
971
|
+
self.memory.add_session(
|
|
972
|
+
session_id=context.session_id,
|
|
973
|
+
key="conversation_insight",
|
|
974
|
+
content=parsed.remember,
|
|
975
|
+
importance=0.6,
|
|
976
|
+
)
|
|
961
977
|
|
|
962
978
|
await self._emit_v2_event(
|
|
963
979
|
"v2_memory_saved",
|
package/core/context_builder.py
CHANGED
|
@@ -124,6 +124,7 @@ class ContextBuilder:
|
|
|
124
124
|
agent_override_prompt: Optional[str] = None,
|
|
125
125
|
get_knowledge: str = "",
|
|
126
126
|
recall: str = "",
|
|
127
|
+
memory_context_prompt: str = "",
|
|
127
128
|
) -> str:
|
|
128
129
|
"""
|
|
129
130
|
构建完整的 <context> XML 字符串。
|
|
@@ -161,11 +162,10 @@ class ContextBuilder:
|
|
|
161
162
|
sections: List[str] = [
|
|
162
163
|
self._build_datetime(),
|
|
163
164
|
self._build_whomi(agent_name, agent_description, agent_override_prompt),
|
|
164
|
-
self._build_memory(query, session_id, recall),
|
|
165
|
+
self._build_memory(query, session_id, recall, memory_context_prompt),
|
|
165
166
|
self._build_knowledge(kb_query),
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
# self._build_recent_dialog(conversation_history, self.max_dialog_chars, session_id),
|
|
167
|
+
# 轻量近期对话兜底:最近 3 轮对话摘要,补充 automemory 搜索的盲区
|
|
168
|
+
self._build_recent_summary(session_id),
|
|
169
169
|
self._build_user_input(user_typed_text, user_voice_text),
|
|
170
170
|
self._build_task_plan(task_plan),
|
|
171
171
|
self._build_tools(self.skill_registry),
|
|
@@ -242,7 +242,7 @@ class ContextBuilder:
|
|
|
242
242
|
parts.append("</whomi>")
|
|
243
243
|
return "\n".join(parts)
|
|
244
244
|
|
|
245
|
-
def _build_memory(self, query: str, session_id: str, recall: str = "") -> str:
|
|
245
|
+
def _build_memory(self, query: str, session_id: str, recall: str = "", memory_context_prompt: str = "") -> str:
|
|
246
246
|
"""
|
|
247
247
|
构建 <automemory> 和 <recall_memory> 段落 —— 双层记忆检索结果。
|
|
248
248
|
|
|
@@ -258,6 +258,7 @@ class ContextBuilder:
|
|
|
258
258
|
query: 搜索查询文本(通常为最新用户消息)
|
|
259
259
|
session_id: 会话 ID
|
|
260
260
|
recall: LLM 上一轮输出的 <recall> 内容(关键字+时间描述)
|
|
261
|
+
memory_context_prompt: MemoryAgent 预加载的用户偏好/错误模式(直接注入)
|
|
261
262
|
|
|
262
263
|
Returns:
|
|
263
264
|
<automemory> + <recall_memory> XML 段落字符串
|
|
@@ -299,6 +300,9 @@ class ContextBuilder:
|
|
|
299
300
|
|
|
300
301
|
if combined:
|
|
301
302
|
auto_lines.append("<automemory>")
|
|
303
|
+
# 注入 MemoryAgent 预加载的用户偏好/错误模式
|
|
304
|
+
if memory_context_prompt and memory_context_prompt.strip():
|
|
305
|
+
auto_lines.append(_xml_escape(memory_context_prompt.strip()))
|
|
302
306
|
for i, entry in enumerate(combined[:10], 1):
|
|
303
307
|
content = entry.content.strip()
|
|
304
308
|
if content:
|
|
@@ -438,6 +442,51 @@ class ContextBuilder:
|
|
|
438
442
|
logger.warning(f"知识库 RAG 检索失败 ({kb_dir}): {e}")
|
|
439
443
|
return ""
|
|
440
444
|
|
|
445
|
+
def _build_recent_summary(self, session_id: str) -> str:
|
|
446
|
+
"""
|
|
447
|
+
构建 <recent_summary> 段落 —— 最近几轮对话的轻量摘要。
|
|
448
|
+
|
|
449
|
+
作为 automemory 搜索的兜底机制,确保 LLM 至少能看到最近几轮
|
|
450
|
+
对话的基本脉络。只取最近 6 条(约 3 轮 user+assistant),
|
|
451
|
+
每条截断到 200 字,总字符控制在 1500 以内。
|
|
452
|
+
"""
|
|
453
|
+
if not self.memory_manager or not session_id:
|
|
454
|
+
return ""
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
from core.utils import truncate_str
|
|
458
|
+
# 只取 user 和 assistant 角色,排除内部审计条目
|
|
459
|
+
entries = self.memory_manager.get_conversation(
|
|
460
|
+
session_id=session_id,
|
|
461
|
+
limit=6,
|
|
462
|
+
include_roles=["user", "assistant"],
|
|
463
|
+
)
|
|
464
|
+
if not entries:
|
|
465
|
+
return ""
|
|
466
|
+
|
|
467
|
+
role_labels = {"user": "用户", "assistant": "助手"}
|
|
468
|
+
lines = []
|
|
469
|
+
total_chars = 0
|
|
470
|
+
for entry in reversed(entries): # 最新的在前
|
|
471
|
+
content = (entry.content or "").strip()
|
|
472
|
+
if not content:
|
|
473
|
+
continue
|
|
474
|
+
label = role_labels.get(entry.role, entry.role)
|
|
475
|
+
# 截断过长内容
|
|
476
|
+
truncated = truncate_str(content, 200)
|
|
477
|
+
lines.append(f"{label}: {_xml_escape(truncated)}")
|
|
478
|
+
total_chars += len(truncated) + 10
|
|
479
|
+
if total_chars > 1500:
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
if not lines:
|
|
483
|
+
return ""
|
|
484
|
+
|
|
485
|
+
return "<recent_summary>\n" + "\n".join(lines) + "\n</recent_summary>"
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.debug(f"recent_summary 构建失败: {e}")
|
|
488
|
+
return ""
|
|
489
|
+
|
|
441
490
|
def _build_recent_dialog(
|
|
442
491
|
self,
|
|
443
492
|
conversation_history: List["Message"],
|
|
@@ -768,8 +817,9 @@ class ContextBuilder:
|
|
|
768
817
|
replacement = f'<{tag}>\n(因 token 预算不足已裁剪)\n</{tag}>'
|
|
769
818
|
return re.sub(pattern, replacement, xml, count=1, flags=re.DOTALL)
|
|
770
819
|
|
|
771
|
-
#
|
|
772
|
-
|
|
820
|
+
# 按优先级从低到高裁剪(记忆最优先保留,知识库和任务计划优先裁剪)
|
|
821
|
+
# recent_summary 是兜底机制,最先裁剪
|
|
822
|
+
for tag in ['recent_summary', 'knowledge', 'task_plan', 'recall_memory', 'automemory']:
|
|
773
823
|
if estimated <= budget:
|
|
774
824
|
break
|
|
775
825
|
if f'<{tag}>' in context_xml:
|
package/memory/manager.py
CHANGED
|
@@ -231,7 +231,15 @@ class MemoryManager:
|
|
|
231
231
|
params.append(timestamp())
|
|
232
232
|
|
|
233
233
|
where = " AND ".join(conditions) if conditions else "1=1"
|
|
234
|
-
|
|
234
|
+
# 安全白名单:仅允许预定义的排序字段
|
|
235
|
+
_allowed_orders = {
|
|
236
|
+
"created_at ASC", "created_at DESC",
|
|
237
|
+
"importance ASC", "importance DESC",
|
|
238
|
+
"access_count ASC", "access_count DESC",
|
|
239
|
+
"updated_at ASC", "updated_at DESC",
|
|
240
|
+
}
|
|
241
|
+
_safe_order = order_by if order_by in _allowed_orders else "created_at ASC"
|
|
242
|
+
sql = f"SELECT * FROM memories WHERE {where} ORDER BY {_safe_order} LIMIT ?"
|
|
235
243
|
params.append(limit)
|
|
236
244
|
|
|
237
245
|
rows = conn.execute(sql, params).fetchall()
|
|
@@ -280,7 +288,8 @@ class MemoryManager:
|
|
|
280
288
|
def add_session(self, session_id, role="", content="", key="", importance=0.5, metadata=None) -> str:
|
|
281
289
|
"""添加会话记忆。内容不包含时间前缀,时间仅存于 created_at 和 metadata。"""
|
|
282
290
|
from datetime import datetime as _dt
|
|
283
|
-
|
|
291
|
+
from core.utils import get_config_tz
|
|
292
|
+
_now_str = _dt.now(get_config_tz()).strftime("%Y-%m-%d %H:%M:%S")
|
|
284
293
|
# 直接存储原始内容,不再注入时间前缀
|
|
285
294
|
entry = MemoryEntry(
|
|
286
295
|
session_id=session_id, category="session", role=role,
|
|
@@ -489,6 +498,7 @@ class MemoryManager:
|
|
|
489
498
|
session_id: str = "",
|
|
490
499
|
key: str = "",
|
|
491
500
|
similarity_threshold: float = 0.85,
|
|
501
|
+
category: str = "global",
|
|
492
502
|
) -> Optional[MemoryEntry]:
|
|
493
503
|
"""
|
|
494
504
|
查找与给定内容高度相似的已有记忆(查重)。
|
|
@@ -501,6 +511,7 @@ class MemoryManager:
|
|
|
501
511
|
session_id: 会话 ID(为空则跨会话查重)
|
|
502
512
|
key: 记忆分类键(为空则不限制分类)
|
|
503
513
|
similarity_threshold: 相似度阈值,超过此值视为重复(默认 0.85)
|
|
514
|
+
category: 记忆类别(默认 "global",可设为 "session" 用于会话记忆查重)
|
|
504
515
|
|
|
505
516
|
Returns:
|
|
506
517
|
匹配的旧 MemoryEntry,或 None(无重复)
|
|
@@ -509,8 +520,8 @@ class MemoryManager:
|
|
|
509
520
|
return None # 空内容不查重
|
|
510
521
|
|
|
511
522
|
conn = self._get_conn()
|
|
512
|
-
conditions = ["category =
|
|
513
|
-
params: list = []
|
|
523
|
+
conditions = ["category = ?"]
|
|
524
|
+
params: list = [category]
|
|
514
525
|
|
|
515
526
|
if session_id:
|
|
516
527
|
conditions.append("session_id = ?")
|
|
@@ -865,10 +876,13 @@ class MemoryManager:
|
|
|
865
876
|
time_decay=time_decay, half_life_days=half_life_days)
|
|
866
877
|
else:
|
|
867
878
|
# 混合模式:取两种搜索结果的加权和 + 时间衰减
|
|
879
|
+
# 注意:传入 update_access=False 避免子搜索重复更新 access_count
|
|
868
880
|
keyword_results = self._search_keyword(conn, query, where, params, limit * 2,
|
|
869
|
-
time_decay=False, half_life_days=half_life_days
|
|
881
|
+
time_decay=False, half_life_days=half_life_days,
|
|
882
|
+
update_access=False)
|
|
870
883
|
semantic_results = self._search_semantic(conn, query, where, params, limit * 2,
|
|
871
|
-
time_decay=False, half_life_days=half_life_days
|
|
884
|
+
time_decay=False, half_life_days=half_life_days,
|
|
885
|
+
update_access=False)
|
|
872
886
|
|
|
873
887
|
# 合并评分(相关性 + 时间衰减)
|
|
874
888
|
combined: Dict[str, Tuple[MemoryEntry, float]] = {}
|
|
@@ -917,6 +931,7 @@ class MemoryManager:
|
|
|
917
931
|
limit: int,
|
|
918
932
|
time_decay: bool = False,
|
|
919
933
|
half_life_days: float = 30.0,
|
|
934
|
+
update_access: bool = True,
|
|
920
935
|
) -> List[MemoryEntry]:
|
|
921
936
|
"""关键词 LIKE 搜索(支持时间衰减重排序)"""
|
|
922
937
|
like_pattern = f"%{query}%"
|
|
@@ -946,12 +961,13 @@ class MemoryManager:
|
|
|
946
961
|
entries = [e for e, _ in scored[:limit]]
|
|
947
962
|
|
|
948
963
|
# 更新访问计数
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
964
|
+
if update_access:
|
|
965
|
+
for entry in entries:
|
|
966
|
+
conn.execute(
|
|
967
|
+
"UPDATE memories SET access_count = access_count + 1 WHERE id = ?",
|
|
968
|
+
(entry.id,),
|
|
969
|
+
)
|
|
970
|
+
conn.commit()
|
|
955
971
|
|
|
956
972
|
return entries
|
|
957
973
|
|
|
@@ -964,6 +980,7 @@ class MemoryManager:
|
|
|
964
980
|
limit: int,
|
|
965
981
|
time_decay: bool = False,
|
|
966
982
|
half_life_days: float = 30.0,
|
|
983
|
+
update_access: bool = True,
|
|
967
984
|
) -> List[MemoryEntry]:
|
|
968
985
|
"""TF-IDF 语义搜索(支持时间衰减重排序)"""
|
|
969
986
|
# 先取一批候选文档
|
|
@@ -998,12 +1015,13 @@ class MemoryManager:
|
|
|
998
1015
|
result = [MemoryEntry.from_row(row_map[doc_id]) for doc_id in top_ids if doc_id in row_map]
|
|
999
1016
|
|
|
1000
1017
|
# 更新访问计数
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1018
|
+
if update_access:
|
|
1019
|
+
for entry in result:
|
|
1020
|
+
conn.execute(
|
|
1021
|
+
"UPDATE memories SET access_count = access_count + 1 WHERE id = ?",
|
|
1022
|
+
(entry.id,),
|
|
1023
|
+
)
|
|
1024
|
+
conn.commit()
|
|
1007
1025
|
|
|
1008
1026
|
return result
|
|
1009
1027
|
|
package/package.json
CHANGED
package/skills/search_skill.py
CHANGED
|
@@ -127,10 +127,21 @@ class WebSearchSkill(Skill):
|
|
|
127
127
|
r.raise_for_status()
|
|
128
128
|
except requests.exceptions.SSLError:
|
|
129
129
|
logger.warning("DuckDuckGo SSL 验证失败,跳过证书验证重试")
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
try:
|
|
131
|
+
r = requests.get("https://html.duckduckgo.com/html/", params={"q": query}, headers=_HEADERS, timeout=15, verify=False)
|
|
132
|
+
r.raise_for_status()
|
|
133
|
+
except Exception as ssl_retry_err:
|
|
134
|
+
logger.error(f"DuckDuckGo SSL 重试也失败: {ssl_retry_err}")
|
|
135
|
+
return ""
|
|
136
|
+
except Exception as req_err:
|
|
137
|
+
logger.error(f"DuckDuckGo 请求失败: {req_err}")
|
|
138
|
+
return ""
|
|
132
139
|
if r is not None:
|
|
133
|
-
|
|
140
|
+
try:
|
|
141
|
+
return r.text or ""
|
|
142
|
+
except Exception as read_err:
|
|
143
|
+
logger.error(f"DuckDuckGo 读取响应失败: {read_err}")
|
|
144
|
+
return ""
|
|
134
145
|
return ""
|
|
135
146
|
|
|
136
147
|
html = await asyncio.wait_for(loop.run_in_executor(None, _fetch), timeout=30)
|
|
@@ -181,10 +192,21 @@ class WebSearchSkill(Skill):
|
|
|
181
192
|
r.raise_for_status()
|
|
182
193
|
except requests.exceptions.SSLError:
|
|
183
194
|
logger.warning("Bing SSL 验证失败,跳过证书验证重试")
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
try:
|
|
196
|
+
r = requests.get("https://www.bing.com/search", params={"q": query, "count": num}, headers=_HEADERS, timeout=15, verify=False)
|
|
197
|
+
r.raise_for_status()
|
|
198
|
+
except Exception as ssl_retry_err:
|
|
199
|
+
logger.error(f"Bing SSL 重试也失败: {ssl_retry_err}")
|
|
200
|
+
return ""
|
|
201
|
+
except Exception as req_err:
|
|
202
|
+
logger.error(f"Bing 请求失败: {req_err}")
|
|
203
|
+
return ""
|
|
186
204
|
if r is not None:
|
|
187
|
-
|
|
205
|
+
try:
|
|
206
|
+
return r.text or ""
|
|
207
|
+
except Exception as read_err:
|
|
208
|
+
logger.error(f"Bing 读取响应失败: {read_err}")
|
|
209
|
+
return ""
|
|
188
210
|
return ""
|
|
189
211
|
|
|
190
212
|
html = await asyncio.wait_for(loop.run_in_executor(None, _fetch), timeout=30)
|
|
@@ -266,12 +288,23 @@ class WebReadSkill(Skill):
|
|
|
266
288
|
r.raise_for_status()
|
|
267
289
|
except requests.exceptions.SSLError:
|
|
268
290
|
logger.warning(f"web_read SSL 验证失败 ({url}),跳过证书验证重试")
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
try:
|
|
292
|
+
r = requests.get(url, headers=_HEADERS, timeout=30, verify=False)
|
|
293
|
+
r.raise_for_status()
|
|
294
|
+
except Exception as ssl_retry_err:
|
|
295
|
+
logger.error(f"web_read SSL 重试也失败 ({url}): {ssl_retry_err}")
|
|
296
|
+
return None
|
|
297
|
+
except Exception as req_err:
|
|
298
|
+
logger.error(f"web_read 请求失败 ({url}): {req_err}")
|
|
299
|
+
return None
|
|
271
300
|
if r is not None:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
301
|
+
try:
|
|
302
|
+
r.encoding = r.apparent_encoding
|
|
303
|
+
return r.text or ""
|
|
304
|
+
except Exception as read_err:
|
|
305
|
+
logger.error(f"web_read 读取响应内容失败 ({url}): {read_err}")
|
|
306
|
+
return None
|
|
307
|
+
return None
|
|
275
308
|
|
|
276
309
|
html = await asyncio.wait_for(
|
|
277
310
|
loop.run_in_executor(None, _fetch), timeout=60
|
|
@@ -323,6 +356,7 @@ class WebReadSkill(Skill):
|
|
|
323
356
|
except asyncio.TimeoutError:
|
|
324
357
|
return SkillResult(success=False, error=f"网页读取超时 (60s): {url}")
|
|
325
358
|
except Exception as e:
|
|
359
|
+
logger.error(f"web_read 异常 ({url}): {e}", exc_info=True)
|
|
326
360
|
return SkillResult(success=False, error=f"网页读取失败: {e}")
|
|
327
361
|
|
|
328
362
|
|
|
@@ -369,23 +403,33 @@ class URLReadSkill(Skill):
|
|
|
369
403
|
)
|
|
370
404
|
except requests.exceptions.SSLError:
|
|
371
405
|
logger.warning(f"url_read SSL 验证失败 ({url}),跳过证书验证重试")
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
406
|
+
try:
|
|
407
|
+
r = requests.request(
|
|
408
|
+
method=method,
|
|
409
|
+
url=url,
|
|
410
|
+
headers=headers,
|
|
411
|
+
data=body,
|
|
412
|
+
timeout=30,
|
|
413
|
+
verify=False,
|
|
414
|
+
)
|
|
415
|
+
except Exception as ssl_retry_err:
|
|
416
|
+
logger.error(f"url_read SSL 重试也失败 ({url}): {ssl_retry_err}")
|
|
417
|
+
return None
|
|
418
|
+
except Exception as req_err:
|
|
419
|
+
logger.error(f"url_read 请求失败 ({url}): {req_err}")
|
|
420
|
+
return None
|
|
380
421
|
if r is not None:
|
|
381
422
|
return r
|
|
382
|
-
|
|
423
|
+
return None
|
|
383
424
|
|
|
384
425
|
response = await asyncio.wait_for(loop.run_in_executor(None, _request), timeout=60)
|
|
385
426
|
|
|
427
|
+
if response is None:
|
|
428
|
+
return SkillResult(success=False, error=f"请求失败: 无法连接到 {url}")
|
|
429
|
+
|
|
386
430
|
try:
|
|
387
431
|
content = response.json()
|
|
388
|
-
except ValueError:
|
|
432
|
+
except (ValueError, AttributeError):
|
|
389
433
|
content = response.text[:20000]
|
|
390
434
|
|
|
391
435
|
return SkillResult(
|
|
@@ -398,4 +442,5 @@ class URLReadSkill(Skill):
|
|
|
398
442
|
message=f"HTTP {response.status_code}",
|
|
399
443
|
)
|
|
400
444
|
except Exception as e:
|
|
445
|
+
logger.error(f"url_read 异常 ({url}): {e}", exc_info=True)
|
|
401
446
|
return SkillResult(success=False, error=f"请求失败: {e}")
|