diff --git a/README.html b/README.html new file mode 100644 index 0000000..4258e42 --- /dev/null +++ b/README.html @@ -0,0 +1,1862 @@ + + + + + + + + + + +minor Agent · 可本地运行的多模态 Agent 桌面应用 + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ +
+

🍃 一本可以滚动的互动故事书 · README

+ +

+ minor Agent +

+ +

一个可本地运行的多模态 Agent 桌面应用 —— 真正能「动手」操作你电脑的 AI 助手

+ +

基于 LangGraph 构建,具备 GUI 自动化、浏览器控制、终端执行、RAG 知识库、邮件收发、图像生成、语音对话、数据库操作、定时任务等 20 项工具,并提供 Electron 桌面端一键安装体验。

+ + + +
+
+ minor Agent + ./git_image/标题.png +
+
+ +
+
+ minor Agent 主界面 + ./git_image/banner.png +
+
+
+ + + + SCROLL + +
+ + +
+
+ 🧭 +

目录

+

像翻书一样,选择你想阅读的章节 —— 点击平滑滚动并自动触发该章节动画。

+
+ +
+ + +
+
+ 📖 +

项目简介

+
+ +
+

minor Agent 最初按 Web 在线应用开发,后转为本地桌面应用开源。它不是又一个「对话框 + 联网搜索」的套壳 Agent,而是一个以「工具执行 + 视觉闭环」为核心的通用智能体:

+ +
+ 🎓项目初衷:这是一个 LangChain + LangGraph 的学习项目。基于两者构建 Agent 工作流,结构清晰、上手容易、扩展方便,适合作为 Agent 开发的参考实现。 +
+ +
+ 🖥️真能操作桌面 — 截图 → 视觉定位 → 键鼠模拟,闭环完成 ERP 录入、表单填写、软件操控 + 🧩20 项内置工具 — 从终端命令到邮件收发,从 PPT 制作到图像生成,覆盖真实办公场景 + 🏠完全本地运行 — Electron 打包,数据不出本机;后端 FastAPI + 前端零延迟直读磁盘 + 🎨可深度定制 — 主题 / 字体 / 字号 / 背景 / 工作空间 / 提示词分层架构,全部可调 + 🔁工程级稳定性 — 循环检测、上下文压缩、阻塞式人工确认、轨迹回放,对抗 LLM 的「幻觉操作」 + 💾存储双后端(接口隔离) — 会话与定时任务存储统一接口驱动,JSON 可视化 / MySQL 一键切换、不双写;新增存储方式只需实现接口,消费方无感知后端 +
+ +
+ 💡项目演进:Web 在线应用 → Electron 桌面应用。源码同一套 src/,既可由 uvicorn 作 Web 服务启动,也可由 Electron 主进程拉起,双模式共用。 +
+ +

+ + 系统架构总览 + ./git_image/architecture.png + +

+
+
+ +
+ + +
+
+ ✨ +

功能亮点

+
+ +

🛠️ 20 项工具能力

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
工具文件能力
🖱️ guitools/gui.py桌面 GUI 自动化:截图、视觉批量定位、点击/输入/快捷键/滚动
🌐 browser_controltools/browser_control.py浏览器打开/关闭、地址导航、标签页交互
💻 terminal_executetools/terminal_execute.py执行任意终端命令,支持工作目录与超时
🪟 software_controltools/software_control.pyWin32 API 启停软件、注册表、os.startfile
📧 emailtools/email.pyIMAP 收邮件 + SMTP 发邮件(含附件)
📄 doctools/doc.py创建/读取/编辑 docx / xlsx / pptx / pdf / txt
📂 directory_listingtools/directory_listing.py递归列目录、文件元信息
📨 send_filetools/send_file.py以邮件附件形式发送本地文件
🔍 web_searchtools/web_search.py联网搜索(Tavily / Firecrawl / 火山引擎)
🗄️ text2sqltools/text2sql.py自然语言/SQL 操作数据库:查表结构、查询、增删改
📚 ragtools/rag.pyChromaDB 向量检索 + Reranker 重排序
🎨 image_gentools/image_gen.py调用 Z-Image-Turbo 生成图片
✅ todo_listtools/todo_list.py多步任务规划与进度跟踪
🤝 human_interactiontools/human_interaction.py阻塞式询问用户:收集信息 / 多选一,答案作为普通工具返回值
🤖 agent_calltools/agent_call.py委派子 Agent 处理子任务
🧭 skill_routertools/skill_router.py匹配并路由到预定义 Skill 流程
📅 date_querytools/date_query.py当前日期时间查询
⏰ cron_managertools/cron_manager.py定时任务管理:查看/新增/修改/删除定时任务,支持 cron/间隔/一次性触发
📊 hard_excel_readtools/hard_excel_read.py复杂 Excel 表结构读取:前 N 行原始数据 + 维度/合并区/数据区范围建议,供大模型解析结构
📥 excel2sqltools/excel2sql.py按大模型给定的数据行列范围,将 Excel 数据区流式批量入库 MySQL(自动建库建表、表注释)
+
+ +

🎯 区别于其他 Agent 项目的核心设计

