优化加载速度、前缀缓存机制
This commit is contained in:
1 parent
25a811ea62
commit
731768deb9
21 files changed
+569
-100
No files matched your search
@@ -151,7 +151,7 @@ Agent 运行过程中的关键交互界面,支持文件从任意位置**拖拽
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ LangGraph ReAct 图 (core/graph.py) │
|
||||
│ agent 节点 → tools 节点 → process_tool_artifact 节点 │
|
||||
│ 内含: 反思注入 · 压缩 · 循环检测 · 路由 │
|
||||
│ 内含: 动态尾部注入 · 压缩 · 循环检测 · 路由 │
|
||||
│ 人机交互: 阻塞式普通工具(core/human_request.py) │
|
||||
└───────────────────────────────┬──────────────────────────────┘
|
||||
│ 调用
|
||||
@@ -171,7 +171,7 @@ START → [agent: call_model] ──有 tool_calls──▶ [tools: 并行工具
|
||||
END
|
||||
```
|
||||
|
||||
- **`call_model`**(`core/nodes.py`):注入反思提示 → LLM 调用 → 每步检查压缩(token 超阈值标记)→ 提取 thinking
|
||||
- **`call_model`**(`core/nodes.py`):追加动态尾部(TodoList 状态 + 循环提醒;固定反思引导已并入系统提示词前缀)→ LLM 调用 → 每步检查压缩(token 超阈值标记)→ 提取 thinking
|
||||
- **`should_continue`**(`core/routing.py`):含 tool_calls → 循环检测 → 路由到 `tools` 或 `END`(需人工确认的工具不在此暂停,而是在工具执行钳点阻塞征求用户意见,见「阻塞式人机交互」)
|
||||
- **`process_tool_artifact`**:gui 截图作为 synthetic HumanMessage 注入,实现视觉闭环
|
||||
- **`compress`**(`core/nodes.py`):token 超 `窗口×COMPRESS_RATE` 时,将历史工具调用压缩为累积摘要(见「实现细节」)
|
||||
@@ -433,11 +433,23 @@ cloudflared tunnel --url http://localhost:8765
|
||||
|
||||
| 层级 | 位置 | 注入时机 | 作用 |
|
||||
|------|------|----------|------|
|
||||
| **L1 系统级** | `memory/system_prompt.py` | 会话开始 | `MAIN_SYSTEM_PROMPT` 定义角色;`PLAN_MODE_PROMPT` 强制先规划 |
|
||||
| **L1 系统级** | `memory/system_prompt.py` | 会话开始 | `MAIN_SYSTEM_PROMPT` 定义角色;`PLAN_MODE_PROMPT` 强制先规划;固定反思引导 `REFLECTION_PROMPT` 并入系统提示词前缀(思考关闭档位,前缀缓存友好) |
|
||||
| **L2 工具描述** | 各 `tools/*.py` 的 `description` | 工具绑定时 | 指导 LLM 正确调用(含 one-shot 示例) |
|
||||
| **L3 运行时注入** | `nodes.py` / `loop_detector.py` | 每轮 ReAct | `REFLECTION_PROMPT` 反思 + 反循环警告,作为 synthetic HumanMessage |
|
||||
| **L3 运行时动态尾部** | `nodes.py` / `loop_detector.py` | 每轮 ReAct | 变化部分(TodoList 状态 + 循环警告)作为动态尾部追加到消息**末尾**,并持久化为长期记忆(`session_meta.dynamic_tail_history`)供后续轮次查看 |
|
||||
| **L4 内部子模型** | `gui.py` `_build_batch_prompt` | 工具内部 | 批量坐标定位,one-shot 强制纯坐标输出 |
|
||||
|
||||
### 1.1 前缀缓存(Prompt Caching)
|
||||
|
||||
上下文按「前缀稳定性」布局,最大化利用 LLM 服务端的前缀缓存:
|
||||
|
||||
```
|
||||
[系统提示词(含固定反思引导,稳定)] + [压缩摘要] + [对话历史] + [用户消息] + [动态尾部(每轮变化)]
|
||||
```
|
||||
|
||||
- **固定部分放前缀**:反思引导等不变提示文本并入系统提示词、位于上下文最前 —— 前缀稳定即可命中缓存;避免把固定文本逐轮塞在消息中间导致整段缓存失效。
|
||||
- **变化部分放末尾**:TodoList 状态、循环警告等每轮变化的内容追加在消息列表**末尾**,变化只影响尾部、不破坏前缀缓存;无 TodoList 且无循环提醒时**不注入任何尾部提示**(省 token)。
|
||||
- **动态尾部作为长期记忆**:每轮尾部文本持久化到 `session_meta` 的 `dynamic_tail_history`(连续相同去重),后续轮次把全部历史尾部注入上下文,Agent 能看到之前所有轮的 TodoList 状态与循环提醒。
|
||||
|
||||
### 2. 上下文分级压缩(图内)
|
||||
|
||||
<!-- 多级压缩机制图占位 -->
|
||||
@@ -499,7 +511,7 @@ LLM 发出 human_interaction / 需确认工具调用
|
||||
| 单步执行(多数项目) | 2N + 1 |
|
||||
| **序列合并(本项目)** | **2**(1 次批量定位 + 1 次决策) |
|
||||
|
||||
通过 `MAIN_SYSTEM_PROMPT` + `REFLECTION_PROMPT` 双重强调"同页操作必须合并为一次 `gui` 调用"实现。
|
||||
通过系统提示词前缀中的固定反思引导(`REFLECTION_PROMPT`)+ `MAIN_SYSTEM_PROMPT` 双重强调"同页操作必须合并为一次 `gui` 调用"实现。
|
||||
|
||||
### 6. 双模式源码共用
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -114,7 +115,7 @@ def load_agent_configs() -> list[AgentConfig]:
|
||||
(忽略 JSON 中的静态值),确保提示词可热更新且包含正确的动态路径。
|
||||
对于 role='sub' 的 Agent,若未指定 system_prompt 则自动使用默认子 Agent 提示词。
|
||||
"""
|
||||
from agent.memory.system_prompt import DEFAULT_SUB_AGENT_PROMPT, get_main_system_prompt
|
||||
from agent.memory.system_prompt import get_default_sub_agent_prompt, get_main_system_prompt
|
||||
raw = _read_json(AGENT_CONFIG_PATH)
|
||||
if not raw or not isinstance(raw, list):
|
||||
raw = _default_agent_config()
|
||||
@@ -128,9 +129,9 @@ def load_agent_configs() -> list[AgentConfig]:
|
||||
system_prompt = get_main_system_prompt()
|
||||
else:
|
||||
system_prompt = item.get("system_prompt", "")
|
||||
# 子 Agent 未指定提示词时使用默认子 Agent 提示词
|
||||
# 子 Agent 未指定提示词时使用默认子 Agent 提示词(含固定反思引导段)
|
||||
if not system_prompt:
|
||||
system_prompt = DEFAULT_SUB_AGENT_PROMPT
|
||||
system_prompt = get_default_sub_agent_prompt()
|
||||
configs.append(AgentConfig(
|
||||
name=item.get("name", ""),
|
||||
description=item.get("description", ""),
|
||||
@@ -169,12 +170,25 @@ _TOOL_DEFAULT_AUTO_RULES: dict[str, list[dict]] = {
|
||||
}
|
||||
|
||||
|
||||
# 工具参数信息缓存(工具注册表运行期静态,首次构建后复用,避免每次打开设置都实例化全部工具类)
|
||||
_tool_parameters_cache: dict[str, list[dict]] | None = None
|
||||
_tool_parameters_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_tool_parameters() -> dict[str, list[dict]]:
|
||||
"""获取每个工具的参数名和类型信息,供前端生成 auto_execute_rules 提示。
|
||||
|
||||
返回:
|
||||
{tool_name: [{"name": "action", "type": "Literal[open,close]"}, ...], ...}
|
||||
|
||||
说明:
|
||||
结果缓存于模块级(工具注册表运行期静态),避免设置页每次打开都
|
||||
重新实例化全部工具类。
|
||||
"""
|
||||
global _tool_parameters_cache
|
||||
with _tool_parameters_cache_lock:
|
||||
if _tool_parameters_cache is not None:
|
||||
return _tool_parameters_cache
|
||||
from agent.tools import _ALL_AVAILABLE_TOOL_CLASSES
|
||||
import typing
|
||||
result: dict[str, list[dict]] = {}
|
||||
@@ -199,6 +213,8 @@ def _get_tool_parameters() -> dict[str, list[dict]]:
|
||||
result[name] = params
|
||||
except Exception:
|
||||
result[name] = []
|
||||
with _tool_parameters_cache_lock:
|
||||
_tool_parameters_cache = result
|
||||
return result
|
||||
|
||||
|
||||
@@ -454,10 +470,25 @@ def _serialize_monitors(monitors) -> list[dict]:
|
||||
return result
|
||||
|
||||
|
||||
# 显示器列表短时缓存(5s TTL):设置页 loadAllConfigs 每次 Promise.all 都会请求
|
||||
# /api/config/gui,screeninfo 枚举显示器在部分系统上较慢,短时缓存避免重复枚举
|
||||
_live_monitors_cache: tuple[float, list[dict]] | None = None
|
||||
_live_monitors_cache_lock = threading.Lock()
|
||||
_LIVE_MONITORS_TTL = 5.0
|
||||
|
||||
|
||||
def _get_live_monitors() -> list[dict]:
|
||||
"""实时获取当前连接的显示器列表(通过 screeninfo)。"""
|
||||
"""实时获取当前连接的显示器列表(通过 screeninfo),带 5s TTL 缓存。"""
|
||||
global _live_monitors_cache
|
||||
now = time.time()
|
||||
with _live_monitors_cache_lock:
|
||||
if _live_monitors_cache and now - _live_monitors_cache[0] < _LIVE_MONITORS_TTL:
|
||||
return _live_monitors_cache[1]
|
||||
from screeninfo import get_monitors
|
||||
return _serialize_monitors(get_monitors())
|
||||
monitors = _serialize_monitors(get_monitors())
|
||||
with _live_monitors_cache_lock:
|
||||
_live_monitors_cache = (time.time(), monitors)
|
||||
return monitors
|
||||
|
||||
|
||||
def load_gui_config() -> dict:
|
||||
|
||||
+63
-17
@@ -21,7 +21,7 @@ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
|
||||
from agent.core.loop_detector import build_loop_reflection_prompt
|
||||
from agent.core.state import AgentState
|
||||
from agent.memory.system_prompt import REFLECTION_PROMPT, SUB_AGENT_OOM_PROMPT
|
||||
from agent.memory.system_prompt import SUB_AGENT_OOM_PROMPT
|
||||
from agent.utils.image_utils import image_bytes_to_openai_image_url_part
|
||||
from agent.utils.tool_call_utils import normalize_tool_call, extract_reasoning_text
|
||||
from langchain_core.messages import SystemMessage
|
||||
@@ -115,29 +115,29 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list:
|
||||
return cleaned
|
||||
|
||||
|
||||
def _build_reflection_prompt(messages: list, session_id: str = "") -> str | None:
|
||||
"""构建每轮注入模型上下文的提示文本,根据思考档位区分注入内容。
|
||||
def _build_dynamic_tail_prompt(messages: list, session_id: str = "") -> str:
|
||||
"""构建每轮追加到消息末尾的动态尾部提示(仅变化部分)。
|
||||
|
||||
输入:
|
||||
messages: 当前消息列表(已过滤短期记忆),供循环检测使用。
|
||||
session_id: 会话 ID,供循环检测与 TodoList 状态重建使用。
|
||||
|
||||
输出:
|
||||
注入文本;无需注入时返回 None。
|
||||
动态尾部文本;无 TodoList 且无循环提醒时返回空串
|
||||
(此时不加任何占位提示,省 token)。
|
||||
|
||||
系统定位:
|
||||
- 思考关闭(low/high):注入反思引导提示词 + TodoList 状态 + 循环警告,
|
||||
引导模型先一句话反思再调用工具;
|
||||
- 思考开启(xhigh/max/ultra):仅注入 TodoList 状态(模型原生深度思考,
|
||||
不注入反思引导与循环警告)。
|
||||
- 固定反思引导已移入系统提示词前缀(system_prompt._reflection_section),
|
||||
不再在节点中间构造固定提示;
|
||||
- 循环警告:思考关闭档位注入(Level 1 仅提醒,不强制终止);
|
||||
- TodoList 状态:思考开/关均注入(事实状态,供模型跟踪计划进度)。
|
||||
"""
|
||||
from agent.utils.env_utils import thinking_enabled
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
# 思考关闭:反思引导 + 循环警告(Level 1 仅提醒,不强制终止)
|
||||
# 思考关闭:循环警告(Level 1 仅提醒,不强制终止)
|
||||
if not thinking_enabled():
|
||||
parts.append(REFLECTION_PROMPT)
|
||||
loop_warning = build_loop_reflection_prompt(messages, session_id)
|
||||
if loop_warning:
|
||||
parts.append(loop_warning)
|
||||
@@ -148,11 +148,47 @@ def _build_reflection_prompt(messages: list, session_id: str = "") -> str | None
|
||||
todo_status = get_todo_status_text(session_id)
|
||||
except Exception:
|
||||
todo_status = ""
|
||||
parts.append(todo_status if todo_status else "当前无 TodoList。")
|
||||
if todo_status:
|
||||
parts.append(todo_status)
|
||||
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def _dynamic_tail_agent_key() -> str:
|
||||
"""动态尾部长期记忆的 agent 键:主 Agent 为 "main",子 Agent 用其名称。
|
||||
|
||||
主/子 Agent 的动态尾部各自隔离,避免子 Agent 覆盖主 Agent 的长期记忆。
|
||||
"""
|
||||
try:
|
||||
from agent.core.human_request import get_current_agent_name
|
||||
name = get_current_agent_name()
|
||||
except Exception:
|
||||
name = ""
|
||||
return "main" if name in ("", "__main__") else name
|
||||
|
||||
|
||||
def _persist_dynamic_tail(session_id: str, text: str) -> None:
|
||||
"""持久化当前轮动态尾部到 session_meta(长期记忆,供后续轮次注入查看)。"""
|
||||
if not session_id:
|
||||
return
|
||||
try:
|
||||
from agent.utils.agent_utils import update_session_dynamic_tail
|
||||
update_session_dynamic_tail(session_id, _dynamic_tail_agent_key(), text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _load_dynamic_tail_history(session_id: str) -> list[str]:
|
||||
"""读取当前 Agent 的动态尾部长期记忆历史(时间正序,最新在末尾)。"""
|
||||
if not session_id:
|
||||
return []
|
||||
try:
|
||||
from agent.utils.agent_utils import get_session_dynamic_tail_history
|
||||
return get_session_dynamic_tail_history(session_id, _dynamic_tail_agent_key())
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ---------- ReAct 循环内上下文压缩 ----------
|
||||
def _extract_usage(response: Any) -> dict:
|
||||
"""从模型 response 提取完整 usage 字典(prompt_tokens/completion_tokens/total_tokens)。"""
|
||||
@@ -414,12 +450,21 @@ def make_call_model_node(
|
||||
except Exception:
|
||||
_sid = ""
|
||||
|
||||
# 构建提示文本并注入到消息列表(仅作引导,不记录到前端)
|
||||
reflection = _build_reflection_prompt(filtered_messages, _sid)
|
||||
# 构建动态尾部提示并注入到消息列表末尾(仅作引导,不记录到前端)。
|
||||
# 固定反思引导已并入系统提示词前缀,此处仅注入变化部分:
|
||||
# TodoList 状态与循环提醒;无 TodoList 且无循环提醒时不注入(省 token)。
|
||||
messages_for_llm = list(filtered_messages)
|
||||
if reflection:
|
||||
reflection_msg = HumanMessage(content=reflection, additional_kwargs={"synthetic": True, "reflection": True})
|
||||
messages_for_llm.append(reflection_msg)
|
||||
tail = _build_dynamic_tail_prompt(filtered_messages, _sid)
|
||||
if tail:
|
||||
# 长期记忆:持久化本轮动态尾部,供后续轮次注入查看
|
||||
_persist_dynamic_tail(_sid, tail)
|
||||
# 注入全部历史动态尾部(含本轮),使 Agent 能看到之前所有轮的
|
||||
# todo 状态与循环提醒;这些消息仅存在于本次调用,不进图状态。
|
||||
for hist_text in _load_dynamic_tail_history(_sid):
|
||||
messages_for_llm.append(HumanMessage(
|
||||
content=hist_text,
|
||||
additional_kwargs={"synthetic": True, "reflection": True},
|
||||
))
|
||||
|
||||
response = model_with_tools.invoke(messages_for_llm)
|
||||
|
||||
@@ -475,7 +520,8 @@ def make_call_model_node(
|
||||
from agent.history.tool_call_recorder import record_tool_call_live, get_current_session
|
||||
sid = get_current_session()
|
||||
if sid:
|
||||
record_tool_call_live(sid, tool_name, tool_args)
|
||||
# 记录工具调用 ID:并行 call_subagent 按 id 匹配注入子轨迹
|
||||
record_tool_call_live(sid, tool_name, tool_args, tc_normalized.get("id", ""))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -44,31 +44,57 @@ _current_session_fallback = ""
|
||||
# 同时子 Agent 轨迹仍通过 record_sub_agent_trace 独立存档供持久化注入。
|
||||
_sub_agent_context: ContextVar[bool] = ContextVar("sub_agent_context", default=False)
|
||||
|
||||
# 当前工具调用 ID(tool_call_id):由并行工具节点在调用工具前设置,
|
||||
# 经上下文传播进入 call_subagent._run,用于并行子 Agent 的轨迹/实时记录隔离。
|
||||
# 同一轮内多个 call_subagent(甚至同名)并行时,各自按 tool_call_id 独立存取。
|
||||
_current_tool_call_id: ContextVar[str] = ContextVar("current_tool_call_id", default="")
|
||||
|
||||
|
||||
def is_sub_agent_context() -> bool:
|
||||
"""当前是否运行在子 Agent 上下文中。"""
|
||||
return _sub_agent_context.get()
|
||||
|
||||
|
||||
def set_current_tool_call_id(tool_call_id: str) -> None:
|
||||
"""设置当前线程的工具调用 ID(并行工具节点在执行工具前调用)。"""
|
||||
_current_tool_call_id.set(tool_call_id or "")
|
||||
|
||||
|
||||
def get_current_tool_call_id() -> str:
|
||||
"""获取当前工具调用 ID(call_subagent._run 内读取,用于轨迹隔离)。"""
|
||||
return _current_tool_call_id.get()
|
||||
|
||||
|
||||
def _sub_key(session_id: str, tool_call_id: str | None = None) -> tuple[str, str]:
|
||||
"""计算子 Agent 记录的隔离 key:(session_id, tool_call_id)。
|
||||
|
||||
tool_call_id 为空时回退到会话级 key(旧行为,顺序执行子 Agent 场景),
|
||||
保证无 tool_call_id 的调用(如人工手动 invoke)也能正常读写。
|
||||
"""
|
||||
tc_id = tool_call_id if tool_call_id is not None else get_current_tool_call_id()
|
||||
return (session_id or "", tc_id or "")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def sub_agent_context():
|
||||
def sub_agent_context(tool_call_id: str | None = None):
|
||||
"""进入子 Agent 执行上下文:将 record_*_live 路由到 call_subagent 嵌套区域。
|
||||
|
||||
子 Agent 的工具调用记录写入独立的 _sub_live_records[session_id],
|
||||
get_live_tool_calls 将其注入到 call_subagent 条目的 sub_tool_calls 中,
|
||||
子 Agent 的工具调用记录写入独立的 _sub_live_records[(session_id, tool_call_id)],
|
||||
get_live_tool_calls 将其注入到对应 call_subagent 条目的 sub_tool_calls 中,
|
||||
前端在 call_subagent 展开时实时渲染嵌套的子工具调用列表。
|
||||
|
||||
进入时清空上一子 Agent 遗留的实时记录与待消费思考,
|
||||
避免同一轮内多个子 Agent 顺序执行时发生记忆串扰。
|
||||
进入时仅清空当前子 Agent 实例遗留的实时记录与待消费思考,
|
||||
多个子 Agent(含同名)并行时按 tool_call_id 隔离,互不串扰。
|
||||
"""
|
||||
token = _sub_agent_context.set(True)
|
||||
try:
|
||||
_sid = get_current_session()
|
||||
if _sid:
|
||||
key = _sub_key(_sid, tool_call_id)
|
||||
with _sub_live_lock:
|
||||
_sub_live_records.pop(_sid, None)
|
||||
_sub_live_records.pop(key, None)
|
||||
with _sub_pending_thinking_lock:
|
||||
_sub_pending_thinking.pop(_sid, None)
|
||||
_sub_pending_thinking.pop(key, None)
|
||||
yield
|
||||
finally:
|
||||
_sub_agent_context.reset(token)
|
||||
@@ -77,13 +103,15 @@ def sub_agent_context():
|
||||
_live_lock = threading.Lock()
|
||||
_live_records: dict[str, list[dict[str, Any]]] = {}
|
||||
|
||||
# 子 Agent 实时工具调用记录(session_id → list),独立存储,
|
||||
# 在 get_live_tool_calls 中合并到 call_subagent 条目的 sub_tool_calls 中供前端嵌套展开。
|
||||
_sub_live_records: dict[str, list[dict[str, Any]]] = {}
|
||||
# 子 Agent 实时工具调用记录((session_id, tool_call_id) → list),独立存储,
|
||||
# 在 get_live_tool_calls 中按 tool_call_id 合并到对应 call_subagent 条目的
|
||||
# sub_tool_calls 中供前端嵌套展开。多个子 Agent 并行时按 (session, tc_id) 隔离。
|
||||
_sub_live_records: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
||||
_sub_live_lock = threading.Lock()
|
||||
|
||||
# 子 Agent 待附加 thinking(与主 Agent 的 _pending_thinking 隔离)
|
||||
_sub_pending_thinking: dict[str, str] = {}
|
||||
# 子 Agent 待附加 thinking(与主 Agent 的 _pending_thinking 隔离),
|
||||
# key 与 _sub_live_records 一致:(session_id, tool_call_id)。
|
||||
_sub_pending_thinking: dict[tuple[str, str], str] = {}
|
||||
_sub_pending_thinking_lock = threading.Lock()
|
||||
|
||||
# 轮次开始时间(key = session_id),用于计时
|
||||
@@ -231,17 +259,30 @@ def init_live_turn_with_id(session_id: str, turn_id: str) -> str:
|
||||
return turn_id
|
||||
|
||||
|
||||
def record_tool_call_live(session_id: str, name: str, args: dict[str, Any]) -> None:
|
||||
"""图节点中调用:记录工具调用开始。同时消费 pending thinking。"""
|
||||
def record_tool_call_live(session_id: str, name: str, args: dict[str, Any], tool_call_id: str | None = None) -> None:
|
||||
"""图节点中调用:记录工具调用开始。同时消费 pending thinking。
|
||||
|
||||
输入:
|
||||
session_id: 会话 ID。
|
||||
name: 工具名。
|
||||
args: 工具参数字典。
|
||||
tool_call_id: 可选工具调用 ID;子 Agent 上下文中作为隔离 key,
|
||||
主 Agent 上下文中存入条目供前端按 call_subagent 匹配子轨迹。
|
||||
"""
|
||||
# 子 Agent 上下文:路由到 _sub_live_records,由 get_live_tool_calls 合并到
|
||||
# call_subagent 条目的 sub_tool_calls 中供前端嵌套展开。
|
||||
# 对应 call_subagent 条目的 sub_tool_calls 中供前端嵌套展开。
|
||||
# 隔离 key 取上下文中的 tool_call_id(即父级 call_subagent 调用 ID,
|
||||
# 由 _execute_single 设置并传播到子 Agent 图线程),而非本条目的子工具 ID;
|
||||
# 多个并行子 Agent(含同名)因此互不串扰。
|
||||
if is_sub_agent_context():
|
||||
key = _sub_key(session_id)
|
||||
with _sub_pending_thinking_lock:
|
||||
thinking = _sub_pending_thinking.pop(session_id, None)
|
||||
thinking = _sub_pending_thinking.pop(key, None)
|
||||
with _sub_live_lock:
|
||||
if session_id not in _sub_live_records:
|
||||
_sub_live_records[session_id] = []
|
||||
if key not in _sub_live_records:
|
||||
_sub_live_records[key] = []
|
||||
entry = {
|
||||
"id": tool_call_id or "",
|
||||
"name": name,
|
||||
"args": _json_safe(args),
|
||||
"args_summary": format_args_summary(args, max_len=40),
|
||||
@@ -250,7 +291,7 @@ def record_tool_call_live(session_id: str, name: str, args: dict[str, Any]) -> N
|
||||
}
|
||||
if thinking:
|
||||
entry["thinking"] = thinking
|
||||
_sub_live_records[session_id].append(entry)
|
||||
_sub_live_records[key].append(entry)
|
||||
_notify_live(session_id)
|
||||
return
|
||||
with _pending_thinking_lock:
|
||||
@@ -259,6 +300,7 @@ def record_tool_call_live(session_id: str, name: str, args: dict[str, Any]) -> N
|
||||
if session_id not in _live_records:
|
||||
_live_records[session_id] = []
|
||||
entry = {
|
||||
"id": tool_call_id or "",
|
||||
"name": name,
|
||||
"args": _json_safe(args),
|
||||
"args_summary": format_args_summary(args, max_len=40),
|
||||
@@ -278,10 +320,11 @@ def record_tool_result_live(
|
||||
artifact: Any | None = None,
|
||||
) -> None:
|
||||
"""记录工具返回结果,供前端实时展示文本、图片预览、音频信息与文件信息。"""
|
||||
# 子 Agent 上下文:路由到 _sub_live_records
|
||||
# 子 Agent 上下文:路由到 _sub_live_records(按 (session, tool_call_id) 隔离)
|
||||
if is_sub_agent_context():
|
||||
key = _sub_key(session_id)
|
||||
with _sub_live_lock:
|
||||
records = _sub_live_records.get(session_id, [])
|
||||
records = _sub_live_records.get(key, [])
|
||||
for rec in reversed(records):
|
||||
if rec["name"] == name and rec["status"] == "running":
|
||||
rec["status"] = "done"
|
||||
@@ -472,9 +515,12 @@ def end_live_turn(session_id: str) -> list[dict[str, Any]]:
|
||||
with _pending_thinking_lock:
|
||||
_pending_thinking.pop(session_id, None)
|
||||
with _sub_pending_thinking_lock:
|
||||
_sub_pending_thinking.pop(session_id, None)
|
||||
# 清理该会话下所有子 Agent 隔离槽(并行子 Agent 按 (session, tc_id) 分槽)
|
||||
for k in [k for k in _sub_pending_thinking if k[0] == session_id]:
|
||||
_sub_pending_thinking.pop(k, None)
|
||||
with _sub_live_lock:
|
||||
_sub_live_records.pop(session_id, None)
|
||||
for k in [k for k in _sub_live_records if k[0] == session_id]:
|
||||
_sub_live_records.pop(k, None)
|
||||
with _live_lock:
|
||||
_active_sessions.discard(session_id)
|
||||
return _live_records.pop(session_id, [])
|
||||
@@ -550,6 +596,7 @@ def save_aborted_turn(session_id: str, turn_id: str) -> None:
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for r in records:
|
||||
entry = {
|
||||
"id": r.get("id", ""),
|
||||
"name": r.get("name", ""),
|
||||
"args": r.get("args", {}),
|
||||
"args_summary": r.get("args_summary", ""),
|
||||
@@ -607,15 +654,31 @@ def get_live_tool_calls(session_id: str) -> list[dict[str, Any]]:
|
||||
if not records:
|
||||
return []
|
||||
|
||||
# ---- 注入子 Agent 实时轨迹到 call_subagent 条目 ----
|
||||
# ---- 注入子 Agent 实时轨迹到 call_subagent 条目(按 tool_call_id 精确匹配) ----
|
||||
# 并行子 Agent(含同名)按 (session, tool_call_id) 分槽,注入到各自对应条目;
|
||||
# 无 id 的旧数据回退到"最后一条 running"(旧行为兼容)。
|
||||
with _sub_live_lock:
|
||||
sub_calls = list(_sub_live_records.get(session_id, []))
|
||||
if sub_calls:
|
||||
# 找到最后一个 running 的 call_subagent 条目(从末尾向前找)
|
||||
for rec in reversed(records):
|
||||
if rec.get("name") in ("call_subagent", "agent_call") and rec.get("status") == "running":
|
||||
sub_map = {k: list(v) for k, v in _sub_live_records.items() if k[0] == session_id}
|
||||
if sub_map:
|
||||
matched_ids: set[str] = set()
|
||||
fallback_rec: dict[str, Any] | None = None
|
||||
for rec in records:
|
||||
if rec.get("name") not in ("call_subagent", "agent_call"):
|
||||
continue
|
||||
tc_id = rec.get("id") or ""
|
||||
sub_calls = sub_map.get((session_id, tc_id))
|
||||
if sub_calls:
|
||||
rec["sub_tool_calls"] = sub_calls
|
||||
break
|
||||
matched_ids.add(tc_id)
|
||||
elif rec.get("status") == "running" and fallback_rec is None:
|
||||
fallback_rec = rec
|
||||
if fallback_rec is not None and not fallback_rec.get("sub_tool_calls"):
|
||||
leftover: list[dict[str, Any]] = []
|
||||
for k, v in sub_map.items():
|
||||
if k[1] not in matched_ids:
|
||||
leftover.extend(v)
|
||||
if leftover:
|
||||
fallback_rec["sub_tool_calls"] = leftover
|
||||
|
||||
return records
|
||||
|
||||
@@ -654,11 +717,11 @@ def record_reflection_live(session_id: str, content: str) -> None:
|
||||
"""
|
||||
if not session_id or not content:
|
||||
return
|
||||
# 子 Agent 上下文:thinking 存入 _sub_pending_thinking,
|
||||
# 子 Agent 上下文:thinking 存入 _sub_pending_thinking(按 (session, tc_id) 隔离),
|
||||
# 由 record_tool_call_live 消费后附加到子工具调用条目。
|
||||
if is_sub_agent_context():
|
||||
with _sub_pending_thinking_lock:
|
||||
_sub_pending_thinking[session_id] = content
|
||||
_sub_pending_thinking[_sub_key(session_id)] = content
|
||||
# 不添加到 _think_store(子 Agent 思考在主 Agent 思考面板中不独立显示,
|
||||
# 而是嵌套在 call_subagent 展开的各个子工具调用条目的 thinking 字段中)
|
||||
return
|
||||
@@ -718,6 +781,7 @@ def extract_tool_calls_from_messages(messages: list[BaseMessage]) -> list[dict[s
|
||||
for tc in msg.tool_calls:
|
||||
d = normalize_tool_call(tc)
|
||||
entry = {
|
||||
"id": d.get("id", "") or "",
|
||||
"name": d.get("name", "unknown"),
|
||||
"args": _json_safe(d.get("args", {})),
|
||||
"args_summary": format_args_summary(d.get("args", {}), max_len=40),
|
||||
@@ -924,7 +988,11 @@ def _merge_extracted_with_live(
|
||||
|
||||
|
||||
def _inject_sub_traces_to_record(tool_calls: list[dict[str, Any]], session_id: str, turn_id: str) -> None:
|
||||
"""将子 Agent 的工具调用轨迹注入到 tool_calls 的 call_subagent 条目中(用于持久化)。"""
|
||||
"""将子 Agent 的工具调用轨迹注入到 tool_calls 的 call_subagent 条目中(用于持久化)。
|
||||
|
||||
优先按条目携带的 tool_call_id 精确匹配(并行/同名子 Agent 各自独立);
|
||||
无 id 的旧数据回退到按 agent_name 匹配(旧行为兼容)。
|
||||
"""
|
||||
for tc in tool_calls:
|
||||
if tc.get("name") not in ("call_subagent", "agent_call"):
|
||||
continue
|
||||
@@ -936,10 +1004,15 @@ def _inject_sub_traces_to_record(tool_calls: list[dict[str, Any]], session_id: s
|
||||
except Exception:
|
||||
args = {}
|
||||
sub_name = args.get("agent_name", "")
|
||||
if sub_name:
|
||||
if not sub_name:
|
||||
continue
|
||||
tc_id = tc.get("id") or ""
|
||||
if tc_id:
|
||||
sub_tc = get_sub_agent_traces_for_turn(session_id, turn_id, sub_name, tc_id)
|
||||
else:
|
||||
sub_tc = get_sub_agent_traces_for_turn(session_id, turn_id, sub_name)
|
||||
if sub_tc:
|
||||
tc["sub_tool_calls"] = sub_tc
|
||||
if sub_tc:
|
||||
tc["sub_tool_calls"] = sub_tc
|
||||
|
||||
|
||||
def list_turns(session_id: str, limit: int = 5) -> list[dict[str, Any]]:
|
||||
@@ -1058,7 +1131,17 @@ def _tool_calls_from_turn_record(record: dict[str, Any], session_id: str) -> lis
|
||||
|
||||
|
||||
# ---------- 子 Agent 轨迹存取 ----------
|
||||
def record_sub_agent_trace(session_id: str, turn_id: str, agent_name: str, tool_calls: list[dict[str, Any]]) -> None:
|
||||
def _sub_trace_key(session_id: str, turn_id: str, agent_name: str, tool_call_id: str = "") -> str:
|
||||
"""子 Agent 轨迹存储 key:f"{session_id}/{turn_id}/{agent_name}[/{tool_call_id}]"。
|
||||
|
||||
携带 tool_call_id 时按调用实例精确隔离(并行/同名子 Agent 各自独立);
|
||||
为空时回退到旧的 agent_name 级 key(顺序执行/旧数据兼容)。
|
||||
"""
|
||||
base = f"{session_id}/{turn_id}/{agent_name}"
|
||||
return f"{base}/{tool_call_id}" if tool_call_id else base
|
||||
|
||||
|
||||
def record_sub_agent_trace(session_id: str, turn_id: str, agent_name: str, tool_calls: list[dict[str, Any]], tool_call_id: str = "") -> None:
|
||||
"""记录子 Agent 的完整工具调用轨迹,供前端嵌套展开。
|
||||
|
||||
输入:
|
||||
@@ -1066,27 +1149,29 @@ def record_sub_agent_trace(session_id: str, turn_id: str, agent_name: str, tool_
|
||||
turn_id: 当前轮次 ID。
|
||||
agent_name: 被调用的子 Agent 名称。
|
||||
tool_calls: extract_tool_calls_from_messages 提取的子 Agent 工具调用列表。
|
||||
tool_call_id: 本次 call_subagent 调用的工具调用 ID(并行隔离用,可为空)。
|
||||
"""
|
||||
if not session_id or not turn_id or not agent_name:
|
||||
return
|
||||
key = f"{session_id}/{turn_id}/{agent_name}"
|
||||
key = _sub_trace_key(session_id, turn_id, agent_name, tool_call_id)
|
||||
with _sub_traces_lock:
|
||||
_sub_traces_store[key] = tool_calls
|
||||
|
||||
|
||||
def get_sub_agent_traces_for_turn(session_id: str, turn_id: str, agent_name: str) -> list[dict[str, Any]]:
|
||||
def get_sub_agent_traces_for_turn(session_id: str, turn_id: str, agent_name: str, tool_call_id: str = "") -> list[dict[str, Any]]:
|
||||
"""获取指定会话/轮次/子 Agent 的轨迹。
|
||||
|
||||
输入:
|
||||
session_id: 会话 ID。
|
||||
turn_id: 轮次 ID。
|
||||
agent_name: 子 Agent 名称。
|
||||
tool_call_id: 可选工具调用 ID;传入时精确匹配该次调用,否则匹配 agent_name 级。
|
||||
|
||||
输出:
|
||||
工具调用轨迹列表。
|
||||
"""
|
||||
if not session_id or not turn_id or not agent_name:
|
||||
return []
|
||||
key = f"{session_id}/{turn_id}/{agent_name}"
|
||||
key = _sub_trace_key(session_id, turn_id, agent_name, tool_call_id)
|
||||
with _sub_traces_lock:
|
||||
return list(_sub_traces_store.get(key, []))
|
||||
@@ -40,7 +40,7 @@ def get_main_system_prompt(workspace_dir: str | None = None) -> str:
|
||||
ws_info = _WS_TAIL.format(workspace_dir=ws_dir)
|
||||
except Exception:
|
||||
ws_info = ""
|
||||
return _BASE_PROMPT + ws_info
|
||||
return _BASE_PROMPT + _reflection_section() + ws_info
|
||||
|
||||
|
||||
def set_current_workspace(workspace_dir: str | None) -> None:
|
||||
@@ -63,10 +63,11 @@ def get_cron_system_prompt(workspace_dir: str | None = None) -> str:
|
||||
try:
|
||||
from agent.utils.env_utils import get_workspace_dir
|
||||
ws_dir = workspace_dir or _current_workspace_dir or get_workspace_dir()
|
||||
return CRON_MODE_PROMPT.format(workspace_dir=ws_dir)
|
||||
base = CRON_MODE_PROMPT.format(workspace_dir=ws_dir)
|
||||
except Exception:
|
||||
# 占位符未填时退回不含工作空间信息的版本,避免 .format 异常中断执行
|
||||
return CRON_MODE_PROMPT.replace("{workspace_dir}", "")
|
||||
base = CRON_MODE_PROMPT.replace("{workspace_dir}", "")
|
||||
return base + _reflection_section()
|
||||
|
||||
SUMMARY_SYSTEM_PROMPT ="""
|
||||
你是一个名为“Summary Agent”的专业摘要助手,专门负责对用户提供的文本内容进行高度凝练、准确的总结。
|
||||
@@ -157,7 +158,7 @@ def get_plan_system_prompt(workspace_dir: str | None = None) -> str:
|
||||
ws_info = _WS_TAIL.format(workspace_dir=ws_dir)
|
||||
except Exception:
|
||||
ws_info = ""
|
||||
return PLAN_MODE_PROMPT + ws_info
|
||||
return PLAN_MODE_PROMPT + _reflection_section() + ws_info
|
||||
|
||||
|
||||
CRON_MODE_PROMPT = """
|
||||
@@ -187,8 +188,10 @@ CRON_MODE_PROMPT = """
|
||||
- 写操作限制:doc_tool 的创建/修改/删除、terminal_execute 的文件变更目标必须位于工作空间内。无人值守场景下超出范围的访问会被直接拦截,请务必使用工作空间内的路径。
|
||||
""".strip()
|
||||
|
||||
# ---------- 反思注入提示(作为 synthetic HumanMessage 注入到对话上下文)----------
|
||||
# 这些不是系统提示词,而是在 call_model 节点中动态拼入消息列表的引导文本。
|
||||
# ---------- 反思引导提示(固定部分 → 系统提示词前缀,前缀缓存友好)----------
|
||||
# 固定不变的反思引导文本作为系统提示词的一部分拼在最前面(缓存命中),
|
||||
# 不再在 call_model 节点中间逐轮构造;变化的部分(TodoList 状态、循环提醒)
|
||||
# 由 call_model 作为动态尾部追加到消息列表末尾,并持久化为长期记忆。
|
||||
|
||||
REFLECTION_PROMPT = """
|
||||
【重要:如果你判断不需要调用任何工具,请直接回复用户,不要输出任何反思内容!】
|
||||
@@ -202,6 +205,26 @@ REFLECTION_PROMPT = """
|
||||
如果你计划在同一页面/界面执行多个连续的 GUI 操作(例如:依次点击多个按钮、填写多个表单字段、连续输入多项内容),必须将所有这些操作合并为一次 gui_tool 调用,在 actions 列表中一次性传入所有动作。严禁将同一页面的连续操作拆分多次 gui_tool 调用——这样会导致严重的延迟。仅当需要等页面跳转/加载后才能确定后续操作时,才分步调用。
|
||||
""".strip()
|
||||
|
||||
|
||||
def _reflection_section() -> str:
|
||||
"""固定反思引导段:思考关闭档位时并入系统提示词前缀(缓存友好),
|
||||
思考开启档位(模型原生深度思考)不注入,保持原语义。
|
||||
|
||||
惰性导入 env_utils 避免模块级循环依赖。
|
||||
"""
|
||||
try:
|
||||
from agent.utils.env_utils import thinking_enabled
|
||||
if thinking_enabled():
|
||||
return ""
|
||||
except Exception:
|
||||
pass
|
||||
return "\n\n" + REFLECTION_PROMPT
|
||||
|
||||
|
||||
def get_default_sub_agent_prompt() -> str:
|
||||
"""获取默认子 Agent 系统提示词(含按思考档位门控的固定反思引导)。"""
|
||||
return DEFAULT_SUB_AGENT_PROMPT + _reflection_section()
|
||||
|
||||
# 子 Agent 上下文超限提示(由 agent 节点在子 Agent token 超阈值时注入,强制其整理进度并文字返回)
|
||||
SUB_AGENT_OOM_PROMPT = """
|
||||
【系统指令 - 上下文超限】
|
||||
|
||||
@@ -153,17 +153,21 @@ class CallSubAgentTool(BaseTool):
|
||||
# 设置子 Agent 上下文:agent 名称供前端浮窗按 agent 分组
|
||||
from agent.core.human_request import set_current_agent_name
|
||||
set_current_agent_name(agent_name)
|
||||
# 当前 call_subagent 的工具调用 ID:并行多个子 Agent(含同名)时,
|
||||
# 子 Agent 的实时记录与轨迹存档均按 (session, tool_call_id) 隔离。
|
||||
from agent.history.tool_call_recorder import get_current_tool_call_id
|
||||
tool_call_id = get_current_tool_call_id()
|
||||
# 进入子 Agent 执行上下文:抑制其 record_*_live 调用,
|
||||
# 避免子 Agent 的思考/工具调用混入主 Agent 的实时列表。
|
||||
# 子 Agent 轨迹仍由 _record_sub_trace 独立存档,持久化时嵌套注入。
|
||||
from agent.history.tool_call_recorder import sub_agent_context
|
||||
try:
|
||||
with sub_agent_context():
|
||||
with sub_agent_context(tool_call_id):
|
||||
result = runtime.graph.invoke({"messages": messages}, config=config)
|
||||
all_messages = result.get("messages", [])
|
||||
|
||||
# 提取子 Agent 工具调用轨迹 → 存档供前端嵌套展开
|
||||
_record_sub_trace(agent_name, all_messages)
|
||||
# 提取子 Agent 工具调用轨迹 → 存档供前端嵌套展开(按 tool_call_id 隔离)
|
||||
_record_sub_trace(agent_name, all_messages, tool_call_id)
|
||||
|
||||
# 提取最后一条非工具调用的助手消息作为最终结果
|
||||
from langchain_core.messages import AIMessage
|
||||
@@ -200,10 +204,11 @@ class CallSubAgentTool(BaseTool):
|
||||
|
||||
|
||||
# ---------- 子 Agent 轨迹记录 ----------
|
||||
def _record_sub_trace(agent_name: str, messages: list) -> None:
|
||||
def _record_sub_trace(agent_name: str, messages: list, tool_call_id: str = "") -> None:
|
||||
"""记录子 Agent 的工具调用轨迹,供前端嵌套展开。
|
||||
|
||||
同时写入 tool_call_recorder 的 session/turn 作用域存储。
|
||||
同时写入 tool_call_recorder 的 session/turn 作用域存储,
|
||||
tool_call_id 用于并行子 Agent(含同名)的轨迹隔离。
|
||||
"""
|
||||
|
||||
# 桥接到 tool_call_recorder(按 session/turn 作用域)
|
||||
@@ -218,7 +223,7 @@ def _record_sub_trace(agent_name: str, messages: list) -> None:
|
||||
turn_id = _recorder_turn()
|
||||
if session_id and turn_id:
|
||||
sub_tc = _recorder_extract(list(messages))
|
||||
_recorder_record(session_id, turn_id, agent_name, sub_tc)
|
||||
_recorder_record(session_id, turn_id, agent_name, sub_tc, tool_call_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -249,16 +254,24 @@ def make_parallel_tool_node(tools: list[Any], max_workers: int = 5):
|
||||
"""在线程池中执行单个工具。
|
||||
|
||||
委托 invoke_tool_and_build_message 统一处理返回类型。
|
||||
执行前设置当前 tool_call_id(经上下文传播进入 call_subagent._run,
|
||||
供并行子 Agent 的轨迹/实时记录按 tool_call_id 隔离)。
|
||||
返回 (tool_name, content, artifact),artifact 为 None 表示无产物。
|
||||
"""
|
||||
tool = tools_by_name.get(tool_name)
|
||||
if tool is None:
|
||||
return tool_name, f"工具 '{tool_name}' 未找到", None
|
||||
from agent.history.tool_call_recorder import set_current_tool_call_id, get_current_tool_call_id
|
||||
_prev_tc = get_current_tool_call_id()
|
||||
set_current_tool_call_id(tool_call_id)
|
||||
try:
|
||||
content, artifact = invoke_tool_and_build_message(tool, tool_name, tool_args, tool_call_id)
|
||||
return tool_name, content, artifact
|
||||
except Exception as e:
|
||||
return tool_name, f"工具执行出错: {e}", None
|
||||
tool = tools_by_name.get(tool_name)
|
||||
if tool is None:
|
||||
return tool_name, f"工具 '{tool_name}' 未找到", None
|
||||
try:
|
||||
content, artifact = invoke_tool_and_build_message(tool, tool_name, tool_args, tool_call_id)
|
||||
return tool_name, content, artifact
|
||||
except Exception as e:
|
||||
return tool_name, f"工具执行出错: {e}", None
|
||||
finally:
|
||||
set_current_tool_call_id(_prev_tc)
|
||||
|
||||
def parallel_tool_node(state: AgentState) -> dict:
|
||||
"""并行执行最后一条 AIMessage 中的所有 tool_calls。"""
|
||||
@@ -285,13 +298,18 @@ def make_parallel_tool_node(tools: list[Any], max_workers: int = 5):
|
||||
}
|
||||
|
||||
# 多个工具调用:线程池并行执行
|
||||
# 用 contextvars.copy_context() 包装 submit,使 worker 线程继承
|
||||
# 图线程的执行上下文(session/turn/agent 名称/sub_agent_context 等),
|
||||
# 保证并行工具(含并行子 Agent)之间的状态隔离与前端归属正确。
|
||||
import contextvars
|
||||
futures: dict[concurrent.futures.Future, dict] = {}
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(max_workers, len(tool_calls))) as executor:
|
||||
for tc in tool_calls:
|
||||
tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "")
|
||||
tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {})
|
||||
tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "")
|
||||
future = executor.submit(_execute_single, tc_name, tc_args, tc_id)
|
||||
ctx = contextvars.copy_context()
|
||||
future = executor.submit(ctx.run, _execute_single, tc_name, tc_args, tc_id)
|
||||
futures[future] = {"id": tc_id, "name": tc_name}
|
||||
|
||||
# 收集结果,保持顺序
|
||||
|
||||
@@ -182,6 +182,49 @@ def update_session_meta(session_id: str, agent_name: str,
|
||||
_save_session_meta(session_id, meta)
|
||||
|
||||
|
||||
# ---------- 动态尾部长期记忆(TodoList 状态 + 循环提醒) ----------
|
||||
def update_session_dynamic_tail(session_id: str, agent_key: str, text: str, max_entries: int = 100) -> None:
|
||||
"""持久化当前轮的动态尾部提示(TodoList 状态 + 循环提醒)为长期记忆。
|
||||
|
||||
输入:
|
||||
session_id: 会话 ID。
|
||||
agent_key: session_meta 中的 agent 键(主 Agent 为 "main",子 Agent 用其名称)。
|
||||
text: 本轮动态尾部文本。
|
||||
max_entries: 历史条目上限(防止无界增长),超出丢弃最旧。
|
||||
|
||||
说明:
|
||||
与 compressed_content 合并保存(不覆盖);连续相同内容去重,
|
||||
使 todo 进度等状态变更形成可追溯的历史记录,供后续轮次注入查看
|
||||
(充分利用前缀缓存:变化部分始终位于消息末尾)。
|
||||
"""
|
||||
if not session_id or not agent_key or not text:
|
||||
return
|
||||
meta = _load_session_meta(session_id)
|
||||
entry = dict(meta.get(agent_key) or {})
|
||||
history = list(entry.get("dynamic_tail_history") or [])
|
||||
if history and history[-1] == text:
|
||||
return # 去重:内容未变化则不重复记录
|
||||
history.append(text)
|
||||
if len(history) > max_entries:
|
||||
history = history[-max_entries:]
|
||||
entry["dynamic_tail_history"] = history
|
||||
meta[agent_key] = entry
|
||||
_save_session_meta(session_id, meta)
|
||||
|
||||
|
||||
def get_session_dynamic_tail_history(session_id: str, agent_key: str) -> list[str]:
|
||||
"""读取指定 Agent 的动态尾部长期记忆历史(时间正序,最新在末尾)。
|
||||
|
||||
输出:
|
||||
[tail_text, ...];无记录返回空列表。
|
||||
"""
|
||||
if not session_id or not agent_key:
|
||||
return []
|
||||
meta = _load_session_meta(session_id)
|
||||
entry = meta.get(agent_key) or {}
|
||||
return list(entry.get("dynamic_tail_history") or [])
|
||||
|
||||
|
||||
def build_tool_history_messages(session_id: str, agent_key: str = "main") -> list:
|
||||
"""构建压缩游标之后的"历史工具调用与返回值"合成消息列表。
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
@@ -24,6 +25,12 @@ def _invoke_with_timeout(tool, tool_call_input: dict, timeout: float) -> Any:
|
||||
超时后工作线程仍在后台运行(Python 无法强杀线程),结果被丢弃。
|
||||
工具内部抛出的异常会在主线程 re-raise(保留 ABORTED_BY_USER 冒泡)。
|
||||
|
||||
上下文传播:
|
||||
通过 ``contextvars.copy_context()`` 把调用方上下文(session/turn、
|
||||
agent 名称、sub_agent_context、tool_call_id 等 ContextVar)复制进
|
||||
超时线程,使工具实现(如 call_subagent 的 _run)在独立线程中也能
|
||||
读到正确的执行上下文,保证并行工具/并行子 Agent 的隔离性。
|
||||
|
||||
输入:
|
||||
tool: LangChain BaseTool 实例。
|
||||
tool_call_input: 传给 tool.invoke 的输入字典。
|
||||
@@ -40,7 +47,8 @@ def _invoke_with_timeout(tool, tool_call_input: dict, timeout: float) -> Any:
|
||||
except BaseException as e: # noqa: BLE001 - 需透传 ABORTED_BY_USER 等所有异常
|
||||
container["error"] = e
|
||||
|
||||
t = threading.Thread(target=_worker, daemon=True)
|
||||
ctx = contextvars.copy_context()
|
||||
t = threading.Thread(target=lambda: ctx.run(_worker), daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=timeout)
|
||||
if t.is_alive():
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/* 本地中文字体(fontsource 子集),离线可用;font-display: swap 保证不阻塞渲染 */
|
||||
/* generated from @fontsource chinese-simplified + latin subsets */
|
||||
|
||||
/* zcool-kuaile - chinese-simplified */
|
||||
/* zcool-kuaile-chinese-simplified-400-normal */
|
||||
@font-face {
|
||||
font-family: 'ZCOOL KuaiLe';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+20000-2A6DF;
|
||||
src: url(./zcool-kuaile-chinese-simplified-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* zcool-kuaile - latin */
|
||||
/* zcool-kuaile-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: 'ZCOOL KuaiLe';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(./zcool-kuaile-latin-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* ma-shan-zheng - chinese-simplified */
|
||||
/* ma-shan-zheng-chinese-simplified-400-normal */
|
||||
@font-face {
|
||||
font-family: 'Ma Shan Zheng';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+20000-2A6DF;
|
||||
src: url(./ma-shan-zheng-chinese-simplified-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* ma-shan-zheng - latin */
|
||||
/* ma-shan-zheng-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: 'Ma Shan Zheng';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(./ma-shan-zheng-latin-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* long-cang - chinese-simplified */
|
||||
/* long-cang-chinese-simplified-400-normal */
|
||||
@font-face {
|
||||
font-family: 'Long Cang';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+20000-2A6DF;
|
||||
src: url(./long-cang-chinese-simplified-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* long-cang - latin */
|
||||
/* long-cang-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: 'Long Cang';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(./long-cang-latin-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* zcool-qingke-huangyou - chinese-simplified */
|
||||
/* zcool-qingke-huangyou-chinese-simplified-400-normal */
|
||||
@font-face {
|
||||
font-family: 'ZCOOL QingKe HuangYou';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+20000-2A6DF;
|
||||
src: url(./zcool-qingke-huangyou-chinese-simplified-400-normal.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* zcool-qingke-huangyou - latin */
|
||||
/* zcool-qingke-huangyou-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: 'ZCOOL QingKe HuangYou';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(./zcool-qingke-huangyou-latin-400-normal.woff2) format('woff2');
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4
-8
@@ -3,12 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Ma+Shan+Zheng&family=Long+Cang&family=ZCOOL+QingKe+HuangYou&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- 本地字体(离线可用,不再依赖 Google Fonts CDN) -->
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
<link rel="icon" href="/image/favicon.svg" sizes="any" />
|
||||
<link rel="stylesheet" href="/app.css?v=6" />
|
||||
</head>
|
||||
@@ -543,8 +539,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast-container" id="toastContainer" aria-live="polite"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
|
||||
<script src="/packages/vendor/marked.min.js"></script>
|
||||
<script src="/packages/vendor/purify.min.js"></script>
|
||||
<!-- Monaco Editor: CDN 优先(Web端),本地 npm 包兜底(Electron 离线) -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* marked v15.0.12 - a markdown parser
|
||||
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
|
||||
* https://github.com/markedjs/marked
|
||||
*/
|
||||
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE
|
||||
* The code in this file is generated from files in ./src/
|
||||
*/
|
||||
(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
|
||||
"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Ce=h("^ {Line truncated
|
||||
]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,W=/[\s\p{P}\p{S}]/u,ce=/[^\s\p{P}\p{S}]/u,Ze=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,W).getRegex(),pe=/(?!~)[\p{P}\p{S}]/u,Ge=/(?!~)[\s\p{P}\p{S}]/u,He=/(?:[^\s\p{P}\p{S}]|~)/u,Ne=/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},nt={...j,br:h(ae).replace("{2,}","*").getRegex(),text:h(j.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},B={normal:X,gfm:Oe,pedantic:Be},P={normal:J,gfm:j,breaks:nt,pedantic:tt};var st={"&":"&","<":"<",">":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(m.slashPipe,"|");return n}function A(l,e,t){let n=l.length;if(n===0)return"";let s=0;for(;s<n;){let i=l.charAt(n-s-1);if(i===e&&!t)s++;else if(i!==e&&t)s++;else break}return l.slice(0,n-s)}function de(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}fLine truncated
|
||||
`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(`
|
||||
`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,`
|
||||
`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],`
|
||||
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],`
|
||||
`).split(`
|
||||
`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c<n.length;c++)if(this.rules.other.blockquoteStart.test(n[c]))a.push(n[c]),o=!0;else if(!o)a.push(n[c]);else break;n=n.slice(c);let p=a.join(`
|
||||
`),u=p.replace(this.rules.other.blockquoteSetextReplace,`
|
||||
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");s=s?`${s}
|
||||
${p}`:p,i=i?`${i}
|
||||
${u}`:u;let d=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(u,r,!0),this.lexer.state.top=d,n.length===0)break;let g=r.at(-1);if(g?.type==="code")break;if(g?.type==="blockquote"){let T=g,f=T.raw+`
|
||||
`+n.join(`
|
||||
`),y=this.blockquote(f);r[r.length-1]=y,s=s.substring(0,s.length-T.raw.length)+y.raw,i=i.substring(0,i.length-T.text.length)+y.text;break}else if(g?.type==="list"){let T=g,f=T.raw+`
|
||||
`+n.join(`
|
||||
`),y=this.list(f);r[r.length-1]=y,s=s.substring(0,s.length-g.raw.length)+y.raw,i=i.substring(0,i.length-T.raw.length)+y.raw,n=f.substring(r.at(-1).raw.length).split(`
|
||||
`);continue}}return{type:"blockquote",raw:s,tokens:r,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),s=n.length>1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(`
|
||||
`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(`
|
||||
`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+`
|
||||
`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(`
|
||||
`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=`
|
||||
`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=`
|
||||
`+g}!T&&!g.trim()&&(T=!0),p+=G+`
|
||||
`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;c<i.items.length;c++)if(this.lexer.state.top=!1,i.items[c].tokens=this.lexer.blockTokens(i.items[c].text,[]),!i.loose){let p=i.items[c].tokens.filter(d=>d.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c<i.items.length;c++)i.items[c].loose=!0;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:s,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=Y(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
|
||||
`):[],r={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?r.align.push("right"):this.rules.other.tableAlignCenter.test(o)?r.align.push("center"):this.rules.other.tableAlignLeft.test(o)?r.align.push("left"):r.align.push(null);for(let o=0;o<n.length;o++)r.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:r.align[o]});for(let o of i)r.rows.push(Y(o,r.header.length).map((a,c)=>({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
|
||||
`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
|
||||
`),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let s;if(this.options.extensions?.block?.some(r=>(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=`
|
||||
`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
|
||||
`+s.raw,r.text+=`
|
||||
`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
|
||||
`+s.raw,r.text+=`
|
||||
`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=`
|
||||
`+s.raw,r.text+=`
|
||||
`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=`
|
||||
`+s.raw,r.text+=`
|
||||
`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
|
||||
`;return s?'<pre><code class="language-'+R(s)+'">'+(n?i:R(i,!0))+`</code></pre>
|
||||
`:"<pre><code>"+(n?i:R(i,!0))+`</code></pre>
|
||||
`}blockquote({tokens:e}){return`<blockquote>
|
||||
${this.parser.parse(e)}</blockquote>
|
||||
`}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
|
||||
`}hr(e){return`<hr>
|
||||
`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o<e.items.length;o++){let a=e.items[o];s+=this.listitem(a)}let i=t?"ol":"ul",r=t&&n!==1?' start="'+n+'"':"";return"<"+i+r+`>
|
||||
`+s+"</"+i+`>
|
||||
`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>
|
||||
`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
|
||||
`}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let s="";for(let i=0;i<e.rows.length;i++){let r=e.rows[i];n="";for(let o=0;o<r.length;o++)n+=this.tablecell(r[o]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),`<table>
|
||||
<thead>
|
||||
`+t+`</thead>
|
||||
`+s+`</table>
|
||||
`}tablerow({text:e}){return`<tr>
|
||||
${e}</tr>
|
||||
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
|
||||
`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${R(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='<a href="'+e+'"';return t&&(r+=' title="'+R(t)+'"'),r+=">"+s+"</a>",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`<img src="${e}" alt="${n}"`;return t&&(r+=` title="${R(t)}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:R(e.text)}};var _=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}};var b=class l{options;renderer;textRenderer;constructor(e){this.options=e||w,this.options.renderer=this.options.renderer||new $,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new _}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(o.type)){n+=a||"";continue}}let r=i;switch(r.type){case"space":{n+=this.renderer.space(r);continue}case"hr":{n+=this.renderer.hr(r);continue}case"heading":{n+=this.renderer.heading(r);continue}case"code":{n+=this.renderer.code(r);continue}case"table":{n+=this.renderer.table(r);continue}case"blockquote":{n+=this.renderer.blockquote(r);continue}case"list":{n+=this.renderer.list(r);continue}case"html":{n+=this.renderer.html(r);continue}case"paragraph":{n+=this.renderer.paragraph(r);continue}case"text":{let o=r,a=this.renderer.text(o);for(;s+1<e.length&&e[s+1].type==="text";)o=e[++s],a+=`
|
||||
`+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let r=i;switch(r.type){case"escape":{n+=t.text(r);break}case"html":{n+=t.html(r);break}case"link":{n+=t.link(r);break}case"image":{n+=t.image(r);break}case"strong":{n+=t.strong(r);break}case"em":{n+=t.em(r);break}case"codespan":{n+=t.codespan(r);break}case"br":{n+=t.br(r);break}case"del":{n+=t.del(r);break}case"text":{n+=t.text(r);break}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var L=class{options;block;constructor(e){this.options=e||w}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?x.lex:x.lexInline}provideParser(){return this.block?b.parse:b.parseInline}};var E=class{defaults=z();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=b;Renderer=$;TextRenderer=_;Lexer=x;Tokenizer=S;Hooks=L;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{let i=s;for(let r of i.header)n=n.concat(this.walkTokens(r.tokens,t));for(let r of i.rows)for(let o of r)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let i=s;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=s;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(r=>{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.
Line truncated
|
||||
Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error occurred:</p><pre>"+R(n.message+"",!0)+"</pre>";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex;
|
||||
|
||||
if(__exports != exports)module.exports = exports;return module.exports}));
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
/*! @license DOMPurify 3.1.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.6/LICENSE */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),_=b(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;function b(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function S(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t<e.length;t++){E(e,t)||(e[t]=null)}return e}function w(t){const n=l(null);for(const[o,r]of e(t)){E(t,o)&&(Array.isArray(r)?n[o]=R(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=w(r):n[o]=r)}return n}function C(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return b(n.get);if("function"==typeof n.value)return b(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),v=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),O=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),x=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),k=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),M=i(["#text"]),I=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readLine truncated
|
||||
//# sourceMappingURL=purify.min.js.map
|
||||
+49
-4
@@ -357,7 +357,7 @@ def _inject_sub_tool_calls(
|
||||
tool_calls: 工具调用列表(原地修改)。
|
||||
session_id: 会话 ID。
|
||||
turn_id: 轮次 ID。
|
||||
lookup_fn: (session_id, turn_id, agent_name) -> list[dict] 的查询函数。
|
||||
lookup_fn: (session_id, turn_id, agent_name, tool_call_id) -> list[dict] 的查询函数。
|
||||
"""
|
||||
for tc in tool_calls:
|
||||
if tc.get("name") != "call_subagent" and tc.get("name") != "agent_call":
|
||||
@@ -374,7 +374,9 @@ def _inject_sub_tool_calls(
|
||||
args = {}
|
||||
sub_name = args.get("agent_name", "")
|
||||
if sub_name:
|
||||
sub_tc = lookup_fn(session_id, turn_id, sub_name)
|
||||
# 优先按条目携带的 tool_call_id 精确匹配(并行/同名子 Agent 各自独立),
|
||||
# lookup_fn 内部对空 tool_call_id 回退到 agent_name 级(旧数据兼容)。
|
||||
sub_tc = lookup_fn(session_id, turn_id, sub_name, tc.get("id", ""))
|
||||
if sub_tc:
|
||||
tc["sub_tool_calls"] = sub_tc
|
||||
|
||||
@@ -406,7 +408,22 @@ def _bootstrap() -> tuple[str, list[str], list[dict[str, Any]]]:
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
"""应用生命周期:启动定时任务调度器,退出时关闭。"""
|
||||
"""应用生命周期:预热重依赖 + 启动定时任务调度器,退出时关闭。
|
||||
|
||||
预热目的:
|
||||
agent.tools(含 chromadb 等重依赖)与 Agent 运行时原先在首次
|
||||
工具调用/首次打开设置页时惰性导入,首次请求要现场付数秒导入成本。
|
||||
这里在启动阶段统一预热(成本被 Electron 健康检查吸收),
|
||||
使设置页与首次聊天即时响应。
|
||||
"""
|
||||
try:
|
||||
# 预热:构建全部 Agent 运行时(顺带导入 agent.tools/chromadb 等重依赖)
|
||||
from agent.agents import agent_runtime # noqa: F401
|
||||
# 预热工具参数缓存(实例化全部工具类读取 args_schema)
|
||||
from agent.core.config_manager import _get_tool_parameters
|
||||
_get_tool_parameters()
|
||||
except Exception as e:
|
||||
print(f"[lifespan] 预热 Agent 运行时失败: {e}")
|
||||
try:
|
||||
from agent.cron.scheduler import get_scheduler
|
||||
get_scheduler().start()
|
||||
@@ -2575,4 +2592,32 @@ def serve_js_module(filename: str) -> FileResponse:
|
||||
@app.get("/css/{filename:path}")
|
||||
def serve_css_module(filename: str) -> FileResponse:
|
||||
"""返回前端 CSS 模块文件。"""
|
||||
return FileResponse(_WEB_DIR / "css" / filename)
|
||||
return FileResponse(_WEB_DIR / "css" / filename)
|
||||
|
||||
|
||||
@app.get("/fonts/{filename}")
|
||||
def serve_font(filename: str) -> FileResponse:
|
||||
"""返回本地字体文件(woff2,本地化后不再依赖 Google Fonts CDN)。
|
||||
|
||||
带路径包含校验,防止目录穿越。
|
||||
"""
|
||||
fonts_root = (_WEB_DIR / "fonts").resolve()
|
||||
font_path = (fonts_root / filename).resolve()
|
||||
if not str(font_path).startswith(str(fonts_root) + os.sep) or not font_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="字体文件不存在")
|
||||
mime, _ = mimetypes.guess_type(str(font_path))
|
||||
return FileResponse(font_path, media_type=mime or "font/woff2")
|
||||
|
||||
|
||||
@app.get("/packages/{filename:path}")
|
||||
def serve_packages(filename: str) -> FileResponse:
|
||||
"""返回本地 vendor 库与 node_modules(marked/dompurify 本地化 + Monaco 离线兜底)。
|
||||
|
||||
带路径包含校验,防止目录穿越。
|
||||
"""
|
||||
pkg_root = (_WEB_DIR / "packages").resolve()
|
||||
target = (pkg_root / filename).resolve()
|
||||
if not str(target).startswith(str(pkg_root) + os.sep) or not target.is_file():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
mime, _ = mimetypes.guess_type(str(target))
|
||||
return FileResponse(target, media_type=mime or "application/octet-stream")
|
||||
Reference in new issue
Block a user