diff --git a/docs/Agent网络工具通用设计-v2.md b/docs/Agent网络工具通用设计-v2.md new file mode 100644 index 0000000..d73ddbf --- /dev/null +++ b/docs/Agent网络工具通用设计-v2.md @@ -0,0 +1,946 @@ +# Agent 网络工具通用设计(增强版) + +> 本文档描述 Agent 网络工具链的整体设计与优化方案,涵盖 SearXNG 元搜索配置、web_search 搜索、web_fetch 抓取、browser 浏览器控制四个核心模块。文档聚焦架构、配置与接口,不绑定具体编程语言与框架实现,并给出各环节可引入的成熟第三方库建议。 + +## 目录 + +1. [SearXNG 配置设计](#1-searxng-配置设计) +2. [web_search 搜索设计](#2-web_search-搜索设计) +3. [web_fetch 抓取设计](#3-web_fetch-抓取设计) +4. [browser 浏览器设计](#4-browser-浏览器设计) +5. [第三方库选型指南](#5-第三方库选型指南) +6. [优化增强路线图](#6-优化增强路线图) + +--- + +## 1. SearXNG 配置设计 + +### 1.1 架构概览 + +SearXNG 是开源元搜索引擎,支持 70+ 搜索引擎聚合,不追踪用户、不记录隐私。本设计将 SearXNG 作为**可选替代方案**:启用后通过 SearXNG 的 JSON 或 HTML API 进行搜索;关闭时回退到内置四引擎 HTML 解析方案(Bing + 百度 + 搜狗 + 360 搜索)。 + +整体分为三层。**UI 层**负责配置模态框的交互与表单同步;**数据层**负责通过本地数据库读写配置项,键值对形式持久化;**执行层**位于主进程,在搜索请求到来时读取配置,决定走 SearXNG 通道还是内置引擎通道,并完成参数拼装、认证注入、请求发送与结果解析。 + +``` +┌─────────────────────────────────────────────────────┐ +│ UI 层(Renderer) │ +│ 配置模态框 / 表单控件 / 状态指示 │ +├─────────────────────────────────────────────────────┤ +│ 数据层(SQLite / LocalStorage) │ +│ 键值对持久化 / 运行时缓存 / 配置校验 │ +├─────────────────────────────────────────────────────┤ +│ 执行层(Main Process) │ +│ 参数拼装 → 认证注入 → API 调用 → 结果解析 │ +│ 双模式切换:SearXNG / 内置四引擎 │ +└─────────────────────────────────────────────────────┘ +``` + +### 1.2 配置项定义 + +SearXNG 相关配置共 12 项,各配置项的默认值与含义如下: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `enabled` | boolean | `false` | 是否启用 SearXNG | +| `url` | string | `''` | SearXNG 实例根地址(不含 `/search` 路径) | +| `engines` | string | `''` | 引擎列表,逗号分隔,留空则使用实例默认引擎 | +| `language` | string | `'zh-CN'` | 搜索语言 | +| `safesearch` | number | `1` | 安全搜索级别(0=关闭,1=中等,2=严格) | +| `time_range` | string | `''` | 时间范围过滤(day/week/month/year) | +| `max_results` | number | `0` | 单次最大结果数,0 表示使用默认 | +| `auth_key` | string | `''` | 认证密钥,内容随认证类型不同而不同 | +| `auth_type` | string | `'bearer'` | 认证类型(bearer 或 basic) | +| `format` | string | `'json'` | 返回格式(json 或 html) | +| `fetch_count` | number | `0` | 自动抓取条数,0 表示由 AI 决定 | +| `fetch_mode` | string | `'sequential'` | 抓取类型(sequential 顺序或 random 随机) | + +### 1.3 持久化存储 + +配置通过本地数据库以键值对形式持久化。写入时逐项调用 `saveSetting(key, value)`,读取时通过 `getSetting(key, default)` 回填到运行时缓存。对应的存储键名汇总如下: + +| 存储键名 | 对应配置项 | 默认值 | +|----------|-----------|--------| +| `searxng_enabled` | enabled | `false` | +| `searxng_url` | url | `''` | +| `searxng_engines` | engines | `''` | +| `searxng_language` | language | `'zh-CN'` | +| `searxng_safesearch` | safesearch | `1` | +| `searxng_time_range` | time_range | `''` | +| `searxng_max_results` | max_results | `0` | +| `searxng_auth_key` | auth_key | `''` | +| `searxng_auth_type` | auth_type | `'bearer'` | +| `searxng_format` | format | `'json'` | +| `fetch_count` | fetch_count | `0` | +| `fetch_mode` | fetch_mode | `'sequential'` | + +### 1.4 UI 模态框 + +配置界面以模态框形式呈现,包含以下交互控件。启用开关为实时保存,切换即写入数据库;其余字段在点击保存按钮时统一校验并落库。URL 字段需通过非空校验与 `http/https` 格式校验。 + +- **启用开关**:实时切换 SearXNG 启用状态,立即持久化 +- **API 地址**:实例根地址,必填,需为 `http/https` 格式 +- **搜索引擎**:逗号分隔的引擎名称,留空使用实例默认 +- **语言选择**:下拉项含 zh-CN / zh-TW / en / ja / ko / 自动检测 +- **安全搜索**:下拉项含 0 关闭 / 1 中等 / 2 严格 +- **时间范围**:下拉项含 不限 / 一天 / 一周 / 一月 / 一年 +- **结果数量**:数字输入,0 表示使用默认 +- **返回格式**:下拉项含 JSON 结构化解析 / HTML 原始网页 +- **自动抓取条数**:数字输入,范围 1–8 +- **抓取类型**:下拉项含 顺序抓取 / 随机抓取 +- **认证设置**:认证类型(bearer / basic)+ 认证密钥(密码型输入框) +- **保存按钮**:校验后批量持久化全部字段 +- **连接测试按钮**(增强):一键验证 URL 可达性与认证有效性 + +工具栏入口按钮上设状态指示圆点:启用时为绿色,未启用时为灰色;模态框内同时显示文字状态徽章(已启用 / 未启用)。 + +### 1.5 SearXNG 认证方式详解 + +SearXNG 实例可配置鉴权,本设计支持两种认证类型,由 `auth_type` 配置项决定,认证密钥统一存放在 `auth_key` 中。两种方式的核心区别在于请求头的构造方式与密钥的填写格式。 + +#### Bearer Token 认证(`auth_type = 'bearer'`,默认值) + +当认证类型为 bearer 时,系统在 HTTP 请求头中添加: + +``` +Authorization: Bearer +``` + +密钥原样放入 token 位置,不做任何编码转换。`auth_key` 应直接填写 SearXNG 实例签发的访问令牌本身。 + +**适用场景**: +- SearXNG 实例通过反向代理(如 Nginx/Caddy)签发的固定令牌鉴权 +- JWT(JSON Web Token)格式的令牌 +- OAuth2 Bearer Token +- 任何基于 Token 的简单鉴权方案 + +**安全要求**:建议配合 HTTPS 使用,防止令牌在网络传输中被截获。 + +#### Basic 认证(`auth_type = 'basic'`) + +当认证类型为 basic 时,系统将 `auth_key` 整体进行 Base64 编码,再在请求头中添加: + +``` +Authorization: Basic +``` + +`auth_key` 应填写 `用户名:密码` 格式的明文字符串,由系统负责编码。 + +**适用场景**: +- SearXNG 实例配置了 HTTP Basic Auth 的场景 +- Nginx/Caddy 反向代理的 `auth_basic` 指令保护 +- 任何标准用户名密码鉴权网关 + +**安全要求**:必须配合 HTTPS 使用。Basic Auth 以 Base64 编码传输凭据(非加密),明文网络可还原。 + +#### 两种方式对比 + +| 维度 | Bearer | Basic | +|------|--------|-------| +| 请求头格式 | `Authorization: Bearer ` | `Authorization: Basic ` | +| 密钥填写内容 | 令牌原值 | `用户名:密码` 明文串 | +| 是否编码 | 否,原样透传 | 是,整体 Base64 编码 | +| 适用场景 | 令牌/JWT/OAuth2 鉴权 | 用户名密码鉴权 | +| 安全要求 | 建议 HTTPS | 必须 HTTPS | +| 凭据可复用性 | 高(无状态令牌) | 低(每次携带凭据) | +| 撤销方式 | 令牌失效/黑名单 | 修改密码 | + +无论哪种方式,仅当 `auth_key` 非空时才会注入 `Authorization` 头;`auth_key` 为空则视为匿名访问,不附加任何认证信息。认证逻辑对 JSON 与 HTML 两种返回格式均生效。 + +### 1.6 主进程搜索调用流程 + +搜索请求到达主进程后,执行层按以下步骤处理: + +1. **读取配置**:从数据库加载全部 SearXNG 相关配置项 +2. **构建参数**: + - 查询词、引擎列表、语言、安全搜索级别拼入查询串 + - 时间范围优先使用调用方传入值,缺省时回退到配置项默认值 + - 最大结果数取配置项与入参的较大有效值 +3. **认证注入**:根据 `auth_type` 构造对应的 `Authorization` 请求头 +4. **发送请求**:向 `/search?<参数>` 发起 HTTP GET 请求 +5. **解析结果**: + - JSON 模式:结构化解析每条结果的标题、URL、摘要、来源引擎 + - HTML 模式:转为纯文本交由 AI 自行分析 +6. **统计引擎状态**:记录各引擎命中条数与异常原因 + +### 1.7 双模式切换逻辑 + +在统一的搜索入口处,通过读取 `searxng_enabled` 判断走哪条通道: + +- **SearXNG 启用时**: + - 自动抓取条数从面板配置读取,取配置项与默认下限的较大值,限制在最大抓取上限内 + - 抓取类型为顺序或随机 + - 日志标记来源为 `[SearXNG]` +- **未启用时**: + - 走内置四引擎并行搜索通道 + - 日志标记来源为 `[内置]` + +--- + +## 2. web_search 搜索设计 + +### 2.1 架构概览 + +web_search 是搜索工具的总入口,内部根据 SearXNG 开关分流到两条通道。SearXNG 通道走元搜索 API;内置通道则并行调度四个搜索引擎,经合并去重、可达性预检、智能排序、摘要增强与缓存后输出结果。无论哪条通道,最终都会进入自动抓取阶段,对前 N 条结果抓取完整内容。 + +``` +web_search 入口 + ├── searxng_enabled? + │ ├── 是 → SearXNG API 搜索 + │ └── 否 → 内置四引擎并行搜索 + │ ├── Bing (权重90) + │ ├── 百度 (权重80) + │ ├── 搜狗 (权重75) + │ └── 360搜索 (权重75) + │ → Promise.allSettled 并行容错 + │ → 合并去重(URL 标准化) + │ → 可达性预检(并发5) + │ → 智能排序(可达性+权重+摘要质量) + │ → 摘要自动增强(web_fetch top3) + │ → LRU 缓存(5分钟TTL) + └── 自动抓取前N条完整内容 + → 相关性过滤 → 逐条抓取 → 失败随机补充 +``` + +### 2.2 函数参数 + +web_search 接受以下参数: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `query` | string | 必填 | 搜索关键词 | +| `max_results` | number | 30 | 最大结果数,上限 30 | +| `time_range` | string | — | 时间过滤(day/week/month/year 等,支持中英文别名) | +| `enhance_snippets` | boolean | true | 是否自动增强过短摘要 | +| `fetch_top` | number | 5 | 自动抓取前 N 条完整内容,范围 3–8 | + +### 2.3 内置四引擎并行搜索 + +内置通道调度四个搜索引擎,每个引擎带有权重,用于后续排序。四个引擎并行执行,采用容错并发模型,任一引擎失败不影响其余引擎。 + +| 引擎 | 权重 | 请求地址 | 特殊处理 | +|------|------|---------|---------| +| Bing | 90 | `bing.com/search` | 支持 freshness 参数映射时间范围 | +| 百度 | 80 | `baidu.com/s` | 从 `data-url` 属性提取真实 URL | +| 搜狗 | 75 | `sogou.com/web` | 过滤 sogou.com 自身链接 | +| 360 搜索 | 75 | `so.com/s` | 过滤 so.com 自身链接 | + +时间范围参数映射规则: + +| 用户输入 | Bing 参数 | 说明 | +|----------|----------|------| +| `day` / `1d` / `一天` | `Day` | 最近一天 | +| `week` / `1w` / `一周` | `Week` | 最近一周 | +| `month` / `1m` / `一月` | `Month` | 最近一月 | + +### 2.4 搜索引擎 HTML 解析 + +每个引擎返回的是 HTML 页面,需要专门的解析器从中提取标题、URL、摘要三要素。四个解析器的提取策略各有差异,但整体思路一致:先以块级结构切分结果条目,再从条目内提取标题链接与摘要文本,最后过滤掉引擎自身的导航类链接。 + +- **Bing 解析**:按 `
  • ` 块切分,从锚点提取标题与 URL,从 `

    ` 或 `.b_caption` 提取描述 +- **百度解析**:按 `

    ` 块切分,标题取自 `

    ` 锚点,真实 URL 取自 `data-url` 属性,摘要取自 `.c-abstract` 或 `.content-right_*` +- **搜狗解析**:按 `.vrwrap` 或 `.rb` 块切分,摘要取自多个候选区域(`.star-wiki` / `.space-txt` / `.str_info`),过滤 sogou.com 自身链接 +- **360 搜索解析**:按 `.res-list` 或 `.result` 块切分,摘要取自 `.res-desc` / `.res-rich` / `.res-summary` / `
    `,过滤 so.com 自身链接 + +> **[增强建议]** 各引擎解析器可迁移至基于 CSS 选择器的 DOM 解析方案(如 Cheerio),替代当前的正则匹配,提升鲁棒性与维护性。详见第 5 章「第三方库选型指南」。 + +### 2.5 智能排序算法 + +合并去重后,对结果计算综合评分并排序。评分由三部分加权构成: + +$$ +\text{score} = \underbrace{\frac{\text{weight}}{100} \times 50}_{\text{引擎权重 50\%}} + \underbrace{\text{可达性加分}}_{\text{可达 +30 / 不可达 -20}} + \underbrace{\frac{\min(\text{snippetLen}, 100)}{100} \times 20}_{\text{摘要质量 20%(上限100字符)}} +$$ + +最终按评分降序排列。 + +### 2.6 URL 可达性预检 + +为剔除死链,排序前对候选结果做可达性预检。采用并发批次控制策略: + +- **并发数**:每批 5 个 URL +- **超时**:每个请求 3 秒 +- **判断标准**:仅判断响应是否成功(HTTP 2xx),不读取正文 +- **结果回填**:写入结果的 `reachable` 字段(`true` / `false`) + +### 2.7 摘要自动增强 + +当 `enhance_snippets` 开启时,对排序前列中摘要过短(< 30 字符)且可达的结果,调用 web_fetch 补充抓取其正文,取前 200 字符替换原摘要。该增强最多对前 3 条执行,避免过度请求。增强后的结果打上 `_enhanced: true` 标记。 + +### 2.8 自动抓取完整内容 + +搜索完成后,对前 N 条结果自动抓取完整正文,供 AI 综合分析。完整流程: + +1. **相关性过滤**:从查询词中提取中文双字/三字片段与英文单词,在标题与摘要中匹配计分,得分上限 100,得分为 0 的结果直接过滤 +2. **构建抓取列表**: + - sequential 模式:按相关性降序取前 N 条 + - random 模式:Fisher-Yates 洗牌后取 N 条 +3. **逐条抓取**:调用 web_fetch,失败时从剩余未抓取结果中随机选 1 条补充重试 +4. **结果存入**:`_fetched` 数组 + `_fetched_count` 计数 + +### 2.9 LRU 搜索缓存 + +为降低重复查询的开销,搜索结果带 LRU 缓存。 + +| 参数 | 值 | +|------|-----| +| 最大容量 | 200 条 | +| 过期时间 | 5 分钟(300,000ms) | +| 缓存键 | 查询词(归一化后) | +| 淘汰策略 | 最早写入淘汰 | +| 命中时行为 | 校验时效,过期删除并返回未命中 | + +> **[增强建议]** 可替换为成熟的 `lru-cache` 库,支持 TTL、大小限制、统计指标等开箱能力。详见第 5 章。 + +### 2.10 响应格式 + +web_search 返回结构化结果,主要字段如下: + +```json +{ + "success": true, + "query": "搜索关键词", + "results": [ + { + "title": "标题", + "url": "https://...", + "snippet": "摘要文本", + "engine": "bing", + "weight": 90, + "reachable": true, + "_score": 68.5, + "_enhanced": false + } + ], + "total": 15, + "formatted": "人类可读文本汇总", + "structured": [...], + "from_cache": false, + "engine_stats": { "bing": "8 条", "百度": "5 条" }, + "_mode": "builtin | searxng", + "_fetched": [{ "url", "title", "content" }], + "_fetched_count": 5 +} +``` + +--- + +## 3. web_fetch 抓取设计 + +### 3.1 架构概览 + +web_fetch 采用**三阶段回退策略**,确保最大概率获取网页内容: + +``` +Phase 1: HTTP 抓取 +├── UA 轮换池(5种)+ Accept-Language 轮换池(3种) +├── 指数退避重试(2s→4s→6s + jitter ±60%) +├── 反爬请求头(Sec-Fetch-* / DNT / Referer 等) +├── 流式读取(10MB 上限) +└── 拦截检测(10 种特征模式) + ↓ 失败/被拦截/内容过短 +Phase 2: SPA 自动升级 +└── HTML 内容 < 200 字符 → 升级浏览器渲染 + ↓ 失败 +Phase 3: 浏览器回退 +├── 打开隐藏窗口 → 等待 2.5s JS 渲染 +├── 提取页面正文 → 关闭窗口 +└── LRU 缓存(100 条, 10 分钟 TTL) +``` + +### 3.2 函数参数 + +web_fetch 接受以下参数: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `url` | string | 必填 | 目标 URL,仅支持 http/https | +| `max_chars` | number | — | 最大字符数,0 表示不截断 | +| `extract_mode` | string | — | 提取模式 | +| `mobile_ua` | boolean | — | 是否使用移动端 UA | +| `retry` | boolean | true | 是否启用重试 | + +### 3.3 反爬请求头 + +为规避基础反爬,HTTP 抓取阶段采用请求头轮换策略。 + +**UA 轮换池(5 种)**:覆盖桌面 Chrome、Edge、Safari、Firefox 与移动端 Chrome,模拟主流浏览器指纹。 + +**Accept-Language 轮换池(3 种)**:中英混合、中文优先、英文优先三种语言偏好。 + +每次重试按尝试序号取模轮换 UA 与语言,同时附带完整的反爬请求头集合: + +| 请求头 | 值 | 用途 | +|--------|-----|------| +| `User-Agent` | 轮换池取模 | 浏览器指纹伪装 | +| `Accept` | `text/html,application/xhtml+xml,...` | 声明接受内容类型 | +| `Accept-Language` | 轮换池取模 | 语言偏好伪装 | +| `Accept-Encoding` | `gzip, deflate, br` | 声明压缩格式 | +| `Cache-Control` | `no-cache` | 禁止缓存 | +| `DNT` | `1` | Do Not Track | +| `Referer` | 目标站点根地址 | 来源页伪装 | +| `Sec-Fetch-Dest` | `document` | 导航目标声明 | +| `Sec-Fetch-Mode` | `navigate` | 请求模式声明 | +| `Sec-Fetch-Site` | `none` | 来源站点声明 | +| `Sec-Fetch-User` | `?1` | 用户触发声明 | +| `Pragma` | `no-cache` | HTTP/1.0 兼容 | + +> **[增强建议]** 可引入 `got-scraping` 库,其内置了更完善的浏览器指纹模拟、Cookie Jar 管理、HTTP/2 支持与代理集成能力,详见第 5 章。 + +### 3.4 重试配置 + +| 参数 | 值 | +|------|-----| +| 最大重试次数 | 3 次(Phase 1 内部) | +| 基础退避间隔 | 2s → 4s → 6s | +| Jitter 范围 | 基础延迟 × [0%, +60%] 随机 | +| 直接跳过重试的状态码 | 403 / 503 / 429 / 502 | +| 可重试状态码 | 5xx(除上述外) | + +### 3.5 HTML 转文本引擎 + +抓取到的 HTML 需转为纯文本。处理流水线: + +1. **移除噪声标签及内容**:script、style、noscript、nav、header、footer、aside、iframe、svg +2. **移除 HTML 注释** +3. **块级标签转行**:p、div、h1-h6、li、tr、blockquote、section、article、pre、br、hr → `\n` +4. **表格单元制表符**:td、th → `\t` +5. **移除剩余标签** +6. **解码 HTML 实体**:30+ 命名实体(` `、`<`、`&`、`…`、`—` 等)+ 全部数字实体(`{`、``) +7. **清理空白**:合并连续空白、清理首尾空行 + +> **[增强建议]** 可引入 `@mozilla/readability` 作为前置步骤,先提取文章主体内容再做文本转换,大幅减少导航/广告/侧边栏等噪声;或使用 `turndown` 将 HTML 转 Markdown 保留结构。详见第 5 章。 + +### 3.6 拦截页面检测 + +维护一组拦截特征模式,命中任一特征即判定为拦截页,触发浏览器回退: + +| 特征模式 | 对应防护 | +|----------|---------| +| `Just a moment...` / Cloudflare 标题 | Cloudflare JS Challenge | +| `Attention Required!` | Cloudflare 托管挑战 | +| `challenge-platform` | Cloudflare 平台标识 | +| `.cf-challenge-` 类名 | Cloudflare Challenge 页面 | +| `Access Denied` 标题 | WAF 拒绝 | +| `403 Forbidden` 标题 | HTTP 403 拦截 | +| 请启用 JavaScript | JS 强制检测 | +| Please enable JavaScript | JS 强制检测(英文) | +| Checking your browser | 浏览器环境检测 | +| DDoS protection | DDoS 防护页 | +| 正文长度 < 80 字符 | 空白/空壳页面 | + +### 3.7 内容过短自动升级 + +对于成功返回但正文过短(< 200 字符)的页面——通常是 SPA 单页应用或空壳页面,其有效内容依赖 JS 渲染——自动升级到浏览器渲染通道。若渲染成功则用渲染结果替换,方法标记为 `browser`。 + +### 3.8 浏览器回退实现 + +浏览器回退通道的完整流程: + +1. **查缓存**:LRU 缓存(100 条,10 分钟 TTL),命中则截断返回 +2. **打开窗口**:隐藏 BrowserWindow 加载目标 URL +3. **等待渲染**:2.5秒让 SPA/动态内容完成 JS 渲染 +4. **提取正文**:移除非内容标签后取 innerText +5. **关闭窗口**:确保资源释放 +6. **写缓存**:正文 ≥ 100 字符时写入缓存 + +异常保障:任何步骤出错均确保调用关闭函数,避免窗口泄漏。 + +### 3.9 触发浏览器回退的条件 + +| 条件 | 说明 | Phase 1 → Phase 3 | +|------|------|-------------------| +| HTTP 403/503/429/502 | 反爬/限流/拦截信号 | ✅ 直接跳转 | +| 拦截特征匹配 | Cloudflare/验证码/JS 要求等 | ✅ 直接跳转 | +| 内容 < 200 字符 | SPA 或空壳页面 | ✅ 经 Phase 2 升级 | +| 请求超时 | 超过 HTTP 超时阈值 | ✅ 直接跳转 | +| 所有重试均失败 | 网络不可达等 | ✅ 直接跳转 | + +### 3.10 流式读取与大文件保护 + +| 参数 | 值 | +|------|-----| +| 硬上限 | 10 MB | +| 无 Content-Length 时 | 流式读取,累计字节数 | +| 超限处理 | 中止下载,返回错误 | +| 有 Content-Length 时 | 预判是否超限 | + +### 3.11 超时封装 + +HTTP 抓取通过带超时的封装发起请求:创建 AbortController,设定超时定时器,超时即中止请求。该封装同时被搜索阶段的可达性预检复用。 + +--- + +## 4. browser 浏览器设计 + +### 4.1 架构概览 + +browser 模块基于隐藏浏览器窗口实现,提供完整的网页加载、截图、JS 执行、内容提取与交互操作能力。整个模块封装为**单例窗口**,包含 9 个核心函数。 + +| 函数 | 职责 | 关键参数 | +|------|------|---------| +| `browserOpen` | 打开 URL | url, waitSelector | +| `browserScreenshot` | 截图 | fullPage, selector(视口/元素/全页三模式) | +| `browserEvaluate` | 执行 JS | js(任意代码) | +| `browserExtract` | 提取内容 | selector, maxChars | +| `browserClick` | 点击元素 | selector, wait | +| `browserType` | 输入文本 | selector, text, clear, submit | +| `browserScroll` | 滚动页面 | direction, selector | +| `browserWait` | 等待条件 | selector, timeMs | +| `browserClose` | 关闭窗口 | — | + +**窗口默认参数**:1280×800、不可见(show:false)、上下文隔离、沙箱模式、页面加载超时 30s。 + +### 4.2 核心状态管理 + +| 维度 | 设计决策 | +|------|---------| +| 实例模式 | 单例懒加载,首次调用时创建 | +| 销毁方式 | browserClose() 显式销毁 | +| 应用退出 | 统一调用 browserClose() 清理 | +| 就绪检查 | 操作前校验窗口存在且已加载页面 | +| 安全配置 | 禁用 Node.js 集成、上下文隔离、沙箱、允许跨域(截图需要) | + +### 4.3 打开 URL + +- 若已有窗口加载了不同 URL → 先关闭重建,避免历史状态干扰 +- 加载目标 URL,30 秒超时 +- 可选等待指定 CSS 选择器出现(轮询 300ms,最长 10s) +- 返回页面标题与实际 URL + +### 4.4 截图 + +三种模式: + +| 模式 | 触发条件 | 输出 | +|------|---------|------| +| 元素截图 | 指定 selector | 该元素矩形区域 PNG(Base64) | +| 全页截图 | fullPage=true | 完整滚动尺寸 PNG(Base64) | +| 视口截图 | 默认 | 当前可见区域 PNG(Base64) | + +### 4.5 执行 JavaScript + +接收任意 JS 在页面上下文中执行。字符串原样返回,其余类型序列化为格式化 JSON。独立超时控制。 + +### 4.6 提取页面内容 + +- 克隆目标根节点,移除 script/style/iframe/svg 后取 innerText +- 支持 CSS 选择器限定提取区域 +- 默认上限 15000 字符 +- 同时提取最多 50 个有效链接(http 开头 + 有可见文本) + +### 4.7 点击元素 + +可选等待元素出现 → 滚动到视口中央 → 触发点击 → 等待 500ms。元素不存在则报错。 + +### 4.8 输入文本 + +聚焦 → clear 决定清空/追加 → 赋值 → 触发 input + change 事件(兼容 React/Vue)→ submit 决定是否提交(form.submit 或 Enter 键)。输入后等 300ms(提交等 1000ms)。 + +### 4.9 滚动页面 + +- 指定 selector → scrollIntoView 到中央 +- 方向滚动:down(+500px) / up(-500px) / top(回顶) / bottom(到底) + +### 4.10 等待条件 + +- 选择器等待:轮询 300ms,默认 10s(可由 timeMs 覆盖) +- 定时等待:按 timeMs 固定延迟,默认 1s + +### 4.11 关闭浏览器 + +销毁单例窗口并置空引用。幂等操作——窗口已销毁时为空操作。 + +### 4.12 工具调度映射 + +所有 browser 函数通过统一工具执行通道调度: + +| 调用名 | 映射函数 | 入参 | +|--------|---------|------| +| `browser_open` | browserOpen | url, wait_selector | +| `browser_screenshot` | browserScreenshot | full_page, selector | +| `browser_evaluate` | browserEvaluate | js | +| `browser_extract` | browserExtract | selector, max_chars | +| `browser_click` | browserClick | selector, wait | +| `browser_type` | browserType | selector, text, clear, submit | +| `browser_scroll` | browserScroll | direction, selector | +| `browser_wait` | browserWait | selector, time_ms | +| `browser_close` | browserClose | — | + +### 4.13 浏览器与 web_fetch 的协作关系 + +``` +web_fetch 三阶段回退: + Phase 1 (HTTP) 失败 + ├─ 403/503/429/502? → browserFallback(url) + ├─ Cloudflare 拦截? → browserFallback(url) + ├─ 内容 < 200 字符? → browserFallback(url) [经 Phase 2] + ├─ 超时? → browserFallback(url) + └─ 所有重试失败? → browserFallback(url) + +browserFallback: + browserOpen(url) → 等待 2.5s → browserExtract() → browserClose() + 结果进入 LRU 缓存 (100条, 10分钟TTL) +``` + +### 4.14 超时配置汇总 + +| 工具 | 默认超时 | 最大超时 | 说明 | +|------|---------|---------|------| +| `web_search` | 3,000ms/引擎 | 300,000ms | 每引擎独立超时 | +| `web_fetch` | 20,000ms | 600,000ms | HTTP 抓取超时 | +| `browser_extract` | 10,000ms | 300,000ms | 页面提取超时 | +| `browser_evaluate` | 8,000ms | — | JS 执行超时 | +| `browser_screenshot` | — | 60,000ms | 截图超时 | +| `browser_open` | 30,000ms | — | 页面加载超时 | + +--- + +## 5. 第三方库选型指南 + +本章针对工具链各环节,给出经过筛选的成熟第三方库推荐,涵盖 HTTP 客户端、HTML 解析、文章提取、浏览器反检测、缓存、URL 处理等领域。所有推荐库均为 npm 生态中活跃维护、广泛使用的项目。 + +### 5.1 HTTP 客户端层 + +当前实现使用原生 `fetch` + 手动请求头构造。以下库可在对应层面提供增强: + +#### got-scraping(⭐ 重点推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `got-scraping` | +| 定位 | 专为网页抓取设计的 HTTP 客户端,基于 `got` 封装 | +| 核心优势 | 内置浏览器指纹轮换、Cookie Jar 自动管理、HTTP/2 支持、代理集成、流式处理 | +| 指纹模拟 | 自动轮换 Header Order、TLS 指纹、HTTP/2 SETTINGS 帧,比手动构造请求头更难被识别 | +| 代理支持 | 原生支持 HTTP/SOCKS5 代理,可配置代理轮换 | +| Cookie 管理 | 自动 Cookie Jar,跨请求保持会话状态 | +| 重试机制 | 内置指数退避重试,可自定义重试条件 | +| 适用环节 | 替代 web_fetch Phase 1 的原生 fetch,显著降低被反爬识别的概率 | +| 替代方案 | `axios`(通用 HTTP 客户端)、`undici`(Node.js 原生 HTTP/1.1 客户端) | + +**为什么选 got-scraping 而非 axios/got**: + +`axios` 是通用 HTTP 客户端,不具备抓取专用能力;`got` 是底层 HTTP 库,功能强大但需自行组装抓取特性;`got-scraping` 在 `got` 之上专门为抓取场景做了封装,开箱即用的指纹模拟、Cookie 管理与代理支持正好覆盖 web_fetch Phase 1 的全部需求。 + +#### axios(备选) + +| 维度 | 详情 | +|------|------| +| 包名 | `axios` | +| 定位 | 最流行的 Node.js/Browser 通用 HTTP 客户端 | +| 核心优势 | 拦截器机制、请求/响应转换、取消令牌、CSRF 保护、JSON 自动处理 | +| 周边生态 | `axios-retry`(自动重试)、`cache-interceptor`(缓存拦截器) | +| 适用环节 | 如不需要抓取专用特性而偏向通用性,可作为基础 HTTP 层 | +| 注意事项 | 不自带指纹模拟,需手动构造请求头;2025 年曾发生供应链攻击事件,锁定版本 | + +### 5.2 HTML 解析层 + +当前实现使用正则表达式解析搜索引擎 HTML 结果。以下库可提供更健壮的 DOM 级解析能力: + +#### cheerio(⭐ 重点推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `cheerio` | +| 定位 | 服务端 jQuery 核心子集实现,轻量快速 DOM 解析 | +| 核心优势 | jQuery 风格 API(`$('.b_algo')`)、极快速度(约比 jsdom 快 8 倍)、零依赖、API 熟悉度高 | +| 适用环节 | 替代四引擎解析器中的正则匹配,改用 CSS 选择器精确提取标题/URL/摘要 | +| 示例场景 | `$('li.b_algo').each((i, el) => { const $el = $(el); ... })` | +| 版本注意 | Cheerio v1(最新版)API 有变化,需参考新版文档 | + +#### jsdom(特定场景备选) + +| 维度 | 详情 | +|------|------| +| 包名 | `jsdom` | +| 定位 | 纯 JavaScript 实现的完整 DOM 环境 | +| 核心优势 | 完整 DOM API(querySelector/getComputedStyle 等)、脚本执行、兼容依赖 DOM 的库 | +| 适用环节 | 需要 DOM 兼容性的复杂解析场景(如运行依赖 DOM 的第三方提取脚本) | +| 权衡 | 比 cheerio 慢且重量大,一般场景 cheerio 已足够 | + +#### parse5(底层备选) + +| 维度 | 详情 | +|------|------| +| 包名 | `parse5` | +| 定位 | WHATWG HTML Living Standard 合规的 HTML 解析/序列化工具集 | +| 核心优势 | 严格符合 HTML5 规范、可处理畸形 HTML、提供 serialize 功能 | +| 适用环节 | 需要规范合规 HTML 解析的底层场景;或作为 cheerio/jsdom 的底层解析引擎 | + +### 5.3 文章内容提取层 + +当前 htmlToText 函数做简单的标签移除与文本提取。以下库可大幅提升文章类页面的提取质量: + +#### @mozilla/readability(⭐ 重点推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `@mozilla/readability` | +| 定位 | Mozilla Firefox Reader View 的算法移植,自动提取文章主体内容 | +| 核心优势 | 基于 Mozilla 成熟算法,自动识别文章正文区、剥离导航/广告/评论/侧边栏噪声 | +| 输入要求 | 需配合 `jsdom` 提供 DOM 环境 | +| 输出 | `{ title, content, textContent, length }` | +| 适用环节 | web_fetch 抓取后作为 htmlToText 的前置步骤:先 readability 提取主体,再转文本 | +| 效果预期 | 对新闻/博客/百科/论坛帖子类页面效果极佳,可直接替代手动噪声标签列表 | +| 依赖 | 需搭配 `jsdom` 使用 | +| 同类项目 | `node-readability`(封装了 request + @mozilla/readability 的老项目,已不太活跃) | +| 注意事项 | 对非文章类页面(如搜索结果页、列表页、数据仪表盘)可能误提取或返回空 | + +#### turndown(HTML → Markdown) + +| 维度 | 详情 | +|------|------| +| 包名 | `turndown` | +| 定位 | HTML 到 Markdown 的转换器 | +| 核心优势 | 保留文档结构(标题层级、表格、列表、代码块、链接),输出格式友好 | +| 适用环节 | 当需要保留 HTML 结构而非纯文本时,作为 htmlToText 的替代或补充 | +| 使用方式 | `TurndownService().turndown(htmlString)` → Markdown 文本 | + +### 5.4 浏览器反检测层 + +当前 browser 模块使用 Electron 原生 BrowserWindow,无额外反检测措施。以下插件可显著降低被网站识别为自动化脚本的概率: + +#### puppeteer-extra-plugin-stealth(⭐ 重点推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `puppeteer-extra-plugin-stealth` | +| 定位 | Puppeteer 反检测插件,隐藏无头浏览器自动化痕迹 | +| 核心优势 | **18 种独立的反检测技术模块**,系统性覆盖所有主要检测维度 | +| 检测维度覆盖 | 见下方详细列表 | +| 架构设计 | 微内核 + 插件化,每种技术封装为独立模块,可单独启停 | +| 依赖关系 | 需配合 `puppeteer-extra` 使用 | +| 适用环节 | browser 模块的 BrowserWindow 创建时注入 stealth 脚本,或在 browser 回退通道中使用 puppeteer-extra 替代原生 BrowserWindow | + +**18 种反检测技术模块清单**: + +| # | 模块名 | 解决的检测点 | +|---|--------|-------------| +| 1 | `navigator.webdriver` | 移除 webdriver=true 标志 | +| 2 | `chrome.runtime` | 模拟 chrome.runtime 对象 | +| 3 | `chrome.loadTimes` | 模拟已废弃但仍被检测的 API | +| 4 | `chrome.csi` | 模拟 Chrome CSI 对象 | +| 5 | `iframe.contentWindow` | 修复 iframe contentWindow 检测 | +| 6 | `media.codecs` | 补全媒体编解码器支持列表 | +| 7 | `navigator.plugins` | 模拟浏览器插件信息 | +| 8 | `navigator.languages` | 修正语言设置 | +| 9 | `navigator.platform` | 修正平台标识 | +| 10 | `navigator.hardwareConcurrency` | 模拟真实的 CPU 核心数 | +| 11 | `webgl.vendor` | 伪装 WebGL 渲染厂商/型号 | +| 12 | `window.outerdimensions` | 修正窗口尺寸关系 | +| 13 | `sourceurl` | 移除源码 URL 泄露 | +| 14 | `user-agent-override` | 同步 navigator.userAgent 与请求头 | +| 15 | `permissions` | 修正 Permissions API 行为 | +| 16 | `runtime.enable` | 修复 chrome.runtime.enable | +| 17 | `eof` | 修复 EOF 检测 | +| 18 | `preload` | 修复脚本 preload 检测 | + +> **集成建议**:由于本项目基于 Electron(非独立 Puppeteer),有两种集成路径:(A) 将 stealth 插件的各模块 JS 脚本提取出来,通过 `webContents.executeJavaScript` 在 BrowserWindow 加载时注入;(B) 在 browser 回退通道中使用 `puppeteer-extra` + stealth 替代原生 BrowserWindow。路径 A 更轻量,路径 B 更彻底。 + +### 5.5 缓存层 + +当前使用手写 Map 实现 LRU 缓存。以下库提供更完善的能力: + +#### lru-cache(⭐ 推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `lru-cache` | +| 定位 | 高性能 LRU 缓存实现,支持 TTL、大小限制、统计 | +| 核心优势 | TTL 自动过期、 maxSize 按 count 或 byte 限制、hit/miss/stale 统计、异步 dispose 回调 | +| 适用环节 | 替代搜索缓存(200 条/5min)和浏览器回退缓存(100 条/10min)的手写实现 | +| API 示例 | `new LRUMax({ max: 200, ttl: 300_000 })` | + +#### cacheable(备选) + +| 维度 | 详情 | +|------|------| +| 包名 | `cacheable` | +| 定位 | 支持多后端(内存/Redis/文件)的统一缓存抽象 | +| 核心优势 | 可无缝切换存储后端,适合未来分布式部署需求 | +| 适用环节 | 如果未来需要跨进程共享缓存(如主进程 + 多个渲染进程) | + +### 5.6 URL 处理层 + +当前 URL 去重依赖简单标准化。以下库可提供更强的 URL 归一化能力: + +#### normalize-url(推荐) + +| 维度 | 详情 | +|------|------| +| 包名 | `normalize-url` | +| 定位 | URL 归一化工具 | +| 核心优势 | 自动去除追踪参数(utm_*)、统一协议/路径/查询串/片段格式、强制小写、去除默认端口 | +| 适用环节 | 搜索结果合并去重阶段的 URL 标准化,减少因 URL 变体导致的重复结果 | + +### 5.7 SearXNG 客户端层 + +当前 SearXNG 调用通过手动拼接 URL 参数 + fetch 发送。以下方案可考虑: + +#### 手动封装(当前方案,合理) + +SearXNG API 本身非常简单(GET `/search?q=...&format=json`),手动拼接完全够用。不建议引入额外的客户端库增加复杂度。如未来需要更多功能(如实例健康检查、引擎状态查询),可考虑封装一个轻量内部模块即可。 + +### 5.8 库选型总览 + +| 环节 | 当前方案 | ⭐ 推荐替代 | 核心收益 | 引入成本 | +|------|---------|------------|---------|---------| +| HTTP 抓取 | 原生 fetch + 手动请求头 | **got-scraping** | 指纹模拟、Cookie管理、HTTP/2、代理 | 中(替换 fetch 调用) | +| HTML 解析(搜索引擎) | 正则匹配 | **cheerio** | CSS选择器、DOM遍历、鲁棒性 | 中(重写4个解析器) | +| 文章内容提取 | 标签移除 + 纯文本 | **@mozilla/readability** | 自动识别正文、剥离噪声 | 中(需jsdom依赖) | +| HTML → 结构化文本 | 自定义 htmlToText | **turndown**(可选) | 保留Markdown结构 | 低(新增转换通道) | +| 浏览器反检测 | 无 | **puppeteer-extra-plugin-stealth** | 18种反检测、绕过Cloudflare | 高(需适配Electron) | +| 缓存 | 手写 Map LRU | **lru-cache** | TTL、统计、dispose回调 | 低(替换数据结构) | +| URL 归一化 | 简单标准化 | **normalize-url** | 去追踪参数、统一格式 | 低(替换标准化函数) | + +--- + +## 6. 优化增强路线图 + +本章按优先级排列可执行的优化项,每项标注预期收益、复杂度与依赖关系。 + +### 6.1 P0 — 高优先级(立即可做) + +#### 6.1.1 引入 lru-cache 替换手写缓存 + +- **改动范围**:搜索缓存 + 浏览器回退缓存 +- **工作量**:小(约 2-3 小时) +- **收益**:消除手写 LRU 的边界 bug 风险,获得 TTL 精确过期与统计指标 +- **依赖**:安装 `lru-cache` 包 + +#### 6.1.2 引入 cheerio 重写四引擎解析器 + +- **改动范围**:Bing/百度/搜狗/360 四个 HTML 解析函数 +- **工作量**:中(约 1-2 天) +- **收益**:正则 → CSS 选择器,解析鲁棒性大幅提升,应对搜索引擎 HTML 变更的成本降低 +- **依赖**:安装 `cheerio` 包 + +#### 6.1.3 引入 normalize-url 强化去重 + +- **改动范围**:搜索结果合并去重的 URL 标准化步骤 +- **工作量**:小(约 1-2 小时) +- **收益**:减少因 utm_* 参数、尾部斜杠、协议差异导致的重复结果 +- **依赖**:安装 `normalize-url` 包 + +### 6.2 P1 — 中优先级(短期规划) + +#### 6.2.1 引入 got-scraping 替换 web_fetch Phase 1 + +- **改动范围**:web_fetch 的 HTTP 抓取阶段 +- **工作量**:中(约 2-3 天) +- **收益**:内置指纹轮换、Cookie Jar、HTTP/2、代理支持,被反爬识别概率显著降低 +- **风险**:got-scraping 基于 got,需确认与 Electron 进程的兼容性;必要时可用 `got` 核心自行封装 +- **依赖**:安装 `got-scraping` 包 + +#### 6.2.2 引入 @mozilla/readability 提升文章提取质量 + +- **改动范围**:web_fetch 的 HTML 转文本流程,新增 readability 前置步骤 +- **工作量**:中(约 1-2 天) +- **收益**:新闻/博客/百科类页面的提取质量质的飞跃,噪声自动剥离 +- **注意事项**:需处理非文章页面的 fallback(readability 返回空时回退到原有 htmlToText) +- **依赖**:安装 `@mozilla/readability` + `jsdom` + +#### 6.2.3 UI 增加「连接测试」按钮 + +- **改动范围**:SearXNG 配置模态框 +- **工作量**:小(约 半天) +- **收益**:用户保存配置前可一键验证 URL 可达性与认证有效性,降低配置错误率 +- **实现要点**:向 `${url}/search?q=test&format=json` 发送带认证头的测试请求,展示响应状态与耗时 + +### 6.3 P2 — 中低优先级(中期规划) + +#### 6.3.1 集成 stealth 反检测 + +- **改动范围**:browser 模块的 BrowserWindow 创建 / browser 回退通道 +- **工作量**:大(约 3-5 天) +- **收益**:绕过 Cloudflare 等反爬检测的概率大幅提升,减少浏览器回退触发频率 +- **推荐路径**:提取 stealth 插件的 JS 脚本,通过 `webContents.executeJavaScript` 注入 BrowserWindow +- **依赖**:分析 `puppeteer-extra-plugin-stealth` 源码,提取各 evasions 模块 + +#### 6.3.2 引入 turndown 新增 Markdown 输出模式 + +- **改动范围**:web_fetch / browserExtract 的内容输出 +- **工作量**:小(约 1 天) +- **收益**:AI 收到的网页内容保留结构(标题层级、表格、列表、代码块),提升理解质量 +- **实现要点**:新增 `format: 'markdown'` 参数选项,默认保持现有纯文本行为不变 + +#### 6.3.3 代理支持 + +- **改动范围**:web_fetch HTTP 抓取阶段 + SearXNG 调用 +- **工作量**:中(约 2-3 天) +- **收益**:企业网络环境下可通过代理访问外网;高频抓取时可轮换 IP 降低封禁风险 +- **实现要点**:配置项新增 `proxy_url` / `proxy_list`,got-scraping 原生支持代理 + +### 6.4 P3 — 低优先级(长期规划) + +#### 6.4.1 分布式缓存后端 + +- **前提**:P0 缓存改造完成 +- **方向**:使用 `cacheable` 或类似方案,支持 Redis / SQLite / 文件后端 +- **收益**:多实例部署时缓存共享,避免重复抓取 + +#### 6.4.2 搜索引擎解析器热更新 + +- **方向**:将四引擎解析规则外部化为配置文件或脚本,支持不重启生效 +- **收益**:搜索引擎 HTML 结构变更时无需发版,运维可快速响应 + +#### 6.4.3 抓取速率自适应限速 + +- **方向**:根据目标站点的响应时间与错误率动态调整请求间隔 +- **收益**:尊重目标站点,减少被封禁概率,体现负责任的抓取行为 + +--- + +## 附录 A:工具联动模式 + +四种模块在实际任务中的典型协作流程: + +**标准搜索流程**: +``` +web_search(query) → 分析结果 → web_fetch(topN URLs) → 综合回答 +``` + +**浏览器交互流程**: +``` +browser_open(url) → browser_wait(selector) → browser_extract() +→ browser_screenshot() → browser_click() / browser_type() → browser_close() +``` + +**自动降级流程**: +``` +web_fetch(url) → [HTTP失败] → browser_open(url) → browser_extract() → browser_close() +``` + +**SearXNG + 自动抓取流程**: +``` +web_search(query) → [SearXNG模式] → JSON结果 → 相关性过滤 +→ applyAutoFetch(topN) → 逐条web_fetch() → 带全文的回答 +``` + +## 附录 B:关键文件索引(原始实现参考) + +| 文件 | 说明 | +|------|------| +| `src/main/browser.ts` | 浏览器控制核心(9 个函数) | +| `src/main/tool-handlers-system.ts` | web_fetch + web_search + SearXNG(核心实现) | +| `src/main/ipc.ts` | IPC 工具调度 | +| `src/main/main.ts` | 应用生命周期(browserClose on quit) | +| `src/renderer/components/searxng-modal.ts` | SearXNG 配置模态框 | +| `src/renderer/index.html` | SearXNG 模态框 HTML | +| `src/renderer/styles/style.css` | SearXNG 样式 | +| `src/renderer/services/tool-registry.ts` | 工具定义和参数声明 | +| `src/renderer/services/agent-engine.ts` | 工具超时配置、并行/串行调度 | + +## 附录 C:第三方库速查表 + +| 库名 | 版本建议 | npm 周下载量级 | 许可证 | 引入优先级 | +|------|---------|--------------|--------|-----------| +| `got-scraping` | 最新稳定版 | ~500K+/周 | MIT | P1 | +| `cheerio` | v1.x | ~10M+/周 | MIT | P0 | +| `@mozilla/readability` | 最新版 | ~2M+/周 | Apache-2.0 | P1 | +| `jsdom` | 最新版 | ~5M+/周 | MIT | P1(readability 依赖) | +| `turndown` | v7.x | ~2M+/周 | MIT | P2 | +| `puppeteer-extra-plugin-stealth` | 最新版 | ~500K+/周 | MIT | P2 | +| `puppeteer-extra` | 最新版 | ~1M+/周 | MIT | P2(stealth 依赖) | +| `lru-cache` | v10.x | ~20M+/周 | ISC | P0 | +| `normalize-url` | 最新版 | ~5M+/周 | MIT | P0 | +| `axios` | ^1.7.x(锁定安全版本) | ~30M+/周 | MIT | 备选 | diff --git a/electron/harness/adapters/agnes-ai.adapter.ts b/electron/harness/adapters/agnes-ai.adapter.ts index ceadc39..419f12f 100644 --- a/electron/harness/adapters/agnes-ai.adapter.ts +++ b/electron/harness/adapters/agnes-ai.adapter.ts @@ -125,7 +125,6 @@ export class AgnesAdapter extends BaseAdapter { contentParts.push({ type: 'text', text: origMsg.content }); } for (const img of origMsg.images) { - const isDataUrl = img.url.startsWith('data:'); contentParts.push({ type: 'image_url', image_url: { url: img.url }, diff --git a/electron/harness/adapters/base-adapter.ts b/electron/harness/adapters/base-adapter.ts index 50b5636..1528081 100644 --- a/electron/harness/adapters/base-adapter.ts +++ b/electron/harness/adapters/base-adapter.ts @@ -30,7 +30,7 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter { async healthCheck(): Promise { try { - await this.listModels?.(); + await this.listModels(); return true; } catch { return false; @@ -58,6 +58,16 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter { }; } + if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('ECONNRESET')) { + return { + code: MetonaErrorCode.NETWORK_ERROR, + message: error.message, + provider: this.provider, + retryable: true, + retryAfterMs: 3000, + }; + } + if (msg.includes('401') || msg.includes('unauthorized')) { return { code: MetonaErrorCode.AUTH_INVALID, diff --git a/electron/harness/adapters/ollama.adapter.ts b/electron/harness/adapters/ollama.adapter.ts index 1c9d577..64d2ae4 100644 --- a/electron/harness/adapters/ollama.adapter.ts +++ b/electron/harness/adapters/ollama.adapter.ts @@ -66,6 +66,7 @@ export class OllamaAdapter extends BaseAdapter { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...nativeRequest, stream: true }), + signal: AbortSignal.timeout(300_000), }); if (!response.ok || !response.body) { @@ -124,6 +125,8 @@ export class OllamaAdapter extends BaseAdapter { // 工具调用(Ollama 在最后一个 chunk 中整块返回) if (chunk.message?.tool_calls) { for (const tc of chunk.message.tool_calls) { + const args = tc.function?.arguments; + const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args ?? {}); yield { type: MetonaStreamEventType.TOOL_CALL_COMPLETE, requestId: request.meta.requestId, @@ -134,7 +137,7 @@ export class OllamaAdapter extends BaseAdapter { toolCall: { id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, name: tc.function?.name ?? '', - args: tc.function?.arguments ?? {}, + args: parsedArgs, iteration: request.meta.iteration, timestamp: Date.now(), }, @@ -441,10 +444,17 @@ export class OllamaAdapter extends BaseAdapter { reasoningContent: message?.thinking as string | undefined, toolCalls: toolCalls?.map((tc, i) => { const fn = tc.function as Record; + const rawArgs = fn?.arguments; + let args: Record = {}; + try { + args = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : (rawArgs as Record) ?? {}; + } catch { + args = {}; + } return { id: `tc_${Date.now()}_${i}`, name: (fn?.name as string) ?? '', - args: (fn?.arguments as Record) ?? {}, + args, iteration: 0, timestamp: Date.now(), }; diff --git a/electron/harness/adapters/shared/sse-stream.ts b/electron/harness/adapters/shared/sse-stream.ts index 15da8d7..e66121a 100644 --- a/electron/harness/adapters/shared/sse-stream.ts +++ b/electron/harness/adapters/shared/sse-stream.ts @@ -166,7 +166,7 @@ export async function* parseSSEStream( // 非 [DONE] 但 finish_reason 为 tool_calls 时提前 flush 缓冲区 const finishReason = chunk.choices?.[0]?.finish_reason as string | undefined; - if (finishReason === 'tool_calls' || finishReason === 'stop') { + if (finishReason === 'tool_calls') { for (const [index, buf] of toolCallsBuffer) { try { yield { diff --git a/electron/harness/agent-loop/engine.ts b/electron/harness/agent-loop/engine.ts index 61e27af..aee9d93 100644 --- a/electron/harness/agent-loop/engine.ts +++ b/electron/harness/agent-loop/engine.ts @@ -44,6 +44,9 @@ const DEFAULT_CONFIG: AgentLoopConfig = { contextWindow: 128_000, retryCount: 3, temperature: 0.0, + maxTokens: 63488, + thinkingEnabled: true, + thinkingEffort: 'high', }; /** @@ -157,11 +160,11 @@ export class AgentLoopEngine extends EventEmitter { messages, tools: this.tools.length > 0 ? this.tools : undefined, params: { - maxTokens: 63488, + maxTokens: this.config.maxTokens, temperature: this.config.temperature, stream: true, - thinkingEnabled: true, - thinkingEffort: 'high', + thinkingEnabled: this.config.thinkingEnabled, + thinkingEffort: this.config.thinkingEffort, contextLength: this.config.contextLength, }, }; @@ -254,9 +257,12 @@ export class AgentLoopEngine extends EventEmitter { let tokenUsage: TokenUsage | undefined; // 流式接收响应 - for await (const event of this.adapter.chatStream(request)) { + for await (const event of this.chatStreamWithRetry(request)) { if (this.aborted) break; + // 过滤掉每轮的 DONE 事件 — 只在全部迭代完成后发送一个最终 DONE + if (event.type === MetonaStreamEventType.DONE) continue; + // 转发流式事件到渲染进程 this.emit('streamEvent', event); @@ -299,9 +305,6 @@ export class AgentLoopEngine extends EventEmitter { } break; - case MetonaStreamEventType.DONE: - break; - case MetonaStreamEventType.ERROR: if (event.error) { throw new Error(event.error.message); @@ -313,18 +316,21 @@ export class AgentLoopEngine extends EventEmitter { // 处理缓冲区中的工具调用(TOOL_CALL_DELTA 拼接) if (toolCallsBuffer.size > 0 && (!step.toolCalls || step.toolCalls.length === 0)) { step.toolCalls = []; - for (const [index, buf] of toolCallsBuffer) { + for (const [, buf] of toolCallsBuffer) { + let args: Record; try { - step.toolCalls.push({ - id: `tc_${nanoid(8)}`, - name: buf.name, - args: buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {}, - iteration: this.currentIteration, - timestamp: Date.now(), - }); + args = buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {}; } catch { - // JSON 解析失败,跳过 + // JSON 解析失败,使用空参数(LLM 仍请求了该工具调用) + args = {}; } + step.toolCalls.push({ + id: `tc_${nanoid(8)}`, + name: buf.name, + args, + iteration: this.currentIteration, + timestamp: Date.now(), + }); } } @@ -361,7 +367,7 @@ export class AgentLoopEngine extends EventEmitter { // 转发工具结果到渲染进程 this.emit('streamEvent', { - type: 'tool_result', + type: MetonaStreamEventType.TOOL_RESULT, requestId: request.meta.requestId, sessionId, iteration: this.currentIteration, @@ -444,10 +450,37 @@ export class AgentLoopEngine extends EventEmitter { return toolResult; } + /** + * 带重试的流式调用 + * + * 如果 adapter 抛出错误,在 retryCount 次数内重试,每次等待 1 秒。 + */ + private async *chatStreamWithRetry(request: MetonaRequest): AsyncIterable { + let lastError: unknown; + for (let attempt = 0; attempt <= this.config.retryCount; attempt++) { + try { + yield* this.adapter.chatStream(request); + return; + } catch (error) { + lastError = error; + if (attempt < this.config.retryCount) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + } + throw lastError; + } + private async transitionTo(state: AgentLoopState): Promise { const previous = this.currentState; this.currentState = state; - this.emit('stateChange', { previous, current: state }); + this.emit('stateChange', { + previous, + current: state, + state, + sessionId: this.currentSessionId, + iteration: this.currentIteration, + }); } private accumulateTokens(usage: TokenUsage): void { @@ -461,6 +494,16 @@ export class AgentLoopEngine extends EventEmitter { answer?: string, error?: Error, ): AgentLoopOutput { + // 发送唯一的最终 DONE 事件 — UI 只在此处结束流式状态 + this.emit('streamEvent', { + type: MetonaStreamEventType.DONE, + requestId: this.currentRequestId, + sessionId: this.currentSessionId, + iteration: this.currentIteration, + seq: 0, + timestamp: Date.now(), + }); + this.currentState = AgentLoopState.TERMINATED; return { finalAnswer: answer ?? (error ? error.message : 'No answer produced'), diff --git a/electron/harness/agent-loop/types.ts b/electron/harness/agent-loop/types.ts index 94f9c43..2b6d1b3 100644 --- a/electron/harness/agent-loop/types.ts +++ b/electron/harness/agent-loop/types.ts @@ -4,6 +4,8 @@ * 用于 Agent Loop 引擎内部的状态管理和迭代记录。 */ +import type { MetonaToolCall, MetonaToolResult } from '../types'; + // ===== Agent Loop 状态机 ===== export enum AgentLoopState { @@ -41,8 +43,8 @@ export interface IterationStep { startedAt: number; completedAt?: number; thought?: Thought; - toolCalls?: import('@shared/index').MetonaToolCall[]; - toolResults?: import('@shared/index').MetonaToolResult[]; + toolCalls?: MetonaToolCall[]; + toolResults?: MetonaToolResult[]; tokenUsage?: TokenUsage; } @@ -63,6 +65,12 @@ export interface AgentLoopConfig { contextWindow: number; retryCount: number; temperature: number; + /** 最大生成 token 数(默认 63488) */ + maxTokens: number; + /** 是否启用思考模式(默认 true) */ + thinkingEnabled: boolean; + /** 思考强度(默认 'high') */ + thinkingEffort: 'low' | 'medium' | 'high' | 'max'; /** Ollama 上下文窗口大小(num_ctx),其他 Provider 忽略 */ contextLength?: number; } diff --git a/electron/harness/hooks/post-tool.ts b/electron/harness/hooks/post-tool.ts index 22d1a5e..8ad9f7c 100644 --- a/electron/harness/hooks/post-tool.ts +++ b/electron/harness/hooks/post-tool.ts @@ -41,13 +41,19 @@ export class MemoryTriggerHook implements PostToolHook { async afterExecute(toolCall: MetonaToolCall, result: MetonaToolResult, sessionId: string): Promise { if (this.memorableTools.includes(toolCall.name) && result.success) { const content = typeof result.result === 'string' ? result.result : JSON.stringify(result.result); - this.memoryManager.store({ - type: 'episodic', - content: `Tool ${toolCall.name} returned: ${content.slice(0, 500)}`, - source: 'tool_result', - sessionId, - importance: 0.6, - }); + try { + this.memoryManager.store({ + type: 'episodic', + content: `Tool ${toolCall.name} returned: ${content.slice(0, 500)}`, + source: 'tool_result', + sessionId, + importance: 0.6, + }); + } catch (error) { + // 记忆存储失败不应影响工具执行结果 + // eslint-disable-next-line no-console + console.error('[MemoryTriggerHook] Failed to store episodic memory:', error); + } } } } diff --git a/electron/harness/prompts/context-builder.ts b/electron/harness/prompts/context-builder.ts index aa7f384..515740e 100644 --- a/electron/harness/prompts/context-builder.ts +++ b/electron/harness/prompts/context-builder.ts @@ -219,8 +219,8 @@ Use tools when needed to gather information or perform actions. Think step by st let skipMeta = true; for (const line of lines) { - // 跳过元数据注释行(以 # 开头的非标题行) - if (skipMeta && line.startsWith('#') && !line.startsWith('## ')) { + // 跳过元数据注释行(以 # 开头且不跟另一个 # 的行,即只跳过一级标题) + if (skipMeta && line.match(/^#[^#]/)) { continue; } skipMeta = false; diff --git a/electron/harness/sandbox/permissions.ts b/electron/harness/sandbox/permissions.ts index 2485e12..acf20c5 100644 --- a/electron/harness/sandbox/permissions.ts +++ b/electron/harness/sandbox/permissions.ts @@ -22,12 +22,12 @@ export interface PermissionPolicy { } export const DEFAULT_POLICIES: PermissionPolicy[] = [ - { toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc\//, /\/proc\//] }, + { toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc\//, /\/proc\//, /C:\\Windows\\/i, /C:\\System32\\/i] }, { toolName: 'web_search', requiredLevel: PermissionLevel.READ, maxFrequency: 10 }, { toolName: 'list_directory', requiredLevel: PermissionLevel.READ }, { toolName: 'search_files', requiredLevel: PermissionLevel.READ }, { toolName: 'memory_search', requiredLevel: PermissionLevel.READ }, - { toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc\//, /\/proc\//, /\/System\//], requireConfirmation: true, maxFrequency: 5 }, + { toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc\//, /\/proc\//, /\/System\//, /C:\\Windows\\/i, /C:\\System32\\/i], requireConfirmation: true, maxFrequency: 5 }, { toolName: 'memory_store', requiredLevel: PermissionLevel.WRITE }, { toolName: 'run_command', requiredLevel: PermissionLevel.EXTERNAL_ACTION, requireConfirmation: true, maxFrequency: 3 }, { toolName: 'web_extract', requiredLevel: PermissionLevel.READ }, diff --git a/electron/harness/tools/registry.ts b/electron/harness/tools/registry.ts index 825cf9c..544e7d7 100644 --- a/electron/harness/tools/registry.ts +++ b/electron/harness/tools/registry.ts @@ -13,6 +13,7 @@ import type { IMetonaTool, ToolRegistryEntry, ToolExecutionContext } from '../ty export class ToolRegistry { private tools = new Map(); + private disabledTools = new Set(); /** 注册内置工具 */ registerBuiltin(tool: IMetonaTool): void { @@ -45,16 +46,27 @@ export class ToolRegistry { /** 获取工具 */ get(name: string): IMetonaTool | undefined { const entry = this.tools.get(name); - return entry?.enabled ? entry.tool : undefined; + if (!entry?.enabled) return undefined; + if (this.disabledTools.has(name)) return undefined; + return entry.tool; } /** 列出所有已启用工具的定义 */ listTools(): MetonaToolDef[] { return Array.from(this.tools.values()) - .filter((e) => e.enabled) + .filter((e) => e.enabled && !this.disabledTools.has(e.tool.definition.name)) .map((e) => e.tool.definition); } + /** 设置工具启用/禁用状态(供 IPC tools:toggle 调用) */ + setToolEnabled(name: string, enabled: boolean): void { + if (enabled) { + this.disabledTools.delete(name); + } else { + this.disabledTools.add(name); + } + } + /** 执行工具 */ async execute( toolCall: MetonaToolCall, @@ -99,6 +111,6 @@ export class ToolRegistry { /** 获取工具数量 */ get size(): number { - return Array.from(this.tools.values()).filter((e) => e.enabled).length; + return Array.from(this.tools.values()).filter((e) => e.enabled && !this.disabledTools.has(e.tool.definition.name)).length; } } diff --git a/electron/harness/types/metona-response.ts b/electron/harness/types/metona-response.ts index 33866bb..f077f72 100644 --- a/electron/harness/types/metona-response.ts +++ b/electron/harness/types/metona-response.ts @@ -83,6 +83,7 @@ export enum MetonaStreamEventType { REASONING_DELTA = 'reasoning_delta', TOOL_CALL_DELTA = 'tool_call_delta', TOOL_CALL_COMPLETE = 'tool_call_complete', + TOOL_RESULT = 'tool_result', THINKING_START = 'thinking_start', THINKING_END = 'thinking_end', ERROR = 'error', @@ -116,6 +117,9 @@ export interface MetonaStreamEvent { /** TOOL_CALL_COMPLETE(拼接完成后的完整调用) */ toolCall?: MetonaToolCall; + /** TOOL_RESULT */ + toolResult?: MetonaToolResult; + /** USAGE */ usage?: MetonaTokenUsage; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 10efafc..ca54cf0 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -20,6 +20,8 @@ import type { AuditService } from '../services/audit.service'; import type { SessionRecorder } from '../services/session-recorder.service'; import type { MemoryManager } from '../harness/memory/manager'; import type { MCPManager } from '../services/mcp-manager.service'; +import type { PromptInjectionDefender } from '../harness/security/prompt-injection-defense'; +import type { OutputValidator } from '../harness/verification/output-validator'; import type { MetonaMessage, MetonaStreamEvent } from '../harness/types'; import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types'; import type { MetonaError } from '../harness/types'; @@ -35,12 +37,14 @@ export function registerAllIPCHandlers( workspaceService: WorkspaceService, contextBuilder: ContextBuilder, agentLoop: AgentLoopEngine, - _toolRegistry: ToolRegistry, + toolRegistry: ToolRegistry, auditService: AuditService, sessionRecorder: SessionRecorder, memoryManager: MemoryManager, mcpManager: MCPManager, reloadAdapter: () => void, + promptInjectionDefender: PromptInjectionDefender, + outputValidator: OutputValidator, ): void { // ===== Agent 交互 ===== @@ -55,11 +59,12 @@ export function registerAllIPCHandlers( auditService.logSessionStart(sessionId); // 保存用户消息到数据库 + // IPC boundary: frontend sends attachments not defined in the MetonaMessage type sessionService.saveMessage({ sessionId, role: 'user', content: userMessage.content, - attachments: (userMessage as any).attachments, + attachments: (userMessage as MetonaMessage & { attachments?: unknown[] }).attachments, }); // 加载历史消息 @@ -71,6 +76,8 @@ export function registerAllIPCHandlers( role: m.role as MetonaMessage['role'], content: m.content, reasoningContent: m.reasoningContent, + toolCalls: m.toolCalls as MetonaMessage['toolCalls'], + toolResult: m.toolResult as MetonaMessage['toolResult'], timestamp: m.timestamp, })); @@ -98,6 +105,40 @@ export function registerAllIPCHandlers( agentLoop.on('stateChange', onStateChange); try { + // 提示注入检测(安全模块) + const injectionResult = promptInjectionDefender.detect(userMessage.content); + if (injectionResult.riskScore >= 7) { + log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings); + if (!mainWindow.isDestroyed()) { + const metonaError: MetonaError = { + code: MetonaErrorCode.UNKNOWN, + message: `Message blocked by prompt injection defense: ${injectionResult.recommendation}`, + retryable: false, + }; + const errorEvent: MetonaStreamEvent = { + type: MetonaStreamEventType.ERROR, + requestId: '', + sessionId, + iteration: 0, + seq: 0, + timestamp: Date.now(), + error: metonaError, + }; + mainWindow.webContents.send('agent:streamEvent', errorEvent); + } + // TRACE 层:停止录制 + sessionRecorder.stopRecording({ + totalIterations: 0, + totalTokens: 0, + durationMs: 0, + terminationReason: 'error', + }); + return { success: false, error: 'Message blocked by prompt injection defense' }; + } + if (injectionResult.riskScore >= 4) { + log.warn('[PromptInjectionDefender] Suspicious patterns detected:', injectionResult.findings); + } + // TRACE 层:记录上下文构建 sessionRecorder.recordContextBuilt({ tokenCount: history.reduce((sum, m) => sum + Math.ceil(m.content.length / 2), 0), @@ -107,12 +148,47 @@ export function registerAllIPCHandlers( // 启动 Agent Loop const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt); - // 保存 assistant 回复到数据库 - sessionService.saveMessage({ - sessionId, - role: 'assistant', - content: output.finalAnswer, - }); + // 输出验证(不阻塞响应,仅记录警告) + try { + const validation = await outputValidator.validate(output.finalAnswer); + if (!validation.valid || validation.issues.length > 0) { + log.warn('[OutputValidator] Validation issues:', validation.issues); + } + log.debug(`[OutputValidator] Score: ${validation.score}, Valid: ${validation.valid}`); + } catch (err) { + log.error('[OutputValidator] Validation failed:', err); + } + + // 保存每轮迭代的 assistant 消息到数据库(含思考内容和工具调用) + for (const step of output.iterations) { + if (!step.thought) continue; + + // 将 MetonaToolCall + MetonaToolResult 转换为前端 ToolCallInfo 格式 + const toolCallsWithResults = step.toolCalls?.map((tc) => { + const result = step.toolResults?.find((r) => r.toolCallId === tc.id); + return { + id: tc.id, + name: tc.name, + args: tc.args, + status: result?.success ? 'success' as const : 'error' as const, + result: result?.result, + durationMs: result?.durationMs, + error: result?.error, + }; + }); + + // 只有当有内容、思考内容或工具调用时才保存 + if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) { + sessionService.saveMessage({ + sessionId, + role: 'assistant', + content: step.thought.content, + reasoningContent: step.thought.reasoningContent || undefined, + toolCalls: toolCallsWithResults, + iteration: step.iteration, + }); + } + } // 更新 Token 统计 if (output.totalTokenUsage.totalTokens > 0) { @@ -284,8 +360,8 @@ export function registerAllIPCHandlers( try { await mcpManager.removeServer(name); return { success: true }; - } catch { - return { success: false }; + } catch (error) { + return { success: false, error: (error instanceof Error ? error.message : String(error)) }; } }); @@ -293,8 +369,8 @@ export function registerAllIPCHandlers( try { await mcpManager.toggleServer(name, enabled); return { success: true }; - } catch { - return { success: false }; + } catch (error) { + return { success: false, error: (error instanceof Error ? error.message : String(error)) }; } }); @@ -363,7 +439,7 @@ export function registerAllIPCHandlers( // ===== 工具管理 ===== ipcMain.handle('tools:list', async () => { - return _toolRegistry.listTools().map((t) => ({ + return toolRegistry.listTools().map((t) => ({ name: t.name, description: t.description, category: t.category, @@ -407,8 +483,10 @@ export function registerAllIPCHandlers( ipcMain.handle('data:clearSessions', async () => { try { const db = sessionService.getDB(); + db.exec('BEGIN'); db.exec('DELETE FROM messages'); db.exec('DELETE FROM sessions'); + db.exec('COMMIT'); log.info('[DATA] All sessions cleared'); return { success: true }; } catch (error) { @@ -433,6 +511,7 @@ export function registerAllIPCHandlers( try { // 审计日志是 INSERT-ONLY,需要先禁用触发器 const db = sessionService.getDB(); + db.exec('BEGIN'); db.exec('DROP TRIGGER IF EXISTS audit_no_delete'); db.exec('DELETE FROM audit_logs'); db.exec(` @@ -441,6 +520,7 @@ export function registerAllIPCHandlers( SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY. Deletion is not allowed.'); END `); + db.exec('COMMIT'); log.info('[DATA] Audit logs cleared'); return { success: true }; } catch (error) { diff --git a/electron/main.ts b/electron/main.ts index e60ab04..9d3a7a8 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -46,6 +46,7 @@ import { PolicyEngine } from './harness/sandbox/permissions'; import { SandboxManager } from './harness/sandbox/sandbox'; import { PromptInjectionDefender } from './harness/security/prompt-injection-defense'; import { OutputValidator } from './harness/verification/output-validator'; +import { UpdateService } from './services/update.service'; // ===== 步骤 1: 初始化日志系统(SYS 层)===== log.transports.file.level = 'info'; @@ -98,6 +99,10 @@ async function initialize(): Promise { log.warn('LLM not configured. Please set provider, baseURL, and model in Settings.'); } + if (!apiKey && provider !== 'ollama') { + throw new Error(`API key is required for provider "${provider || 'unknown'}". Please set it in Settings.`); + } + const adapterConfig = { provider, baseURL, apiKey, defaultModel: model }; switch (provider) { case 'agnes': return new AgnesAdapter(adapterConfig); @@ -150,6 +155,7 @@ async function initialize(): Promise { ]; // ===== Agent Loop ===== + // TODO: Re-read agent config on each runStream call or when config changes const ollamaNumCtx = configService.get('ollama.numCtx'); const agentMaxIter = configService.get('agent.maxIterations'); const agentTimeout = configService.get('agent.totalTimeoutMs'); @@ -187,13 +193,14 @@ async function initialize(): Promise { windowManager.registerGlobalShortcuts(); // ===== Agent 状态同步到托盘 ===== - agentLoop.on('stateChange', (data: { previous: string; current: string }) => { + agentLoop.on('stateChange', (data: { previous: string; current: string; state?: string }) => { const statusMap: Record = { INIT: 'idle', THINKING: 'thinking', PARSING: 'thinking', EXECUTING: 'executing', OBSERVING: 'executing', REFLECTING: 'thinking', COMPRESSING: 'thinking', TERMINATED: 'idle', }; - trayManager?.setStatus(statusMap[data.current] ?? 'idle'); + const stateValue = (data.current || data.state) ?? ''; + trayManager?.setStatus(statusMap[stateValue] ?? 'idle'); }); // ===== Agent 完成时发送系统通知 ===== @@ -210,8 +217,13 @@ async function initialize(): Promise { mainWindow, sessionService, configService, workspaceService, contextBuilder, agentLoop, toolRegistry, auditService, sessionRecorder, memoryManager, mcpManager, createAdapter, + promptDefender, outputValidator, ); + // TODO: Initialize UpdateService for auto-update functionality + // const updateService = new UpdateService(); + // updateService.initialize(mainWindow); + // ===== 应用生命周期 ===== app.on('window-all-closed', () => { // macOS: 保持应用运行(托盘模式) @@ -232,7 +244,7 @@ async function initialize(): Promise { windowManager?.unregisterGlobalShortcuts(); trayManager?.destroy(); windowManager?.closeAll(); - try { await mcpManager.shutdown(); } catch {} + try { await mcpManager.shutdown(); } catch (err) { log.error('[Shutdown] MCP shutdown failed:', err); } if (databaseService) { databaseService.close(); databaseService = null; } }); diff --git a/electron/services/config.service.ts b/electron/services/config.service.ts index 6627901..7c52de9 100644 --- a/electron/services/config.service.ts +++ b/electron/services/config.service.ts @@ -41,7 +41,7 @@ export class ConfigService { */ set(key: string, value: unknown): void { const db = this.getDB(); - const jsonValue = typeof value === 'string' ? value : JSON.stringify(value); + const jsonValue = JSON.stringify(value); // 查询已有 category,避免 INSERT OR REPLACE 覆盖为默认值 const existing = db.prepare('SELECT category FROM app_config WHERE key = ?').get(key) as ConfigRow | undefined; diff --git a/electron/services/database.service.ts b/electron/services/database.service.ts index a14fe06..a9d2243 100644 --- a/electron/services/database.service.ts +++ b/electron/services/database.service.ts @@ -227,16 +227,24 @@ export class DatabaseService { try { db.exec('ALTER TABLE messages ADD COLUMN attachments TEXT'); log.info('[DB] Migration: added attachments column to messages'); - } catch { - // 列已存在,忽略 + } catch (error) { + // 只忽略 "duplicate column" 错误(列已存在),其他错误必须抛出 + const msg = error instanceof Error ? error.message : String(error); + if (!msg.includes('duplicate column')) { + throw error; + } } // 迁移 2: audit_logs 表添加 iteration 列 try { db.exec('ALTER TABLE audit_logs ADD COLUMN iteration INTEGER'); log.info('[DB] Migration: added iteration column to audit_logs'); - } catch { - // 列已存在,忽略 + } catch (error) { + // 只忽略 "duplicate column" 错误(列已存在),其他错误必须抛出 + const msg = error instanceof Error ? error.message : String(error); + if (!msg.includes('duplicate column')) { + throw error; + } } } diff --git a/electron/services/session.service.ts b/electron/services/session.service.ts index f626177..b67bebc 100644 --- a/electron/services/session.service.ts +++ b/electron/services/session.service.ts @@ -86,7 +86,7 @@ export class SessionService { ORDER BY pinned DESC, updated_at DESC `).all(archived ? 1 : 0) as SessionRow[]; - return rows.map(this.toSessionInfo); + return rows.map((row) => this.toSessionInfo(row)); } /** @@ -183,7 +183,7 @@ export class SessionService { ORDER BY created_at ASC `).all(sessionId) as MessageRow[]; - return rows.map(this.toMessageInfo); + return rows.map((row) => this.toMessageInfo(row)); } /** @@ -316,11 +316,22 @@ export class SessionService { role: row.role, content: row.content, reasoningContent: row.reasoning_content ?? undefined, - toolCalls: row.tool_calls ? JSON.parse(row.tool_calls) : undefined, - toolResult: row.tool_result ? JSON.parse(row.tool_result) : undefined, - attachments: row.attachments ? JSON.parse(row.attachments) : undefined, + toolCalls: row.tool_calls ? this.safeJsonParse(row.tool_calls) as unknown[] : undefined, + toolResult: row.tool_result ? this.safeJsonParse(row.tool_result) : undefined, + attachments: row.attachments ? this.safeJsonParse(row.attachments) as unknown[] : undefined, iteration: row.iteration ?? undefined, timestamp: row.created_at, }; } + + /** + * 安全 JSON 解析:解析失败时返回 undefined 而非抛出异常 + */ + private safeJsonParse(json: string): unknown { + try { + return JSON.parse(json); + } catch { + return undefined; + } + } } diff --git a/electron/services/workspace.service.ts b/electron/services/workspace.service.ts index 73f924f..9a0374f 100644 --- a/electron/services/workspace.service.ts +++ b/electron/services/workspace.service.ts @@ -40,8 +40,8 @@ const AUTO_CREATED_DIRS = ['logs', 'traces', '.metona'] as const; const MEMORY_TEMPLATE = `# MEMORY.md — AI 持久记忆 # # 格式版本: 1.0 -# 创建时间: ${new Date().toISOString()} -# 最后更新: ${new Date().toISOString()} +# 创建时间: __CREATED_AT__ +# 最后更新: __UPDATED_AT__ # 工作空间: __WORKSPACE_PATH__ # # 此文件由 Metona Agent 自动维护,用户可手动编辑。 @@ -287,7 +287,11 @@ export class WorkspaceService { switch (fileName) { case 'MEMORY.md': { - const content = MEMORY_TEMPLATE.replace('__WORKSPACE_PATH__', this.workspacePath); + const now = new Date().toISOString(); + const content = MEMORY_TEMPLATE + .replace('__WORKSPACE_PATH__', this.workspacePath) + .replace('__CREATED_AT__', now) + .replace('__UPDATED_AT__', now); writeFileSync(filePath, content, 'utf-8'); break; } diff --git a/electron/utils/slo.ts b/electron/utils/slo.ts index 72c00e1..7df7ea0 100644 --- a/electron/utils/slo.ts +++ b/electron/utils/slo.ts @@ -36,7 +36,7 @@ export class HealthChecker { async check(): Promise { const checks: HealthCheck[] = []; checks.push(await this.checkDatabase()); - checks.push(await this.checkDiskSpace()); + checks.push(await this.checkFreeMemory()); checks.push(await this.checkMemoryUsage()); const allHealthy = checks.every((c) => c.healthy); return { healthy: allHealthy, checks, timestamp: new Date() }; @@ -66,28 +66,28 @@ export class HealthChecker { } } - private async checkDiskSpace(): Promise { + private async checkFreeMemory(): Promise { try { const userDataPath = app.getPath('userData'); if (!existsSync(userDataPath)) { - return { name: 'disk_space', healthy: false, error: 'User data path not accessible' }; + return { name: 'free_memory', healthy: false, error: 'User data path not accessible' }; } - // 检查实际可用磁盘空间(Windows/Linux 返回字节数) + // 检查实际可用内存(Windows/Linux 返回字节数) const { freemem } = await import('os'); const freeBytes = freemem(); const freeMB = freeBytes / (1024 * 1024); // 少于 200MB 视为不健康 if (freeMB < 200) { return { - name: 'disk_space', + name: 'free_memory', healthy: false, error: `Free memory critically low: ${freeMB.toFixed(0)}MB available`, }; } - return { name: 'disk_space', healthy: true }; + return { name: 'free_memory', healthy: true }; } catch (error) { return { - name: 'disk_space', + name: 'free_memory', healthy: false, error: error instanceof Error ? error.message : String(error), }; diff --git a/package-lock.json b/package-lock.json index e09f727..b3aa31e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,6 @@ "@mui/icons-material": "^9.1.1", "@mui/material": "^9.1.2", "better-sqlite3": "^11.9.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "date-fns": "^4.1.0", "electron-log": "^5.3.3", "electron-store": "^10.0.1", @@ -24,16 +22,13 @@ "lru-cache": "^11.1.0", "metona-toast": "^2.0.0", "nanoid": "^5.1.5", - "radix-ui": "^1.6.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.6.1", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "sql.js": "^1.12.0", - "tailwind-merge": "^3.6.0", "zod": "^3.25.67", "zustand": "^5.0.5" }, @@ -1658,44 +1653,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.14.tgz", @@ -2275,1499 +2232,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@radix-ui/number": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.2.tgz", - "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.4.tgz", - "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.10.tgz", - "integrity": "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.14", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz", - "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collapsible": "1.1.14", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.17", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.17.tgz", - "integrity": "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dialog": "1.1.17", - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", - "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.10.tgz", - "integrity": "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.2.0.tgz", - "integrity": "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.5", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz", - "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz", - "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", - "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-slot": "1.3.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.4.tgz", - "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.3.1.tgz", - "integrity": "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-menu": "2.1.18", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.17", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", - "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.13", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", - "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-escape-keydown": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.18", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz", - "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.18", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", - "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", - "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-form/-/react-form-0.1.10.tgz", - "integrity": "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-label": "2.1.10", - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.17", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.17.tgz", - "integrity": "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.10.tgz", - "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.18", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.18.tgz", - "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-menubar/-/react-menubar-1.1.18.tgz", - "integrity": "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-menu": "2.1.18", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.16", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz", - "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.10.tgz", - "integrity": "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.5", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.5.tgz", - "integrity": "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-is-hydrated": "0.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.17", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.17.tgz", - "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", - "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-rect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/rect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.12", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", - "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", - "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", - "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.3.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.10.tgz", - "integrity": "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.4.1.tgz", - "integrity": "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.13", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", - "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.12", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz", - "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.3.1.tgz", - "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.6", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", - "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.4.1.tgz", - "integrity": "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.2", - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.3.1.tgz", - "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-previous": "1.1.2", - "@radix-ui/react-use-size": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.15", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", - "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.17", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.17.tgz", - "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.12", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.12.tgz", - "integrity": "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.13", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.13.tgz", - "integrity": "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-toggle": "1.1.12", - "@radix-ui/react-use-controllable-state": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.13", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.13.tgz", - "integrity": "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-separator": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.13" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", - "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-visually-hidden": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", - "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", - "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", - "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", - "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", - "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.2.tgz", - "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", - "license": "MIT" - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -4690,7 +3154,7 @@ "version": "19.2.3", "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5203,18 +3667,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/asn1js": { "version": "3.0.10", "resolved": "https://registry.npmmirror.com/asn1js/-/asn1js-3.0.10.tgz", @@ -5890,18 +4342,6 @@ "node": ">=8" } }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", @@ -6395,12 +4835,6 @@ "license": "MIT", "optional": true }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", @@ -7842,15 +6276,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", @@ -11235,83 +9660,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/radix-ui": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/radix-ui/-/radix-ui-1.6.0.tgz", - "integrity": "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-accessible-icon": "1.1.10", - "@radix-ui/react-accordion": "1.2.14", - "@radix-ui/react-alert-dialog": "1.1.17", - "@radix-ui/react-arrow": "1.1.10", - "@radix-ui/react-aspect-ratio": "1.1.10", - "@radix-ui/react-avatar": "1.2.0", - "@radix-ui/react-checkbox": "1.3.5", - "@radix-ui/react-collapsible": "1.1.14", - "@radix-ui/react-collection": "1.1.10", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-context-menu": "2.3.1", - "@radix-ui/react-dialog": "1.1.17", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.13", - "@radix-ui/react-dropdown-menu": "2.1.18", - "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", - "@radix-ui/react-form": "0.1.10", - "@radix-ui/react-hover-card": "1.1.17", - "@radix-ui/react-label": "2.1.10", - "@radix-ui/react-menu": "2.1.18", - "@radix-ui/react-menubar": "1.1.18", - "@radix-ui/react-navigation-menu": "1.2.16", - "@radix-ui/react-one-time-password-field": "0.1.10", - "@radix-ui/react-password-toggle-field": "0.1.5", - "@radix-ui/react-popover": "1.1.17", - "@radix-ui/react-popper": "1.3.1", - "@radix-ui/react-portal": "1.1.12", - "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-progress": "1.1.10", - "@radix-ui/react-radio-group": "1.4.1", - "@radix-ui/react-roving-focus": "1.1.13", - "@radix-ui/react-scroll-area": "1.2.12", - "@radix-ui/react-select": "2.3.1", - "@radix-ui/react-separator": "1.1.10", - "@radix-ui/react-slider": "1.4.1", - "@radix-ui/react-slot": "1.3.0", - "@radix-ui/react-switch": "1.3.1", - "@radix-ui/react-tabs": "1.1.15", - "@radix-ui/react-toast": "1.2.17", - "@radix-ui/react-toggle": "1.1.12", - "@radix-ui/react-toggle-group": "1.1.13", - "@radix-ui/react-toolbar": "1.1.13", - "@radix-ui/react-tooltip": "1.2.10", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-escape-keydown": "1.1.2", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2", - "@radix-ui/react-use-size": "1.1.2", - "@radix-ui/react-visually-hidden": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.3.0.tgz", @@ -11428,126 +9776,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.18.0.tgz", - "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", - "license": "MIT", - "dependencies": { - "react-router": "7.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -12027,12 +10255,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -12466,16 +10688,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tailwind-merge": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz", - "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "4.3.1", "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.1.tgz", @@ -12756,6 +10968,7 @@ "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tunnel-agent": { @@ -13082,49 +11295,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index 20eacf0..2ee96a1 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,6 @@ "@mui/icons-material": "^9.1.1", "@mui/material": "^9.1.2", "better-sqlite3": "^11.9.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "date-fns": "^4.1.0", "electron-log": "^5.3.3", "electron-store": "^10.0.1", @@ -37,16 +35,13 @@ "lru-cache": "^11.1.0", "metona-toast": "^2.0.0", "nanoid": "^5.1.5", - "radix-ui": "^1.6.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.6.1", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "sql.js": "^1.12.0", - "tailwind-merge": "^3.6.0", "zod": "^3.25.67", "zustand": "^5.0.5" }, diff --git a/src/App.tsx b/src/App.tsx index ed6acb4..7bb7741 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,13 +37,13 @@ export default function App(): React.JSX.Element { if (window.metona?.config?.get) { window.metona.config.get('onboarding.completed').then((v) => { if (v === true) useUIStore.getState().setOnboardingCompleted(true); - }).catch(() => {}); + }).catch((err) => { console.error('[App]', err); }); Promise.all([ window.metona.config.get('llm.provider'), window.metona.config.get('llm.model'), ]).then(([provider, model]) => { - if (provider || model) useAgentStore.getState().setProvider(provider ?? '', model ?? ''); - }).catch(() => {}); + if (provider || model) useAgentStore.getState().setProvider((provider as string) || '', (model as string) || ''); + }).catch((err) => { console.error('[App]', err); }); } }, []); diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 00b95be..0f9d04b 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -26,7 +26,7 @@ export function CommandPalette(): React.JSX.Element { const getCommands = useCallback((): CommandItem[] => { const cmds: CommandItem[] = [ - { id: 'new-session', icon: Plus, label: '新建会话', description: '创建一个新的对话会话', action: async () => { if (window.metona?.sessions?.create) { const r = await window.metona.sessions.create() as any; useSessionStore.getState().addSession(r); useSessionStore.getState().setCurrentSession(r.id); useAgentStore.getState().setCurrentSession(r.id); } setOpen(false); } }, + { id: 'new-session', icon: Plus, label: '新建会话', description: '创建一个新的对话会话', action: async () => { if (window.metona?.sessions?.create) { const r = await window.metona.sessions.create() as MetonaSessionInfo; useSessionStore.getState().addSession(r); useSessionStore.getState().setCurrentSession(r.id); useAgentStore.getState().setCurrentSession(r.id); } setOpen(false); } }, { id: 'clear', icon: Trash2, label: '清空当前会话', action: () => { useAgentStore.getState().clearMessages(); setOpen(false); } }, { id: 'settings', icon: Settings, label: '打开设置', action: () => { useUIStore.getState().openSettings(); setOpen(false); } }, ]; @@ -40,7 +40,7 @@ export function CommandPalette(): React.JSX.Element { const commands = getCommands(); return ( - setOpen(false)} maxWidth="sm" fullWidth PaperProps={{ sx: { mt: '20vh', maxWidth: 520, borderRadius: 3, overflow: 'hidden' } }}> + setOpen(false)} maxWidth="sm" fullWidth slotProps={{ paper: { sx: { mt: '20vh', maxWidth: 520, borderRadius: 3, overflow: 'hidden' } } }}> { setQuery(e.target.value); setSelectedIndex(0); }} diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 3124a6e..dc96941 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -22,7 +22,6 @@ interface ContextMenuItem { } interface ContextMenuProps { - type: ContextMenuType; x: number; y: number; onClose: () => void; @@ -31,13 +30,13 @@ interface ContextMenuProps { export function ContextMenu({ x, y, onClose, items }: ContextMenuProps): React.JSX.Element { return ( - + {items.map((item) => { const Icon = item.icon; return ( { if (!item.disabled) { item.action(); onClose(); } }} disabled={item.disabled} dense> - {item.label} + {item.label} ); })} @@ -53,7 +52,7 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C case 'message': { const content = (data as { content?: string })?.content ?? ''; return [ - { id: 'copy', icon: Copy, label: '复制', action: () => navigator.clipboard.writeText(content).catch(() => {}) }, + { id: 'copy', icon: Copy, label: '复制', action: () => navigator.clipboard.writeText(content).catch((err) => console.error('[Clipboard]', err)) }, { id: 'quote', icon: Quote, label: '引用回复', action: () => { const input = document.querySelector('[data-chat-input]'); if (input) { input.value = content.split('\n').map((l: string) => `> ${l}`).join('\n') + '\n\n'; input.dispatchEvent(new Event('input', { bubbles: true })); input.focus(); } @@ -64,9 +63,9 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C case 'tool-call': { const tc = data as { args?: Record; result?: unknown } | undefined; return [ - { id: 'view-params', icon: Eye, label: '查看参数', action: () => { if (tc?.args) navigator.clipboard.writeText(JSON.stringify(tc.args, null, 2)).catch(() => {}); }}, - { id: 'view-result', icon: Eye, label: '查看完整结果', action: () => { if (tc?.result) navigator.clipboard.writeText(JSON.stringify(tc.result, null, 2)).catch(() => {}); }}, - { id: 'copy-result', icon: Copy, label: '复制结果', action: () => { if (tc?.result) navigator.clipboard.writeText(typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result)).catch(() => {}); }}, + { id: 'view-params', icon: Eye, label: '查看参数', action: () => { if (tc?.args) navigator.clipboard.writeText(JSON.stringify(tc.args, null, 2)).catch((err) => console.error('[Clipboard]', err)); }}, + { id: 'view-result', icon: Eye, label: '查看完整结果', action: () => { if (tc?.result) navigator.clipboard.writeText(JSON.stringify(tc.result, null, 2)).catch((err) => console.error('[Clipboard]', err)); }}, + { id: 'copy-result', icon: Copy, label: '复制结果', action: () => { if (tc?.result) navigator.clipboard.writeText(typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result)).catch((err) => console.error('[Clipboard]', err)); }}, { id: 're-execute', icon: RotateCcw, label: '重新执行', action: () => { const m = [...useAgentStore.getState().messages].reverse().find((m) => m.role === 'user'); if (m) useAgentStore.getState().sendMessage(m.content); @@ -78,7 +77,8 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C const sid = (data as { sessionId?: string })?.sessionId; return [ { id: 'rename', icon: Edit, label: '重命名', action: () => { - if (sid) { const t = prompt('新会话名称:'); if (t?.trim()) { window.metona?.sessions.rename(sid, t.trim()); useSessionStore.getState().updateSession(sid, { title: t.trim() }); } } + // window.prompt may be unreliable in Electron; fall back to sidebar double-click rename + if (sid) { try { const t = prompt('新会话名称:'); if (t?.trim()) { window.metona?.sessions.rename(sid, t.trim()); useSessionStore.getState().updateSession(sid, { title: t.trim() }); } } catch { /* prompt unavailable, user can rename via sidebar double-click */ } } }}, { id: 'pin', icon: Pin, label: '置顶', action: () => { if (sid) { const s = useSessionStore.getState().sessions.find((x) => x.id === sid); if (s) { window.metona?.sessions.pin(sid, !s.pinned); useSessionStore.getState().pinSession(sid, !s.pinned); } } @@ -88,10 +88,11 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C if (sid) window.metona?.sessions.getMessages(sid).then((msgs) => { const b = new Blob([JSON.stringify(msgs, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `session-${sid}.json`; a.click(); - }).catch(() => {}); + }).catch((err) => console.error('[ContextMenu]', err)); }}, { id: 'delete', icon: Trash2, label: '删除', action: () => { - if (sid && confirm('确定删除此会话?')) { window.metona?.sessions.delete(sid); useSessionStore.getState().removeSession(sid); } + // window.confirm may be unreliable in Electron + if (sid) { try { if (confirm('确定删除此会话?')) { window.metona?.sessions.delete(sid); useSessionStore.getState().removeSession(sid); } } catch { /* confirm unavailable */ } } }}, ]; } @@ -99,7 +100,7 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C case 'code-block': { const code = (data as { code?: string })?.code; return [ - { id: 'copy-code', icon: Copy, label: '复制代码', action: () => { if (code) navigator.clipboard.writeText(code).catch(() => {}); }}, + { id: 'copy-code', icon: Copy, label: '复制代码', action: () => { if (code) navigator.clipboard.writeText(code).catch((err) => console.error('[Clipboard]', err)); }}, { id: 'open-editor', icon: Code, label: '在编辑器中打开', action: () => { if (code) window.open(URL.createObjectURL(new Blob([code], { type: 'text/plain' })), '_blank'); }}, ]; } @@ -107,9 +108,9 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C case 'trace-step': { const step = data as { thought?: string; toolCalls?: Array<{ name: string; args: Record }> } | undefined; return [ - { id: 'copy-thought', icon: Copy, label: '复制 Thought', action: () => { if (step?.thought) navigator.clipboard.writeText(step.thought).catch(() => {}); }}, + { id: 'copy-thought', icon: Copy, label: '复制 Thought', action: () => { if (step?.thought) navigator.clipboard.writeText(step.thought).catch((err) => console.error('[Clipboard]', err)); }}, { id: 'copy-params', icon: Copy, label: '复制工具参数', action: () => { - if (step?.toolCalls) navigator.clipboard.writeText(step.toolCalls.map((tc) => `${tc.name}: ${JSON.stringify(tc.args, null, 2)}`).join('\n')).catch(() => {}); + if (step?.toolCalls) navigator.clipboard.writeText(step.toolCalls.map((tc) => `${tc.name}: ${JSON.stringify(tc.args, null, 2)}`).join('\n')).catch((err) => console.error('[Clipboard]', err)); }}, { id: 'export', icon: ExternalLink, label: '导出步骤详情', action: () => { if (step) { const b = new Blob([JSON.stringify(step, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `trace-step-${Date.now()}.json`; a.click(); } diff --git a/src/components/ToastContainer.tsx b/src/components/ToastContainer.tsx index 66c8954..12c2758 100644 --- a/src/components/ToastContainer.tsx +++ b/src/components/ToastContainer.tsx @@ -41,11 +41,11 @@ export function ToastContainer(): null { MeToast.use('keyboard'); // ESC 关闭所有 MeToast.use('persistence'); // 配置持久化 MeToast.use('accessibility'); // 屏幕阅读器 - } catch { - // 插件可能已安装 + } catch (err) { + console.error('[Toast]', 'Failed to install plugin:', err); } - }).catch(() => { - // metona-toast 未安装时静默 + }).catch((err) => { + console.error('[Toast]', 'Failed to load metona-toast:', err); }); // 监听主进程通知桥接 @@ -61,8 +61,8 @@ export function ToastContainer(): null { case 'info': MeToast.info(message, options); break; default: MeToast.info(message, options); } - } catch { - // 静默 + } catch (err) { + console.error('[Toast]', 'Failed to show toast:', err); } }); return unsubscribe; diff --git a/src/components/chat/AssistantMessage.tsx b/src/components/chat/AssistantMessage.tsx index 8a186f4..3fa77fb 100644 --- a/src/components/chat/AssistantMessage.tsx +++ b/src/components/chat/AssistantMessage.tsx @@ -18,17 +18,18 @@ import type { ChatMessage } from '@renderer/stores/agent-store'; import { useAgentStore } from '@renderer/stores/agent-store'; import { ThoughtBlock } from './ThoughtBlock'; import { ToolCallCard } from './ToolCallCard'; +import { ToolResultBlock } from './ToolResultBlock'; import { formatTime } from '@renderer/lib/formatters'; import { ContextMenu, createContextMenuItems } from '@renderer/components/ContextMenu'; interface AssistantMessageProps { message: ChatMessage; isStreaming?: boolean; - streamContent?: string; } -export function AssistantMessage({ message, isStreaming, streamContent }: AssistantMessageProps): React.JSX.Element { - const content = isStreaming ? streamContent ?? '' : message.content; +export function AssistantMessage({ message, isStreaming }: AssistantMessageProps): React.JSX.Element { + // 直接使用 message.content — updateLastAssistantMessage 已实时更新此字段 + const content = message.content; const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const agentStatus = useAgentStore((s) => s.agentStatus); @@ -61,7 +62,7 @@ export function AssistantMessage({ message, isStreaming, streamContent }: Assist {/* ===== 阶段 1: 思考过程 ===== */} {hasThinking && ( <> - + 💭 {isThinking ? '正在思考...' : '思考过程'} @@ -77,13 +78,18 @@ export function AssistantMessage({ message, isStreaming, streamContent }: Assist {hasTools && ( <> {hasThinking && } - + 🔧 工具调用 {message.toolCalls!.map((tc) => ( - + + + {tc.status === 'success' && tc.result != null && ( + + )} + ))} )} @@ -92,7 +98,7 @@ export function AssistantMessage({ message, isStreaming, streamContent }: Assist {(hasContent || isStreaming) && ( <> {(hasThinking || hasTools) && ( - + ✏️ 回复 @@ -144,7 +150,6 @@ export function AssistantMessage({ message, isStreaming, streamContent }: Assist {contextMenu && ( - + {language} @@ -186,7 +191,7 @@ function extractTextContent(children: React.ReactNode): string { if (!children) return ''; if (Array.isArray(children)) return children.map(extractTextContent).join(''); if (typeof children === 'object' && 'props' in children) { - return extractTextContent((children as React.ReactElement).props.children); + return extractTextContent((children as React.ReactElement).props as React.ReactNode); } return ''; } diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 4d2b3e4..9c100f8 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -251,7 +251,7 @@ export function ChatInput(): React.JSX.Element { style={{ width: '100%', background: 'transparent', border: 'none', outline: 'none', color: 'inherit', fontSize: 14, lineHeight: '28px', resize: 'none', fontFamily: 'inherit', minHeight: 42, maxHeight: 160 }} /> - + {/* 左侧:附件按钮 */} @@ -260,7 +260,7 @@ export function ChatInput(): React.JSX.Element { {/* 右侧:发送按钮 */} - + {isStreaming ? ( @@ -193,20 +193,20 @@ function AgentSettings() { function ToolsSettings() { const [tools, setTools] = useState>([]); - useEffect(() => { if (window.metona?.tools?.list) window.metona.tools.list().then((l) => setTools((l as any[]).map((t) => ({ ...t, enabled: true })))).catch(() => {}); }, []); - const handleToggle = (name: string, enabled: boolean) => { setTools((p) => p.map((t) => t.name === name ? { ...t, enabled } : t)); window.metona?.tools?.toggle(name, enabled).catch(() => {}); }; + useEffect(() => { if (window.metona?.tools?.list) window.metona.tools.list().then((l) => setTools((l as MetonaToolInfo[]).map((t) => ({ ...t, enabled: true })))).catch((err) => { console.error('[SettingsModal]', err); }); }, []); + const handleToggle = (name: string, enabled: boolean) => { setTools((p) => p.map((t) => t.name === name ? { ...t, enabled } : t)); window.metona?.tools?.toggle(name, enabled).catch((err) => { console.error('[SettingsModal]', err); }); }; const riskColors: Record = { safe: 'success', low: 'info', medium: 'warning', high: 'error' }; return ( 工具管理 {tools.length === 0 ? 加载中... : tools.map((t) => ( - - + + {t.name} {t.description.slice(0, 40)} - + handleToggle(t.name, e.target.checked)} size="small" /> @@ -222,17 +222,17 @@ function MCPSettings() { const [newName, setNewName] = useState(''); const [newCommand, setNewCommand] = useState(''); const [newArgs, setNewArgs] = useState(''); - const loadServers = () => { if (window.metona?.mcp?.listServers) window.metona.mcp.listServers().then((l) => setServers(l as any[])).catch(() => {}); }; + const loadServers = () => { if (window.metona?.mcp?.listServers) window.metona.mcp.listServers().then((l) => setServers(l as MetonaMCPServerStatus[])).catch((err) => { console.error('[SettingsModal]', err); }); }; useEffect(() => { loadServers(); }, []); - const handleAdd = async () => { if (!newName.trim() || !newCommand.trim()) return; await window.metona?.mcp?.addServer({ name: newName.trim(), transport: 'stdio', command: newCommand.trim(), args: newArgs.trim() ? newArgs.trim().split(/\s+/) : [] }); setNewName(''); setNewCommand(''); setNewArgs(''); setShowAdd(false); loadServers(); }; + const handleAdd = async () => { if (!newName.trim() || !newCommand.trim()) return; await window.metona?.mcp?.addServer({ name: newName.trim(), transport: 'stdio', command: newCommand.trim(), args: newArgs.trim() ? newArgs.trim().split(/\s+/) : [], enabled: true }); setNewName(''); setNewCommand(''); setNewArgs(''); setShowAdd(false); loadServers(); }; const statusColors: Record = { connected: 'success.main', connecting: 'warning.main', disconnected: 'text.secondary', error: 'error.main' }; return ( MCP 服务 {servers.length === 0 ? 暂无 MCP 服务 : servers.map((s) => ( - - + + {s.name} {s.status} diff --git a/src/components/trace/TokenUsage.tsx b/src/components/trace/TokenUsage.tsx index f2736ac..4c95e30 100644 --- a/src/components/trace/TokenUsage.tsx +++ b/src/components/trace/TokenUsage.tsx @@ -27,7 +27,7 @@ export function TokenUsage(): React.JSX.Element { return ( {/* 标题 */} - + Token 用量 diff --git a/src/components/trace/TraceStep.tsx b/src/components/trace/TraceStep.tsx index 86a099e..8fd3f76 100644 --- a/src/components/trace/TraceStep.tsx +++ b/src/components/trace/TraceStep.tsx @@ -2,7 +2,7 @@ * TraceStep — 单个 Trace 步骤 */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Box, Typography, IconButton, Collapse, Stack } from '@mui/material'; import { ChevronDown, ChevronRight, CircleDot, CheckCircle, Loader2 } from 'lucide-react'; import { TRACE_STATE_COLORS, TRACE_STATE_LABELS } from '@renderer/lib/constants'; @@ -13,6 +13,7 @@ interface TraceStepProps { step: TraceStepType; isCurrent?: boolean; } export function TraceStep({ step, isCurrent }: TraceStepProps): React.JSX.Element { const [expanded, setExpanded] = useState(false); + useEffect(() => { setExpanded(isCurrent ?? false); }, [isCurrent]); const color = TRACE_STATE_COLORS[step.state] ?? '#8b8fa7'; const label = TRACE_STATE_LABELS[step.state] ?? step.state; const duration = step.completedAt ? step.completedAt - step.startedAt : null; diff --git a/src/components/trace/TraceViewer.tsx b/src/components/trace/TraceViewer.tsx index f8489bb..c779ac6 100644 --- a/src/components/trace/TraceViewer.tsx +++ b/src/components/trace/TraceViewer.tsx @@ -15,7 +15,7 @@ export function TraceViewer(): React.JSX.Element { return ( - + Trace Viewer diff --git a/src/hooks/useAgentStream.ts b/src/hooks/useAgentStream.ts index f10faea..3550770 100644 --- a/src/hooks/useAgentStream.ts +++ b/src/hooks/useAgentStream.ts @@ -41,8 +41,24 @@ export function useAgentStream(): void { // 每次收到迭代号时同步更新 if (data.iteration != null && data.iteration !== getStore().currentIteration) { - console.log(`[useAgentStream] iteration: ${getStore().currentIteration} → ${data.iteration} (event: ${data.type})`); + const prevIteration = getStore().currentIteration; getStore().setCurrentIteration(data.iteration); + + // 迭代号增大 → 新一轮 ReAct 迭代开始,创建新的 assistant 消息卡片 + if (data.iteration > prevIteration && prevIteration > 0) { + const messages = getStore().messages; + const lastMsg = messages[messages.length - 1]; + // 仅当上一条 assistant 消息已有内容时才创建新消息(避免空消息堆叠) + if (lastMsg?.role === 'assistant' && (lastMsg.content || lastMsg.toolCalls?.length || lastMsg.reasoningContent)) { + getStore().addMessage({ + id: `msg_${Date.now()}_assistant`, + role: 'assistant', + content: '', + timestamp: Date.now(), + iteration: data.iteration, + }); + } + } } switch (data.type) { @@ -62,7 +78,7 @@ export function useAgentStream(): void { const messages = getStore().messages; const lastMsg = messages[messages.length - 1]; if (lastMsg?.role === 'assistant') { - // 追加到最后一条 assistant 消息 + // 追加到最后一条 assistant 消息(不可变更新) getStore().updateMessage(lastMsg.id, { reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta, }); @@ -74,6 +90,7 @@ export function useAgentStream(): void { content: '', reasoningContent: data.delta, timestamp: Date.now(), + iteration: data.iteration ?? getStore().currentIteration, }); } } @@ -129,7 +146,7 @@ export function useAgentStream(): void { if (data.toolResult) { const msgs = getStore().messages; const lastMsg = msgs[msgs.length - 1]; - if (lastMsg?.toolCalls) { + if (lastMsg?.role === 'assistant' && lastMsg?.toolCalls) { const updatedToolCalls = lastMsg.toolCalls.map((tc) => tc.id === data.toolResult!.toolCallId ? { @@ -150,10 +167,11 @@ export function useAgentStream(): void { // Token 使用统计 case 'usage': if (data.usage) { + const cur = getStore().tokenUsage; getStore().updateTokenUsage({ - inputTokens: data.usage.inputTokens ?? 0, - outputTokens: data.usage.outputTokens ?? 0, - totalTokens: data.usage.totalTokens ?? 0, + inputTokens: cur.inputTokens + (data.usage.inputTokens ?? 0), + outputTokens: cur.outputTokens + (data.usage.outputTokens ?? 0), + totalTokens: cur.totalTokens + (data.usage.totalTokens ?? 0), }); } break; diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index 64136f0..90868a1 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -52,10 +52,10 @@ export function useKeyboardShortcuts(): void { // Ctrl+N — 新建会话 if (key === 'n' && matchModifier(e, true)) { if (window.metona?.sessions?.create) { - window.metona.sessions.create().then((result: any) => { + window.metona.sessions.create().then((result: MetonaSessionInfo) => { useSessionStore.getState().addSession(result); useSessionStore.getState().setCurrentSession(result.id); - }).catch(() => {}); + }).catch((err) => { console.error('[KeyboardShortcuts]', err); }); } e.preventDefault(); return; @@ -123,7 +123,7 @@ export function useKeyboardShortcuts(): void { const { messages } = useAgentStore.getState(); const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant'); if (lastAssistant?.content) { - navigator.clipboard.writeText(lastAssistant.content).catch(() => {}); + navigator.clipboard.writeText(lastAssistant.content).catch((err) => console.error('[KeyboardShortcuts]', err)); } e.preventDefault(); return; diff --git a/src/lib/cn.ts b/src/lib/cn.ts index 6fab0c9..c8a5cbd 100644 --- a/src/lib/cn.ts +++ b/src/lib/cn.ts @@ -1,7 +1,9 @@ /** * className 合并工具 * - * @see standard/开发规范.md — 第一铁律 + * MUI 项目中主要通过 sx prop 设置样式,此工具仅用于极少数场景。 */ -export { default as cn } from 'clsx'; +export function cn(...classes: (string | false | null | undefined)[]): string { + return classes.filter(Boolean).join(' '); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f7a32a3..def7307 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -69,9 +69,10 @@ export const TOOL_CALL_STATUS_COLORS = { export const SHORTCUTS = { NEW_SESSION: { key: 'n', ctrl: true }, QUICK_SEARCH: { key: 'k', ctrl: true }, - SEND_MESSAGE: { key: 'Enter', ctrl: true }, - NEW_LINE: { key: 'Enter', ctrl: true, shift: true }, + SEND_MESSAGE: { key: 'Enter', ctrl: false }, + NEW_LINE: { key: 'Enter', ctrl: true }, ABORT: { key: '.', ctrl: true }, + // TODO: implement in future version FOCUS_MODE: { key: 'f', ctrl: true, shift: true }, TOGGLE_SIDEBAR: { key: 'b', ctrl: true }, TOGGLE_DETAIL: { key: 'j', ctrl: true }, diff --git a/src/lib/ipc-client.ts b/src/lib/ipc-client.ts index b4e004d..7a8de83 100644 --- a/src/lib/ipc-client.ts +++ b/src/lib/ipc-client.ts @@ -1,3 +1,5 @@ +// DEPRECATED: Components use window.metona directly. This file is kept for future type-safe refactoring. + /** * IPC Client — 渲染进程 IPC 封装 * @@ -8,7 +10,9 @@ * @see src/types/global.d.ts — 类型声明 */ -import type { MetonaBridge, MetonaSessionInfo, MetonaStreamEventData, MetonaMCPServerStatus, MetonaMemorySearchResult } from '@renderer/types/global'; +// DEPRECATED: Components use window.metona directly. This file is kept for future type-safe refactoring. +// Types are now globally available via src/types/global.d.ts (script file, no import needed). + // ===== 桥接层访问 ===== @@ -103,7 +107,7 @@ export const mcpAPI = { return getBridge().mcp.listServers(); }, - addServer(config: { name: string; transport: 'stdio' | 'sse'; command?: string; args?: string[]; url?: string }): Promise<{ success: boolean }> { + addServer(config: { name: string; transport: 'stdio' | 'sse'; command?: string; args?: string[]; url?: string; enabled: boolean }): Promise<{ success: boolean }> { return getBridge().mcp.addServer(config); }, diff --git a/src/stores/agent-store.ts b/src/stores/agent-store.ts index 1f365ea..2212138 100644 --- a/src/stores/agent-store.ts +++ b/src/stores/agent-store.ts @@ -125,7 +125,7 @@ export const useAgentStore = create((set, get) => ({ streamingContent: '', provider: '', model: '', - contextWindow: 1_000_000, + contextWindow: 0, // ===== Actions ===== @@ -138,7 +138,9 @@ export const useAgentStore = create((set, get) => ({ const messages = (msgs as Array<{ id: string; role: string; content: string; reasoningContent?: string; toolCalls?: unknown[]; + toolResult?: unknown; attachments?: Array<{ id: string; name: string; type: string; size: number; preview?: string; textContent?: string }>; + iteration?: number; timestamp: number; }>).map((m) => ({ id: m.id, @@ -147,10 +149,11 @@ export const useAgentStore = create((set, get) => ({ reasoningContent: m.reasoningContent, toolCalls: m.toolCalls as ToolCallInfo[] | undefined, attachments: m.attachments as AttachmentInfo[] | undefined, + iteration: m.iteration, timestamp: m.timestamp, })); set({ messages }); - }).catch(() => {}); + }).catch((err) => { console.error('[AgentStore]', err); }); } // 从数据库加载该会话的 trace 步骤和 token 用量 @@ -160,7 +163,7 @@ export const useAgentStore = create((set, get) => ({ if (data.traceSteps) set({ traceSteps: data.traceSteps as TraceStep[] }); if (data.tokenUsage) set({ tokenUsage: data.tokenUsage as TokenUsage }); } - }).catch(() => {}); + }).catch((err) => { console.error('[AgentStore]', err); }); } }, @@ -190,8 +193,16 @@ export const useAgentStore = create((set, get) => ({ }); sessionId = session.id; set({ currentSessionId: sessionId }); - } catch { - // 创建失败时静默 + } catch (err) { + console.error('[AgentStore]', 'Failed to create session:', err); + set({ agentStatus: 'error', isStreaming: false }); + get().addMessage({ + id: `msg_${Date.now()}_error`, + role: 'system', + content: `错误: 无法创建会话 — ${(err as Error).message}`, + timestamp: Date.now(), + }); + return; } } @@ -220,7 +231,7 @@ export const useAgentStore = create((set, get) => ({ window.metona?.sessions?.rename(sessionId, title); useSessionStore.getState().updateSession(sessionId, { title }); } - }).catch(() => {}); + }).catch((err) => { console.error('[AgentStore]', err); }); } if (sessionId && window.metona?.agent?.sendMessage) { @@ -257,22 +268,34 @@ export const useAgentStore = create((set, get) => ({ content: llmContent, images: llmImages.length > 0 ? llmImages : undefined, }; - window.metona.agent.sendMessage(messageWithImages, sessionId).catch(() => {}); + window.metona.agent.sendMessage(messageWithImages, sessionId).catch((err) => { + console.error('[AgentStore]', 'IPC sendMessage failed:', err); + set({ agentStatus: 'error', isStreaming: false }); + get().addMessage({ + id: `msg_${Date.now()}_error`, + role: 'system', + content: `错误: 消息发送失败 — ${(err as Error).message}`, + timestamp: Date.now(), + }); + }); } }, updateLastAssistantMessage: (delta: string) => { set((s) => { const messages = [...s.messages]; - const lastMsg = messages[messages.length - 1]; + const lastIdx = messages.length - 1; + const lastMsg = messages[lastIdx]; if (lastMsg?.role === 'assistant') { - lastMsg.content += delta; + // 不可变更新:创建新对象而非直接突变 + messages[lastIdx] = { ...lastMsg, content: lastMsg.content + delta }; } else { messages.push({ id: `msg_${Date.now()}_assistant`, role: 'assistant', content: delta, timestamp: Date.now(), + iteration: s.currentIteration || undefined, }); } return { messages }; @@ -303,7 +326,7 @@ export const useAgentStore = create((set, get) => ({ setProvider: (provider, model) => { // DeepSeek/Agnes 固定 1M,Ollama 从配置读取 - const ollamaCtx = provider === 'ollama' ? 128_000 : 1_000_000; + const ollamaCtx = provider === 'ollama' ? 4096 : 1_000_000; set({ provider, model, contextWindow: ollamaCtx }); // 异步读取 Ollama 实际配置 if (provider === 'ollama' && window.metona?.config?.get) { @@ -311,7 +334,7 @@ export const useAgentStore = create((set, get) => ({ if (v != null && typeof v === 'number' && v > 0) { set({ contextWindow: v }); } - }).catch(() => {}); + }).catch((err) => { console.error('[AgentStore]', err); }); } }, @@ -322,7 +345,7 @@ export const useAgentStore = create((set, get) => ({ saveTraceData: () => { const { currentSessionId, traceSteps, tokenUsage } = get(); if (currentSessionId && window.metona?.sessions?.saveTrace) { - window.metona.sessions.saveTrace(currentSessionId, { traceSteps, tokenUsage }).catch(() => {}); + window.metona.sessions.saveTrace(currentSessionId, { traceSteps, tokenUsage }).catch((err) => { console.error('[AgentStore]', err); }); } }, @@ -341,7 +364,7 @@ export const useAgentStore = create((set, get) => ({ const sessionId = get().currentSessionId; set({ agentStatus: 'idle', isStreaming: false }); if (sessionId && window.metona?.agent?.abortSession) { - window.metona.agent.abortSession(sessionId).catch(() => {}); + window.metona.agent.abortSession(sessionId).catch((err) => { console.error('[AgentStore]', err); }); } }, })); diff --git a/src/styles/globals.css b/src/styles/globals.css index 82156ea..d7f2afe 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -111,10 +111,6 @@ --radius: var(--radius); } -:root[data-theme="light"] { - color-scheme: light; -} - /* ===== 全局基础样式 ===== */ * { margin: 0; padding: 0; box-sizing: border-box; } @@ -130,8 +126,8 @@ body { ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: #2a2d3a; border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: #8b8fa7; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-dim); } /* ===== 代码字体 ===== */ diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 5693423..283292c 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -87,7 +87,9 @@ interface MetonaSessionsAPI { content: string; reasoningContent?: string; toolCalls?: unknown[]; + toolResult?: unknown; attachments?: Array<{ id: string; name: string; type: string; size: number; preview?: string; textContent?: string }>; + iteration?: number; timestamp: number; }>>; pin: (sessionId: string, pinned: boolean) => Promise<{ success: boolean }>; @@ -145,7 +147,7 @@ interface MetonaMemoryAPI { // ===== Config API ===== interface MetonaConfigAPI { - get: (key: string) => Promise; + get: (key: string) => Promise; set: (key: string, value: unknown) => Promise<{ success: boolean }>; } @@ -195,7 +197,7 @@ interface MetonaDataAPI { // ===== Bridge 接口 ===== -export interface MetonaBridge { +interface MetonaBridge { agent: MetonaAgentAPI; sessions: MetonaSessionsAPI; mcp: MetonaMCPAPI; @@ -207,9 +209,9 @@ export interface MetonaBridge { data: MetonaDataAPI; } -declare global { - interface Window { - metona: MetonaBridge; - electron: unknown; - } +// ===== 全局 Window 增强 ===== + +interface Window { + metona: MetonaBridge; + electron: unknown; }