+ +
+ + + + + + + + + + + + + + +
设计点多数 Agent 项目minor Agent
GUI 操作粒度一步一调用,N 个动作 = 2N+1 次 LLM 调用动作序列合并:同页多操作打包为一次 gui 调用,N 个动作仅 2 次 LLM 调用
视觉反馈仅返回坐标文本截图作为视觉闭环注入上下文,多模态 LLM 看到真实屏幕状态
坐标定位逐元素请求批量定位 _batch_ground_elements:N 个元素 1 次子模型调用
循环失控无防护,token 烧穿两级循环检测:连续 3 次同工具警告反思 → 连续 5 次强制终止
上下文膨胀简单截断图内分级压缩:token 超 窗口×COMPRESS_RATE 触发,压缩历史工具调用为累积摘要,每步 LLM 调用后检查
部署形态仅 Web / 仅 API同一套源码双模式:uvicorn Web 服务 ↔ Electron 桌面应用
文件操作HTTP 上传下载桌面端走 IPC 直读磁盘,零延迟;Web 端走 /api/fs/*
提示词单层 system prompt四层提示词架构:系统级 / 工具描述 / 运行时注入 / 内部子模型
+
+ +

🖼️ 运行界面

+

Agent 运行过程中的关键交互界面,支持文件从任意位置拖拽到聊天区或编辑区,灵活组织工作流:

+ +
+
+ 人机交互确认 + ./git_image/运行-人机交互.png +
+
+ 任务规划 TodoList + ./git_image/运行-todolist.png +
+
+

人机交互确认 — 敏感操作(终端执行、文件写入等)与信息收集以浮窗形式实时弹出,用户可同意、拒绝、跳过或补充信息;拒绝也只是一次工具返回值,Agent 自行调整方案继续,不会结束本轮 | 任务规划 TodoList — 多步任务自动拆解与进度跟踪,支持主 / 子 Agent 独立任务列表

+ +
+
+ 文件编辑模式 + ./git_image/运行-文件修改.png +
+
+ 工具调用记录 + ./git_image/运行-工具调用1.png +
+
+

文件编辑模式 — 内置 Monaco 编辑器,支持代码高亮、差异对比、文件树拖拽组织 | 工具调用记录 — 实时展示每轮 ReAct 的工具调用链、参数及返回值

+ +
+
+ 子 Agent 嵌套调用 + ./git_image/运行-工具调用2.png +
+
+

子 Agent 嵌套调用 — 主 Agent 委派子 Agent 执行子任务,工具调用以嵌套结构展示,思考过程穿插显示,支持展开/折叠查看详情

+ +
+
+ 定时任务创建 + ./git_image/运行-定时任务.png +
+
+ Edit 编辑模式 + ./git_image/运行-edit模式.png +
+
+

定时任务管理 — 支持 cron / 间隔 / 一次性三种触发方式,状态实时可见 | Edit 编辑模式 — 独立编辑区,Monaco 编辑器 + 文档树侧栏,支持拖拽文件/文件夹到附件区发送给 Agent

+ +
+
+ 图片修改画板 + ./git_image/修改图片.png +
+
+

图片修改画板 — 附件图片缩略图 hover 显示删除按钮,点击图片打开预览浮窗,支持下载/修改/关闭;点击「修改」进入画板编辑器,在底图上使用画笔/矩形/圆形/直线/文字/橡皮擦进行标注编辑,Ctrl+滚轮缩放,中键拖动平移,支持撤销/重做,确定后替换原图

+ +
+
+ 空白画板 + ./git_image/画板.png +
+
+

空白画板 — 输入区画板按钮一键打开 1536×768 空白画布,支持画笔/矩形/圆形/直线/文字/橡皮擦绘图,颜色与粗细可调,文字对象可拖拽/缩放/旋转,Ctrl+滚轮缩放画布,中键平移,完成导出为 PNG 自动添至附件区

+
+ +
+ + +
+
+ 🏗️ +

项目架构

+
+ +

整体分层

+ +
+ +
┌──────────────────────────────────────────────────────────────┐
+│                   Electron 主进程 (main.js)                    │
+│   窗口管理 · 首次配置向导 · Python 生命周期 · 端口清理 · IPC    │
+└───────────────────────────────┬──────────────────────────────┘
+                                 │ spawn uvicorn (127.0.0.1:8765)
+                                 ▼
+┌──────────────────────────────────────────────────────────────┐
+│                   FastAPI 后端 (web/server.py)                 │
+│   会话管理 · 多模态收发 · SSE 流式 · 文件服务 · 工具调用推送    │
+└───────────────────────────────┬──────────────────────────────┘
+                                 │ turn_runner.start_turn()
+                                 │(每会话一个后台线程,HTTP 请求
+                                 │  挂载等待;断连不影响执行)
+                                 ▼
+┌──────────────────────────────────────────────────────────────┐
+│                Agent 编排层 (agents/agent_runtime.py)          │
+│   消息拼装 · 历史持久化 · 跨轮工具上下文 · 实时推送             │
+└───────────────────────────────┬──────────────────────────────┘
+                                 │ graph.invoke()
+                                 ▼
+┌──────────────────────────────────────────────────────────────┐
+│            LangGraph ReAct 图 (core/graph.py)                 │
+│   agent 节点 → tools 节点 → process_tool_artifact 节点         │
+│   内含: 动态尾部注入 · 压缩 · 循环检测 · 路由                  │
+│   人机交互: 阻塞式普通工具(core/human_request.py)            │
+└───────────────────────────────┬──────────────────────────────┘
+                                 │ 调用
+                                 ▼
+┌──────────────────────────────────────────────────────────────┐
+│             20 项工具 (tools/*.py) · 5 项技能 (skills/)        │
+└──────────────────────────────────────────────────────────────┘
+
+ +

ReAct 循环(图内)

+ +
+
START → [agent: call_model] ──有 tool_calls──▶ [tools: 并行工具节点] ──▶ [process_tool_artifact] ──▶ [compress]
+              ▲                                      ▲                     │                            │
+              │ 无 tool_calls                        │                     ◀────────────────────────────┘
+              ▼                                      └── 子 Agent 结果 / 工具返回值注入 ◀── 并行工具节点(线程池)
+             END
+
+ +
+
    +
  • 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 时,将历史工具调用压缩为累积摘要(见「实现细节」)
  • +
+
+ +

多 Agent 模式

+ +

+ 多 Agent 并非独立图,而是同一张 react 图内的工具级并行:tools 节点由 make_parallel_tool_node(线程池)取代 ToolNode,一轮内的多个工具调用并行执行;其中 agent_call 工具会以主 Agent 当前消息快照启动子 Agent 的独立图实例执行子任务,结果以工具返回值注入主 Agent 上下文。子 Agent 内的人机交互同样为阻塞式——在子图线程中等待浮窗应答,完成后自然回到主图: +

+ +
+
[agent] ──有 agent_call──▶ [tools: parallel_tool_node(线程池)]
+                               │
+                               ├─ 普通工具:直接并行执行
+                               └─ agent_call:spawn 子 Agent Runtime → 子图 invoke
+                                              │(子图内 human_interaction 阻塞等待
+                                              │  浮窗应答,答案作为工具返回值)
+                                              └─ 结果作为 ToolMessage 注入主 Agent ◄┐
+          ▲                                                                          │
+          └──────────────── 有 agent_call → 继续循环;无 → END ──────────────────────┘
+
+
+ +
+ + +
+
+ 📂 +

项目结构

+

进入视口时逐层「生长」:先出根目录,再一层一层展开枝叶 —— 目录奶油绿,注释暖灰斜体。

+
+ +
+
Agent/
+├── electron/                    # 🖥️ Electron 桌面端
+│   ├── main.js                  #   主进程:窗口/IPC/Python生命周期/端口清理
+│   ├── preload.js               #   安全桥接层:fs/dialog/window/Python setup API
+│   └── setup.html               #   首次配置向导(3 步:Python环境 → LLM → 高级参数)
+│
+├── src/
+│   ├── agent/                   # 🧠 Agent 核心
+│   │   ├── agents/
+│   │   │   └── agent_runtime.py #   编排层入口:execute_agent(动态 Agent 工厂)
+│   │   ├── core/
+│   │   │   ├── graph.py         #   LangGraph 构建(单 Agent + 多 Agent)
+│   │   │   ├── nodes.py         #   ReAct 节点:call_model / process_tool_artifact / 压缩
+│   │   │   ├── runtime.py       #   图执行器:消息拼装、历史加载、轨迹落盘
+│   │   │   ├── routing.py       #   should_continue 路由 + 强制终止
+│   │   │   ├── state.py         #   Graph 状态定义(messages / agent_mode)
+│   │   │   ├── loop_detector.py #   循环检测:重复工具调用,两级响应
+│   │   │   ├── human_request.py #   ★ 人工请求注册表:交互类工具的通用阻塞通道(ask_human)
+│   │   │   ├── turn_runner.py   #   ★ 后台线程轮次执行器(解耦 HTTP 与图执行生命周期)
+│   │   │   ├── llm.py           #   ChatQwen 多模态封装(图像/附件/非标准 tool_call)
+│   │   │   ├── tool_policy.py   #   工具执行策略(直执 / 需确认)
+│   │   │   ├── storage.py       #   ★ 统一存储层:接口 + JSON/MySQL 双实现 + 工厂 + scope
+│   │   │   └── config_manager.py#   配置读写:agent/tool/env/gui/model/theme
+│   │   ├── memory/
+│   │   │   └── system_prompt.py #   ★ 四层提示词:MAIN / PLAN / REFLECTION / VISION...
+│   │   ├── tools/               #   20 项工具(见上表)
+│   │   │   ├── ...(gui / browser_control / terminal_execute / email / doc / web_search / text2sql / rag / cron_manager 等)
+│   │   ├── cron/                 #   ⏰ 定时任务引擎
+│   │   │   ├── models.py         #     数据模型(CronTask / Trigger)
+│   │   │   ├── storage.py        #     转发层(实现统一在 core/storage.py)
+│   │   │   ├── scheduler.py      #     调度计算与触发扫描
+│   │   │   ├── runner.py         #     任务执行器(子进程,切换存储 cron 作用域)
+│   │   │   └── live.py           #     常驻调度循环
+│   │   ├── skills/              #   5 项技能(skill.json + skill.md)
+│   │   │   ├── netease_mail_read/
+│   │   │   ├── netease_mail_send/
+│   │   │   ├── ppt_maker/       #   HTML 模板 → PPT(含 9 个附件文档 + 模板)
+│   │   │   ├── taste/           #   代码生成界面美化
+│   │   │   └── wechat_send_message/
+│   │   ├── tts/
+│   │   │   └── streaming_client.py  # 流式 TTS 客户端(SSE 收音频块)
+│   │   ├── history/
+│   │   │   ├── session_storage.py     # 转发层(实现统一在 core/storage.py)
+│   │   │   └── tool_call_recorder.py  # 每轮工具调用轨迹记录 (tool_{turn_id}.json)
+│   │   ├── utils/               #   env_utils / agent_utils / image_utils / ppt_utils / tool_call_utils
+│   │   └── config/              #   ★ 配置文件目录
+│   │       ├── env_config.json       #   运行时配置(首次向导生成,勿手动改)
+│   │       ├── env_config_dist.json  #   配置模板(默认值)
+│   │       ├── agent_config.json     #   Agent 行为配置 + 兜底 system_prompt
+│   │       ├── tool_config.json      #   各工具启用/参数
+│   │       ├── gui_config.json       #   GUI 工具参数
+│   │       ├── theme_config.json     #   主题
+│   │       └── workspace_config.json #   工作空间列表与当前选择
+│   │
+│   └── web/                     # 🎨 前端 UI
+│       ├── server.py            #   FastAPI 后端(75+ 路由)
+│       ├── ui_session.py        #   UI 会话状态管理
+│       ├── index.html           #   主页面(侧边栏/聊天/编辑/Agent面板/操作栏)
+│       ├── app.js / app.css     #   入口
+│       ├── js/                  #   30 个前端模块(见开发手册)
+│       ├── css/                 #   18 个样式模块 + variables.css 主题变量
+│       ├── packages/            #   Monaco Editor 本地 npm 包
+│       └── image/               #   SVG 图标资源
+│
+├── llm_server/                  # 🧠 LLM 推理服务(独立部署,详见该目录 README)
+├── git_image/                   # 📸 README 展示图片
+├── package.json                 # Electron + electron-builder 配置
+├── requirements.txt             # Python 依赖
+└── README.md                    # 本文件
+
+
+ +
+ + +
+
+ 🚀 +

快速开始

+

minor Agent 提供两种运行方式:桌面应用(推荐) 与 Web 开发模式。两者共用同一套 src/ 源码。

+
+ +

方式一:桌面应用(推荐体验)

+ +

1. 安装依赖

+
+
PowerShell
+
cd C:\Users\86166\Desktop\Agent_Learning_minor\Agent
+
+# 设置国内镜像(可选,加速 Electron 下载)
+$env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
+
+npm install
+
+ +

2. 启动应用

+
+
PowerShell
+
npm start
+
+ +

+ 首次运行会弹出配置向导(3 步),写入 src/agent/config/env_config.json 后自动启动: +

+ +
+ + + + + + + +
步骤配置内容说明
Step 1 Python 环境选择 / 自动检测系统 Python主进程会调用系统 Python 启动 uvicorn;可选自动 pip install 依赖
Step 2 LLM 设置base_url / api_key / model必填;可填任何 OpenAI 兼容端点
Step 3 高级参数TTS / ASR / RAG / 搜索 / 压缩 / 循环阈值可选;不填则对应能力禁用
+
+ +
💡 之后启动直接进入主界面,不再弹向导。如需重配,删除 src/agent/config/env_config.json 即可重触首次流程。
+ +

3. 打包为安装程序

+
+
PowerShell
+
# 设置镜像 + 自定义缓存目录后构建
+$env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
+$env:ELECTRON_BUILDER_CACHE="$PWD\electron-builder-cache"
+npm run build
+
+ +

+ 输出 dist/minor Agent Setup 1.0.0.exe,双击安装即可。安装包使用系统 Python + 运行时安装依赖方案,体积远小于打包完整 venv 的方案。 +

+ +
+ 📦 打包原理(点击展开) +
+
    +
  • package.json 的 build.files 仅打包 electron/、src/、requirements.txt,排除 env_config.json(每个用户独立生成)
  • +
  • asar: false:源码不加密,便于审计与二次开发
  • +
  • NSIS 安装器:允许改安装目录、创建桌面/开始菜单快捷方式、卸载时清理 AppData
  • +
  • 首次启动由 setup.html 向导引导:检测 Python → 配置 LLM → 写 env_config.json → 启动 FastAPI → 加载 UI
  • +
  • 主进程 findSystemPython() 依次检查:用户配置路径 → 常见 Python 安装位置 → PATH 中的 python
  • +
+
+
+ +
+ +

方式二:Web 开发模式(调试用)

+

适合前端开发与快速迭代,无需 Electron。

+ +
+
PowerShell
+
cd C:\Users\86166\Desktop\Agent_Learning_minor\Agent
+
+# 1. 创建并激活虚拟环境
+python -m venv myenv
+.\myenv\Scripts\Activate.ps1          # Windows
+# source myenv/bin/activate           # macOS / Linux
+
+# 2. 安装依赖
+pip install --upgrade pip
+pip install -r requirements.txt
+
+# 3. 配置环境(首次)
+#    复制模板并填写 LLM 凭证
+Copy-Item src\agent\config\env_config_dist.json src\agent\config\env_config.json
+#    编辑 env_config.json,至少填写 models[].base_url / api_key / model
+
+# 4. 启动 Web 服务
+$env:PYTHONPATH="src;$env:PYTHONPATH"
+uvicorn web.server:app --host 127.0.0.1 --port 8765 --reload
+
+# 5. 浏览器打开
+#    http://127.0.0.1:8765
+
+ +
+ 🌐 (可选)公网暴露 +
+
+
PowerShell
+
# 通过 Cloudflare Tunnel 临时暴露
+cloudflared tunnel --url http://localhost:8765
+
+
+
+
+ +
+ + +
+
+ ⚙️ +

配置说明

+

所有运行时配置集中在 src/agent/config/env_config.json,首次向导自动生成。

+
+ +

关键字段:

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段默认值说明
models[]SETUP_REQUIREDLLM 列表:{id, name, model, api_key, base_url, timeout, max_retries}
gui_model_id""GUI 视觉定位专用模型(留空则回退主模型)
WORKING_DIR项目根目录工作根目录(桌面端自动设为安装目录)
WORKSPACE_DIR""工作空间目录(相对 WORKING_DIR),留空则使用 WORKING_DIR
USER_PYTHON_PATH""指定 Python 解释器路径
LLM_CONTEXT_WINDOW262144上下文窗口大小(token)
COMPRESS_RATE0.6上下文压缩阈值比例(0~1),token 用量超过 窗口×该值 时触发压缩
THINKING_LEVEL"low"深度思考档位:low / high / xhigh / max / ultra(xhigh 及以上启用深度思考)
IMG_SIZE768截图短边尺寸
GROUNDING_WIDTH/HEIGHT1000视觉定位输入分辨率
LOOP_DETECT_REPEATED_TOOL_WARN3连续同工具同参多少次警告
LOOP_DETECT_REPEATED_TOOL_END5连续同工具同参多少次强制终止
RAG_CHUNK_SIZE / OVERLAP500 / 50RAG 分块大小与重叠
IMAGE_GEN_URLhttp://localhost:8904图像生成服务地址
SEND_FILE_SIZE_LIMIT30发送文件大小上限(MB)
WEB_SEARCH_API_KEY""联网搜索 API Key
WEB_SEARCH_ENGINE"tavily"搜索引擎选择(tavily / firecrawl / volcengine)
STORAGE_BACKEND"json"存储后端(json / mysql)
MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE""MySQL 连接参数(STORAGE_BACKEND=mysql 时生效)
TOOL_TIMEOUT300工具执行超时时间(秒)
CRON_TIME_PERIOD_MINUTES30定时任务时段长度(分钟),同一时段全局并发=1
ASR_BASE_URL""语音识别服务地址
STREAMING_TTS_URL""流式 TTS 服务地址
RAG_BASE_URL""RAG 嵌入/重排序服务地址
EMAIL_ADDRESS / EMAIL_AUTH_CODE""邮箱凭证(IMAP/SMTP)
+
+ +
⚙️ 以上参数均可在主界面「设置」面板热修改,或通过 /api/config/* 接口程序化调整。
+ +

设置面板包含五个标签页,覆盖 Agent 运行的全部可调参数:

+ +
+
+ Agent 配置 + ./git_image/5.png +
+
+ 工具配置 + ./git_image/1.png +
+
+

Agent 配置 — 管理多个 AI 角色,各自绑定独立模型与工具集 | 工具配置 — 启用/禁用工具、调整参数及执行策略

+ +
+
+ 环境变量 + ./git_image/2.png +
+
+ GUI 设置 + ./git_image/3.png +
+
+

环境变量 — LLM 密钥、路径、压缩阈值等全局参数 | GUI 设置 — 选择 GUI 自动化操作的目标显示器

+ +
+
+ 主题配置 + ./git_image/4.png +
+
+

主题配置 — 12 套预设主题 + 自定义背景 / 模糊度 / 暗角 / 字体

+
+ +
+ + +
+
+ 🧠 +

实现细节

+
+ + +
+ 1 +

四层提示词架构

+
+
+ + + + + + + + +
层级位置注入时机作用
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变化部分(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 +

上下文分级压缩(图内)

+
+

+ + 多级压缩机制 + ./git_image/compress.png + +

+
+
每次 LLM 调用后读取 response.usage_metadata.total_tokens
+   ├─ 未超阈值        → 不处理,继续下一轮
+   ├─ 主 Agent 超阈值 → 标记压缩,由 compress 节点统一执行:
+   │    将注入的历史工具调用与返回压缩为累积摘要(RemoveMessage 移除已覆盖消息)
+   └─ 子 Agent 超阈值 → 注入整理提示词,整理进度后直接返回主 Agent(子 Agent 不压缩)
+
+
+
    +
  • 触发:call_model 内每步检查 total_tokens,超过 LLM_CONTEXT_WINDOW × COMPRESS_RATE(默认 0.6)即标记压缩
  • +
  • 执行:压缩发生在图内 compress 节点(process_tool_artifact 之后),只压缩注入的历史工具上下文,系统提示词与对话历史保留,摘要按压缩游标累积
  • +
  • 子 Agent:不做压缩,超限时整理任务进度直接返回主 Agent,避免上下文继续膨胀
  • +
+
+ + +
+ 3 +

循环检测两级响应

+
+
+ + + + + + +
级别触发条件响应
⚠️ 警告连续 3 次同工具同参注入反思提示引导模型自我纠偏
🛑 终止连续 5 次同工具同参should_force_end() 返回 END,向前端输出错误报告
+
+ + +
+ 4 +

阻塞式人机交互(交互类 / 展示类工具的统一模式)

+
+

+ 人机交互不再「暂停图 + 存快照 + 续跑」,而是阻塞式普通工具:工具在图上线程中等待前端浮窗应答,答案(同意 / 拒绝 / 跳过 / 补充信息)只是普通工具返回值,图自然继续——拒绝不会结束整轮,Agent 自行调整方案。 +

+
+
LLM 发出 human_interaction / 需确认工具调用
+   → tools 节点执行工具
+   → human_request.ask_human() 阻塞等待(注册到请求表,随 live snapshot 推送前端)
+   → 前端浮窗出现,工具调用行显示"运行中"
+   → 用户点击 → POST /api/human-action → 写入答案并唤醒
+   → 工具返回答案文本 → ToolMessage → 图继续(无快照重放、无续跑状态机)
+
+
+ + + + + + + + + + + +
设计点传统做法(本项目旧版)阻塞式(当前)
图执行请求线程内同步 invoke后台线程(core/turn_runner.py),断连/关页不影响执行,双发请求挂同一轮
等待答案图 END + 消息快照存 PENDING_TOOL_APPROVALS工具内 threading.Event 阻塞(core/human_request.py)
决策处理continue_after_human_action 重放快照续跑答案即返回值,无续跑路径
拒绝结束整轮(等同手动暂停)仅返回值,Agent 调整后继续
工具列表确认时清空重建、续跑时重放自然展示:等待中 running → 应答后 done
子 Agent 交互异常冒泡(SubAgentPendingError)+ 恢复状态机子图线程内直接阻塞,完成自然回主图
无人值守(cron)悬死靠超时兜底ask_human 检测 headless 立即返回默认答案
+
+

两类工具的扩展模式(新增能力零框架改动):

+
+
    +
  • 交互类工具(如 human_interaction):工具内调一次 ask_human(meta) 阻塞,自己格式化返回值
  • +
  • 展示类工具(如 todo_list):纯文本返回、不维护全局 store,前端从通用 live 记录(工具调用的 args/结果)按工具名写一个小 adapter 渲染浮窗(如 js/todo.js 的 updateTodoOverlayFromRecords)
  • +
+
+ + +
+ 5 +

GUI 动作序列化(性能关键)

+
+
+ + + + + + +
模式N 个动作的 LLM 调用次数
单步执行(多数项目)2N + 1
序列合并(本项目)2(1 次批量定位 + 1 次决策)
+
+

通过系统提示词前缀中的固定反思引导(REFLECTION_PROMPT)+ MAIN_SYSTEM_PROMPT 双重强调「同页操作必须合并为一次 gui 调用」实现。

+ + +
+ 6 +

双模式源码共用

+
+

src/web/ 同一套代码两种部署:

+
+ + + + + + + + +
Web 模式桌面模式
启动uvicorn web.server:appElectron spawn uvicorn
文件操作/api/fs/* HTTPelectron-api.js → IPC 直读磁盘
对话框浏览器原生Electron dialog 模块
加载协议http://file:// + IPC
+
+

前端通过 window.electronAPI?.isElectron 自动适配。

+ + +
+ 7 +

存储双后端(JSON 可视化 / MySQL,统一接口隔离)

+
+

+ 会话历史与定时任务存储统一在 agent/core/storage.py:接口(Protocol)驱动、双后端实现、消费方零感知后端选择(不判断 STORAGE_BACKEND)。 +

+

接口与实现

+
+
    +
  • ConversationRecordRepository(对话消息 / 工具调用 / 轮次元数据 / 会话生命周期,16 方法)与 TaskConfigRepository(cron 任务配置,6 方法)两个 Protocol
  • +
  • 每后端一个实现类:JsonRecordRepository / MysqlRecordRepository(含会话与 cron 双作用域)、JsonTaskConfigRepository / MysqlTaskConfigRepository
  • +
  • 新增存储后端只需实现上述接口并在工厂注册一行;消费方新增功能/字段无需改动存储层(消息原样 JSON 存储、工具记录 meta 全量透传)
  • +
+
+

作用域(scope)

+
+
    +
  • session(默认):主进程普通会话,JSON 落盘 history/sessions/{session_id}/,MySQL 用 sessions / session_messages / session_tool_calls 三表
  • +
  • cron:定时任务,JSON 落盘 history/cron/{task_id}/,MySQL 用 cron_messages / cron_tool_calls 两表;cron 子进程切换存储作用域后直写记录,主进程经 cron 作用域仓库读取
  • +
+
+
+ + + + + + +
后端存储位置说明
json(默认)history/sessions/{session_id}/消息与工具调用以人类可读 JSON 文件落盘(turn_{turn_id}.json / tool_{turn_id}.json / session_meta.json / _session_tokens.json),可直接打开查看/修改,便于审计与调试
mysqlsession 三表 / cron 两表消息与工具调用记录原样 JSON 入库、读取只走数据库;二进制产物(图片/附件等)两个后端均落盘
+
+
+
    +
  • 由 STORAGE_BACKEND + MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE 配置,agent/core/db.py 提供统一连接与幂等建表
  • +
  • 不双写:选哪种后端,读写就只走该后端;写入幂等(同一 (session_id, turn_id) 先 DELETE 再 INSERT);DB 异常仅打印日志,不阻断主流程
  • +
  • 分支(/api/sessions/branch)与回滚等会话功能均基于存储接口实现,两个后端一致可用
  • +
+
+

双后端行为差异(设计内)

+
+ + + + + + + +
场景jsonmysql
轮次中实时工具记录每次工具结果后增量写盘 tool_*.json,进程崩溃后重启可回退恢复最近记录不增量入库,由轮次结束 save_tool_calls 统一落库(无额外磁盘 IO,但崩溃时该轮记录丢失)
服务重启后运行中轮次get_live_tool_calls 回退读取增量文件,可继续展示内存记录丢失、数据库无该轮记录,实时面板为空
cron 历史读取直接读 history/cron/ 下文件子进程直写 cron 表,历史可正常读取
+
+
+ +
+ + +
+
+ 🔌 +

LLM 推理服务

+

项目自带一套 6 服务推理后端(llm_server/),可独立部署在 GPU 服务器上,为 Agent 提供 LLM / ASR / TTS / RAG / 图像生成能力。

+
+
+ + + + + + + + + +
服务端口模型脚本
LLM8900Qwen3.6-35B-A3B-FP8start_llm.sh
ASR8901Qwen3-ASR-1.7Bstart_asr.sh
TTS8902VoxCPMstart_streaming_tts.sh
RAG8903Qwen3-Embedding-0.6B + Reranker-4Bstart_rag_server.sh
Image Gen8904Z-Image-Turbostart_image_gen.sh
+
+
📄 完整部署、模型下载、测试方法见 llm_server/README.md。
+
💡 也可不自建推理服务,直接在配置向导填入任何 OpenAI 兼容 API(如云端 Qwen / DeepSeek / OpenAI)。
+
+ +
+ + +
+
+ 🛠️ +

开发手册

+
+ +

前端模块职责(src/web/js/)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模块职责
app.js应用入口,初始化与事件绑定
api.js封装所有后端 API 请求
state.js全局状态管理
send.js消息发送逻辑
chat-render.js聊天消息渲染
cron.js定时任务管理面板
sessions.js会话列表管理
toolcalls.js工具调用实时面板
todo.jsTodoList 面板
edit-mode.js编辑模式(Monaco)
doc-tree.js / doc-mod-panel.js文档树与修改面板
action-bar.js底部操作栏
skills.js技能管理
settings.js设置面板
themes.js主题切换
layout.js布局与分隔条拖拽
electron-api.js★ Electron IPC 适配层
file-preview.js文件预览(图片/HTML/PDF)
canvas-editor.js画板编辑器(画笔/形状/文字/橡皮擦,支持缩放与图片修改)
audio.js语音录制与播放
i18n.js国际化
token-ring.js上下文 Token 用量环形指示器(含压缩阈值刻度与三类占比)
access-mode.js工作空间访问模式切换(限制访问 / 权限审查 / 完全访问)
think-level.js思考模式档位选择(low / high / xhigh / max / ultra)
pending.js / pending-overlay.js人工请求浮窗与决策提交(数据源为 live snapshot 的 human_requests)
rollback.js消息回滚
workspace.js工作空间切换
dialog.js对话框
utils.js通用工具
+
+ +

CSS 架构(src/web/css/)

+

variables.css 定义主题变量,其余按模块拆分:base / layout / chat / composer / cron / dialog / settings / skills / todo / toolcalls / edit-mode / canvas-editor / audio / animations / pending / pending-overlay / toast / responsive。

+ +

添加自定义工具

+
+
    +
  • 在 src/agent/tools/ 新建 my_tool.py,继承 LangChain BaseTool,实现 _run,填写 name / description / args_schema
  • +
  • 在 tool_config.json 注册并配置启用状态与参数(权限设为 confirm 时,工具执行前会自动弹出确认浮窗,同意后执行、拒绝/跳过作为返回值继续)
  • +
  • 交互类工具(需要向用户提问)在 _run 中调用 core/human_request.ask_human(meta) 阻塞等待答案;展示类工具(如进度面板)返回纯文本即可,前端从 live 工具记录按工具名写 adapter 渲染浮窗
  • +
  • 重启后端,工具自动绑定到 LLM
  • +
+
+ +

添加自定义技能

+
+
    +
  • 在 src/agent/skills/ 新建 my_skill/ 目录
  • +
  • 编写 skill.json(名称、描述、标签)与 skill.md(操作流程,支持 {SKILL_DIR} 占位)
  • +
  • 可放 attachments/ 附件(模板、脚本)
  • +
  • skill_router 会自动匹配并路由
  • +
+
+ +

同步 Web ↔ 桌面端

+

桌面端与 Web 端共用 src/web/。UI 改动无需同步;仅当分叉修改时手动合并。常用同步命令:

+
+
PowerShell
+
# 桌面端 → Web 端(按需)
+Copy-Item "src\web\index.html"      "src\web\index.html"      -Force
+Copy-Item "src\web\css\*.css"       "src\web\css\"            -Force
+Copy-Item "src\web\js\themes.js"    "src\web\js\themes.js"    -Force
+
+ +

调试技巧

+
+
    +
  • 后端日志:桌面端 npm start 会在终端打印 [FastAPI] 日志
  • +
  • 端口占用:main.js 的 killPortProcess(8765) 启动前自动清理
  • +
  • 重置配置:删除 src/agent/config/env_config.json 重触首次向导
  • +
  • 工具调用轨迹:history/sessions/{session_id}/tool_{turn_id}.json
  • +
+
+
+ +
+ + +
+
+ ⚠️ +

注意事项

+
+
+ + + + + + + + + + + + +
事项说明
🖥️ 平台桌面自动化工具(gui / software_control)依赖 Win32 / pywin32 / pyautogui,目前仅支持 Windows;Web 模式下这些工具不可用
🐍 Python 版本建议 Python 3.11 / 3.12 / 3.13;首次向导会自动检测
🔐 未签名安装包.exe 可能被 Windows Defender 拦截,点「更多信息 → 仍要运行」
🌐 首次启动联网加载 Google Fonts 需联网;离线环境字体选择器有 HTML 硬编码兜底
🔑 API Key 安全env_config.json 含明文凭证,勿提交到公开仓库(已在 .gitignore 排除 env_config.json,仅保留 env_config_dist.json 模板)
🎨 图标当前为 SVG,NSIS 打包后若显示默认图标,需准备 .ico 文件替换 src/web/image/icon.ico
💾 历史存储默认会话历史与工具轨迹以 JSON 落盘在 history/(可切换 MySQL 后端),长期使用注意清理
⚡ GUI 性能同页多操作务必合并为一次 gui 调用,否则延迟显著(见「实现细节」)
+
+
+ +
+ + +
+
+ 👥 +

Contributors

+

新增贡献者请按此格式在下方追加一行:- [用户名](GitHub 个人链接):贡献内容

+
+
+
m
+
+

minor

+

项目话事人

+

主导整体架构设计与核心实现(LangGraph 编排、20 项工具体系、多 Agent 模式、上下文分级压缩、循环检测、Electron 桌面端等),完成大部分的开发工作。

+
+
+
+ +
+ + + + + + + diff --git a/README.md b/README.md index b829c88..fc8d6bc 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ | **GUI 操作粒度** | 一步一调用,N 个动作 = 2N+1 次 LLM 调用 | **动作序列合并**:同页多操作打包为一次 `gui` 调用,N 个动作仅 2 次 LLM 调用 | | **视觉反馈** | 仅返回坐标文本 | **截图作为视觉闭环**注入上下文,多模态 LLM 看到真实屏幕状态 | | **坐标定位** | 逐元素请求 | **批量定位** `_batch_ground_elements`:N 个元素 1 次子模型调用 | -| **循环失控** | 无防护,token 烧穿 | **两级循环检测**:连续 3 次同工具警告反思 → 连续 5 次强制终止 | +| **循环失控** | 无防护,token 烧穿 | **循环检测**:连续同工具同参(默认 3 次)注入反思警告,引导模型自我纠偏 | | **上下文膨胀** | 简单截断 | **图内分级压缩**:token 超 `窗口×COMPRESS_RATE` 触发,压缩历史工具调用为累积摘要,每步 LLM 调用后检查 | | **部署形态** | 仅 Web / 仅 API | **同一套源码双模式**:`uvicorn` Web 服务 ↔ Electron 桌面应用 | | **文件操作** | HTTP 上传下载 | 桌面端走 **IPC 直读磁盘**,零延迟;Web 端走 `/api/fs/*` | @@ -211,9 +211,9 @@ Agent/ │ │ │ ├── graph.py # LangGraph 构建(单 Agent + 多 Agent) │ │ │ ├── nodes.py # ReAct 节点:call_model / process_tool_artifact / 压缩 │ │ │ ├── runtime.py # 图执行器:消息拼装、历史加载、轨迹落盘 -│ │ │ ├── routing.py # should_continue 路由 + 强制终止 +│ │ │ ├── routing.py # should_continue 路由 │ │ │ ├── state.py # Graph 状态定义(messages / agent_mode) -│ │ │ ├── loop_detector.py # 循环检测:重复工具调用,两级响应 +│ │ │ ├── loop_detector.py # 循环检测:重复工具调用,反思警告 │ │ │ ├── human_request.py # ★ 人工请求注册表:交互类工具的通用阻塞通道(ask_human) │ │ │ ├── turn_runner.py # ★ 后台线程轮次执行器(解耦 HTTP 与图执行生命周期) │ │ │ ├── llm.py # ChatQwen 多模态封装(图像/附件/非标准 tool_call) @@ -384,8 +384,7 @@ cloudflared tunnel --url http://localhost:8765 | `THINKING_LEVEL` | `"low"` | 深度思考档位:low / high / xhigh / max / ultra(xhigh 及以上启用深度思考) | | `IMG_SIZE` | `768` | 截图短边尺寸 | | `GROUNDING_WIDTH/HEIGHT` | `1000` | 视觉定位输入分辨率 | -| `LOOP_DETECT_REPEATED_TOOL_WARN` | `3` | 连续同工具同参多少次警告 | -| `LOOP_DETECT_REPEATED_TOOL_END` | `5` | 连续同工具同参多少次强制终止 | +| `LOOP_DETECT_REPEATED_TOOL_WARN` | `3` | 连续同工具同参多少次注入反思警告 | | `RAG_CHUNK_SIZE / OVERLAP` | `500 / 50` | RAG 分块大小与重叠 | | `IMAGE_GEN_URL` | `http://localhost:8904` | 图像生成服务地址 | | `SEND_FILE_SIZE_LIMIT` | `30` | 发送文件大小上限(MB) | @@ -469,12 +468,13 @@ cloudflared tunnel --url http://localhost:8765 - **执行**:压缩发生在图内 `compress` 节点(`process_tool_artifact` 之后),只压缩注入的历史工具上下文,系统提示词与对话历史保留,摘要按压缩游标累积 - **子 Agent**:不做压缩,超限时整理任务进度直接返回主 Agent,避免上下文继续膨胀 -### 3. 循环检测两级响应 +### 3. 循环检测(反思警告) -| 级别 | 触发条件 | 响应 | -|------|----------|------| -| ⚠️ 警告 | 连续 3 次同工具同参 | 注入反思提示引导模型自我纠偏 | -| 🛑 终止 | 连续 5 次同工具同参 | `should_force_end()` 返回 END,向前端输出错误报告 | +在 ReAct 循环的 agent 节点调用 LLM 前,检测连续相同工具调用(仅保留警告响应,强制终止已移除): + +| 触发条件 | 响应 | +|----------|------| +| 连续同工具同参 ≥ `LOOP_DETECT_REPEATED_TOOL_WARN`(默认 3) | 注入反思提示引导模型自我纠偏:更换工具或策略、检查之前的工具返回结果、无法推进时向用户说明困难并请求帮助 | ### 4. 阻塞式人机交互(交互类 / 展示类工具的统一模式) diff --git a/analysis_report.md b/analysis_report.md new file mode 100644 index 0000000..d599d97 --- /dev/null +++ b/analysis_report.md @@ -0,0 +1,370 @@ +# minor Agent 代码深度分析报告 + +> 分析范围:`Agent/llm_server/`、`Agent/src/agent/cron/`、`Agent/src/agent/tts/streaming_client.py`、`Agent/src/agent/utils/`、`Agent/src/agent/memory/system_prompt.py`、`Agent/src/agent/agents/agent_runtime.py`、`Agent/src/agent/history/`、`Agent/src/web/ui_session.py`。全部路径精确到文件与关键函数/类/变量。 + +--- + +## 一、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\llm_server\`(本地推理后端,6 个模型/5 个服务端口) + +### 1. README.md —— 部署与总览 + +- 定位:为 minor Agent 提供**本地化推理后端**,全部跑在 GPU 服务器上,以 OpenAI 兼容 / 自定义 HTTP API 暴露;可整体部署或按需启用。 +- 服务总览表(README 自称 6 个独立服务,实际为 **5 个服务、6 个模型** —— RAG 一个服务内装 Embedding+Reranker 两个模型): + +| 服务 | 端口 | 模型 | 启动脚本 | Python 服务端 | 关键端点 | +|---|---|---|---|---|---| +| LLM | 8900 | Qwen3.6-35B-A3B-FP8 | `start_llm.sh` | vLLM 内置 | `/v1/chat/completions` | +| ASR | 8901 | Qwen3-ASR-1.7B | `start_asr.sh` | vLLM 内置 | `/v1/chat/completions` | +| TTS | 8902 | VoxCPM 1.5 / 2 | `start_streaming_tts.sh` | `streaming_tts_server.py` | `/stream_tts` | +| RAG | 8903 | Qwen3-Embedding-0.6B + Qwen3-Reranker-4B | `start_rag_server.sh` | `rag_server.py` | `/embed` `/rerank` | +| Image Gen | 8904 | Z-Image-Turbo | `start_image_gen.sh` | `image_gen_server.py` | `/generate` | + +- 所有脚本/服务端通过 `os.path.dirname(__file__)` 与 `${SCRIPT_DIR}` 动态定位模型路径,**目录整体迁移无需改代码**。 +- 部署流程要点: + 1. `scp -r Agent/llm_server/*` 上传 → 服务器上 `chmod +x *.sh`。 + 2. 环境准备:`export HF_ENDPOINT=https://hf-mirror.com`、`HF_HUB_ENABLE_XET=0`(国内必需);`python3 -m venv myenv`;一次性安装 `vllm modelscope transformers fastapi uvicorn torch numpy torchaudio soundfile bitsandbytes huggingface_hub voxcpm diffusers`;`apt` 安装 ffmpeg 全家桶。 + 3. 模型统一存 `~/llm_server/models/`,用 `modelscope download --model ... --local_dir ./models/...` 下载(注意 modelscope 目录名中 `/` 转 `___`、`.` 转 `_`,如 `Qwen3___6-35B-A3B-FP8`、`Qwen3-Embedding-0___6B`、`VoxCPM1__5`)。 + 4. 每个 `start_*.sh` 支持环境变量覆盖:`HOST_PORT`(端口)、`LOCAL_MODEL_DIR`/`MODEL_PATH`/`VOXCPM_MODEL_PATH`(模型目录)、`CUDA_VISIBLE_DEVICES`/`SPECIFIC_GPU`(GPU 指定)。 +- 本地调试:`ssh -fN -L 8900:0.0.0.0:8900 ...` 一次性映射 5 个端口;`curl http://localhost:$p/health` 验证。 +- 停止服务:`for p in 8900..8904; do kill $(lsof -t -i:$p); done`。 +- 接入 Agent(配置向导或 `env_config.json`):`models[].base_url=http://localhost:8900/v1`、`ASR_BASE_URL=http://localhost:8901/v1`、`STREAMING_TTS_URL=http://localhost:8902`、`RAG_BASE_URL=http://localhost:8903`、图像生成由 `image_gen` 工具读取 `http://localhost:8904`。也支持任意 OpenAI 兼容云端 API。 + +### 2. start_llm.sh —— LLM 启动参数(vLLM 推理引擎) + +固定 `CUDA_VISIBLE_DEVICES=3`,`VLLM_USE_MODELSCOPE=true`、`MODELSCOPE_DOWNLOAD_MODE=local`、`HF_HUB_OFFLINE=1` 等离线环境变量。vLLM 关键参数: + +- `vllm serve `:`--tensor-parallel-size 1`、`--max-model-len 262144`(26 万超长上下文)、`--reasoning-parser qwen3`、`--enable-auto-tool-choice` + `--tool-call-parser qwen3_coder`(自动工具调用)、`--speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":2}'`(MTP 投机解码,2 个投机 token)、`--enable-prefix-caching`、`--max_num_batched_tokens 4096`、`--max_num_seqs 8`、`--kv-cache-dtype fp8_e4m3`(KV 缓存 FP8)、`--chat-template chat_template.jinja`(自定义模板)、`--safetensors-load-strategy eager`、`--served-model-name Qwen/Qwen3.6-35B-A3B-FP8`。 + +### 3. chat_template.jinja —— ChatML 风格自定义模板 + +- `enable_thinking` 默认 `false`(`|default(false)`)。 +- 有工具时:system 段 = 首条 system 消息(content 为 list 时把 image/audio/video part 替换为 `<|vision_start|><|image_pad|><|vision_end|>`、`<|audio_start|><|audio_pad|><|audio_end|>`、`<|vision_start|><|video_pad|><|vision_end|>` 占位符)+ 固定的 `# Tools\n...` 说明 + `...` 内逐条 `tool | tojson` 序列化工具签名;工具调用格式约束为 `\n{"name": ..., "arguments": ...}\n`。 +- 无工具时:首条 system 按 content 类型输出多模态占位符或文本。 +- **multi_step_tool 检测**(第 47-54 行):从后往前找第一个"非 `` 包裹"的 user 消息,记为 `last_query_index`,用于判断后续 assistant 是否需要显式输出 `` 块。 +- assistant 消息:优先用 `message.reasoning_content`(字符串);否则若 content 含 ``,用 `split('')`/`split('')` 拆出 reasoning 与正文。`loop.index0 > last_query_index` 且(末条或含 reasoning)时,输出 `...` 包裹的思考段。`tool_calls` 逐个渲染为 `...`。 +- tool(函数返回)消息:连续的 tool 消息被**合并进同一个 `user` 角色块**,每条包在 `\n...\n` 中,首条前加 `<|im_start|>user`、末条后加 `<|im_end|>`。 +- `add_generation_prompt` 时输出 `<|im_start|>assistant\n`;若显式传 `enable_thinking=false` 则先输出空 `\n\n\n\n`(禁用思考)。 + +### 4. test_llm.py —— 测试方式 + +- `python test_llm.py [--url http://127.0.0.1:8900] [--model Qwen/Qwen3.6-35B-A3B-FP8] [--stream]`。 +- 非流式 `test_non_stream`:POST `/v1/chat/completions`,打印完整响应 + 提取 model/finish_reason/content/tool_calls/usage(prompt/completion/total tokens)。 +- 流式 `test_stream`:`stream=True`,逐行解析 `data: ` 前缀的 SSE,`[DONE]` 结束,累加 `delta.content` 输出。 + +### 5. start_asr.sh + test_asr.py —— ASR 服务 + +- **引擎 = vLLM**,模型 **Qwen3-ASR-1.7B**(README 标注;test_asr.py 文档字符串仍引用旧模型 Qwen3-Omni-30B-A3B-Captioner,说明测试脚本是从旧版继承)。启动参数:`--quantization fp8`、`--dtype auto`、`--kv-cache-dtype fp8`、`--max-model-len 16384`、`--allowed-local-media-path /`(允许读取本地媒体路径)、`--tensor-parallel-size 1`、`--gpu-memory-utilization 0.15`(只占 15% 显存)、`--max-num-batched-tokens 4096`;`CUDA_VISIBLE_DEVICES=3`。 +- **调用方式**:`test_asr.py --audio [--url http://127.0.0.1:8901/v1/chat/completions] [--base64]`。核心函数 `caption_audio(audio_path, server_url, use_base64)`: + - 音频转 URL:本地直传用 `file://` + 绝对路径(`/` 分隔);远程场景 `--base64` 用 `data:;base64,...`(`file_to_url`)。 + - **不带任何文本提示**,payload 仅一条 user 消息、content 为 `[{"type": "audio_url", "audio_url": {"url": audio_url}}]`(音频描述式 ASR,模型输出整段音频内容的描述/转写)。 + - 用 `soundfile`(回退 librosa)检查时长,>30s 打警告(建议裁剪到 ≤30s)。`timeout=120`,取 `result["choices"][0]["message"]["content"]`。 + +### 6. streaming_tts_server.py —— 流式 TTS 服务器(FastAPI + VoxCPM) + +- **引擎 = OpenBMB VoxCPM**(`from voxcpm import VoxCPM`,README 注释"Whispera 同款")。`DEFAULT_MODEL_PATH=models/VoxCPM1__5`,`DEFAULT_PORT=8902`。 +- 音色克隆:`DEFAULT_PROMPT_WAV=reference_audio.wav` + `DEFAULT_PROMPT_TEXT="日子缓缓前行,不必急于追赶…"`,每次合成都克隆该音色;音频质量参数 `DEFAULT_CFG_VALUE=1.5`(音色贴合度)、`DEFAULT_INFERENCE_STEPS=15`(推理步数)。 +- 单例与并发控制:`_get_model(model_path)` 线程安全惰性加载(`_singleton_lock`,`enable_denoiser=False, optimize=True`,采样率取 `model.tts_model.sample_rate`,默认 24000Hz);`_model_lock` 全局模型锁(VoxCPM 只允许单线程);`cancel_current_and_set(new_request_id)` 在**新请求到达时自动取消旧请求**(设置 cancel Event,并强制释放被旧请求持有的模型锁);`set_cancel`/`is_cancelled` 请求级取消标志。 +- 端点: + - `POST /stream_tts`:body `{"text": ..., "request_id": ...}`(缺省 request_id 自动生成 `tts-{ms}`)。返回 `StreamingResponse(media_type="text/event-stream")`,头带 `Cache-Control: no-cache`、`X-Accel-Buffering: no`。空文本直接返回 `audio.error`。 + - `POST /cancel_tts`:`{"request_id": ...}` 取消指定合成。 + - `GET /health`。 +- **流式音频块格式(SSE,`_build_sse` 为 `data: {json}\n\n`)**: + - `audio.start`:`{type, request_id, sample_rate, audio_format: "pcm_f32le"}`。 + - `audio.chunk`:`{type, request_id, index, num_samples, sample_rate, data: base64(PCM float32 LE 字节)}` —— `np.asarray(chunk, float32).ravel()` → `tobytes()` → base64;生成循环中检查 `is_cancelled`,取消则发 `audio.cancelled` 并 return。 + - `audio.done`:`{type, request_id, total_samples, interrupted: false}`。 + - `audio.error`:`{type, request_id, message}`(模型加载失败、等锁超时 3 次等)。 + - 模型调用:`model.generate_streaming(text=text, prompt_wav_path=..., prompt_text=..., cfg_value=..., inference_timesteps=..., retry_badcase=False)`。 +- **关于"分句"**:本文件**没有显式分句逻辑**——整段 text 一次性交给 `generate_streaming`,由 VoxCPM 内部流式产出音频块(docstring 说"按句分块合成"是沿用旧设计的注释;`test_tts.py` 里读取的 `num_segments`/`total_segments` 字段服务器实际并不发送,客户端读到默认值)。真实分句若需要,应在客户端或未来版本做文本切分。 +- 启动:`uvicorn.run(app, host="0.0.0.0", port=args.port)`;lifespan 启动时预加载模型。 + +### 7. start_streaming_tts.sh + test_tts.py + +- 启动脚本:`VOXCPM_MODEL_PATH` 可覆盖(默认 `models/VoxCPM1__5`);依次尝试 `../myenv`、`../../myenv` 激活虚拟环境;`CUDA_VISIBLE_DEVICES` 默认 3;`exec python streaming_tts_server.py --port --model-path`。 +- 测试脚本:健康检查 → `POST /stream_tts`(`stream=True, timeout=300`)→ 逐行解析 `data: ` SSE → 按事件类型处理:`audio.start` 取 sample_rate;`audio.chunk` 累积 base64 解码的 PCM、进度打印(idx%5==0);`audio.done` 汇总;`audio.error` 退出;`audio.cancelled` break。结束后计算 RTF(耗时/音频时长,<1 即比实时快)。`_save_wav`:f32 → `(arr*32767).clip(-32768,32767).astype(int16)` 写 16-bit 单声道 WAV;`_play_pcm` 用 pyaudio 播放。实测产物 `test_output.wav` 已存在。 + +### 8. rag_server.py —— RAG 服务器(FastAPI + Transformers) + +- **框架**:FastAPI + `modelscope.AutoTokenizer/AutoModel/AutoModelForCausalLM`(无 LangChain/RAG 框架,纯手写检索基座)。 +- 模型路径:`EMBED_MODEL_PATH=models/Qwen/Qwen3-Embedding-0___6B`、`RERANK_MODEL_PATH=models/Qwen/Qwen3-Reranker-4B`。 +- 加载(`@app.on_event("startup")` `load_models`):Embedding 用 `AutoModel.from_pretrained(..., device_map="auto", dtype=torch.float16)`(fp16,未量化,注释里 8-bit 被禁用);Reranker 用 `AutoModelForCausalLM.from_pretrained(..., quantization_config=BitsAndBytesConfig(load_in_8bit=True), device_map="auto", dtype=torch.float16)`。 +- 工具函数:`last_token_pool`(取每条样本最后有效 token 的 hidden state,兼容左/右 padding)、`get_detailed_instruct`(query 拼 `Instruct: {task}\nQuery:{query}`,**document 不加指令**)、`format_rerank_instruction`(`: ...\n: ...\n: ...`,默认 instruct 文本 "Given a web search query, retrieve relevant passages that answer the query")。 +- Reranker 技巧:预编码固定 prefix/suffix(ChatML system+user 头、assistant 尾带空 think),`max_rerank_length=32768`;取 `logits[:, -1, :]` 中 `"yes"`/`"no"` 两个 token id 的 logit,`log_softmax([false, true])` 后取 true 的 `exp` 作为得分(生成式 rerank)。 +- **API 端点**: + - `POST /embed`:`{task_description, queries: [...], documents: [...]}` → `{query_embeddings, doc_embeddings}`(L2 归一化,queries 在前 documents 在后拼 batch)。 + - `POST /rerank`:`{task_description, query, documents}` → `{scores: [...]}`。 + - `GET /health`。 +- 启动:`start_rag_server.sh` 用 `uvicorn rag_server:app --host 0.0.0.0 --port 8903 --workers 1`(离线环境变量 + `CUDA_VISIBLE_DEVICES=3`)。测试:`test_rag.py` 依次测 health/embed/rerank,打印向量维度与得分。 + +### 9. image_gen_server.py —— 图像生成服务器(Diffusers) + +- **模型 = Tongyi-MAI/Z-Image-Turbo**(README:基于 Diffusers,9 步推理出 1024×1024;代码默认 `num_inference_steps=11`)。`LOCAL_MODEL_DIR` 环境变量优先本地路径,否则回退远程 model id;`SPECIFIC_GPU` 直接写 `CUDA_VISIBLE_DEVICES`。 +- 加载:`DiffusionPipeline.from_pretrained(MODEL_PATH, torch_dtype=torch.bfloat16(if cuda), low_cpu_mem_usage=False, trust_remote_code=True)`;本地路径时 `local_files_only=True`;惰性单例 `_get_pipe()` + `_pipe_lock`。 +- **`POST /generate`**:入参 `prompt`(必填)、`negative_prompt`、`height/width`(默认 1024,**须为 64 的整数倍**且 ∈[256,2048])、`num_inference_steps`(默认 11)、`guidance_scale`(默认 0.0)、`seed`(-1 随机,≥0 用 `torch.Generator(device).manual_seed`)、`return_format`(`base64` 默认 | `url`)。响应 `{success, image: base64-PNG 或原始 PNG 流, format:"png", width, height, elapsed_seconds}`。`GET /health`。启动时 lifespan 预加载模型。 + +--- + +## 二、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\cron\`(定时任务子系统) + +### models.py —— 数据模型(3 个 dataclass + 类型别名) + +- `TriggerType = Literal["cron","interval","once"]`;`TaskStatus = Literal["pending","running","completed","failed","expired","disabled"]`;`TriggerSource = Literal["schedule","manual"]`。 +- `Trigger`(触发配置):`type`(默认 "cron")、`cron`(标准 5 段 cron 表达式如 `"0 9 * * *"`)、`interval_seconds`、`run_at`(once 的目标本地时间 ISO)。有 `to_dict`/`from_dict`。 +- `ExecutionLog`(单次执行记录):`started_at`、`ended_at`、`status`、`error`、`trigger`(schedule/manual)、`turn_id`(关联工具调用记录)。 +- `CronTask`(任务完整定义):`task_id`(时间戳风格,与 session_id 同构)、`name`、`prompt`(触发时投递给 Agent 的提示词)、`trigger`、`enabled`、`timeout_seconds`(默认 300)、`created_at`、`next_run_at`、`last_run_at`、`last_status`、`last_error`、`executions: list[ExecutionLog]`。`summary()` 返回不含 executions 全量的精简 dict(附 `executions_count`)供前端列表。 +- `now_iso()`:`datetime.now().strftime("%Y-%m-%dT%H:%M:%S")`(无时区,与前端一致)。 + +### scheduler.py —— 调度器(CronScheduler 单例) + +- 常量:`_SCAN_INTERVAL = 5.0`(扫描周期秒)、`_CRON_TIME_PERIOD_MINUTES = 30.0`(时段长度,同一时间段只允许一个任务运行,全局并发=1;由 `env_config.json` 的 `CRON_TIME_PERIOD_MINUTES` 配置)。 +- `_load_time_period_minutes()` / `get_cron_time_period()` / `set_cron_time_period(minutes)`:读取/热更新时段长度并持久化到 env_config.json(`save_env_config`)。 +- `_parse_iso(dt_str)`:解析 `%Y-%m-%dT%H:%M:%S`,失败 None。 +- `_compute_next_run(trigger, now)`:cron → `croniter(trigger.cron, now).get_next(datetime)`(异常返回 "");interval → `now + timedelta(interval_seconds)`;once → `run_at` 若未来,否则 ""(不再调度)。`compute_next_fire` 包装为 datetime。 +- `detect_time_period_conflict(trigger, exclude_task_id=None)`:计算候选任务下次触发时刻,与所有 enabled 任务(排除自身)的下次触发时刻求**绝对差 < 时段长度 → conflict=True**,返回 `{"conflict", "conflict_with": [name...], "period_minutes", "next_fire"}`。供前端 check-conflict API 与 cron_manager 工具复用。 +- `CronScheduler`(`get_instance()`/模块级 `get_scheduler()` 单例): + - 状态:`_thread`、`_stop_event`、`_running_procs: dict[task_id, Popen]`、`_procs_lock`、`_app_start_time`、`_started`、`_period_start_ts`(当前占用时段起点,None 空闲)。 + - `start()`:启动 daemon 线程 `cron-scheduler` 跑 `_run_loop`;`shutdown()`:set stop、terminate 所有子进程、join 3s。 + - `_run_loop`:先 `_on_startup()`(启动扫描),再每 5s `_scan_once()`。 + - `_on_startup`/`_handle_startup_task`(错失/过期处理):once 目标已过且从未成功 → 追加 `expired` 执行记录并 `update_status(...,"expired", error="App was not running, missed execution time", next_run_at="")` + `enabled=False` 禁用;cron 若 `next_run_at < app 启动时间` → 记 expired 执行记录并推进 `_compute_next_run(trigger, now)`;interval 直接推进到 now+interval。 + - `_scan_once`:遍历 enabled 任务,`next_run_at <= now` 且不在 `_running_procs` → `_spawn(task, trigger_source="schedule", started_iso=now_iso_str)`。 + - `_get_python_and_cwd`:Python 解释器优先 `env_config.json` 的 `USER_PYTHON_PATH`(文件存在才用),否则 `sys.executable`;cwd = `agent/cron/storage.py` 的 `parents[2]`(即 src 目录)。 + - `_spawn(task, trigger_source, started_iso, prompt=None)`:**时段 gate** —— 锁内检查 `_running_procs` 非空或 `now - _period_start_ts < 时段秒数` 则跳过;否则占用新时段 `_period_start_ts = now_ts`。命令:`[py, "-m", "agent.cron.runner", "--task-id", ..., "--prompt", use_prompt, "--trigger", trigger_source, "--timeout", str(task.timeout_seconds)]`;env 注入 `PYTHONPATH=cwd`;Windows 用 `CREATE_NEW_PROCESS_GROUP`。spawn 失败 → `update_status(...,"failed", error="Failed to spawn subprocess: ...")` 并释放时段。成功后 `update_status(...,"running", last_run_at=started_iso)`,起 daemon 线程 `_monitor_proc`。 + - `_monitor_proc`:`proc.wait()` 回收;**子进程自身会 POST `/api/cron/internal/finished` 回传终态,本线程仅兜底**——若 `last_status` 已是 completed/failed/expired 则不覆盖;否则按 exit code 推断(0→completed,非 0→failed `Subprocess exited abnormally (exit code N)`),并补记 execution。 + - `spawn_now(task_id, prompt=None)`:手动立即运行/追问(trigger=manual,prompt 可覆盖原任务提示词);`is_running(task_id)`;`kill_task(task_id)`(terminate);`reload_task(task_id)`(配置变更后重算 next_run_at,供 cron_manager 工具调用)。 + +### runner.py —— 子进程执行器(`python -m agent.cron.runner`) + +- 回传地址常量:`_MAIN_HOST="127.0.0.1"`、`_MAIN_PORT=8765`、`_LIVE_URL=http://127.0.0.1:8765/api/cron/internal/live`、`_FINISHED_URL=http://127.0.0.1:8765/api/cron/internal/finished`。 +- `_redirect_storage_roots(task_id)`(**必须在构建 runtime 前调用**):把三个模块的全局根目录显式覆盖到 cron 目录,并把统一存储作用域切到 "cron": + - `agent.utils.agent_utils.SESSIONS_ROOT = CRON_ROOT`(turn 文件/session_dir/get_history) + - `agent.history.tool_call_recorder.TOOL_CALLING_ROOT = Path(CRON_ROOT)`(tool_*.json、token) + - `web.ui_session.SESSIONS_ROOT = CRON_ROOT`(turn relpath 计算) + - `agent.core.storage.set_storage_scope("cron")`(消息/工具调用记录直写 cron 作用域:CRON_ROOT 文件或 cron_* 表,**无需执行后的文件→DB 桥接**) + - 任务目录 `task_dir = CRON_ROOT/{task_id}` 并 mkdir。 +- `_start_live_forwarder(task_id)`:后台线程 `cron-live-{task_id}` 订阅 `subscribe_live(task_id)`,把每个快照 `POST {"task_id": task_id, "snapshot": snapshot}` 到 `_LIVE_URL`(timeout=5,失败不阻塞),返回 stop Event。 +- `_post_finished(task_id, status, error, turn_id)`:POST `{"task_id","status","error","turn_id"}` 到 `_FINISHED_URL`(timeout=10,失败仅打印)。 +- `_run_with_timeout(func, timeout)`:daemon 工作线程执行,`done.wait(timeout)` 判超时;超时返回 `(None, None, True)`(Python 无法杀线程,靠子进程随后 `sys.exit` 一起销毁);异常 `(None, exc, False)`;成功 `(value, None, False)`。 +- `_classify_runner_error`:复用 `agent.core.runtime._classify_error`。 +- `main()` 流程: + 1. 解析 `--task-id/--prompt/--trigger/--timeout`(timeout 下限 10s)→ `_redirect_storage_roots(task_id)`,失败则 `_post_finished(failed, "存储初始化失败...")` 返回 1。 + 2. `agent.core.workspace_policy.mark_headless()`:无人值守标记,approval 模式退化为拦截(待审批项随进程退出丢失)。 + 3. `build_all_agent_runtimes()` + `get_main_agent_runtime()`,无主 Agent → failed。 + 4. `_start_live_forwarder(task_id)`。 + 5. `execute_runtime_agent(runtime=runtime, chat_history=[{"role":"user","content":prompt}], session_id=task_id, agent_mode="cron")`(直接走 core.runtime,绕开 {"agent","plan"} 归一化),包 `_run_with_timeout`;超时→failed"任务超时";异常→`_classify_runner_error`;正常完成时检查最后一条 assistant content 是否以错误前缀开头(`"OOM:"、"执行过程中出现错误"、"API Key"、"无法连接"、"指定的模型"、"模型服务端"、"对话内容超过"、"API 调用频率"、"Agent 执行轮数超过限制"`)→ 判定 failed。 + 6. `stop_forwarder.set()` → `get_last_turn_id(task_id)` 取 turn_id → `_post_finished(task_id, status, error_msg, turn_id)` → `sleep(0.3)` 给转发线程发最后快照 → 返回 0/1。 + +### storage.py —— 转发层 + +纯转发到 `agent.core.storage`:导出 `CRON_ROOT`、`TaskConfigRepository`、`ConversationRecordRepository`、`get_task_repository`、`new_task_id`、`set_storage_scope`、`get_storage_scope`;并自定义 `get_record_repository()` = `_get_core_record_repository(scope="cron")`(固定返回 cron 作用域记录仓库:`cron_messages`/`cron_tool_calls` 表或 `history/cron` 下 JSON 文件,按 `STORAGE_BACKEND` 选择 json/mysql)。 + +### live.py —— 主进程侧实时流中继 + +- 子进程无法直接与前端建 SSE,故经 HTTP 回传主进程后由本模块按 task_id 分发。 +- `_cron_live_subs: dict[task_id, list[Queue]]` + `_subscribers_lock`。 +- `subscribe(task_id)`/`unsubscribe(task_id, q)`:每任务订阅者队列(maxsize=64)。 +- `push_snapshot(task_id, snapshot)`:向所有订阅者 `put_nowait({"type":"update","data":snapshot})`(队列满丢最旧)。 +- `push_finished(task_id, payload)`:推 `{"type":"finished","data":payload}`,前端据此重载消息并刷新状态。 +- 快照格式与 `tool_call_recorder._build_live_snapshot` 一致:`{tool_calls, reflections, started_at}`(前端复用 toolcalls.js 渲染)。 + +### __init__.py + +包说明:Cron 模式与 Edit/Chat 模式并列,通过子进程隔离执行;子模块 roles:models/storage/scheduler/runner/live。 + +### ★ cron 与主进程的通信机制(总结) + +1. **调度**:主进程 FastAPI lifespan 启动 `CronScheduler`(daemon 线程)→ 5s 扫描 → `subprocess.Popen([py, "-m", "agent.cron.runner", ...])` 拉起子进程。 +2. **子进程 → 主进程**(HTTP 回传,主进程固定监听 127.0.0.1:8765): + - `POST /api/cron/internal/live`:`{task_id, snapshot}` → 主进程 `cron.live.push_snapshot` → 分发给前端 SSE 订阅者。 + - `POST /api/cron/internal/finished`:`{task_id, status, error, turn_id}` → 主进程 finished handler 更新任务状态(scheduler `_monitor_proc` 以此为兜底依据,不重复覆盖终态)。 +3. **主进程 → 前端**:SSE(cron live 订阅队列),前端收 `update`/`finished` 事件。 +4. 存储天然隔离:子进程把 `SESSIONS_ROOT`/`TOOL_CALLING_ROOT`/`ui_session.SESSIONS_ROOT` 重定向到 `history/cron/{task_id}/`,且统一存储 scope="cron",无需桥接导入。 + +--- + +## 三、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\tts\streaming_client.py`(流式 TTS 客户端) + +- `TTSEvent` dataclass:`type`(audio.start | audio.chunk | audio.done | audio.error | audio.cancelled)、`request_id`、`index=0`、`num_samples=0`、`sample_rate=24000`、`data: bytes|None`(**PCM f32le 原始字节**,已由 base64 解码)、`message=""`。 +- `StreamingTTSClient(base_url="http://localhost:8902")`: + - `stream_tts(text, request_id="")` → **Generator[TTSEvent]**:`requests.post(f"{base_url}/stream_tts", json={"text","request_id"}, stream=True, timeout=300)`;`resp.raise_for_status()`;`resp.iter_lines(decode_unicode=True)` 逐行,跳过非 `data: ` 行;`json.loads(line[len("data: "):])`,解码失败 continue;按 payload 字段构造 `TTSEvent`,`data=_decode_pcm_b64(payload.get("data"))`;`yield evt`。 + - `cancel_tts(request_id)` → bool:POST `{base_url}/cancel_tts`(timeout=5),200 即成功。 +- `_decode_pcm_b64(b64_data)`:base64 解码失败返回 None。 + +--- + +## 四、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\utils\`(工具函数集) + +### env_utils.py —— 环境变量加载与全局配置 + +- `_get_env(name, default, cast, required)`:读 os.environ,支持必填校验(缺失抛 `EnvironmentError`)与类型转换(失败抛 `ValueError`)。 +- 配置加载链:`_load_config()` 优先 `agent.core.config_manager.load_env_config()`(单一数据源),把非 `models` 键写入 `os.environ`,并从 `models[0]` 提取 `api_key/base_url/model/timeout` 写入 `LLM_API_KEY/LLM_BASE_URL/LLM_MODEL/LLM_TIMEOUT`;config_manager 不可用时回退直接读 `src/agent/config/env_config.json`;最后 `_refresh_globals()`。模块导入时执行 `_load_config(); _refresh_globals()`。 +- `_refresh_globals()` 定义的模块级常量(均可用环境变量覆盖): + - 路径:`WORKING_DIR`(相对路径转绝对)、`WORKSPACE_DIR`、`USER_PYTHON_PATH`。 + - LLM:`LLM_BASE_URL`/`LLM_API_KEY`/`LLM_MODEL`(**required=True**)、`LLM_TIMEOUT`(默认 60.0)、`LLM_CONTEXT_WINDOW`(默认 262144)。 + - 多模态服务:`STREAMING_TTS_URL`、`ASR_BASE_URL`、`RAG_BASE_URL`(三者均为可选,对应 llm_server 的 8902/8901/v1、8903)。 + - 压缩/图像:`COMPRESS_RATE`(0.6)、`IMG_SIZE`(768)、`RAG_CHUNK_SIZE`(500)、`RAG_CHUNK_OVERLAP`(50)、`GROUNDING_WIDTH/HEIGHT`(1000)、`SEND_FILE_SIZE_LIMIT`(30MB)。 +- **THINKING_LEVELS 各档位**(`("low","high","xhigh","max","ultra")`,默认 `"low"`;非法值回退 low): + - `low` = agent 模式 + 不思考;`high` = plan 模式 + 不思考;`xhigh` = agent 模式 + 思考;`max` = plan 模式 + 思考;`ultra` 为未来 react→审批图预留占位,暂按 "agent+思考" 处理。 + - `_THINKING_ENABLED_LEVELS = {"xhigh","max","ultra"}`。 +- `thinking_enabled(level=None)`:档位是否启用深度思考。 +- `get_thinking_extra_body(level=None)`:思考开启返回 `{}`(恢复模型原生行为=启用思考);否则返回 `None`(禁用思考,默认)。该 extra_body 注入 `Multimodel_LLM`。 +- `reload_config()`:热重载并刷新全局(调用方须 `import agent.utils.env_utils as env_utils` 访问属性,`from ... import` 会绑定旧值)。 +- `BROWSER_MAP`:edge/chrome/firefox/safari/opera/brave → (可执行名, 显示名)。 +- `get_workspace_dir()`:WORKSPACE_DIR 绝对化(相对则相对 WORKING_DIR),缺省回退 WORKING_DIR,确保目录存在。 +- `get_venv_dir()`:由 `USER_PYTHON_PATH`(`.../Scripts/python.exe`)取上级目录。 + +### agent_utils.py —— 会话/历史/附件/文档核心工具 + +- 常量:`AUDIO_FILE_EXTENSIONS`(wav/mp3/flac/ogg/m4a/aac/wma/opus/aiff/ape/amr/au)、`DOC_EXT_MAP`(txt/json/md/cpp/c/java/m/html/svg→plain_text;docx→docx;pptx→pptx;pdf→pdf;csv/xlsx/xls→tabular)、**路径**:`HISTORY_ROOT = src/agent/history`、`SESSIONS_ROOT = history/sessions`(启动即 mkdir)。 +- `JsonChatMessageHistory(session_id)`(替代 SQLite):基于 turn 文件读写 LangChain 消息,`_load()` 用 `load_all_turn_messages`,`_dict_to_langchain` 把 user/assistant 条目转 HumanMessage/AIMessage;`get_history(session_id)` 返回实例。 +- session_meta:`_load_session_meta`/`_save_session_meta`(经 `session_storage.load_session_extra/save_session_extra` 的 `"agent_meta"` 键);`get_session_meta(session_id, agent_name)` → `{cursor_index, compressed_content}`;`update_session_meta` 写入。 +- **动态尾部长期记忆**:`update_session_dynamic_tail(session_id, agent_key, text, max_entries=100)` —— 存 `dynamic_tail_history` 列表,连续相同内容去重,超出上限丢最旧;`get_session_dynamic_tail_history` 读取(时间正序)。用于 TodoList 状态/循环提醒长期记忆,与 compressed_content 合并保存不覆盖(前缀缓存友好:变化部分在消息末尾)。 +- `build_tool_history_messages(session_id, agent_key="main")`:读 cursor_index,把压缩游标**之后**所有轮次工具调用构造成 synthetic HumanMessage(`additional_kwargs={"synthetic":True,"tool_ctx":True}`,`id=f"tool_ctx_{idx}"` 供压缩节点 RemoveMessage 精确移除),内容 `[历史工具调用] {name}({args[:300]}) → {result[:500]}`。 +- Turn 存储:`TURN_JSON_PREFIX="turn_"`;`turn_files_dir(session_id, turn_id)` = `sessions/{sid}/{turn_id}/files`;`save_turn_messages`(→ `session_storage.save_turn`,后端无关,附件二进制由调用方落盘);`load_all_turn_messages`;`delete_turns_after(session_id, keep_until_turn_id)`(按目录名排序删除 > keep 的 turn 目录 + 存储记录,**keep 自身保留**);`get_last_turn_id`;`session_dir(session_id)` = `sessions/{sid}`。 +- pending turn_id:`set_pending_turn_id`/`take_pending_turn_id`(跨请求共享,`_pending_turn_ids` dict + 锁,仅取一次)。 +- `summarize_chat_text(llm, chat_text)`:`ChatPromptTemplate(SUMMARY_SYSTEM_PROMPT + 压缩指令) | llm`,`config={"metadata":{"output_type":"text"}}`,供 in-loop 压缩节点复用。 +- `assistant_text(value)`:str/list/dict 多形态归一为纯文本(dict 依次尝试 text/content/input/output/query/answer/result 键,失败 json.dumps)。 +- `make_attachment_text(category, file_path, extra="")`:生成附件描述文本——图片(含尺寸 `[用户提供了图片,文件路径: ...,尺寸: ...]`)、音频(路径 + extra)、有 extra 时直接返回 extra(如 ASR 结果/文档内容)、附件文件/通用格式。 +- `Documents_process`(静态方法类):`doc_classify(file_path)` 按扩展名+MIME 返回 `plain_text|docx|pdf|image|audio|tabular`(不支持抛 ValueError);`process_plain_text`(UTF-8 读取);`process_docx`(python-docx,按文档顺序遍历段落/表格,run 中提取 `N` 占位与图片字节,表格转 ``,支持"表1/图1"短标题缓存合并);`process_pdf`(pypdf,逐页文字 + `N` 占位 + 图片字节);`process_pptx`(python-pptx 提文字 + `ppt_utils.convert_pptx_to_svg` 提布局,SVG 内 base64 data URL 替换为 `#[内嵌图片 N]` 占位,输出 `N[文字][布局]` 结构);`process_tabular`(csv → `
Sheet1...`;xlsx openpyxl 多 sheet;xls xlrd 日期转可读);`process_image`/`process_audio`(读字节,文本为空);`process(file_path)` 统一分发入口(供 `core/llm._preprocess_messages` 调用)。 +- `audio_file_to_text(audio_path)`:**ASR 调用链** —— 非 wav 先用 pydub `AudioSegment.from_file` 转临时 wav(`tempfile.mkstemp(prefix="asr_", suffix=".wav")`);读字节 → base64 → `data:audio/wav;base64,...`;payload `messages=[{role:user, content:[{"type":"audio_url","audio_url":{"url": data_url}}]}]`;POST `{ASR_BASE_URL.rstrip('/')}/v1/chat/completions`(timeout=120);返回 `choices[0].message.content.strip()`;finally 删除临时 wav。 +- **附件存储规则(归纳)**: + - 用户上传附件:`web/ui_session._rewrite_user_files_to_dir` 把外部文件 `shutil.copy2` 复制到 `SESSIONS_ROOT/{session_id}/`(会话根目录),命名 `{清洗后文件名主干}_{session_id}_{递增序号}{ext}`(`_next_session_copy_name`);已在该目录内的跳过。 + - 单 turn 附件目录:`turn_files_dir` = `sessions/{sid}/{turn_id}/files`。 + - 工具产物(tool_call_recorder):直接写 `TOOL_CALLING_ROOT/{session_id}/`,命名 `_artifact_{工具名}_{序号}.png`(图片)、`_file_{工具名}{序号}{ext}`(send_file 文件),音频不落盘只存路径元信息。 + +### image_utils.py —— 图片多模态适配 + +- `IMAGE_FILE_EXTENSIONS = {.png,.jpg,.jpeg,.bmp,.gif,.webp,.ico}`。 +- `resize_image_if_needed(image_path_or_bytes, max_size=IMG_SIZE=768)`:超长边等比缩放(LANCZOS);RGBA/LA 透明图贴白底后转 JPEG;JPEG quality=70 + optimize + progressive,PNG 走 optimize。 +- `image_to_base64`:路径/字节 → base64(失败 None)。 +- `_mime_from_magic`:按魔数判 jpeg(`FFD8`)/png(`\x89PNG`)/gif,默认 jpeg。 +- `image_bytes_to_openai_image_url_part(image_bytes, detail=None)` / `image_path_to_openai_image_url_part(file_path, detail=None)`:统一返回 `{"type":"image_url","image_url":{"url": "data:{mime};base64,...", "detail": 可选}}`。供 `core/nodes.process_tool_artifact`(工具截图注入对话)与 `core/llm`(用户上传图片)使用。 + +### tool_call_utils.py —— 工具执行钳点 + +- `_invoke_with_timeout(tool, tool_call_input, timeout)`:daemon 线程执行 `tool.invoke`;**用 `contextvars.copy_context()` 把调用方上下文(session/turn、agent 名称、sub_agent_context、tool_call_id 等 ContextVar)复制进线程**(保证并行工具/子 Agent 隔离);`t.join(timeout)`,超时返回 None;工具异常在主线程 re-raise(保留 ABORTED_BY_USER 冒泡)。 +- `invoke_tool_and_build_message(tool, tool_name, tool_args, tool_call_id="", timeout=None)` → `(content, artifact)`。**工具执行前的唯一钳点,顺序**: + 1. **工作空间越界拦截**:`workspace_policy.decision(tool_name, tool_args) == "block"` → 直接返回 `build_block_message(tool_name, tool_args)`(策略模块异常不阻断执行)。 + 2. **人工确认钳点**(human_interaction 除外):`classify_tool_execution(...)=="confirm"` 或 workspace decision=="approve"(附 `check_violation` 说明)→ `ask_human(session_id, meta)`,meta 含 `{type:"tool_call", title:"待确认执行工具:{name}", args, instruction, options:["approve","reject","skip"], policy_note, agent_name, is_sub_agent, resolved:false}`;decision reject/skip → 返回 `"用户拒绝/已跳过此工具调用({name}):{hint}"`;timeout → `"等待用户确认超时,未执行工具"`;approve 继续。`RuntimeError`(ABORTED_BY_USER)向上冒泡。 + 3. 超时取值:显式 timeout > `config_manager.get_tool_timeout(tool_name)`(缺省 `DEFAULT_TOOL_TIMEOUT` 300)。 + 4. `human_interaction` 为阻塞工具**绕过超时包装直连 invoke**(内部 ask_human 的 ABORTED_BY_USER 直接冒泡 runtime 按暂停处理);其余走 `_invoke_with_timeout`,`None` → `"工具执行超时({n}s)"`。 + 5. 返回归一化:ToolMessage → (content, artifact);tuple → (str(t0), t1);否则 str(result)。 +- `json_safe(value)`:递归转 JSON 可序列化(UUID→str、datetime→isoformat、dict/list/tuple 递归、其余 str)。 +- `normalize_tool_call(tool_call)`:兼容 dict 或对象,args 为 JSON 字符串时解析(失败 `{"raw": args}`),输出固定 `{"id","name","args"}`。供 `core/routing` 判断工具执行策略/提取待确认项。 +- `split_inline_thinking(text)`:按**最后一个** `` 拆分(兼容 `"思考\n\n\n回复"` 与 `思考回复`),返回 (thinking, reply);无标记 → ("", text)。 +- `extract_reasoning_text(msg)`:思考开启(xhigh/max/ultra)优先 `additional_kwargs.reasoning_content`,其次 content 内联 `` 块;思考关闭(low/high)content 即一句话反思直接返回。供 nodes 与 tool_call_recorder 提取思维链。 +- `format_args_summary(args, max_len=80)`:`json.dumps(default=str)` 截断 + "...",供死循环检测与记录展示。 + +### ppt_utils.py —— PPT 读取/预览(仅 pptx→SVG,不生成 PPT) + +- 模块头明确:**原先 svg→pptx 制作路线已移除**,仅保留 PPT 读取/解析与预览渲染(自研 PPTX 解析,不依赖外部 ppt-master 项目)。 +- 常量:`EMU_PER_INCH=914400`、`EMU_PER_PX=9525`、`HUNDREDTHS_PT_PER_PX=75`、`ANGLE_UNIT=60000`;XML 命名空间 NS_A/NS_P/NS_R/NS_REL/NS_SVG;`_ns`/`_etree_fromstring`。 +- 单位换算:`emu_to_px`、`px_to_emu`、`fmt_num`(去尾零)。 +- 颜色:`_resolve_srgb`(`srgbClr`→`#RRGGBB`)、`_find_color_elem`、`_resolve_color`(srgbClr/schemeClr/sysClr/prstClr + alpha)、`_parse_color_simple`。 +- `OoxmlPackage(pptx_path)`:zipfile 打开,读 `_rels/.rels` → presentation.xml(`sldSz` 取画布尺寸 px)→ `sldIdLst` 按顺序列出 slide → 每 slide 解析 layout/master 链(`_resolve_related`);`resolve_theme(master)` 从 master→theme 的 `clrScheme` 取主题色映射;`iter_slides`/`read_media`/`close`(支持 with)。 +- `Xfrm`(坐标变换):x/y/w/h/rot/flip_h/flip_v/ch_*(group),`to_svg_transform` 生成 rotate/translate+scale 字符串;`parse_xfrm`。 +- 预设几何 `_convert_prst_geom`:rect/roundRect(rx=min*0.05)/ellipse/line。 +- `_resolve_fill_svg`:noFill→`fill:none`,solidFill→hex,默认 `#CCCCCC`。 +- `_convert_picture`:媒体按扩展名映射 MIME,内联 `data:...;base64` 的 `` 元素(preserveAspectRatio="none")。 +- `_convert_txbody`:bodyPr 内边距/锚点/换行;段落对齐(l/ctr/r→start/middle/end);run 级 sz(/100 得 px)、b/i、latin/ea typeface、颜色;行高 1.25、y = inner_y + font_size + idx*1.25*font_size;超出形状高度截断;wrap none 加 `white-space:pre`。 +- `ShapeNode`(kind: shape/picture/group/graphic)+ `walk_sp_tree`(递归遍历 sp/cxnSp、pic、grpSp、graphicFrame,读 hidden)。 +- 主入口 `convert_pptx_to_svg(pptx_path)` → `PptxToSvgResult{slides:[SlideSvgResult{index, svg, media_files}], canvas_px, theme_colors}`:每页画背景 rect → 遍历形状树 `_convert_node` → 组装 ``。`_convert_shape_to_svg`/`_convert_pic_to_svg`/`_convert_group_to_svg` 递归实现。 + +--- + +## 五、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\memory\system_prompt.py`(提示词体系) + +- `_BASE_PROMPT`(基础主提示词,未命名 MAIN_SYSTEM_PROMPT,实际入口为 `get_main_system_prompt()`):多模态桌面操作助手。要点:理解意图高效完成任务;图片/音频/文件直接多模态分析,**不为此调用截图等无关工具**;只有信息确实无法获取才调工具;拿到工具结果必须观察实际返回继续推进、严禁编造;**同一页面 GUI 连续操作合并为一次 gui_tool 调用(actions 列表),严禁逐个单步**;复杂任务才用 todo_list 拆解并 done_step 标记进度,简单任务直接执行;完成时总结。 +- `_WS_TAIL`:`{workspace_dir}` 占位的工作空间配置(代码/脚本/图片默认存工作空间;terminal_execute 默认 cwd;doc_tool 用绝对路径;越界访问拦截或审批)。 +- `get_main_system_prompt(workspace_dir=None)` = `_BASE_PROMPT + _reflection_section() + _WS_TAIL`;`set_current_workspace(dir)` 热改 `_current_workspace_dir`。 +- `get_cron_system_prompt(workspace_dir=None)` = `CRON_MODE_PROMPT.format(workspace_dir=...) + _reflection_section()`(.format 失败时替换占位符兜底)。 +- `SUMMARY_SYSTEM_PROMPT`:Summary Agent 摘要助手(客观准确、抓要点、简洁、保原意;默认压到 20%~30% 或 3~5 句;对话记录总结主题/立场/共识/分歧/行动项;过短回复"内容过短,无法生成有效摘要";多语言同语言输出)。 +- `VISION_SYSTEM_PROMPT`:专业图像理解助手(客观中文描述、命名品牌/时间/地点/人物、多图编号「图1」「图2」、看不清如实说明、结合图片回答问题)。 +- `DB_SYSTEM_PROMT`(注意拼写为 PROMT):PostgreSQL 数据分析助手(只读 SELECT/WITH、先展示表/查结构/验证后执行、结果归纳成人话、安全底线、非 DB 问题直接回复)。 +- `_PLAN_CORE_RULES`(Plan 模式核心规则):任务模糊先 `human_interaction` 确认;计划写入工作空间 `.minorAgent/任务名.md`(目标/步骤/产出物);`send_file` 发给用户确认,**确认前不得执行**;每步完成 doc_tool 更新 `[x]` 进度;完成总结并告知计划位置。 +- `PLAN_MODE_PROMPT = _PLAN_CORE_RULES + "\n\n" + _BASE_PROMPT`;`get_plan_system_prompt` = PLAN_MODE_PROMPT + 反思段 + WS_TAIL。 +- `CRON_MODE_PROMPT`:无人值守自主完成;**慎用 human_interaction**(用户大概率不在场);复杂任务 todo_list;时效性判断(滞后说明/已过时效跳过);执行汇报(做了什么/关键结果/是否有异常);**【强制规则】PPT 制作、代码项目等复杂产出或系统操作类任务必须先调 skill_router 查询 Skill 并严格遵循**;可自用 cron_manager 管理定时任务;无人值守越界访问直接拦截。 +- `REFLECTION_PROMPT`(固定反思引导):不需要工具直接回复不输出反思;需要工具先一句话反思再调用;GUI 操作未命中按最新截图重估坐标重试;成功则继续。**gui_tool 批量调用规则**同上(一次 actions 传全部动作)。 +- `_reflection_section()`:**思考关闭档位(low/high)注入 REFLECTION_PROMPT 到系统提示词前缀(前缀缓存友好),思考开启档位(xhigh/max/ultra)返回空**(保持模型原生思考语义)。惰性导入 env_utils 防循环依赖。 +- `SUB_AGENT_OOM_PROMPT`:`{total_tokens}/{threshold}` 占位,子 Agent 上下文超限时注入——立即停止调用工具、整理进度(已完成/关键结果/问题原因)、直接文字回复主 Agent。 +- Skill Router 相关:`SKILL_ROUTER_PROMPT`(从 Skill 列表选 1~3 个,每行一个 name,无匹配输出 NONE)、`SKILL_ROUTER_NO_MATCH_MSG`/`SKILL_ROUTER_EMPTY_MSG`/`SKILL_ROUTER_NO_QUERY_MSG`/`SKILL_ROUTER_NO_SKILLS_MSG`。 +- `DEFAULT_SUB_AGENT_PROMPT`(子 Agent 默认提示词):主 Agent 委派的专业执行助手;专注任务不越权;工作流程(接收→工具逐步完成→清晰摘要→复杂先 todo_list);关键准则(聚焦/高效/如实/按需用工具/结果导向);输出格式(简洁概括、分点编号、`[完成]` 标记)。 +- `get_default_sub_agent_prompt()` = `DEFAULT_SUB_AGENT_PROMPT + _reflection_section()`。 + +--- + +## 六、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\agents\agent_runtime.py`(动态 Agent 工厂 + Web 入口) + +- `AgentMode = Literal["agent","plan","cron"]`;缓存 `_all_agent_runtimes: dict[str, AgentRuntime]`、`_main_agent_name`。 +- `_build_llm_for_agent(cfg)`:从 `load_models()`(env_config 的 models 列表)按 `cfg.llm_model_id` 找模型配置,构造 `Multimodel_LLM(model, api_key, base_url, timeout, max_retries, extra_body=get_thinking_extra_body())`——深度思考由全局 THINKING_LEVEL 档位控制,取代 per-model enable_thinking 开关;未匹配回退 `build_default_llm()`(优先主 Agent 配置模型)。 +- `_build_agent_runtime(cfg)`:① 构建 LLM;② `get_tools()` 全量按 `cfg.tools` 过滤;**主 Agent(role=="main")强制加载 doc_tool 与 todo_list**(不可移除);③ `agent_llm.bind_tools(agent_tools, tool_choice="auto")`;④ `build_agent_graph(tools, model_with_tools, trajectory_rounds=cfg.trajectory_rounds)`;⑤ 组装 `AgentRuntime(name, description, graph, llm, tools, tools_by_name, system_prompt, max_iterations, trajectory_rounds)`。 +- `_refresh_subagent_tool_description()`:遍历非 main 的 runtime 收集 `sub_names`/`sub_descriptions`,调用主 Agent 中 `CallSubAgentTool._refresh_description(sub_names, sub_descriptions)`,让主 Agent 知晓可委派的子 Agent。 +- `build_all_agent_runtimes()`:`load_agent_configs()` 只构建 enabled;第一个 `role=="main"` 记为 `_main_agent_name`;无 main 时自动把第一个设为主;**先刷新 call_subagent 描述,再重建主 Agent**(bind_tools 携带最新子 Agent 列表)。 +- 取值函数:`get_main_agent_runtime()`/`get_all_agent_runtimes()`/`get_agent_runtime(name)`(缓存空则自动构建)。 +- `reload_all_agent_runtimes()`:清空重建;同步模块级引用 `main_agent_runtime/agent_graph/tools/TOOLS_BY_NAME/llm`;再刷新子 Agent 描述;返回 `{"agents":[{name,description,tools_count,max_iterations}], "main_agent"}`。 +- `execute_agent(chat_history, session_id="test", agent_mode="agent")`:**Web/API 层入口**,直接委托 `execute_runtime_agent(runtime=main_agent_runtime, ...)`。 +- 模块导入时(底部)即执行 `build_all_agent_runtimes()` 并缓存 `main_agent_runtime/agent_graph/tools/TOOLS_BY_NAME/llm`。 + +--- + +## 七、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\agent\history\`(会话/工具调用记录) + +### session_storage.py —— 转发层 + +纯转发 `agent.core.storage`:`ConversationRecordRepository`/`JsonRecordRepository`/`MysqlRecordRepository`(Protocol+实现)、便捷函数 `save_turn`/`load_messages`/`load_turn_messages`/`list_turn_ids`/`save_tool_calls`/`get_turn_record`/`get_latest_turn_record`/`list_turn_records`/`persist_live_records`/`load_session_extra`/`save_session_extra`/`list_session_ids`/`get_last_turn_id`/`delete_turns_after`/`delete_tool_records`/`delete_session`,作用域 `set_storage_scope`/`get_storage_scope`/`get_record_repository`。消费方无需感知 json/mysql 后端。 + +### tool_call_recorder.py —— 工具调用记录器(live 实时推送 + 持久化) + +- 全局:`TOOL_CALLING_ROOT = Path(SESSIONS_ROOT)`(默认 `history/sessions`;cron 子进程改指 CRON_ROOT)。 +- 上下文 ContextVar:`_current_turn_id`/`_current_session_id`(带线程安全 fallback `_current_turn_fallback`/`_current_session_fallback`)、`_sub_agent_context`(子 Agent 执行标记)、`_current_tool_call_id`(并行工具隔离)。配套 `set_current_turn`/`get_current_turn`/`set_current_session`/`get_current_session`/`set_current_tool_call_id`/`get_current_tool_call_id`/`is_sub_agent_context`。 +- `start_live_turn(session_id)`:turn_id = `datetime.now().strftime("%Y%m%d_%H%M%S")`;`init_live_turn_with_id(session_id, turn_id)`:清空该 session 的 `_live_records`、记 `_turn_start_times`、加 `_active_sessions`、清 pending thinking。 +- `record_tool_call_live(session_id, name, args, tool_call_id=None)`:条目 `{id, name, args(JSON安全), args_summary(format_args_summary 40 字符), status:"running", result_text:"", 可选 thinking}`;**消费 pending thinking**(上一条反思附加到本条);子 Agent 上下文路由到 `_sub_live_records[(session, tool_call_id)]`(隔离 key 取父级 call_subagent 的 tool_call_id,并行/同名子 Agent 互不串扰);结尾 `_notify_live(session_id)`。 +- `record_tool_result_live(session_id, name, result_text, artifact=None)`:从后往前找同名 running 条目 → status="done"、result_text 截 500 字、`result_images`(`_artifact_image_data_urls`:artifact 内 bytes/image_bytes/images 列表/data_url/http URL → data URL 列表)、`result_audio`(`_artifact_audio_info`,仅路径元数据)、`result_file_info`(`_artifact_file_infos`:file/image/folder);json 后端额外 `_safe_persist_live_records` 增量落盘防崩溃丢失。 +- `record_reflection_live(session_id, content)`:写 `_think_store`(去重,summary=content[:60] 换行转空格)并置 pending thinking;子 Agent 上下文只写 `_sub_pending_thinking`(不独立显示,嵌套在各子工具条目的 thinking 中)。`get_live_reflections`/`clear_live_reflections`。 +- **订阅推送**:`subscribe_live(session_id)` → Queue(maxsize=64);`unsubscribe_live`;`_build_live_snapshot(session_id)` = `{tool_calls: get_live_tool_calls(), reflections, started_at, human_requests(惰性导入 human_request)}`;`_notify_live` 向所有订阅者 `put_nowait(snapshot)`(满则丢最旧放最新)。cron 子进程正是复用该 subscribe_live 转发到主进程。 +- `get_live_tool_calls(session_id)`:内存优先;空且 session 活跃 → 回退磁盘最近 `tool_*.json`;session 已结束直接返回空(避免跨 turn 残留);并把 `_sub_live_records` 按 tool_call_id 精确注入 call_subagent/agent_call 条目的 `sub_tool_calls`(无 id 回退"最后一条 running")。 +- `get_turn_started_at(session_id)`:轮次开始时间戳(内存 → 磁盘回退)。 +- `end_live_turn(session_id)`:清 turn/反思/pending、清理该 session 所有子 Agent 槽、`_active_sessions.discard`、返回并弹出 `_live_records`。 +- `save_aborted_turn(session_id, turn_id)`:Abort 时把内存 live 记录转 save_turn 兼容格式落盘(meta 含 `aborted: True`)。 +- `extract_tool_calls_from_messages(messages)`:从图执行结果消息提取完整工具链——AIMessage.tool_calls(首个附加 thinking,`extract_reasoning_text`)→ pending;ToolMessage 配对 → result_text + artifact 落盘(`_save_artifact_files` → `_artifact_{name}_{idx}.png` / `_file_{name}{ext}`)+ result_images/audio/file_info;未配对 pending 标记 done。 +- `save_turn(session_id, turn_id, user_message, messages)`:工具调用持久化唯一入口。① extract;② 产物字节直接写 `session_tool_dir(session_id)`;③ 从存储读 live 记录(`get_turn_record`)合并(`_merge_extracted_with_live`:按索引优先、name+顺序回退,从 live 补 thinking/args_summary);④ `_inject_sub_traces_to_record` 注入子 Agent 轨迹;⑤ `session_storage.save_tool_calls(..., meta={started_at, ended_at, duration})`(轮次级计时,单条工具计时已移除)。 +- 读取 API:`list_turns(session_id, limit=5)`(倒序)、`get_latest_turn`、`get_tool_calls_for_latest_turn`、`get_tool_calls_for_turn`、`get_turn_meta`(started_at/ended_at/duration)、`get_thinking_for_turn`(从 tool_calls 各条 thinking 提取链)。 +- `_tool_calls_from_turn_record(record, session_id)`:为每条工具调用补 `result_files_full`(`session_tool_dir(sid)/fname` 绝对路径)、`result_images`(`_artifact_*.png` → `/api/tool-image?path=...&session={turn_id}`;仅当 live 无 data URL 时回退)、`result_download_files`(`_file_*` → `/api/tool-file?path=...&session={turn_id}`)、`result_audio.api_url`、`turn_id`。 +- 子 Agent 轨迹:`_sub_trace_key = f"{session_id}/{turn_id}/{agent_name}[/{tool_call_id}]"`;`record_sub_agent_trace`/`get_sub_agent_traces_for_turn`(内存 `_sub_traces_store`,并行按 tool_call_id 精确隔离)。 +- Token 统计:`set_session_tokens(session_id, total_tokens, breakdown)`(内存 + 经 session extra 持久化;breakdown={messages, tools, system} 估算占比由 `nodes._estimate_token_breakdown` 算)、`get_session_tokens`(内存→磁盘→0)、`get_session_token_breakdown`、`clear_session_tokens`。 +- **目录结构(session_tool_dir)**:`history/sessions/{session_id}/` 下:`turn_*.json`(对话消息,由 ui_session 写)、`tool_*.json`(每 turn 工具调用记录 + started_at/ended_at/duration)、`_artifact_*.png`/`_file_*`(工具产物字节)、session extra(`agent_meta` 键含 cursor/compressed_content/dynamic_tail_history,`tokens`/`token_breakdown`);turn 附件在 `{session_id}/{turn_id}/files/`。cron 场景整体换到 `history/cron/{task_id}/`。 + +--- + +## 八、`C:\Users\86166\Desktop\Agent_Learning_minor\Agent\src\web\ui_session.py`(会话 UI 状态管理) + +- `_AUDIO_EXTENSIONS = AUDIO_FILE_EXTENSIONS`(别名,向后兼容);`_memory_session_ids: set[str]`(内存会话 id 缓存防冲突)。 +- `session_dir(session_id)` = agent_utils 的 `sessions/{sid}`。 +- `list_session_ids_ordered()`:`session_storage.list_session_ids()`(mysql 按 sessions 表 updated_at 倒序;json 扫描目录含 turn_*.json 的按 mtime 倒序)。 +- `new_timestamp_session_id()`:`datetime.now().strftime("%Y%m%d_%H%M%S")`,与磁盘+内存已有 id 冲突时追加 `_1`、`_2` 后缀;新 id 加入 `_memory_session_ids`。 +- `prune_memory_session_ids()`:把已持久化的 id 从内存集合移除,防无限增长(每次会话操作后调用)。 +- `delete_session(session_id)`:删内存缓存 → `shutil.rmtree(session_dir)` + `session_tool_dir` → `session_storage.delete_session`(后端无关)。 +- `_build_user_chat_content(query)`:`query={text, files, file_names}` → 多模态 content:图片文件 → `{"type":"image_url","image_url":{"url": filepath}, "name": 原名}`;其余(含音频)→ `{"path": filepath, "name": 原名}`(**音频作为普通文件 part,不再生成 audio part**;麦克风录音由 `/api/chat/start` 阻塞 ASR 转 text,不走此函数);文本 → `{"type":"text","text": text}`;仅一段纯文本时直接返回 str。 +- `_sanitize_filename_stem(name)`:清洗 `<>:"/\|?*` 为 `_`、去尾部空格点、空回退 "file"。 +- `_next_session_copy_name(dest_dir, session_id, original_name, ext)`:`{stem}_{session_id}_{max_index+1}{ext}`(扫描目录现有同名前缀取最大序号递增)。 +- `_rewrite_user_files_to_dir(user_msg, dest_dir, session_id)`:**附件从临时目录复制到会话目录的核心**——遍历 content list,text 直接保留;对 path/image_url.url/audio_path 源文件:若已在 dest_dir 内跳过;否则 `_next_session_copy_name` 生成唯一名 → `shutil.copy2(src, dest)` → 原地更新 `piece["path"]`(image_url 同时更新 `image_url.url`,audio 更新 `audio_path`)。 +- `rewrite_user_message_files_to_session_dir(user_msg, session_id)`:向后兼容包装,dest = 会话根目录。 +- `_serialize_user_msg_for_turn(user_msg)`:序列化为 turn 文件 user 条目——str 拆 text part;list 中 text 保留、附件转 `{"type":"image_url"|"file", "relpath": 相对 SESSIONS_ROOT 的路径, "name"}`。 +- `_turn_user_entry_to_content(user_entry)`:还原 UI/LLM content——relpath 拼绝对路径并校验在 SESSIONS_ROOT 内;image_url part 文件存在给 `{"type":"image_url","image_url":{"url": abs}, "path", "name"}`,缺失累计 `any_image_missing`;历史 audio part 降级为普通文件;file part 缺失 → `[附件已丢失: name]`;有图丢失补 `[图片已丢失]`;纯文本单段直接返回 str。 +- `_serialize_assistant_entry`:`{text, output_type, turn_id}`;voice 输出额外 `autoplay`/`audio_duration_seconds`/`audio_relpath`(相对 SESSIONS_ROOT)。 +- `_serialize_message_for_turn(msg)`:按 role 分发 → `{"role":"user","user":...}` / `{"role":"assistant","assistant":...}`。 +- `sync_turn_from_chat_history(session_id, chat_history, latest_user_image_description)`:**turn 级消息落盘**——`get_last_turn_id` 得 turn_id;从最后一条 user 消息起截取本轮消息区间;逐条序列化并把 `turn_id` 注入 meta;`save_turn_messages(session_id, turn_id, messages_out)`(内部 → `session_storage.save_turn`)。 +- `rollback_turns_from_history(session_id, chat_history)`:从 chat_history 找最后一个 turn_id,`delete_turns_after(session_id, keep_turn_id)` 删除多余 turn 目录。 +- `_assistant_turn_entry_to_content`/`_assistant_turn_entry_to_message`:还原 assistant 消息(text/voice → `{"type":"text"}` + `{"type":"audio","audio_path","autoplay","duration_seconds"}`,meta 含 output_type/autoplay/pending_action/turn_id/audio{path,format:"wav",sample_rate:24000})。 +- `_load_ui_messages_from_turn(session_id)` / `load_ui_messages_for_session(session_id)`:从 turn 文件重建 UI 消息列表(无数据返回空列表)。 +- `rewrite_user_files_for_last_turn(chat_history, session_id)`:若末条为 user 消息,将其附件复制到会话目录(API 层添加用户消息后立即调用)。 +- `add_message(chat_history, query)`:`_build_user_chat_content` → 无有效内容返回 None,否则 append `{"role":"user","content": user_content}` 并返回新 chat_history(API 层构造用户消息核心)。 +- `clear_ui_and_turns(session_id)`:`delete_turns_after(session_id, "00000000_000000")` 全删,返回 `([], {"text":"","files":[]})`。 + +--- + +## 附:跨模块关键链路速查 + +- **语音输入**:`/api/chat/start`(阻塞 ASR)→ `agent_utils.audio_file_to_text` → POST `{ASR_BASE_URL}/v1/chat/completions`(audio_url data URL,无文本提示)→ `choices[0].message.content`。 +- **语音输出**:`StreamingTTSClient.stream_tts`(SSE 逐事件收 PCM f32le chunk)→ 前端 Web Audio 播放;服务端新请求自动打断旧请求。 +- **图片入模**:`image_utils.image_path_to_openai_image_url_part`(缩放→base64→data URL)或 `Documents_process.process`(docx/pdf/pptx 提取文本+图片字节)。 +- **工具执行**:`tool_call_utils.invoke_tool_and_build_message`(workspace block → confirm ask_human → 超时 → 归一化 ToolMessage)→ `tool_call_recorder` 实时/持久化记录 → SSE 推送前端。 +- **Cron**:`CronScheduler`(5s 扫描 + 时段互斥)→ 子进程 `agent.cron.runner`(重定向存储根 + headless + execute_agent(agent_mode="cron"))→ HTTP 回传 `:8765/api/cron/internal/live|finished` → 主进程 `cron.live` 分发 SSE。 diff --git a/electron/main.js b/electron/main.js index 1c015f7..ae0dbcd 100644 --- a/electron/main.js +++ b/electron/main.js @@ -30,11 +30,31 @@ const SRC_DIR = isPackaged // 配置文件路径 (src/agent/config/env_config.json) const CONFIG_PATH = path.join(SRC_DIR, 'agent', 'config', 'env_config.json'); const CONFIG_DIST_PATH = path.join(SRC_DIR, 'agent', 'config', 'env_config_dist.json'); +const THEME_CONFIG_PATH = path.join(SRC_DIR, 'agent', 'config', 'theme_config.json'); const HOST = '127.0.0.1'; const PORT = 8765; const BASE_URL = `http://${HOST}:${PORT}`; +// ==================== 主题检测(启动玻璃层随主题色变化) ==================== +// 与前端 themes.js 的 dark 标记保持一致 +const DARK_THEMES = new Set(['vscode-dark', 'cyber-neon', 'deep-space-indigo', 'pixel-gold']); + +function getThemeMode() { + try { + if (fs.existsSync(THEME_CONFIG_PATH)) { + const cfg = JSON.parse(fs.readFileSync(THEME_CONFIG_PATH, 'utf-8')); + const id = cfg.theme || ''; + if (DARK_THEMES.has(id)) return 'dark'; + if (id) { + // 未知主题按 id 推断(与 themes.js 的回退逻辑一致) + return (!id.includes('light') && !id.includes('solarized')) ? 'dark' : 'light'; + } + } + } catch {} + return 'dark'; // 默认深色,与窗口默认底色一致 +} + // ==================== 全局状态 ==================== let mainWindow = null; let pythonProcess = null; @@ -166,11 +186,13 @@ function findSystemPython() { } // ==================== FastAPI 生命周期 ==================== -let _fastapiStderr = ''; // 累积 stderr 用于超时诊断 +let _fastapiStderr = ''; // 累积 stderr 用于超时诊断 +let _fastapiReadySettled = false; // 健康检查已结束(成功或超时),停止轮询 function startFastAPI() { return new Promise((resolve, reject) => { killPortProcess(PORT); + _fastapiReadySettled = false; const pythonExe = findSystemPython(); @@ -234,16 +256,22 @@ function startFastAPI() { } function waitForReady(resolve, reject, retriesLeft) { + if (_fastapiReadySettled) return; if (retriesLeft <= 0) { + _fastapiReadySettled = true; reject(new Error( `FastAPI 启动超时\n\nPython: ${findSystemPython()}\n工作目录: ${SRC_DIR}\n\n最后 stderr 输出:\n${_fastapiStderr || '(无输出)'}` )); return; } const req = http.get(`${BASE_URL}/api/health`, (res) => { + res.resume(); // 及时排空响应体,避免 keep-alive 悬挂 if (res.statusCode === 200) { - console.log('[Electron] FastAPI Ready'); - resolve(); + if (!_fastapiReadySettled) { + _fastapiReadySettled = true; + console.log('[Electron] FastAPI Ready'); + resolve(); + } } else { retry(); } @@ -251,7 +279,10 @@ function waitForReady(resolve, reject, retriesLeft) { req.on('error', () => retry()); req.setTimeout(2000, () => { req.destroy(); retry(); }); function retry() { - setTimeout(() => waitForReady(resolve, reject, retriesLeft - 1), 1000); + // 已就绪/已超时后不再轮询,防止反复打印日志 + setTimeout(() => { + if (!_fastapiReadySettled) waitForReady(resolve, reject, retriesLeft - 1); + }, 1000); } } @@ -273,14 +304,16 @@ function stopFastAPI() { } // ==================== 窗口管理 ==================== -async function createMainWindow() { +async function createMainWindow(themeMode) { + themeMode = themeMode || getThemeMode(); + mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, title: 'minor Agent', - backgroundColor: '#1a1a2e', + backgroundColor: themeMode === 'dark' ? '#1a1a2e' : '#eef1f7', frame: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), @@ -294,7 +327,9 @@ async function createMainWindow() { await mainWindow.webContents.session.clearCache(); await mainWindow.webContents.session.clearStorageData({ storages: ['caches', 'serviceworkers'] }); - mainWindow.loadURL(BASE_URL); + // 先显示本地过渡页:毛玻璃 + 中央图标(无任何按钮),主题随 theme_config.json 变化; + // FastAPI 就绪后再切到应用页;应用页自带的同款玻璃覆盖层会无缝接手 + mainWindow.loadFile(path.join(__dirname, 'splash.html'), { query: { theme: themeMode } }); mainWindow.once('ready-to-show', () => mainWindow.show()); @@ -304,6 +339,26 @@ async function createMainWindow() { require('electron').shell.openExternal(url).catch(() => {}); return { action: 'deny' }; }); + + return mainWindow; +} + +// 启动主流程:立即弹出毛玻璃窗口 → 后台启动 FastAPI → 就绪后加载应用页 +async function launchMainFlow() { + const themeMode = getThemeMode(); + try { + // 1. 创建并显示主窗口(本地过渡页,秒开,无按钮),主题随 theme_config.json + await createMainWindow(themeMode); + // 2. 后台启动 FastAPI(窗口已先显示,用户不会盯着空白等待) + await startFastAPI(); + if (!mainWindow) return; // 启动期间用户已关闭窗口(window-all-closed 会走退出流程) + // 3. 后端就绪后加载应用页;带上主题参数,保证应用页首帧即为当前主题(避免深色闪屏), + // 页面加载完成后由前端播放玻璃层淡出动画 + mainWindow.loadURL(`${BASE_URL}?theme=${themeMode}`); + } catch (err) { + dialog.showErrorBox('启动失败', `无法启动 minor Agent:\n${err.message}`); + app.quit(); + } } // ==================== 应用生命周期 ==================== @@ -342,14 +397,8 @@ app.whenReady().then(async () => { // 首次运行:打开配置窗口(Python 环境安装 + LLM 配置) createSetupWindow(); } else { - // 非首次:直接启动 FastAPI 和主窗口 - try { - await startFastAPI(); - await createMainWindow(); - } catch (err) { - dialog.showErrorBox('启动失败', `无法启动 minor Agent:\n${err.message}`); - app.quit(); - } + // 非首次:立即显示毛玻璃启动窗口,同时后台启动 FastAPI 和主流程 + await launchMainFlow(); } }); @@ -376,13 +425,9 @@ ipcMain.on('launch-app', async () => { setupWindow = null; } - // 启动 FastAPI 并创建主窗口 + // 立即显示毛玻璃启动窗口,后台启动 FastAPI 并加载应用页 try { - await startFastAPI(); - await createMainWindow(); - } catch (err) { - dialog.showErrorBox('启动失败', `无法启动 minor Agent:\n${err.message}`); - app.quit(); + await launchMainFlow(); } finally { isLaunching = false; } @@ -888,7 +933,7 @@ app.on('before-quit', () => { app.on('activate', () => { if (mainWindow === null) { - createMainWindow(); + launchMainFlow(); } }); diff --git a/electron/splash.html b/electron/splash.html new file mode 100644 index 0000000..1a69845 --- /dev/null +++ b/electron/splash.html @@ -0,0 +1,29 @@ + + + + + + minor Agent + + + + + + +
+ + + + + +
+ + diff --git a/src/agent/config/workspace_config.json b/src/agent/config/workspace_config.json index 4e10a96..ec64eee 100644 --- a/src/agent/config/workspace_config.json +++ b/src/agent/config/workspace_config.json @@ -1,6 +1,8 @@ { - "current": "", + "current": "C:\\Users\\86166\\Desktop\\新建文件夹", "list": [ + "C:\\Users\\86166\\Desktop\\新建文件夹", + "C:\\Users\\86166\\Desktop", "C:\\Users\\86166\\Desktop\\Agent_Learning_minor\\Agent\\src\\workspace" ], "access_mode": "full" diff --git a/src/agent/history/usage_stats.json b/src/agent/history/usage_stats.json index fecc345..1820b38 100644 --- a/src/agent/history/usage_stats.json +++ b/src/agent/history/usage_stats.json @@ -1 +1 @@ -{"hours": {"2026-08-12T22": {"total": 41613, "by_model": {"Qwen3.5-122B-A10B": 41613}}}} \ No newline at end of file +{"hours": {"2026-08-12T22": {"total": 41613, "by_model": {"Qwen3.5-122B-A10B": 41613}}, "2026-08-14T09": {"total": 535476, "by_model": {"Qwen3.5-122B-A10B": 535476}}, "2026-08-14T10": {"total": 91339, "by_model": {"Qwen3.5-122B-A10B": 91339}}, "2026-08-14T11": {"total": 67389, "by_model": {"Qwen3.5-122B-A10B": 67389}}}} \ No newline at end of file diff --git a/src/web/app.js b/src/web/app.js index 5836b00..c769cff 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -1018,6 +1018,18 @@ if (window.electronAPI?.minimizeWindow) { initTheme(); initI18n(); +// ===== 启动玻璃层过渡(毛玻璃 + 中央图标) ===== +// 加载完成后:图标向下滑出、毛玻璃渐变消失,露出完整应用界面 +function hideAppSplash() { + const splash = document.getElementById("appSplash"); + if (!splash || splash.classList.contains("app-splash--out")) return; + splash.classList.add("app-splash--out"); + // 动画结束后移除覆盖层(放行对底层界面的交互) + setTimeout(() => { + if (splash.parentNode) splash.parentNode.removeChild(splash); + }, 800); +} + // ===== 启动 ===== bootstrap().then(() => { initTokenRing(); @@ -1028,10 +1040,15 @@ bootstrap().then(() => { initThinkLevel(); loadNickname(); initServicesStatus(); + hideAppSplash(); }).catch((e) => { const chatPlaceholder = $("chatPlaceholder"); if (chatPlaceholder) { chatPlaceholder.hidden = false; chatPlaceholder.innerHTML = `

无法连接服务

${escapeHtml(e.message)}

请从 Agent 目录执行:uvicorn web.server:app --reload --host 127.0.0.1 --port 8765,并保证 PYTHONPATH 包含 src。

`; } + hideAppSplash(); }); + +// 兜底:若前端初始化异常导致 bootstrap 永不完成,超时后强制淡出玻璃层 +setTimeout(hideAppSplash, 12000); diff --git a/src/web/css/pending-overlay.css b/src/web/css/pending-overlay.css index 6610b0c..ad08ddb 100644 --- a/src/web/css/pending-overlay.css +++ b/src/web/css/pending-overlay.css @@ -14,6 +14,7 @@ border-radius: var(--radius, 12px); box-shadow: 0 8px 32px var(--overlay-bg); border: 1px solid var(--border); + border-top: 2px solid var(--accent); overflow: hidden; transition: all 0.3s ease; color: var(--text); @@ -88,6 +89,7 @@ border-radius: 10px; overflow: hidden; background: var(--surface-hover); + box-shadow: 0 1px 4px var(--shadow); transition: opacity 0.3s; } @@ -96,6 +98,11 @@ pointer-events: none; } +.pending-overlay-card--resolved .pending-overlay-btn, +.pending-overlay-card--resolved .pending-overlay-send-btn { + pointer-events: none; +} + .pending-overlay-card-header { display: flex; align-items: center; @@ -149,9 +156,9 @@ /* 工作空间越界审批提示(访问策略 policy_note) */ .pending-overlay-note { font-size: 0.78rem; - color: #b45309; - background: color-mix(in srgb, #f59e0b 12%, transparent); - border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent); + color: var(--warning, #b45309); + background: var(--warning-bg, color-mix(in srgb, #f59e0b 12%, transparent)); + border: 1px solid color-mix(in srgb, var(--warning, #f59e0b) 45%, transparent); border-radius: 8px; padding: 0.4rem 0.55rem; margin-bottom: 0.5rem; @@ -160,7 +167,7 @@ word-break: break-all; } -/* ===== tool_call 卡片:参数只读展示 ===== */ +/* ===== tool_call 卡片:参数摘要只读展示 ===== */ .pending-overlay-tool-name { font-size: 0.8rem; font-weight: 600; @@ -169,18 +176,19 @@ } .pending-overlay-tool-args { - font-size: 0.76rem; + font-size: 0.78rem; color: var(--text); - background: color-mix(in srgb, var(--text) 6%, transparent); - border: 1px solid var(--border); + background: color-mix(in srgb, var(--accent) 5%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); border-radius: 8px; padding: 0.4rem 0.55rem; margin-bottom: 0.5rem; white-space: pre-wrap; word-break: break-all; - max-height: 160px; + max-height: 120px; overflow-y: auto; font-family: var(--font-family, inherit); + line-height: 1.55; } /* ===== 按钮行(同意 / 拒绝 / 跳过)===== */ @@ -206,6 +214,16 @@ .pending-overlay-btn:active { transform: translateY(1px); } +.pending-overlay-btn:focus-visible, +.pending-overlay-send-btn:focus-visible, +.pending-overlay-submit-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.pending-overlay-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} .pending-overlay-btn--approve { background: var(--accent); diff --git a/src/web/css/splash.css b/src/web/css/splash.css new file mode 100644 index 0000000..08e285c --- /dev/null +++ b/src/web/css/splash.css @@ -0,0 +1,130 @@ +/* splash.css — 启动玻璃层(毛玻璃 + 中央图标) + * + * 被两个页面共用: + * - electron/splash.html(本地过渡页,FastAPI 就绪前显示,file:// 加载) + * - index.html 的 #appSplash 覆盖层(应用页加载完成后淡出,露出完整界面) + * + * 主题随动: + * - splash.html 由 main.js 读取 theme_config.json 后,通过 query 注入 + * theme=dark/light,页面在 上挂 theme-dark / theme-light 类 + * - index.html 由 themes.js 在 上挂 theme-dark / theme-light 类 + * 两类名都通过 --splash-* 自定义属性切换毛玻璃底色与图标颜色。 + * + * 退出动画(通过 .app-splash--out 触发): + * - 图标向下滑出消失 + * - 毛玻璃层透明度、模糊度渐变消失,底层内容由模糊变清晰 + */ + +/* 默认按深色主题(与应用默认 vscode-dark、窗口默认底色一致) */ +:root { + --splash-tint: rgba(8, 10, 20, 0.62); + --splash-icon-filter: invert(1) drop-shadow(0 6px 16px rgba(0, 0, 0, 0.5)); + --splash-page-bg: radial-gradient(1200px 800px at 30% 20%, #2b2f57 0%, #1a1a2e 45%, #10101f 100%); +} +.theme-light { + --splash-tint: rgba(236, 240, 248, 0.55); + --splash-icon-filter: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.25)); + --splash-page-bg: radial-gradient(1200px 800px at 30% 20%, #dfe5f2 0%, #eef1f7 45%, #ffffff 100%); +} + +/* 本地过渡页(electron/splash.html)的底色,供玻璃层模糊 */ +.splash-page { + margin: 0; + min-height: 100%; + min-height: 100dvh; + background: var(--splash-page-bg); +} + +/* 全屏毛玻璃层 */ +.app-splash { + position: fixed; + inset: 0; + z-index: 100000; /* 盖住自绘标题栏(z-index: 9999)与全部界面,加载期间无任何按钮 */ + display: flex; + align-items: center; + justify-content: center; + background: var(--splash-tint); + -webkit-backdrop-filter: blur(24px) saturate(130%); + backdrop-filter: blur(24px) saturate(130%); + opacity: 1; + pointer-events: auto; + overflow: hidden; + -webkit-app-region: drag; /* 无边框窗口:玻璃层可拖动 */ + transition: + opacity 0.65s ease, + -webkit-backdrop-filter 0.65s ease, + backdrop-filter 0.65s ease; + will-change: opacity, backdrop-filter; +} + +/* 斜向光带扫过(“刷新”质感) */ +.app-splash::before { + content: ""; + position: absolute; + top: -60%; + left: -30%; + width: 60%; + height: 220%; + background: linear-gradient( + 105deg, + transparent 42%, + rgba(255, 255, 255, 0.1) 50%, + transparent 58% + ); + transform: rotate(12deg) translateX(-160%); + animation: splash-sweep 2.4s ease-in-out infinite; + pointer-events: none; +} +@keyframes splash-sweep { + 0% { + transform: rotate(12deg) translateX(-160%); + } + 55%, + 100% { + transform: rotate(12deg) translateX(320%); + } +} + +/* 中央图标(无边框/无卡片,直接浮在毛玻璃上;颜色随主题反色) */ +.app-splash-icon { + width: 120px; + height: 120px; + filter: var(--splash-icon-filter); + user-select: none; + animation: splash-breathe 2.2s ease-in-out infinite; +} +@keyframes splash-breathe { + 0%, + 100% { + transform: translateY(0) scale(1); + } + 50% { + transform: translateY(-6px) scale(1.02); + } +} + +/* 退出动画:玻璃层淡出 + 图标向下滑出 */ +.app-splash--out { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); + pointer-events: none; +} +.app-splash--out .app-splash-icon { + animation: splash-icon-out 0.7s cubic-bezier(0.55, 0, 0.85, 0.36) forwards; +} +@keyframes splash-icon-out { + to { + transform: translateY(170px) scale(0.96); + opacity: 0; + } +} + +/* 跟随系统“减少动态效果”偏好 */ +@media (prefers-reduced-motion: reduce) { + .app-splash, + .app-splash::before, + .app-splash-icon { + animation: none; + } +} diff --git a/src/web/index.html b/src/web/index.html index e8ba948..842a839 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -3,12 +3,29 @@ + + + +