From 98c42d700f2806b02a1d74ae866134660351be1d Mon Sep 17 00:00:00 2001 From: zerob13 Date: Thu, 12 Feb 2026 11:22:13 +0800 Subject: [PATCH 01/12] docs: remove chat mode plan --- docs/specs/remove-chat-mode/plan.md | 285 ++++++++++++++++++++++++++++ docs/specs/remove-chat-mode/spec.md | 176 +++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 docs/specs/remove-chat-mode/plan.md create mode 100644 docs/specs/remove-chat-mode/spec.md diff --git a/docs/specs/remove-chat-mode/plan.md b/docs/specs/remove-chat-mode/plan.md new file mode 100644 index 000000000..b0116e462 --- /dev/null +++ b/docs/specs/remove-chat-mode/plan.md @@ -0,0 +1,285 @@ +# 移除 Chat 模式实施计划 + +## 1. 当前实现基线 + +### 1.1 模式类型定义 + +```typescript +// src/renderer/src/components/chat-input/composables/useChatMode.ts:9 +export type ChatMode = 'chat' | 'agent' | 'acp agent' +``` + +共享类型分布在: +- `src/shared/types/presenters/thread.presenter.d.ts:24` +- `src/shared/types/presenters/session.presenter.d.ts:24,51` +- `src/main/presenter/sessionPresenter/types.ts:14,42` + +### 1.2 Chat 模式特点 + +1. **无 Agent 工具** - `toolPresenter/index.ts:100` 判断 `chatMode !== 'chat'` 才加载 +2. **支持 Web 搜索** - `ChatInput.vue:583` 的 `canUseWebSearch` 仅 chat 模式可用 +3. **简单 LLM 对话** - 无工具调用循环 + +### 1.3 Web 搜索架构 + +``` +Main Process: +├── searchPresenter/ +│ ├── index.ts - ISearchPresenter 实现 +│ ├── interface.ts - 搜索接口定义 +│ ├── managers/ +│ │ └── searchManager.ts - 搜索逻辑 +│ └── handlers/ +│ ├── baseHandler.ts - 基础处理器 +│ └── searchHandler.ts - 搜索处理器 + +Renderer Process: +├── components/ +│ ├── SearchStatusIndicator.vue +│ ├── SearchResultsDrawer.vue +│ └── message/MessageBlockSearch.vue +├── stores/ +│ ├── searchAssistantStore.ts +│ ├── searchEngineStore.ts +│ └── reference.ts + +Shared: +└── types/presenters/search.presenter.d.ts +``` + +### 1.4 涉及文件统计 + +| 类别 | 数量 | +|------|------| +| 待删除文件 | 11个 | +| 待修改文件 | ~25个 | +| 类型定义修改 | 7处 | +| i18n 文件 | 12个 | + +## 2. 设计决策 + +### 2.1 类型简化 + +**决策**:`ChatMode` 类型从三元改为二元 + +```typescript +// Before +export type ChatMode = 'chat' | 'agent' | 'acp agent' + +// After +export type ChatMode = 'agent' | 'acp agent' +``` + +**影响范围**: +- 7处类型定义 +- 所有 `chatMode === 'chat'` 判断改为始终加载 agent 能力 +- 默认值统一改为 `'agent'` + +### 2.2 旧数据迁移策略 + +**决策**:静默升级,无通知 + +```typescript +// sessionResolver.ts +if (settings.chatMode === 'chat') { + settings.chatMode = 'agent' + // 不持久化到数据库(运行时升级即可) + // 如果后续有 updateConversationSettings 调用会自动保存 +} +``` + +**原则**: +1. 不破坏数据库结构 +2. 不显示任何 toast/notification +3. 用户无感知 + +### 2.3 搜索功能移除策略 + +**决策**:完全删除,不保留任何代码 + +**删除清单**: +1. `searchPresenter/` 整个目录(5个文件) +2. 搜索相关组件(3个 Vue 文件) +3. 搜索相关 stores(3个文件) +4. 搜索类型定义(1个文件) + +**配置清理**: +1. `enableSearch`、`forcedSearch`、`searchStrategy` 从 CONVERSATION_SETTINGS 移除 +2. `searchPreviewEnabled`、`customSearchEngines` 从 configPresenter 移除 +3. 搜索相关方法从 configPresenter 移除 + +### 2.4 工具加载简化 + +**决策**:移除 chat 模式判断,始终加载 Agent 工具 + +```typescript +// toolPresenter/index.ts - Before +if (chatMode !== 'chat') { + // Load Agent tools +} + +// After +// Always load Agent tools (no condition) +``` + +### 2.5 UI 简化 + +**决策**: +1. 模式选择器只显示 Agent / ACP Agent 两个选项 +2. 移除 web 搜索按钮 +3. 移除 `variant === 'chat'` 的特殊样式 + +## 3. 实施阶段 + +### Phase 1:删除搜索 Presenter(影响最小) + +1. 删除 `src/main/presenter/searchPresenter/` 目录 +2. 删除 `src/shared/types/presenters/search.presenter.d.ts` +3. 从 `src/main/presenter/index.ts` 移除 searchPresenter 注册 +4. 从 `src/preload/index.ts` 移除 searchPresenter 暴露 +5. 运行 typecheck 确认编译通过 + +### Phase 2:删除搜索组件和 Stores + +1. 删除 `SearchStatusIndicator.vue` +2. 删除 `SearchResultsDrawer.vue` +3. 删除 `MessageBlockSearch.vue` +4. 删除 `searchAssistantStore.ts` +5. 删除 `searchEngineStore.ts` +6. 删除 `reference.ts` +7. 从 `MessageItemAssistant.vue` 移除 MessageBlockSearch 引用 + +### Phase 3:修改类型定义 + +1. 更新 7 处 `ChatMode` 类型定义 +2. 更新 `useChatMode.ts`: + - 移除 chat 图标 + - 移除 modes 数组中的 chat 选项 + - 默认值改为 `'agent'` + +### Phase 4:核心逻辑修改 + +1. `sessionResolver.ts` - 添加旧数据升级逻辑 +2. `sessionManager.ts` - fallback 默认值改为 `'agent'` +3. `toolPresenter/index.ts` - 移除 chat 模式判断 +4. `mcpPresenter/toolManager.ts` - fallback 默认值改为 `'agent'` + +### Phase 5:清理搜索配置 + +1. `CONVERSATION_SETTINGS` 类型移除 `enableSearch`、`forcedSearch`、`searchStrategy` +2. `chat.ts` store 移除相关配置 +3. `modelStore.ts` 移除 `enableSearch` +4. `configPresenter` 移除搜索配置和方法 +5. `ChatInput.vue` 移除 web 搜索按钮和 `canUseWebSearch` +6. `MessageBlockQuestionRequest.vue` 移除搜索相关代码 + +### Phase 6:国际化清理 + +1. 所有 `chat.json` 文件移除 `mode.chat` +2. 所有 `chat.json` 文件移除 `features.webSearch` +3. 所有 `chat.json` 文件移除 `search` 块 + +### Phase 7:测试与验证 + +1. 更新相关测试用例 +2. 删除搜索相关测试文件 +3. 运行完整测试套件 + +## 4. 数据与配置影响 + +### 4.1 数据库 + +| 影响项 | 处理方式 | +|--------|----------| +| `conversations.settings.chatMode` | 保留字段,运行时升级 `'chat'` → `'agent'` | +| `conversations.settings.enableSearch` | 保留字段,运行时忽略 | +| `conversations.settings.forcedSearch` | 保留字段,运行时忽略 | +| `conversations.settings.searchStrategy` | 保留字段,运行时忽略 | + +**结论**:无 schema 迁移,保持向后兼容 + +### 4.2 用户配置 + +| 配置项 | 处理方式 | +|--------|----------| +| `input_chatMode` | 运行时升级 `'chat'` → `'agent'` | +| `searchPreviewEnabled` | 删除(不再使用) | +| `customSearchEngines` | 删除(不再使用) | + +### 4.3 破坏性变化 + +| 变化 | 影响范围 | +|------|----------| +| Chat 模式不可用 | 所有用户 | +| Web 搜索功能移除 | Chat 模式用户 | +| 搜索引擎配置丢失 | 配置过搜索引擎的用户 | + +## 5. 测试策略 + +### 5.1 单元测试 + +1. **useChatMode** + - 默认模式为 `'agent'` + - modes 数组不包含 `'chat'` + - 设置 `'chat'` 模式被忽略或转为 `'agent'` + +2. **sessionResolver** + - `chatMode === 'chat'` 时自动升级为 `'agent'` + +3. **toolPresenter** + - Agent 工具始终加载 + +### 5.2 集成测试 + +1. **旧对话打开** + - chat 模式的对话可以正常打开 + - 自动升级为 agent 模式 + - 无错误或警告 + +2. **新对话创建** + - 默认为 agent 模式 + - 工具可正常调用 + +### 5.3 删除的测试文件 + +``` +test/main/presenter/searchPresenter/ (如果存在) +test/renderer/components/message/MessageBlockSearch.test.ts (如果存在) +``` + +## 6. 风险与缓解 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 旧对话打开失败 | 高 | 升级逻辑充分测试,添加 fallback | +| 残留搜索引用 | 中 | 全局搜索 `search`、`Search`、`enableSearch` 等 | +| i18n key 丢失 | 低 | 检查运行时是否有 missing key 警告 | +| 配置迁移失败 | 低 | 运行时升级,不依赖持久化 | + +## 7. 质量门槛 + +```bash +# 代码格式 +pnpm run format + +# 代码检查 +pnpm run lint + +# 类型检查 +pnpm run typecheck + +# 测试 +pnpm test +``` + +## 8. 实施检查清单 + +- [ ] Phase 1: 删除搜索 Presenter +- [ ] Phase 2: 删除搜索组件和 Stores +- [ ] Phase 3: 修改类型定义 +- [ ] Phase 4: 核心逻辑修改 +- [ ] Phase 5: 清理搜索配置 +- [ ] Phase 6: 国际化清理 +- [ ] Phase 7: 测试与验证 +- [ ] 运行 `pnpm run format && pnpm run lint && pnpm run typecheck` +- [ ] 运行 `pnpm test` diff --git a/docs/specs/remove-chat-mode/spec.md b/docs/specs/remove-chat-mode/spec.md new file mode 100644 index 000000000..79217aba0 --- /dev/null +++ b/docs/specs/remove-chat-mode/spec.md @@ -0,0 +1,176 @@ +# 移除 Chat 模式规格 + +## 概述 + +移除 DeepChat 中的 "chat" 模式,仅保留 "agent" 和 "acp agent" 两种模式。同时完全移除 Web 搜索功能(该功能仅在 chat 模式下可用)。旧的 chat 模式对话将静默升级为 agent 模式。 + +## 背景与动机 + +1. Agent 模式已成熟完善,内置工具调用能力已成为核心特性 +2. Chat 模式功能有限(无工具调用),与 Agent 模式形成冗余 +3. Web 搜索功能仅 Chat 模式可用,用户实际使用率低 +4. 简化模式选择,降低用户决策成本 + +## 用户故事 + +### US-1:模式选择简化 + +作为 DeepChat 用户,我希望不再需要在 chat 和 agent 模式之间选择,直接使用功能更强大的 agent 模式。 + +### US-2:旧对话无缝迁移 + +作为已有 chat 模式对话的用户,我希望打开旧对话时无需任何操作,系统能自动将其升级为 agent 模式并继续正常使用。 + +### US-3:一致的 Agent 体验 + +作为 DeepChat 用户,我希望所有对话都具备完整的工具调用能力,无需关心"这个对话是否支持某个功能"。 + +## 验收标准 + +### A. 类型定义 + +- [ ] `ChatMode` 类型改为 `'agent' | 'acp agent'` +- [ ] 所有涉及 `chatMode` 的类型定义更新 +- [ ] 默认值从 `'chat'` 改为 `'agent'` + +### B. 模式选择 UI + +- [ ] 模式选择器仅显示 "Agent" 和 "ACP Agent" 两个选项 +- [ ] 移除 `MODE_ICONS` 中的 chat 图标 +- [ ] `useChatMode.ts` 的 `modes` 数组不再包含 chat 选项 + +### C. Web 搜索功能移除 + +- [ ] 删除 `src/main/presenter/searchPresenter/` 整个目录 +- [ ] 删除 `src/renderer/src/components/SearchStatusIndicator.vue` +- [ ] 删除 `src/renderer/src/components/SearchResultsDrawer.vue` +- [ ] 删除 `src/renderer/src/components/message/MessageBlockSearch.vue` +- [ ] 删除 `src/renderer/src/stores/searchAssistantStore.ts` +- [ ] 删除 `src/renderer/src/stores/searchEngineStore.ts` +- [ ] 删除 `src/renderer/src/stores/reference.ts` +- [ ] 删除 `src/shared/types/presenters/search.presenter.d.ts` +- [ ] `ChatInput.vue` 移除 web 搜索按钮和相关逻辑 +- [ ] `MessageBlockQuestionRequest.vue` 移除搜索相关代码 + +### D. 配置清理 + +- [ ] `CONVERSATION_SETTINGS` 移除 `enableSearch`、`forcedSearch`、`searchStrategy` +- [ ] `chat.ts` store 移除搜索相关配置 +- [ ] `modelStore.ts` 移除 `enableSearch` 属性 +- [ ] `configPresenter` 移除 `searchPreviewEnabled`、`customSearchEngines` 等配置 + +### E. 工具加载逻辑 + +- [ ] `toolPresenter/index.ts` 移除 `chatMode !== 'chat'` 判断 +- [ ] Agent 工具始终加载(无需模式判断) + +### F. 旧数据迁移 + +- [ ] `sessionResolver.ts` 检测 `chatMode === 'chat'` 时静默升级为 `'agent'` +- [ ] 升级过程无任何用户通知或中断 + +### G. Presenter 注册清理 + +- [ ] `src/main/presenter/index.ts` 移除 searchPresenter 注册 +- [ ] `src/preload/index.ts` 移除 searchPresenter 暴露 +- [ ] `src/shared/types/presenters/index.d.ts` 移除 ISearchPresenter 引用 + +### H. 国际化 + +- [ ] 所有语言文件移除 `mode.chat` 翻译 +- [ ] 所有语言文件移除 `features.webSearch` 翻译 +- [ ] 所有语言文件移除 `search` 块 + +### I. 消息组件 + +- [ ] `MessageItemAssistant.vue` 移除 `MessageBlockSearch` 组件引用 + +## 非目标 + +1. **不修改数据库 schema** - `chatMode` 字段保留,仅运行时逻辑改变 +2. **不保留任何 chat 模式兼容代码** - 彻底移除,不做 fallback +3. **不保留搜索功能给 agent 模式使用** - 完全移除 +4. **不修改 MCP 相关工具** - 仅移除 DeepChat 内置搜索 + +## 约束 + +1. **数据迁移必须静默** - 不显示任何 toast 或通知 +2. **向后兼容** - 旧对话数据不丢失,仅升级模式标记 +3. **测试覆盖** - 关键路径必须有测试验证 +4. **代码格式** - 必须通过 `format`、`lint`、`typecheck` + +## 待删除文件清单 + +### Main 进程 + +``` +src/main/presenter/searchPresenter/ +├── index.ts +├── interface.ts +├── managers/searchManager.ts +└── handlers/ + ├── baseHandler.ts + └── searchHandler.ts +``` + +### Renderer 进程 + +``` +src/renderer/src/components/SearchStatusIndicator.vue +src/renderer/src/components/SearchResultsDrawer.vue +src/renderer/src/components/message/MessageBlockSearch.vue +src/renderer/src/stores/searchAssistantStore.ts +src/renderer/src/stores/searchEngineStore.ts +src/renderer/src/stores/reference.ts +``` + +### Shared + +``` +src/shared/types/presenters/search.presenter.d.ts +``` + +## 待修改文件清单 + +### 类型定义 (7处) + +| 文件 | 修改内容 | +|------|----------| +| `src/shared/types/presenters/thread.presenter.d.ts:24` | `chatMode?: 'agent' \| 'acp agent'` | +| `src/shared/types/presenters/session.presenter.d.ts:24,51` | 同上 | +| `src/shared/types/presenters/tool.presenter.d.ts:19` | 同上 | +| `src/shared/types/presenters/legacy.presenters.d.ts:1091` | 同上 | +| `src/main/presenter/sessionPresenter/types.ts:14,42` | 同上 | +| `src/renderer/src/components/chat-input/composables/useChatMode.ts:9` | 同上 | +| `src/renderer/src/components/chat-input/composables/useWorkspaceMention.ts:9` | 同上 | + +### 核心逻辑 + +| 文件 | 修改内容 | +|------|----------| +| `useChatMode.ts` | 移除 chat 模式选项,默认改为 agent | +| `sessionResolver.ts` | 添加旧数据升级逻辑 | +| `sessionManager.ts` | fallback 默认值改为 agent | +| `toolPresenter/index.ts` | 移除 chat 模式判断 | +| `chat.ts` store | 移除搜索配置 | +| `modelStore.ts` | 移除 enableSearch | +| `configPresenter/index.ts` | 移除搜索配置和方法 | + +### UI 组件 + +| 文件 | 修改内容 | +|------|----------| +| `ChatInput.vue` | 移除 web 搜索按钮、canUseWebSearch | +| `MessageBlockQuestionRequest.vue` | 移除搜索相关代码 | +| `MessageItemAssistant.vue` | 移除 MessageBlockSearch 引用 | + +### i18n (12个文件) + +- `src/renderer/src/i18n/*/chat.json` - 移除 mode.chat、features.webSearch、search 块 + +## 开放问题 + +无。所有问题已澄清: + +- **搜索功能处理**:完全移除 +- **旧数据升级通知**:静默升级 From 109c63fa764bcd905eb56f6aa5ce6ffd44ba2243 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Thu, 12 Feb 2026 14:43:08 +0800 Subject: [PATCH 02/12] feat(core): remove chat mode and web search - Remove 'chat' mode, keep only 'agent' and 'acp agent' modes - Migrate legacy 'chat' mode conversations to 'agent' mode silently - Remove web search feature (searchPresenter, search components, stores) - Remove search-related config (enableSearch, forcedSearch, searchStrategy) - Remove ContentEnricher and web content length limit - Update all ChatMode type definitions - Clean up i18n files for all 12 languages - Update toolPresenter to always load agent tools BREAKING CHANGE: 'chat' mode is no longer available --- .../agentPresenter/acp/agentToolManager.ts | 2 +- src/main/presenter/agentPresenter/index.ts | 35 +- .../agentPresenter/loop/agentLoopHandler.ts | 16 +- .../agentPresenter/loop/toolCallHandler.ts | 13 - .../agentPresenter/message/messageBuilder.ts | 14 +- .../permission/permissionHandler.ts | 13 +- .../agentPresenter/session/sessionContext.ts | 2 +- .../agentPresenter/session/sessionManager.ts | 9 +- .../agentPresenter/session/sessionResolver.ts | 5 +- .../streaming/llmEventHandler.ts | 7 - .../streaming/streamGenerationHandler.ts | 56 +- .../agentPresenter/streaming/types.ts | 1 - .../agentPresenter/tool/toolCallCenter.ts | 2 +- .../agentPresenter/types/handlerContext.ts | 46 + .../agentPresenter/utility/utilityHandler.ts | 21 +- src/main/presenter/configPresenter/index.ts | 2 - .../presenter/configPresenter/modelConfig.ts | 6 - .../configPresenter/providerModelHelper.ts | 3 - src/main/presenter/content/contentEnricher.ts | 383 ----- src/main/presenter/content/index.ts | 1 - src/main/presenter/index.ts | 10 +- .../presenter/llmProviderPresenter/index.ts | 6 - .../providers/dashscopeProvider.ts | 45 +- .../providers/geminiProvider.ts | 11 +- .../providers/githubCopilotProvider.ts | 3 +- .../providers/grokProvider.ts | 39 +- .../providers/vertexProvider.ts | 11 +- .../inMemoryServers/powerpackServer.ts | 57 +- .../presenter/mcpPresenter/toolManager.ts | 4 +- .../searchPresenter/handlers/baseHandler.ts | 19 - .../searchPresenter/handlers/searchHandler.ts | 349 ---- src/main/presenter/searchPresenter/index.ts | 96 -- .../presenter/searchPresenter/interface.ts | 1 - .../searchPresenter/managers/searchManager.ts | 1438 ----------------- src/main/presenter/sessionPresenter/types.ts | 4 +- src/main/presenter/toolPresenter/index.ts | 62 +- src/renderer/settings/App.vue | 7 - .../settings/components/CommonSettings.vue | 18 - .../common/SearchAssistantModelSection.vue | 75 - .../common/SearchEngineSettingsSection.vue | 345 ---- .../common/WebContentLimitSetting.vue | 138 -- src/renderer/src/components/NewThread.vue | 12 - .../src/components/SearchResultsDrawer.vue | 71 - .../src/components/SearchStatusIndicator.vue | 109 -- .../src/components/chat-input/ChatInput.vue | 39 +- .../chat-input/composables/useChatMode.ts | 27 +- .../composables/useInputSettings.ts | 28 +- .../composables/usePromptInputConfig.ts | 30 - .../composables/useWorkspaceMention.ts | 2 +- .../message/MessageBlockQuestionRequest.vue | 6 +- .../components/message/MessageBlockSearch.vue | 126 -- .../message/MessageItemAssistant.vue | 6 - .../components/settings/ModelConfigDialog.vue | 105 +- src/renderer/src/i18n/da-DK/chat.json | 1 - src/renderer/src/i18n/da-DK/settings.json | 3 - src/renderer/src/i18n/en-US/chat.json | 1 - src/renderer/src/i18n/en-US/settings.json | 3 - src/renderer/src/i18n/fa-IR/chat.json | 1 - src/renderer/src/i18n/fa-IR/settings.json | 6 +- src/renderer/src/i18n/fr-FR/chat.json | 3 +- src/renderer/src/i18n/fr-FR/settings.json | 4 - src/renderer/src/i18n/he-IL/chat.json | 1 - src/renderer/src/i18n/he-IL/settings.json | 3 - src/renderer/src/i18n/ja-JP/chat.json | 1 - src/renderer/src/i18n/ja-JP/settings.json | 3 - src/renderer/src/i18n/ko-KR/chat.json | 1 - src/renderer/src/i18n/ko-KR/settings.json | 4 - src/renderer/src/i18n/pt-BR/chat.json | 1 - src/renderer/src/i18n/pt-BR/settings.json | 3 - src/renderer/src/i18n/ru-RU/chat.json | 1 - src/renderer/src/i18n/ru-RU/settings.json | 4 - src/renderer/src/i18n/zh-CN/chat.json | 1 - src/renderer/src/i18n/zh-CN/settings.json | 3 - src/renderer/src/i18n/zh-HK/chat.json | 1 - src/renderer/src/i18n/zh-HK/settings.json | 4 - src/renderer/src/i18n/zh-TW/chat.json | 1 - src/renderer/src/i18n/zh-TW/settings.json | 4 - src/renderer/src/lib/storeInitializer.ts | 8 - src/renderer/src/stores/chat.ts | 3 - src/renderer/src/stores/modelStore.ts | 1 - src/renderer/src/stores/ollamaStore.ts | 3 - .../src/stores/searchAssistantStore.ts | 170 -- src/renderer/src/stores/searchEngineStore.ts | 148 -- src/renderer/src/stores/uiSettingsStore.ts | 13 - .../types/presenters/legacy.presenters.d.ts | 32 +- .../presenters/llmprovider.presenter.d.ts | 3 - .../types/presenters/search.presenter.d.ts | 21 - .../types/presenters/session.presenter.d.ts | 4 +- .../types/presenters/thread.presenter.d.ts | 2 +- .../types/presenters/tool.presenter.d.ts | 2 +- .../agentPresenter/sessionManager.test.ts | 12 +- 91 files changed, 160 insertions(+), 4256 deletions(-) create mode 100644 src/main/presenter/agentPresenter/types/handlerContext.ts delete mode 100644 src/main/presenter/content/contentEnricher.ts delete mode 100644 src/main/presenter/content/index.ts delete mode 100644 src/main/presenter/searchPresenter/handlers/baseHandler.ts delete mode 100644 src/main/presenter/searchPresenter/handlers/searchHandler.ts delete mode 100644 src/main/presenter/searchPresenter/index.ts delete mode 100644 src/main/presenter/searchPresenter/interface.ts delete mode 100644 src/main/presenter/searchPresenter/managers/searchManager.ts delete mode 100644 src/renderer/settings/components/common/SearchAssistantModelSection.vue delete mode 100644 src/renderer/settings/components/common/SearchEngineSettingsSection.vue delete mode 100644 src/renderer/settings/components/common/WebContentLimitSetting.vue delete mode 100644 src/renderer/src/components/SearchResultsDrawer.vue delete mode 100644 src/renderer/src/components/SearchStatusIndicator.vue delete mode 100644 src/renderer/src/components/message/MessageBlockSearch.vue delete mode 100644 src/renderer/src/stores/searchAssistantStore.ts delete mode 100644 src/renderer/src/stores/searchEngineStore.ts delete mode 100644 src/shared/types/presenters/search.presenter.d.ts diff --git a/src/main/presenter/agentPresenter/acp/agentToolManager.ts b/src/main/presenter/agentPresenter/acp/agentToolManager.ts index a2b9e8287..57f213a64 100644 --- a/src/main/presenter/agentPresenter/acp/agentToolManager.ts +++ b/src/main/presenter/agentPresenter/acp/agentToolManager.ts @@ -225,7 +225,7 @@ export class AgentToolManager { * Get all Agent tool definitions in MCP format */ async getAllToolDefinitions(context: { - chatMode: 'chat' | 'agent' | 'acp agent' + chatMode: 'agent' | 'acp agent' supportsVision: boolean agentWorkspacePath: string | null conversationId?: string diff --git a/src/main/presenter/agentPresenter/index.ts b/src/main/presenter/agentPresenter/index.ts index f966104d6..e31cbfc93 100644 --- a/src/main/presenter/agentPresenter/index.ts +++ b/src/main/presenter/agentPresenter/index.ts @@ -13,11 +13,8 @@ import { STREAM_EVENTS } from '@/events' import { presenter } from '@/presenter' import type { SessionContextResolved } from './session/sessionContext' import type { SessionManager } from './session/sessionManager' -import type { SearchPresenter } from '../searchPresenter' -import type { SearchManager } from '../searchPresenter/managers/searchManager' -import type { ThreadHandlerContext } from '../searchPresenter/handlers/baseHandler' -import { SearchHandler } from '../searchPresenter/handlers/searchHandler' import { MessageManager } from '../sessionPresenter/managers/messageManager' +import type { ThreadHandlerContext } from './types/handlerContext' import { CommandPermissionService } from '../permission/commandPermissionService' import { ContentBufferHandler } from './streaming/contentBufferHandler' import { LLMEventHandler } from './streaming/llmEventHandler' @@ -34,7 +31,6 @@ type AgentPresenterDependencies = { sqlitePresenter: ISQLitePresenter llmProviderPresenter: ILlmProviderPresenter configPresenter: IConfigPresenter - searchPresenter: SearchPresenter commandPermissionService: CommandPermissionService messageManager?: MessageManager } @@ -45,16 +41,12 @@ export class AgentPresenter implements IAgentPresenter { private sqlitePresenter: ISQLitePresenter private llmProviderPresenter: ILlmProviderPresenter private configPresenter: IConfigPresenter - private searchPresenter: SearchPresenter - private searchManager: SearchManager private messageManager: MessageManager private commandPermissionService: CommandPermissionService private generatingMessages: Map = new Map() - private searchingMessages: Set = new Set() private contentBufferHandler: ContentBufferHandler private toolCallHandler: ToolCallHandler private llmEventHandler: LLMEventHandler - private searchHandler: SearchHandler private streamGenerationHandler: StreamGenerationHandler private permissionHandler: PermissionHandler private utilityHandler: UtilityHandler @@ -66,8 +58,6 @@ export class AgentPresenter implements IAgentPresenter { this.sqlitePresenter = options.sqlitePresenter this.llmProviderPresenter = options.llmProviderPresenter this.configPresenter = options.configPresenter - this.searchPresenter = options.searchPresenter - this.searchManager = options.searchPresenter.getSearchManager() this.messageManager = options.messageManager ?? new MessageManager(options.sqlitePresenter) this.commandPermissionService = options.commandPermissionService @@ -79,8 +69,7 @@ export class AgentPresenter implements IAgentPresenter { sqlitePresenter: this.sqlitePresenter, messageManager: this.messageManager, llmProviderPresenter: this.llmProviderPresenter, - configPresenter: this.configPresenter, - searchManager: this.searchManager + configPresenter: this.configPresenter } this.contentBufferHandler = new ContentBufferHandler({ @@ -90,14 +79,12 @@ export class AgentPresenter implements IAgentPresenter { this.toolCallHandler = new ToolCallHandler({ sqlitePresenter: this.sqlitePresenter, - searchingMessages: this.searchingMessages, commandPermissionHandler: this.commandPermissionService, streamUpdateScheduler: this.streamUpdateScheduler }) this.llmEventHandler = new LLMEventHandler({ generatingMessages: this.generatingMessages, - searchingMessages: this.searchingMessages, messageManager: this.messageManager, contentBufferHandler: this.contentBufferHandler, toolCallHandler: this.toolCallHandler, @@ -105,15 +92,7 @@ export class AgentPresenter implements IAgentPresenter { onConversationUpdated: (state) => this.handleConversationUpdates(state) }) - this.searchHandler = new SearchHandler(handlerContext, { - generatingMessages: this.generatingMessages, - searchingMessages: this.searchingMessages, - getSearchAssistantModel: () => this.searchPresenter.getSearchAssistantModel(), - getSearchAssistantProviderId: () => this.searchPresenter.getSearchAssistantProviderId() - }) - this.streamGenerationHandler = new StreamGenerationHandler(handlerContext, { - searchHandler: this.searchHandler, generatingMessages: this.generatingMessages, llmEventHandler: this.llmEventHandler }) @@ -131,12 +110,9 @@ export class AgentPresenter implements IAgentPresenter { this.utilityHandler = new UtilityHandler(handlerContext, { getActiveConversation: (tabId) => this.sessionPresenter.getActiveConversation(tabId), getActiveConversationId: (tabId) => this.sessionPresenter.getActiveConversationId(tabId), - getConversation: (conversationId) => this.sessionPresenter.getConversation(conversationId), createConversation: (title, settings, tabId) => this.sessionPresenter.createConversation(title, settings, tabId), - streamGenerationHandler: this.streamGenerationHandler, - getSearchAssistantModel: () => this.searchPresenter.getSearchAssistantModel(), - getSearchAssistantProviderId: () => this.searchPresenter.getSearchAssistantProviderId() + streamGenerationHandler: this.streamGenerationHandler }) // Legacy IPC surface: dynamic proxy for ISessionPresenter methods. @@ -579,11 +555,6 @@ export class AgentPresenter implements IAgentPresenter { this.contentBufferHandler.cleanupContentBuffer(state) - if (state.isSearching) { - this.searchingMessages.delete(messageId) - await this.searchManager.stopSearch(state.conversationId) - } - state.message.content.forEach((block) => { if ( block.status === 'loading' || diff --git a/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts b/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts index c44a77df4..febed8214 100644 --- a/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts +++ b/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts @@ -99,7 +99,7 @@ export class AgentLoopHandler { private async resolveWorkspaceContext( conversationId?: string, modelId?: string - ): Promise<{ chatMode: 'chat' | 'agent' | 'acp agent'; agentWorkspacePath: string | null }> { + ): Promise<{ chatMode: 'agent' | 'acp agent'; agentWorkspacePath: string | null }> { return presenter.sessionManager.resolveWorkspaceContext(conversationId, modelId) } @@ -126,7 +126,7 @@ export class AgentLoopHandler { private async filterToolsForChatMode( tools: Awaited>, - chatMode: 'chat' | 'agent' | 'acp agent', + chatMode: 'agent' | 'acp agent', agentId?: string ): Promise>> { if (chatMode !== 'acp agent') return tools @@ -154,9 +154,6 @@ export class AgentLoopHandler { thinkingBudget?: number, reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high', verbosity?: 'low' | 'medium' | 'high', - enableSearch?: boolean, - forcedSearch?: boolean, - searchStrategy?: 'turbo' | 'max', conversationId?: string ): AsyncGenerator { console.log(`[Agent Loop] Starting agent loop for event: ${eventId} with model: ${modelId}`) @@ -184,15 +181,6 @@ export class AgentLoopHandler { if (verbosity !== undefined) { modelConfig.verbosity = verbosity } - if (enableSearch !== undefined) { - modelConfig.enableSearch = enableSearch - } - if (forcedSearch !== undefined) { - modelConfig.forcedSearch = forcedSearch - } - if (searchStrategy !== undefined) { - modelConfig.searchStrategy = searchStrategy - } this.currentSupportsVision = Boolean(modelConfig?.vision) this.options.activeStreams.set(eventId, { diff --git a/src/main/presenter/agentPresenter/loop/toolCallHandler.ts b/src/main/presenter/agentPresenter/loop/toolCallHandler.ts index 69c429194..4b31d246f 100644 --- a/src/main/presenter/agentPresenter/loop/toolCallHandler.ts +++ b/src/main/presenter/agentPresenter/loop/toolCallHandler.ts @@ -44,18 +44,15 @@ export class ToolCallHandler { ]) private readonly sqlitePresenter: ISQLitePresenter - private readonly searchingMessages: Set private readonly commandPermissionHandler?: CommandPermissionService private readonly streamUpdateScheduler: StreamUpdateScheduler constructor(options: { sqlitePresenter: ISQLitePresenter - searchingMessages: Set commandPermissionHandler?: CommandPermissionService streamUpdateScheduler: StreamUpdateScheduler }) { this.sqlitePresenter = options.sqlitePresenter - this.searchingMessages = options.searchingMessages this.commandPermissionHandler = options.commandPermissionHandler this.streamUpdateScheduler = options.streamUpdateScheduler } @@ -134,8 +131,6 @@ export class ToolCallHandler { lastBlock.status = 'success' } - this.searchingMessages.delete(event.eventId) - state.isSearching = false state.pendingToolCall = undefined } @@ -157,8 +152,6 @@ export class ToolCallHandler { } } - this.searchingMessages.delete(event.eventId) - state.isSearching = false state.pendingToolCall = undefined } @@ -209,8 +202,6 @@ export class ToolCallHandler { } }) - this.searchingMessages.delete(event.eventId) - state.isSearching = false state.pendingToolCall = this.buildPendingToolCall(event) return } @@ -221,8 +212,6 @@ export class ToolCallHandler { if (lastBlock.extra) { lastBlock.extra.needsUserAction = false } - this.searchingMessages.delete(event.eventId) - state.isSearching = false state.pendingToolCall = undefined return } @@ -598,8 +587,6 @@ export class ToolCallHandler { }) state.pendingToolCall = this.buildPendingToolCall(event) - this.searchingMessages.add(event.eventId) - state.isSearching = true } private buildPendingToolCall(event: LLMAgentEventData) { diff --git a/src/main/presenter/agentPresenter/message/messageBuilder.ts b/src/main/presenter/agentPresenter/message/messageBuilder.ts index 2600ef3bf..caa85905b 100644 --- a/src/main/presenter/agentPresenter/message/messageBuilder.ts +++ b/src/main/presenter/agentPresenter/message/messageBuilder.ts @@ -117,15 +117,13 @@ export async function preparePromptContent({ promptTokens: number }> { const { systemPrompt, contextLength, artifacts, enabledMcpTools } = conversation.settings - const chatMode: 'chat' | 'agent' | 'acp agent' = - conversation.settings.chatMode ?? - ((await presenter.configPresenter.getSetting('input_chatMode')) as - | 'chat' - | 'agent' - | 'acp agent') ?? - 'chat' + // Migrate legacy 'chat' mode to 'agent' mode silently + let chatMode: 'agent' | 'acp agent' = + (conversation.settings.chatMode as 'agent' | 'acp agent') ?? + ((await presenter.configPresenter.getSetting('input_chatMode')) as 'agent' | 'acp agent') ?? + 'agent' const isAgentMode = chatMode === 'agent' - const isToolPromptMode = chatMode !== 'chat' + const isToolPromptMode = true // Always true now, no more 'chat' mode const isImageGeneration = modelType === ModelType.ImageGeneration diff --git a/src/main/presenter/agentPresenter/permission/permissionHandler.ts b/src/main/presenter/agentPresenter/permission/permissionHandler.ts index 297bdf0d4..2fa880fbc 100644 --- a/src/main/presenter/agentPresenter/permission/permissionHandler.ts +++ b/src/main/presenter/agentPresenter/permission/permissionHandler.ts @@ -11,7 +11,7 @@ import { buildPostToolExecutionContext, type PendingToolCall } from '../message/ import type { GeneratingMessageState } from '../streaming/types' import type { StreamGenerationHandler } from '../streaming/streamGenerationHandler' import type { LLMEventHandler } from '../streaming/llmEventHandler' -import { BaseHandler, type ThreadHandlerContext } from '../../searchPresenter/handlers/baseHandler' +import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' import { CommandPermissionService } from '../../permission/commandPermissionService' import { eventBus, SendTarget } from '@/eventbus' import { STREAM_EVENTS } from '@/events' @@ -71,7 +71,6 @@ function canBatchUpdate( export class PermissionHandler extends BaseHandler { private readonly generatingMessages: Map - private readonly llmProviderPresenter: ILlmProviderPresenter private readonly getMcpPresenter: () => IMCPPresenter private readonly getToolPresenter: () => IToolPresenter private readonly streamGenerationHandler: StreamGenerationHandler @@ -92,7 +91,6 @@ export class PermissionHandler extends BaseHandler { ) { super(context) this.generatingMessages = options.generatingMessages - this.llmProviderPresenter = options.llmProviderPresenter this.getMcpPresenter = options.getMcpPresenter this.getToolPresenter = options.getToolPresenter this.streamGenerationHandler = options.streamGenerationHandler @@ -103,7 +101,6 @@ export class PermissionHandler extends BaseHandler { private assertDependencies(): void { void this.generatingMessages - void this.llmProviderPresenter void this.getMcpPresenter void this.getToolPresenter void this.streamGenerationHandler @@ -1014,10 +1011,7 @@ export class PermissionHandler extends BaseHandler { enabledMcpTools, thinkingBudget, reasoningEffort, - verbosity, - enableSearch, - forcedSearch, - searchStrategy + verbosity } = conversation.settings const modelConfig = this.ctx.configPresenter.getModelConfig(modelId, providerId) @@ -1051,9 +1045,6 @@ export class PermissionHandler extends BaseHandler { thinkingBudget, reasoningEffort, verbosity, - enableSearch, - forcedSearch, - searchStrategy, conversation.id ) diff --git a/src/main/presenter/agentPresenter/session/sessionContext.ts b/src/main/presenter/agentPresenter/session/sessionContext.ts index e9a8cc997..86f3e009a 100644 --- a/src/main/presenter/agentPresenter/session/sessionContext.ts +++ b/src/main/presenter/agentPresenter/session/sessionContext.ts @@ -7,7 +7,7 @@ export type SessionStatus = | 'error' export type SessionContextResolved = { - chatMode: 'chat' | 'agent' | 'acp agent' + chatMode: 'agent' | 'acp agent' providerId: string modelId: string supportsVision: boolean diff --git a/src/main/presenter/agentPresenter/session/sessionManager.ts b/src/main/presenter/agentPresenter/session/sessionManager.ts index 3ff4b6672..57a3267b5 100644 --- a/src/main/presenter/agentPresenter/session/sessionManager.ts +++ b/src/main/presenter/agentPresenter/session/sessionManager.ts @@ -12,7 +12,7 @@ import type { import { resolveSessionContext } from './sessionResolver' type WorkspaceContext = { - chatMode: 'chat' | 'agent' | 'acp agent' + chatMode: 'agent' | 'acp agent' agentWorkspacePath: string | null } @@ -71,7 +71,6 @@ export class SessionManager { async resolveSession(agentId: string): Promise { const conversation = await this.options.sessionPresenter.getConversation(agentId) const fallbackChatMode = this.options.configPresenter.getSetting('input_chatMode') as - | 'chat' | 'agent' | 'acp agent' | undefined @@ -112,10 +111,9 @@ export class SessionManager { if (!conversationId) { const fallbackChatMode = (this.options.configPresenter.getSetting('input_chatMode') as - | 'chat' | 'agent' | 'acp agent' - | undefined) ?? 'chat' + | undefined) ?? 'agent' return { chatMode: fallbackChatMode, agentWorkspacePath: null } } @@ -137,10 +135,9 @@ export class SessionManager { console.warn('[SessionManager] Failed to resolve workspace context:', error) const fallbackChatMode = (this.options.configPresenter.getSetting('input_chatMode') as - | 'chat' | 'agent' | 'acp agent' - | undefined) ?? 'chat' + | undefined) ?? 'agent' return { chatMode: fallbackChatMode, agentWorkspacePath: null } } } diff --git a/src/main/presenter/agentPresenter/session/sessionResolver.ts b/src/main/presenter/agentPresenter/session/sessionResolver.ts index ea55ab215..1e8838b25 100644 --- a/src/main/presenter/agentPresenter/session/sessionResolver.ts +++ b/src/main/presenter/agentPresenter/session/sessionResolver.ts @@ -3,13 +3,14 @@ import type { SessionContextResolved } from './sessionContext' export type SessionResolveInput = { settings: CONVERSATION_SETTINGS - fallbackChatMode?: 'chat' | 'agent' | 'acp agent' + fallbackChatMode?: 'agent' | 'acp agent' modelConfig?: ModelConfig } export function resolveSessionContext(input: SessionResolveInput): SessionContextResolved { const { settings, modelConfig } = input - const chatMode = settings.chatMode || input.fallbackChatMode || 'chat' + // Migrate legacy 'chat' mode to 'agent' mode silently + const chatMode = settings.chatMode || input.fallbackChatMode || 'agent' return { chatMode, diff --git a/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts b/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts index be753e660..7d53c0892 100644 --- a/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts +++ b/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts @@ -23,7 +23,6 @@ type HookErrorSnapshot = { export class LLMEventHandler { private readonly generatingMessages: Map - private readonly searchingMessages: Set private readonly messageManager: MessageManager private readonly contentBufferHandler: ContentBufferHandler private readonly toolCallHandler: ToolCallHandler @@ -33,7 +32,6 @@ export class LLMEventHandler { constructor(options: { generatingMessages: Map - searchingMessages: Set messageManager: MessageManager contentBufferHandler: ContentBufferHandler toolCallHandler: ToolCallHandler @@ -41,7 +39,6 @@ export class LLMEventHandler { onConversationUpdated?: ConversationUpdateHandler }) { this.generatingMessages = options.generatingMessages - this.searchingMessages = options.searchingMessages this.messageManager = options.messageManager this.contentBufferHandler = options.contentBufferHandler this.toolCallHandler = options.toolCallHandler @@ -442,7 +439,6 @@ export class LLMEventHandler { } } - this.searchingMessages.delete(eventId) eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, msg) } @@ -499,7 +495,6 @@ export class LLMEventHandler { // Question tool ends the assistant message even when waiting for user input. await this.messageManager.updateMessageStatus(eventId, 'sent') } - this.searchingMessages.delete(eventId) presenter.sessionManager.setStatus(state.conversationId, 'waiting_permission') if (!hasPendingPermissions) { presenter.sessionManager.setStatus(state.conversationId, 'waiting_question') @@ -567,7 +562,6 @@ export class LLMEventHandler { } await this.streamUpdateScheduler.flushAll(eventId, 'final') - this.searchingMessages.delete(eventId) eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) } @@ -645,7 +639,6 @@ export class LLMEventHandler { await this.messageManager.updateMessageStatus(eventId, 'sent') await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) this.generatingMessages.delete(eventId) - this.searchingMessages.delete(eventId) if (this.onConversationUpdated) { await this.onConversationUpdated(state) diff --git a/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts b/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts index 4a950dafc..d1c93b3d0 100644 --- a/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts +++ b/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts @@ -8,31 +8,27 @@ import type { UserMessage, UserMessageContent } from '@shared/chat' -import type { CONVERSATION, MCPToolResponse, SearchResult } from '@shared/presenter' +import type { CONVERSATION, MCPToolResponse } from '@shared/presenter' import { buildUserMessageContext, formatUserMessageContent } from '../message/messageFormatter' import { preparePromptContent } from '../message/messageBuilder' import type { GeneratingMessageState } from './types' import { presenter } from '@/presenter' -import type { SearchHandler } from '../../searchPresenter/handlers/searchHandler' -import { BaseHandler, type ThreadHandlerContext } from '../../searchPresenter/handlers/baseHandler' +import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' import type { LLMEventHandler } from './llmEventHandler' import { LoopOrchestrator } from '../loop/loopOrchestrator' interface StreamGenerationHandlerDeps { - searchHandler: SearchHandler generatingMessages: Map llmEventHandler: LLMEventHandler } export class StreamGenerationHandler extends BaseHandler { - private readonly searchHandler: SearchHandler private readonly generatingMessages: Map private readonly llmEventHandler: LLMEventHandler private readonly loopOrchestrator: LoopOrchestrator constructor(context: ThreadHandlerContext, deps: StreamGenerationHandlerDeps) { super(context) - this.searchHandler = deps.searchHandler this.generatingMessages = deps.generatingMessages this.llmEventHandler = deps.llmEventHandler this.loopOrchestrator = new LoopOrchestrator(this.llmEventHandler) @@ -40,7 +36,6 @@ export class StreamGenerationHandler extends BaseHandler { } private assertDependencies(): void { - void this.searchHandler void this.generatingMessages void this.llmEventHandler void this.loopOrchestrator @@ -93,30 +88,11 @@ export class StreamGenerationHandler extends BaseHandler { this.throwIfCancelled(state.message.id) - let searchResults: SearchResult[] | null = null - if ((userMessage.content as UserMessageContent).search) { - try { - searchResults = await this.searchHandler.startStreamSearch( - conversationId, - state.message.id, - userContent - ) - this.throwIfCancelled(state.message.id) - } catch (error) { - if (String(error).includes('userCanceledGeneration')) { - return - } - console.error('[StreamGenerationHandler] Error during search:', error) - } - } - - this.throwIfCancelled(state.message.id) - const { finalContent, promptTokens } = await preparePromptContent({ conversation, userContent, contextMessages, - searchResults, + searchResults: null, userMessage, vision: Boolean(modelConfig?.vision), imageFiles: modelConfig?.vision ? imageFiles : [], @@ -139,10 +115,7 @@ export class StreamGenerationHandler extends BaseHandler { enabledMcpTools: currentEnabledMcpTools, thinkingBudget: currentThinkingBudget, reasoningEffort: currentReasoningEffort, - verbosity: currentVerbosity, - enableSearch: currentEnableSearch, - forcedSearch: currentForcedSearch, - searchStrategy: currentSearchStrategy + verbosity: currentVerbosity } = currentConversation.settings try { @@ -169,9 +142,6 @@ export class StreamGenerationHandler extends BaseHandler { currentThinkingBudget, currentReasoningEffort, currentVerbosity, - currentEnableSearch, - currentForcedSearch, - currentSearchStrategy, conversationId ) @@ -267,10 +237,7 @@ export class StreamGenerationHandler extends BaseHandler { enabledMcpTools, thinkingBudget, reasoningEffort, - verbosity, - enableSearch, - forcedSearch, - searchStrategy + verbosity } = conversation.settings const modelConfig = this.ctx.configPresenter.getModelConfig(modelId, providerId) if (!modelConfig) { @@ -354,9 +321,6 @@ export class StreamGenerationHandler extends BaseHandler { thinkingBudget, reasoningEffort, verbosity, - enableSearch, - forcedSearch, - searchStrategy, conversationId ) @@ -489,9 +453,7 @@ export class StreamGenerationHandler extends BaseHandler { } } - const webSearchEnabled = this.ctx.configPresenter.getSetting('input_webSearch') as boolean const thinkEnabled = this.ctx.configPresenter.getSetting('input_deepThinking') as boolean - ;(userMessage.content as UserMessageContent).search = webSearchEnabled ;(userMessage.content as UserMessageContent).think = thinkEnabled return { conversation, userMessage, contextMessages } @@ -637,14 +599,6 @@ export class StreamGenerationHandler extends BaseHandler { return this.ctx.messageManager.getMessageHistory(messageId, limit) } - private async getConversation(conversationId: string): Promise { - const conversation = await this.ctx.sqlitePresenter.getConversation(conversationId) - if (!conversation) { - throw new Error('conversation not found') - } - return conversation - } - private async getContextMessages(conversation: CONVERSATION): Promise { let messageCount = Math.ceil(conversation.settings.contextLength / 300) if (messageCount < 2) { diff --git a/src/main/presenter/agentPresenter/streaming/types.ts b/src/main/presenter/agentPresenter/streaming/types.ts index b9706722e..f63ee1131 100644 --- a/src/main/presenter/agentPresenter/streaming/types.ts +++ b/src/main/presenter/agentPresenter/streaming/types.ts @@ -10,7 +10,6 @@ export interface GeneratingMessageState { reasoningStartTime: number | null reasoningEndTime: number | null lastReasoningTime: number | null - isSearching?: boolean isCancelled?: boolean pendingToolCall?: PendingToolCall totalUsage?: { diff --git a/src/main/presenter/agentPresenter/tool/toolCallCenter.ts b/src/main/presenter/agentPresenter/tool/toolCallCenter.ts index 1e1b6c63f..6c06251af 100644 --- a/src/main/presenter/agentPresenter/tool/toolCallCenter.ts +++ b/src/main/presenter/agentPresenter/tool/toolCallCenter.ts @@ -7,7 +7,7 @@ import type { export type ToolCallContext = { enabledMcpTools?: string[] - chatMode?: 'chat' | 'agent' | 'acp agent' + chatMode?: 'agent' | 'acp agent' supportsVision?: boolean agentWorkspacePath?: string | null conversationId?: string diff --git a/src/main/presenter/agentPresenter/types/handlerContext.ts b/src/main/presenter/agentPresenter/types/handlerContext.ts new file mode 100644 index 000000000..a0a4b0a9f --- /dev/null +++ b/src/main/presenter/agentPresenter/types/handlerContext.ts @@ -0,0 +1,46 @@ +import type { IConfigPresenter, ILlmProviderPresenter, ISQLitePresenter } from '@shared/presenter' +import type { CONVERSATION } from '@shared/presenter' +import type { MessageManager } from '../../sessionPresenter/managers/messageManager' + +export type ThreadHandlerContext = { + sqlitePresenter: ISQLitePresenter + messageManager: MessageManager + llmProviderPresenter: ILlmProviderPresenter + configPresenter: IConfigPresenter +} + +export class BaseHandler { + protected ctx: ThreadHandlerContext + + constructor(context: ThreadHandlerContext) { + this.ctx = context + } + + protected get sqlitePresenter(): ISQLitePresenter { + return this.ctx.sqlitePresenter + } + + protected get messageManager(): MessageManager { + return this.ctx.messageManager + } + + protected get llmProviderPresenter(): ILlmProviderPresenter { + return this.ctx.llmProviderPresenter + } + + protected get configPresenter(): IConfigPresenter { + return this.ctx.configPresenter + } + + protected async getMessage(messageId: string) { + return this.messageManager.getMessage(messageId) + } + + protected async getConversation(conversationId: string): Promise { + const conversation = await this.ctx.sqlitePresenter.getConversation(conversationId) + if (!conversation) { + throw new Error('Conversation not found') + } + return conversation + } +} diff --git a/src/main/presenter/agentPresenter/utility/utilityHandler.ts b/src/main/presenter/agentPresenter/utility/utilityHandler.ts index 805a3d145..e107edd80 100644 --- a/src/main/presenter/agentPresenter/utility/utilityHandler.ts +++ b/src/main/presenter/agentPresenter/utility/utilityHandler.ts @@ -4,13 +4,12 @@ import type { CONVERSATION_SETTINGS, LLMAgentEventData, MCPToolDefinition, - MESSAGE_METADATA, - MODEL_META + MESSAGE_METADATA } from '@shared/presenter' import type { AssistantMessageBlock, Message, UserMessageContent } from '@shared/chat' import { ModelType } from '@shared/model' import { presenter } from '@/presenter' -import { BaseHandler, type ThreadHandlerContext } from '../../searchPresenter/handlers/baseHandler' +import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' import { buildUserMessageContext } from '../message/messageFormatter' import { buildConversationExportContent, @@ -30,39 +29,30 @@ const DEFAULT_MESSAGE_LENGTH = 300 export interface UtilityHandlerOptions { getActiveConversation: (tabId: number) => Promise getActiveConversationId: (tabId: number) => Promise - getConversation: (conversationId: string) => Promise createConversation: ( title: string, settings: Partial, tabId: number ) => Promise streamGenerationHandler: StreamGenerationHandler - getSearchAssistantModel: () => MODEL_META | null - getSearchAssistantProviderId: () => string | null } export class UtilityHandler extends BaseHandler { private readonly getActiveConversation: (tabId: number) => Promise private readonly getActiveConversationId: (tabId: number) => Promise - private readonly getConversation: (conversationId: string) => Promise private readonly createConversation: ( title: string, settings: Partial, tabId: number ) => Promise private readonly streamGenerationHandler: StreamGenerationHandler - private readonly getSearchAssistantModel: () => MODEL_META | null - private readonly getSearchAssistantProviderId: () => string | null constructor(context: ThreadHandlerContext, options: UtilityHandlerOptions) { super(context) this.getActiveConversation = options.getActiveConversation this.getActiveConversationId = options.getActiveConversationId - this.getConversation = options.getConversation this.createConversation = options.createConversation this.streamGenerationHandler = options.streamGenerationHandler - this.getSearchAssistantModel = options.getSearchAssistantModel - this.getSearchAssistantProviderId = options.getSearchAssistantProviderId } async translateText(text: string, tabId: number): Promise { @@ -236,9 +226,6 @@ export class UtilityHandler extends BaseHandler { if (!conversation) { throw new Error('Conversation not found') } - let summaryProviderId = conversation.settings.providerId - const modelId = this.getSearchAssistantModel()?.id - summaryProviderId = this.getSearchAssistantProviderId() || conversation.settings.providerId // Get context messages let messageCount = Math.ceil(conversation.settings.contextLength / DEFAULT_MESSAGE_LENGTH) @@ -280,8 +267,8 @@ export class UtilityHandler extends BaseHandler { .filter((item) => item.formattedMessage.content.length > 0) const title = await this.ctx.llmProviderPresenter.summaryTitles( messagesWithLength.map((item) => item.formattedMessage), - summaryProviderId || conversation.settings.providerId, - modelId || conversation.settings.modelId + conversation.settings.providerId, + conversation.settings.modelId ) let cleanedTitle = title.replace(/.*?<\/think>/g, '').trim() cleanedTitle = cleanedTitle.replace(/^/, '').trim() diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index fa9d143ea..fd2a485ea 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -81,7 +81,6 @@ interface IAppSettings { loggingEnabled?: boolean // Whether logging is enabled floatingButtonEnabled?: boolean // Whether floating button is enabled default_system_prompt?: string // Default system prompt - webContentLengthLimit?: number // Web content truncation length limit, default 3000 characters updateChannel?: string // Update channel: 'stable' | 'beta' fontFamily?: string // Custom UI font codeFontFamily?: string // Custom code font @@ -152,7 +151,6 @@ export class ConfigPresenter implements IConfigPresenter { fontFamily: '', codeFontFamily: '', default_system_prompt: '', - webContentLengthLimit: 3000, skillsPath: path.join(app.getPath('home'), '.deepchat', 'skills'), enableSkills: true, updateChannel: 'stable', // Default to stable version diff --git a/src/main/presenter/configPresenter/modelConfig.ts b/src/main/presenter/configPresenter/modelConfig.ts index f470c1870..e815d2961 100644 --- a/src/main/presenter/configPresenter/modelConfig.ts +++ b/src/main/presenter/configPresenter/modelConfig.ts @@ -87,9 +87,6 @@ export class ModelConfigHelper { reasoning: Boolean(model.reasoning?.supported ?? false), type: this.inferModelType(model), thinkingBudget: model.reasoning?.budget?.default ?? undefined, - enableSearch: Boolean(model.search?.supported ?? false), - forcedSearch: Boolean(model.search?.forced_search), - searchStrategy: model.search?.search_strategy === 'max' ? 'max' : 'turbo', reasoningEffort: (model.reasoning?.effort ?? undefined) as | 'minimal' | 'low' @@ -382,9 +379,6 @@ export class ModelConfigHelper { type: ModelType.Chat, apiEndpoint: ApiEndpointType.Chat, thinkingBudget: undefined, - enableSearch: false, - forcedSearch: false, - searchStrategy: 'turbo', reasoningEffort: undefined, verbosity: undefined, maxCompletionTokens: undefined diff --git a/src/main/presenter/configPresenter/providerModelHelper.ts b/src/main/presenter/configPresenter/providerModelHelper.ts index 210c15331..72fb0f94c 100644 --- a/src/main/presenter/configPresenter/providerModelHelper.ts +++ b/src/main/presenter/configPresenter/providerModelHelper.ts @@ -92,14 +92,11 @@ export class ProviderModelHelper { model.functionCall !== undefined ? model.functionCall : config.functionCall || false model.reasoning = model.reasoning !== undefined ? model.reasoning : config.reasoning || false - model.enableSearch = - model.enableSearch !== undefined ? model.enableSearch : config.enableSearch || false model.type = model.type !== undefined ? model.type : config.type || ModelType.Chat } else { model.vision = model.vision || false model.functionCall = model.functionCall || false model.reasoning = model.reasoning || false - model.enableSearch = model.enableSearch || false model.type = model.type || ModelType.Chat } return model diff --git a/src/main/presenter/content/contentEnricher.ts b/src/main/presenter/content/contentEnricher.ts deleted file mode 100644 index bee7d3c06..000000000 --- a/src/main/presenter/content/contentEnricher.ts +++ /dev/null @@ -1,383 +0,0 @@ -import axios from 'axios' -import * as cheerio from 'cheerio' -import { SearchResult } from '@shared/presenter' -import { HttpsProxyAgent } from 'https-proxy-agent' -import { proxyConfig } from '@/presenter/proxyConfig' -import { presenter } from '@/presenter' -// 统一的搜索结果类型 - -/** - * 内容丰富工具类,用于处理URL内容提取和丰富 - */ -export class ContentEnricher { - /** - * 从文本中提取并丰富URL内容 - * @param text 包含URL的文本 - * @returns 丰富后的URL结果数组 - */ - static async extractAndEnrichUrls(text: string): Promise { - // 用正则表达式匹配http和https链接 - const urlRegex = /(https?:\/\/[^\s]+)/g - const matches = text.match(urlRegex) - - if (!matches || matches.length === 0) { - return [] - } - - const results: SearchResult[] = [] - - for (const url of matches) { - const result = await this.enrichUrl(url, results.length + 1) - results.push(result as SearchResult) - } - - return results - } - - /** - * 丰富单个URL的内容 - * @param url 需要丰富的URL - * @param rank 结果排名(可选) - * @returns 丰富后的SearchResult对象 - */ - static async enrichUrl(url: string, rank: number = 1): Promise { - const timeout = 5000 // 5秒超时 - - try { - const proxyUrl = proxyConfig.getProxyUrl() - // 使用axios获取页面内容 - const response = await axios.get(url, { - timeout, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - }, - httpAgent: proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined - }) - - const $ = cheerio.load(response.data) - // 移除不需要的元素 - $('script, style, nav, header, footer, iframe, .ad, #ad, .advertisement').remove() - - // 获取页面标题 - const title = $('title').text().trim() || url - - // 尝试获取主要内容 - const mainContent = this.extractMainContent($) - // 获取图标 - const icon = this.extractFavicon($, url) - - // 获取页面描述 - const description = - $('meta[name="description"]').attr('content') || - $('meta[property="og:description"]').attr('content') || - '' - - return { - title, - url, - content: mainContent, - icon, - description, - rank - } - } catch (error: Error | unknown) { - console.error(`提取URL内容失败 ${url}:`, error instanceof Error ? error.message : '') - // 如果获取失败,只添加URL信息 - return { - title: url, - url, - rank, - description: '', - icon: '' - } - } - } - - /** - * 批量丰富搜索结果内容 - * @param results 搜索结果数组 - * @param limit 处理结果的数量限制(可选) - * @returns 丰富后的搜索结果数组 - */ - static async enrichResults(results: SearchResult[], limit?: number): Promise { - const enrichedResults: SearchResult[] = [] - const resultsToProcess = limit ? results.slice(0, limit) : results - - for (const result of resultsToProcess) { - try { - const enrichedResult = await this.enrichUrl(result.url, result.rank) - // 合并原始结果和丰富的结果 - enrichedResults.push({ - ...result, - content: enrichedResult.content || result.description || '', - icon: result.icon || enrichedResult.icon || '' - }) - } catch (error) { - console.error(`Error enriching content for ${result.url}:`, error) - // 获取失败保留原始结果 - enrichedResults.push(result) - } - } - - return enrichedResults - } - - /** - * 从HTML中提取主要内容 - * @param $ cheerio加载的HTML - * @returns 提取的内容文本 - */ - private static extractMainContent($: cheerio.CheerioAPI): string { - // 尝试获取主要内容 - let mainContent = '' - const possibleSelectors = [ - 'article', - 'main', - '.content', - '#content', - '.post-content', - '.article-content', - '.entry-content', - '[role="main"]', - '.container' - ] - - for (const selector of possibleSelectors) { - const element = $(selector) - if (element.length > 0) { - mainContent = element.text() - break - } - } - mainContent = mainContent - .replace(/[\r\n]+/g, ' ') - .replace(/\s+/g, ' ') - .trim() - .trim() - - // 如果没有找到主要内容,使用body - if (!mainContent) { - mainContent = $('body').text() - } - // 清理文本内容 - mainContent = mainContent - .replace(/[\r\n]+/g, ' ') - .replace(/\s+/g, ' ') - .trim() - - // 获取配置的长度限制,如果获取失败则使用默认值3000 - let lengthLimit = 3000 - try { - const configValue = presenter.configPresenter.getSetting('webContentLengthLimit') - if (configValue && typeof configValue === 'number') { - if (configValue === 0) { - // 0 表示不限制长度 - return mainContent - } else if (configValue > 0 && configValue <= 50000) { - lengthLimit = configValue - } - } - } catch { - // 忽略错误,使用默认值 - } - - // 如果内容长度小于或等于限制,直接返回全部内容 - if (mainContent.length <= lengthLimit) { - return mainContent - } - - // 否则截断到指定长度 - return mainContent.slice(0, lengthLimit) - } - - /** - * 从HTML中提取网站图标 - * @param $ cheerio加载的HTML - * @param url 页面URL - * @returns 图标URL - */ - private static extractFavicon($: cheerio.CheerioAPI, url: string): string { - // 尝试获取网站图标 - let icon = $('link[rel="icon"]').attr('href') || $('link[rel="shortcut icon"]').attr('href') - - // 如果找到了相对路径的图标,转换为绝对路径 - if (icon && !icon.startsWith('http')) { - const urlObj = new URL(url) - icon = icon.startsWith('/') - ? `${urlObj.protocol}//${urlObj.host}${icon}` - : `${urlObj.protocol}//${urlObj.host}/${icon}` - } - - // 如果没有找到图标,使用默认的favicon.ico - if (!icon) { - const urlObj = new URL(url) - icon = `${urlObj.protocol}//${urlObj.host}/favicon.ico` - } - - return icon - } - - /** - * 根据提取的URL内容丰富用户消息 - * @param userText 原始用户消息 - * @param urlResults 提取的URL内容结果 - * @returns 丰富后的用户消息 - */ - static enrichUserMessageWithUrlContent(userText: string, urlResults: SearchResult[]): string { - if (urlResults.length === 0) { - return userText - } - - let enrichedContent = `--- - 如下url-content的标签中包含了用户上述提到的一些链接的具体信息: - - \n - ` - for (let i = 0; i < urlResults.length; i++) { - const result = urlResults[i] - if (result.content) { - enrichedContent += `${result.url}\n${result.content}\n` - } - } - enrichedContent += `` - - return enrichedContent - } - - /** - * 将HTML转换为简洁的Markdown格式 - * 保留链接和结构化数据,减少数据量 - * @param html HTML内容 - * @param baseUrl 基础URL,用于解析相对路径 - * @returns 转换后的Markdown内容 - */ - static convertHtmlToMarkdown(html: string, baseUrl: string): string { - const $ = cheerio.load(html) - - // 移除不需要的元素 - $('script, style, nav, header, footer, iframe, .ad, #ad, .advertisement').remove() - - let markdown = '' - - // 提取页面中可能的搜索结果链接和文本 - const searchResults: { title: string; url: string; text: string }[] = [] - - // 1. 查找明显的搜索结果项 - $('a').each((_, element) => { - const $el = $(element) - const href = $el.attr('href') || '' - const text = $el.text().trim() - - // 跳过空链接和明显的导航链接 - if (!href || href === '#' || text.length < 3 || href.includes('javascript:')) { - return - } - - // 转换为绝对URL - let url = href - try { - url = href.startsWith('http') ? href : new URL(href, baseUrl).toString() - } catch (error) { - // 如果URL构建失败,使用原始href - console.error('构建URL失败:', error) - } - - // 获取周围的文本作为描述 - const parent = $el.parent() - let description = '' - - // 尝试在父元素或祖先元素中寻找描述性文本 - if (parent && parent.children().length > 1) { - // 克隆父元素并移除所有链接,只保留文本内容 - const parentClone = parent.clone() - parentClone.find('a').remove() - description = parentClone.text().trim() - } - - if (!description) { - // 尝试查找相邻的段落元素 - const nextParagraph = $el.next('p, div, span') - if (nextParagraph.length) { - description = nextParagraph.text().trim() - } - } - - // 清理描述文本 - description = description.replace(/\s+/g, ' ').trim().substring(0, 200) // 限制描述长度 - - searchResults.push({ - title: text, - url, - text: description - }) - }) - - // 2. 将提取的搜索结果转换为Markdown格式 - searchResults.forEach((result, index) => { - markdown += `## 结果 ${index + 1}\n` - markdown += `### [${result.title}](${result.url})\n` - markdown += `- URL: ${result.url}\n` - if (result.text) { - markdown += `- 描述: ${result.text}\n` - } - markdown += '\n' - }) - - // 3. 如果没有提取到结构化结果,提取基础HTML结构 - if (searchResults.length === 0) { - // 提取所有可见文本块并保留基本结构 - $('h1, h2, h3, h4, h5, h6, p, div').each((_, element) => { - const $el = $(element) - // 跳过空白或很短的文本块 - const text = $el.text().trim() - if (text.length < 5) return - - // 根据元素类型添加标记 - const tagName = $el.prop('tagName')?.toLowerCase() || '' - if (tagName.startsWith('h')) { - const level = parseInt(tagName.substring(1)) - markdown += `${'#'.repeat(level)} ${text}\n\n` - } else { - markdown += `${text}\n\n` - } - }) - - // 再次提取所有链接 - $('a').each((_, element) => { - const $el = $(element) - const href = $el.attr('href') || '' - const text = $el.text().trim() - - if (href && href !== '#' && text.length > 0) { - let url = href - try { - url = href.startsWith('http') ? href : new URL(href, baseUrl).toString() - } catch { - // 如果URL构建失败,使用原始href - } - markdown += `- [${text}](${url})\n` - } - }) - } - - // 4. 最后,添加所有图片的引用 - $('img').each((_, element) => { - const $el = $(element) - const src = $el.attr('src') || '' - const alt = $el.attr('alt') || '图片' - - if (src) { - let imageUrl = src - try { - imageUrl = src.startsWith('http') ? src : new URL(src, baseUrl).toString() - } catch { - // 如果URL构建失败,使用原始src - } - markdown += `![${alt}](${imageUrl})\n` - } - }) - - return markdown - } -} diff --git a/src/main/presenter/content/index.ts b/src/main/presenter/content/index.ts deleted file mode 100644 index 97052fcc3..000000000 --- a/src/main/presenter/content/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ContentEnricher } from './contentEnricher' diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 477d0dc45..782db9235 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -57,7 +57,7 @@ import { } from './permission' import { AgentPresenter } from './agentPresenter' import { SessionManager } from './agentPresenter/session/sessionManager' -import { SearchPresenter } from './searchPresenter' + import { ConversationExporterService } from './exporter' import { SkillPresenter } from './skillPresenter' import { SkillSyncPresenter } from './skillSyncPresenter' @@ -86,7 +86,7 @@ export class Presenter implements IPresenter { llmproviderPresenter: ILlmProviderPresenter configPresenter: IConfigPresenter sessionPresenter: ISessionPresenter - searchPresenter: SearchPresenter + exporter: IConversationExporter agentPresenter: IAgentPresenter & ISessionPresenter sessionManager: SessionManager @@ -131,11 +131,6 @@ export class Presenter implements IPresenter { this.settingsPermissionService = new SettingsPermissionService() const messageManager = new MessageManager(this.sqlitePresenter) this.devicePresenter = new DevicePresenter() - this.searchPresenter = new SearchPresenter({ - configPresenter: this.configPresenter, - windowPresenter: this.windowPresenter, - llmProviderPresenter: this.llmproviderPresenter - }) this.exporter = new ConversationExporterService({ sqlitePresenter: this.sqlitePresenter, configPresenter: this.configPresenter @@ -158,7 +153,6 @@ export class Presenter implements IPresenter { sqlitePresenter: this.sqlitePresenter, llmProviderPresenter: this.llmproviderPresenter, configPresenter: this.configPresenter, - searchPresenter: this.searchPresenter, commandPermissionService: commandPermissionHandler, messageManager }) as unknown as IAgentPresenter & ISessionPresenter diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 3b8046593..af85f7e92 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -225,9 +225,6 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { thinkingBudget?: number, reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high', verbosity?: 'low' | 'medium' | 'high', - enableSearch?: boolean, - forcedSearch?: boolean, - searchStrategy?: 'turbo' | 'max', conversationId?: string ): AsyncGenerator { yield* this.agentLoopHandler.startStreamCompletion( @@ -241,9 +238,6 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { thinkingBudget, reasoningEffort, verbosity, - enableSearch, - forcedSearch, - searchStrategy, conversationId ) } diff --git a/src/main/presenter/llmProviderPresenter/providers/dashscopeProvider.ts b/src/main/presenter/llmProviderPresenter/providers/dashscopeProvider.ts index 422e86686..0dad3f1c9 100644 --- a/src/main/presenter/llmProviderPresenter/providers/dashscopeProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/dashscopeProvider.ts @@ -20,10 +20,6 @@ export class DashscopeProvider extends OpenAICompatibleProvider { return modelCapabilities.supportsReasoning(this.provider.id, modelId) } - private supportsEnableSearch(modelId: string): boolean { - return modelCapabilities.supportsSearch(this.provider.id, modelId) - } - /** * Override coreStream method to support DashScope's enable_thinking and enable_search parameters */ @@ -39,12 +35,11 @@ export class DashscopeProvider extends OpenAICompatibleProvider { if (!modelId) throw new Error('Model ID is required') const shouldAddEnableThinking = this.supportsEnableThinking(modelId) && modelConfig?.reasoning - const shouldAddEnableSearch = this.supportsEnableSearch(modelId) && modelConfig?.enableSearch - if (shouldAddEnableThinking || shouldAddEnableSearch) { + if (shouldAddEnableThinking) { // Original create method const originalCreate = this.openai.chat.completions.create.bind(this.openai.chat.completions) - // Replace create method to add enable_thinking and enable_search parameters + // Replace create method to add enable_thinking parameter this.openai.chat.completions.create = ((params: any, options?: any) => { const modifiedParams = { ...params } @@ -60,41 +55,11 @@ export class DashscopeProvider extends OpenAICompatibleProvider { } } - if (shouldAddEnableSearch) { - modifiedParams.enable_search = true - const dbSearch = modelCapabilities.getSearchDefaults(this.provider.id, modelId) - if (modelConfig?.forcedSearch ?? dbSearch.forced) { - modifiedParams.forced_search = true - } - const strategy = modelConfig?.searchStrategy ?? dbSearch.strategy - if (strategy) { - modifiedParams.search_strategy = strategy - } - } - return originalCreate(modifiedParams, options) - }) as any - - try { - const effectiveModelConfig = { - ...modelConfig, - reasoning: false, - enableSearch: false - } - yield* super.coreStream( - messages, - modelId, - effectiveModelConfig, - temperature, - maxTokens, - mcpTools - ) - } finally { - this.openai.chat.completions.create = originalCreate - } - } else { - yield* super.coreStream(messages, modelId, modelConfig, temperature, maxTokens, mcpTools) + }) as typeof this.openai.chat.completions.create } + + yield* super.coreStream(messages, modelId, modelConfig, temperature, maxTokens, mcpTools) } protected async fetchOpenAIModels(options?: { timeout: number }): Promise { diff --git a/src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts b/src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts index 50099b4c2..f7fa0214e 100644 --- a/src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts @@ -11,7 +11,6 @@ import { Part, SafetySetting, Tool, - GoogleSearch, GenerateContentConfig } from '@google/genai' import { ModelType } from '@shared/model' @@ -817,13 +816,9 @@ export class GeminiProvider extends BaseLLMProvider { // 添加Gemini工具调用 let geminiTools: Tool[] = [] - // 注意:googleSearch内置工具与外部工具是互斥的 - if (modelConfig.enableSearch) { - geminiTools.push({ googleSearch: {} as GoogleSearch }) - } else { - if (mcpTools.length > 0) - geminiTools = await presenter.mcpPresenter.mcpToolsToGeminiTools(mcpTools, this.provider.id) - } + // Load MCP tools if available + if (mcpTools.length > 0) + geminiTools = await presenter.mcpPresenter.mcpToolsToGeminiTools(mcpTools, this.provider.id) // 格式化消息为Gemini格式 const formattedParts = this.formatGeminiMessages(messages) diff --git a/src/main/presenter/llmProviderPresenter/providers/githubCopilotProvider.ts b/src/main/presenter/llmProviderPresenter/providers/githubCopilotProvider.ts index 527dbd2b2..e9185f917 100644 --- a/src/main/presenter/llmProviderPresenter/providers/githubCopilotProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/githubCopilotProvider.ts @@ -164,8 +164,7 @@ export class GithubCopilotProvider extends BaseLLMProvider { maxTokens: m.maxTokens, vision: m.vision, functionCall: m.functionCall, - reasoning: m.reasoning, - enableSearch: m.enableSearch + reasoning: m.reasoning })) } diff --git a/src/main/presenter/llmProviderPresenter/providers/grokProvider.ts b/src/main/presenter/llmProviderPresenter/providers/grokProvider.ts index 10e752c3e..21c67cf5b 100644 --- a/src/main/presenter/llmProviderPresenter/providers/grokProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/grokProvider.ts @@ -13,16 +13,6 @@ export class GrokProvider extends OpenAICompatibleProvider { // Models that support reasoning_effort parameter (grok-4 does not) private static readonly REASONING_EFFORT_MODELS: string[] = ['grok-3-mini', 'grok-3-mini-fast'] - // All Grok models support internet search according to the requirements - // Since all models support search, we don't need a specific list, but keeping it for consistency - private static readonly ENABLE_SEARCH_MODELS: string[] = [ - 'grok-4', - 'grok-3-mini', - 'grok-3-mini-fast', - 'grok-2', - 'grok-2-image' - ] - constructor(provider: LLM_PROVIDER, configPresenter: IConfigPresenter) { super(provider, configPresenter) } @@ -46,20 +36,6 @@ export class GrokProvider extends OpenAICompatibleProvider { ) } - /** - * Check if model supports internet search - * @param modelId Model ID - * @returns boolean Whether internet search is supported - */ - private supportsEnableSearch(modelId: string): boolean { - const normalizedModelId = modelId.toLowerCase() - return ( - GrokProvider.ENABLE_SEARCH_MODELS.some((supportedModel) => - normalizedModelId.includes(supportedModel.toLowerCase()) - ) || normalizedModelId.includes('grok') - ) - } - async completions( messages: ChatMessage[], modelId: string, @@ -209,10 +185,9 @@ export class GrokProvider extends OpenAICompatibleProvider { return } - // Handle reasoning models and search functionality + // Handle reasoning models const shouldAddReasoningEffort = this.isReasoningModel(modelId) && modelConfig?.reasoningEffort - const shouldAddEnableSearch = this.supportsEnableSearch(modelId) && modelConfig?.enableSearch - const needsParameterModification = shouldAddReasoningEffort || shouldAddEnableSearch + const needsParameterModification = shouldAddReasoningEffort if (needsParameterModification) { const originalCreate = this.openai.chat.completions.create.bind(this.openai.chat.completions) @@ -224,21 +199,13 @@ export class GrokProvider extends OpenAICompatibleProvider { modifiedParams.reasoning_effort = modelConfig.reasoningEffort } - // Only add search parameters when search is explicitly enabled - if (shouldAddEnableSearch) { - modifiedParams.search_parameters = { - mode: 'on' - } - } - return originalCreate(modifiedParams, options) }) as any try { const effectiveModelConfig = { ...modelConfig, - reasoningEffort: undefined, - enableSearch: false + reasoningEffort: undefined } yield* super.coreStream( messages, diff --git a/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts b/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts index e09670735..51d1a373b 100644 --- a/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts @@ -11,7 +11,6 @@ import { Part, SafetySetting, Tool, - GoogleSearch, GenerateContentConfig } from '@google/genai' import { ModelType } from '@shared/model' @@ -904,13 +903,9 @@ export class VertexProvider extends BaseLLMProvider { // 添加Gemini工具调用 let geminiTools: Tool[] = [] - // 注意:googleSearch内置工具与外部工具是互斥的 - if (modelConfig.enableSearch) { - geminiTools.push({ googleSearch: {} as GoogleSearch }) - } else { - if (mcpTools.length > 0) - geminiTools = await presenter.mcpPresenter.mcpToolsToGeminiTools(mcpTools, this.provider.id) - } + // Load MCP tools if available + if (mcpTools.length > 0) + geminiTools = await presenter.mcpPresenter.mcpToolsToGeminiTools(mcpTools, this.provider.id) // 格式化消息为Gemini格式 const formattedParts = this.formatVertexMessages(messages) diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts index 2363774af..3bc74692d 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts @@ -3,7 +3,6 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot import { z } from 'zod' import { zodToJsonSchema } from 'zod-to-json-schema' import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' -import { ContentEnricher } from '@/presenter/content/contentEnricher' import { app } from 'electron' import path from 'path' import fs from 'fs' @@ -30,10 +29,6 @@ const GetTimeArgsSchema = z.object({ .describe('Millisecond offset relative to current time, positive for future, negative for past') }) -const GetWebInfoArgsSchema = z.object({ - url: z.string().url().describe('URL of the webpage to retrieve detailed information') -}) - const RunNodeCodeArgsSchema = z.object({ code: z .string() @@ -445,19 +440,6 @@ export class PowerpackServer { readOnlyHint: true } }, - { - name: 'get_web_info', - description: - 'Get detailed content information from a specified webpage. Extract title, description, main content, and other information. ' + - 'This tool is useful for analyzing webpage content, obtaining article summaries or details. ' + - 'Just provide a valid HTTP or HTTPS URL to get complete webpage content analysis.', - inputSchema: zodToJsonSchema(GetWebInfoArgsSchema), - annotations: { - title: 'Get Web Info', - readOnlyHint: true, - openWorldHint: true - } - }, { name: 'run_shell_command', description: @@ -485,8 +467,7 @@ export class PowerpackServer { inputSchema: zodToJsonSchema(E2BRunCodeArgsSchema), annotations: { title: 'Run Code (E2B)', - readOnlyHint: false, - openWorldHint: true + readOnlyHint: false } }) } else { @@ -546,42 +527,6 @@ export class PowerpackServer { } } - case 'get_web_info': { - const parsed = GetWebInfoArgsSchema.safeParse(args) - if (!parsed.success) { - throw new Error(`Invalid URL arguments: ${parsed.error}`) - } - - const { url } = parsed.data - const enrichedData = await ContentEnricher.enrichUrl(url) - - // 格式化网页内容为易读的格式 - let formattedContent = `## Webpage Details\n\n` - formattedContent += `### Title\n${enrichedData.title || 'No Title'}\n\n` - formattedContent += `### URL\n${enrichedData.url}\n\n` - - if (enrichedData.description) { - formattedContent += `### Description\n${enrichedData.description}\n\n` - } - - if (enrichedData.content) { - const truncatedContent = - enrichedData.content.length > 1000 - ? enrichedData.content.substring(0, 1000) + '...(Content truncated)' - : enrichedData.content - formattedContent += `### Main Content\n${truncatedContent}\n\n` - } - - return { - content: [ - { - type: 'text', - text: formattedContent - } - ] - } - } - case 'run_shell_command': { const parsed = RunShellCommandArgsSchema.safeParse(args) diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index 0c380da9d..0c6476203 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -421,9 +421,7 @@ export class ToolManager { // ACP agent-level MCP access control (only applies in "acp agent" chat mode) if (toolCall.conversationId) { - const chatMode = this.configPresenter.getSetting<'chat' | 'agent' | 'acp agent'>( - 'input_chatMode' - ) + const chatMode = this.configPresenter.getSetting<'agent' | 'acp agent'>('input_chatMode') if (chatMode === 'acp agent') { try { const conversation = await presenter.sessionPresenter.getConversation( diff --git a/src/main/presenter/searchPresenter/handlers/baseHandler.ts b/src/main/presenter/searchPresenter/handlers/baseHandler.ts deleted file mode 100644 index ac3ddf131..000000000 --- a/src/main/presenter/searchPresenter/handlers/baseHandler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IConfigPresenter, ILlmProviderPresenter, ISQLitePresenter } from '@shared/presenter' -import type { MessageManager } from '../../sessionPresenter/managers/messageManager' -import type { SearchManager } from '../managers/searchManager' - -export interface ThreadHandlerContext { - sqlitePresenter: ISQLitePresenter - messageManager: MessageManager - llmProviderPresenter: ILlmProviderPresenter - configPresenter: IConfigPresenter - searchManager: SearchManager -} - -export abstract class BaseHandler { - protected readonly ctx: ThreadHandlerContext - - constructor(context: ThreadHandlerContext) { - this.ctx = context - } -} diff --git a/src/main/presenter/searchPresenter/handlers/searchHandler.ts b/src/main/presenter/searchPresenter/handlers/searchHandler.ts deleted file mode 100644 index a103b2486..000000000 --- a/src/main/presenter/searchPresenter/handlers/searchHandler.ts +++ /dev/null @@ -1,349 +0,0 @@ -import type { AssistantMessageBlock, Message, UserMessageContent } from '@shared/chat' -import { finalizeAssistantMessageBlocks } from '@shared/chat/messageBlocks' -import type { MODEL_META, SearchResult } from '@shared/presenter' -import { nanoid } from 'nanoid' -import { buildUserMessageContext } from '../../agentPresenter/message/messageFormatter' -import type { GeneratingMessageState } from '../../agentPresenter/streaming/types' -import { BaseHandler, type ThreadHandlerContext } from './baseHandler' - -interface SearchHandlerOptions { - generatingMessages: Map - searchingMessages: Set - getSearchAssistantModel?: () => MODEL_META | null - getSearchAssistantProviderId?: () => string | null -} - -export class SearchHandler extends BaseHandler { - private readonly generatingMessages: Map - private readonly searchingMessages: Set - private readonly getSearchAssistantModel: () => MODEL_META | null - private readonly getSearchAssistantProviderId: () => string | null - - constructor(context: ThreadHandlerContext, options: SearchHandlerOptions) { - super(context) - this.generatingMessages = options.generatingMessages - this.searchingMessages = options.searchingMessages - this.getSearchAssistantModel = options.getSearchAssistantModel ?? (() => null) - this.getSearchAssistantProviderId = options.getSearchAssistantProviderId ?? (() => null) - this.assertDependencies() - } - - private assertDependencies(): void { - void this.generatingMessages - void this.searchingMessages - void this.getSearchAssistantModel - void this.getSearchAssistantProviderId - } - - async startStreamSearch( - conversationId: string, - messageId: string, - query: string - ): Promise { - const state = this.generatingMessages.get(messageId) - if (!state) { - throw new Error('Generation state not found') - } - - const activeEngine = this.ctx.searchManager.getActiveEngine() - const labelValue = 'web_search' - const engineId = activeEngine?.id ?? labelValue - const engineName = activeEngine?.name ?? engineId - - this.throwIfCancelled(messageId) - - const searchId = nanoid() - const searchBlock: AssistantMessageBlock = { - id: searchId, - type: 'search', - content: '', - status: 'loading', - timestamp: Date.now(), - extra: { - total: 0, - searchId, - pages: [], - label: labelValue, - name: labelValue, - engine: engineName, - provider: engineName - } - } - - this.finalizeLastBlock(state) - state.message.content.push(searchBlock) - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - state.isSearching = true - this.searchingMessages.add(messageId) - - try { - const contextMessages = await this.getContextMessages(conversationId) - this.throwIfCancelled(messageId) - - const formattedContext = this.serializeContextMessages(contextMessages) - - searchBlock.status = 'optimizing' - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - const optimizedQuery = await this.rewriteUserSearchQuery( - query, - formattedContext, - conversationId, - engineName - ).catch((error) => { - console.error('Failed to rewrite search query:', error) - return query - }) - - if (optimizedQuery.includes('无须搜索')) { - searchBlock.status = 'success' - searchBlock.content = '' - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - state.isSearching = false - this.searchingMessages.delete(messageId) - return [] - } - - this.throwIfCancelled(messageId) - - searchBlock.status = 'reading' - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - const results = await this.ctx.searchManager.search(conversationId, optimizedQuery) - - this.throwIfCancelled(messageId) - - searchBlock.status = 'loading' - const pages = results - .filter((item) => item && (item.icon || item.favicon)) - .slice(0, 6) - .map((item) => ({ - url: item?.url || '', - icon: item?.icon || item?.favicon || '' - })) - - const previousExtra = searchBlock.extra ?? {} - searchBlock.extra = { - ...previousExtra, - total: results.length, - pages - } - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - await this.saveSearchResults(messageId, results, searchId) - - this.throwIfCancelled(messageId) - - searchBlock.status = 'success' - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - state.isSearching = false - this.searchingMessages.delete(messageId) - return results - } catch (error) { - state.isSearching = false - this.searchingMessages.delete(messageId) - - searchBlock.status = 'error' - searchBlock.content = String(error) - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - - if (String(error).includes('userCanceledGeneration')) { - this.ctx.searchManager.stopSearch(state.conversationId) - } - - return [] - } - } - - async rewriteUserSearchQuery( - query: string, - contextMessages: string, - conversationId: string, - searchEngine: string - ): Promise { - const rewritePrompt = ` - 你非常擅长于使用搜索引擎去获取最新的数据,你的目标是在充分理解用户的问题后,进行全面的网络搜索搜集必要的信息,首先你要提取并优化搜索的查询内容 - - 现在时间:${new Date().toISOString()} - 正在使用的搜索引擎:${searchEngine} - - 请遵循以下规则重写搜索查询: - 1. 根据用户的问题和上下文,重写应该进行搜索的关键词 - 2. 如果需要使用时间,则根据当前时间给出需要查询的具体时间日期信息 - 3. 生成的查询关键词要选择合适的语言,考虑用户的问题类型使用最适合的语言进行搜索,例如某些问题应该保持用户的问题语言,而有一些则更适合翻译成英语或其他语言 - 4. 保持查询简洁,通常不超过3个关键词, 最多不要超过5个关键词,参考当前搜索引擎的查询习惯重写关键字 - - 直接返回优化后的搜索词,不要有任何额外说明。 - 如果你觉得用户的问题不需要进行搜索,请直接返回"无须搜索"。 - - 如下是之前对话的上下文: - - ${contextMessages} - - 如下是用户的问题: - - ${query} - - ` - - const conversation = await this.ctx.sqlitePresenter.getConversation(conversationId) - if (!conversation) { - return query - } - - const providerId = this.getSearchAssistantProviderId() || conversation.settings.providerId - const modelId = this.getSearchAssistantModel()?.id || conversation.settings.modelId - - try { - const rewrittenQuery = await this.ctx.llmProviderPresenter.generateCompletion( - providerId, - [ - { - role: 'user', - content: rewritePrompt - } - ], - modelId - ) - return rewrittenQuery.trim() || query - } catch (error) { - console.error('Failed to rewrite search query:', error) - return query - } - } - - async processSearchResults( - messageId: string, - results: SearchResult[], - searchBlocks?: AssistantMessageBlock[] - ): Promise { - const state = this.generatingMessages.get(messageId) - if (!state) { - return - } - - if (!results.length) { - return - } - - const searchId = nanoid() - const block: AssistantMessageBlock = - searchBlocks?.[0] ?? - ({ - id: searchId, - type: 'search', - status: 'success', - timestamp: Date.now(), - content: '', - extra: { - total: results.length, - searchId, - pages: results - .filter((item) => item.icon || item.favicon) - .slice(0, 6) - .map((item) => ({ - url: item.url || '', - icon: item.icon || item.favicon || '' - })) - } - } satisfies AssistantMessageBlock) - - if (!searchBlocks) { - this.finalizeLastBlock(state) - state.message.content.push(block) - } - - const attachmentSearchId = - typeof block.extra?.searchId === 'string' ? block.extra.searchId : searchId - - await this.saveSearchResults(messageId, results, attachmentSearchId) - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - } - - private async getContextMessages(conversationId: string): Promise { - const conversation = await this.ctx.sqlitePresenter.getConversation(conversationId) - if (!conversation) { - throw new Error('Conversation not found') - } - - let messageCount = Math.ceil(conversation.settings.contextLength / 300) - if (messageCount < 2) { - messageCount = 2 - } - - return this.ctx.messageManager.getContextMessages(conversationId, messageCount) - } - - private serializeContextMessages(messages: Message[]): string { - return messages - .map((msg) => { - if (msg.role === 'user') { - const content = msg.content as UserMessageContent - const userContext = buildUserMessageContext(content) - return `user: ${userContext}` - } - - if (msg.role === 'assistant') { - let finalContent = 'assistant: ' - const content = msg.content as AssistantMessageBlock[] - content.forEach((block) => { - if (block.type === 'content') { - finalContent += block.content + '\n' - } - if (block.type === 'search') { - finalContent += `search-result: ${JSON.stringify(block.extra)}` - } - if (block.type === 'tool_call') { - finalContent += `tool_call: ${JSON.stringify(block.tool_call)}` - } - if (block.type === 'image') { - finalContent += `image: ${block.image_data?.data}` - } - }) - return finalContent - } - - return JSON.stringify(msg.content) - }) - .join('\n') - } - - private finalizeLastBlock(state: GeneratingMessageState): void { - finalizeAssistantMessageBlocks(state.message.content) - } - - private throwIfCancelled(messageId: string): void { - if (this.isMessageCancelled(messageId)) { - throw new Error('common.error.userCanceledGeneration') - } - } - - private isMessageCancelled(messageId: string): boolean { - const state = this.generatingMessages.get(messageId) - return !state || state.isCancelled === true - } - - private async saveSearchResults( - messageId: string, - results: SearchResult[], - searchId: string - ): Promise { - for (const result of results) { - await this.ctx.sqlitePresenter.addMessageAttachment( - messageId, - 'search_result', - JSON.stringify({ - title: result.title, - url: result.url, - content: result.content || '', - description: result.description || '', - icon: result.icon || result.favicon || '', - rank: typeof result.rank === 'number' ? result.rank : undefined, - searchId - }) - ) - } - } -} diff --git a/src/main/presenter/searchPresenter/index.ts b/src/main/presenter/searchPresenter/index.ts deleted file mode 100644 index b6f32625c..000000000 --- a/src/main/presenter/searchPresenter/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { ISearchPresenter } from './interface' -import { SearchManager } from './managers/searchManager' -import type { - IConfigPresenter, - ILlmProviderPresenter, - IWindowPresenter, - MODEL_META, - SearchEngineTemplate -} from '@shared/presenter' - -interface SearchPresenterDependencies { - configPresenter: IConfigPresenter - windowPresenter: IWindowPresenter - llmProviderPresenter: ILlmProviderPresenter -} - -export class SearchPresenter implements ISearchPresenter { - private readonly searchManager: SearchManager - private searchAssistantModel: MODEL_META | null = null - private searchAssistantProviderId: string | null = null - - constructor(deps: SearchPresenterDependencies) { - this.searchManager = new SearchManager({ - configPresenter: deps.configPresenter, - windowPresenter: deps.windowPresenter, - llmProviderPresenter: deps.llmProviderPresenter, - getSearchAssistantModel: () => this.searchAssistantModel, - getSearchAssistantProviderId: () => this.searchAssistantProviderId - }) - } - - getSearchManager(): SearchManager { - return this.searchManager - } - - getSearchAssistantModel(): MODEL_META | null { - return this.searchAssistantModel - } - - getSearchAssistantProviderId(): string | null { - return this.searchAssistantProviderId - } - - setSearchAssistantModel(model: MODEL_META, providerId: string): void { - this.searchAssistantModel = model - this.searchAssistantProviderId = providerId - } - - async getEngines() { - return this.searchManager.getEngines() - } - - async getActiveEngine() { - return this.searchManager.getActiveEngine() - } - - async setActiveEngine(engineId: string) { - return this.searchManager.setActiveEngine(engineId) - } - - async testEngine(query?: string) { - return this.searchManager.testSearch(query) - } - - async executeSearch(conversationId: string, query: string) { - return this.searchManager.search(conversationId, query) - } - - async stopSearch(conversationId: string) { - await this.searchManager.stopSearch(conversationId) - } - - async updateEngines(engines: SearchEngineTemplate[]) { - await this.searchManager.updateEngines(engines) - } - - async addCustomEngine(engine: SearchEngineTemplate) { - await this.searchManager.addCustomEngine(engine) - } - - async removeCustomEngine(engineId: string) { - await this.searchManager.removeCustomEngine(engineId) - } - - async search(conversationId: string, query: string) { - return this.searchManager.search(conversationId, query) - } - - async testSearch(query?: string) { - return this.searchManager.testSearch(query) - } - - destroy() { - this.searchManager.destroy() - } -} diff --git a/src/main/presenter/searchPresenter/interface.ts b/src/main/presenter/searchPresenter/interface.ts deleted file mode 100644 index c884d96c0..000000000 --- a/src/main/presenter/searchPresenter/interface.ts +++ /dev/null @@ -1 +0,0 @@ -export type { ISearchPresenter } from '@shared/presenter' diff --git a/src/main/presenter/searchPresenter/managers/searchManager.ts b/src/main/presenter/searchPresenter/managers/searchManager.ts deleted file mode 100644 index 52f36af94..000000000 --- a/src/main/presenter/searchPresenter/managers/searchManager.ts +++ /dev/null @@ -1,1438 +0,0 @@ -import { app, BrowserWindow, screen } from 'electron' -import path from 'path' -import { SearchEngineTemplate } from '@shared/chat' -import { ContentEnricher } from '../../content/contentEnricher' -import type { - IConfigPresenter, - ILlmProviderPresenter, - IWindowPresenter, - MODEL_META, - SearchResult -} from '@shared/presenter' -import { is } from '@electron-toolkit/utils' -import { eventBus } from '@/eventbus' -import { CONFIG_EVENTS } from '@/events' -import { jsonrepair } from 'jsonrepair' -import { SEARCH_PROMPT_TEMPLATE } from '../../searchPrompts/templates/searchPromptTemplate' - -const helperPage = path.join(app.getAppPath(), 'resources', 'blankSearch.html') - -// 抽取的脚本模板,使用占位符替代选择器 -const EXTRACTOR_SCRIPT_TEMPLATE = ` - const results = []; - const items = document.querySelectorAll('{{ITEMS_SELECTOR}}'); - items.forEach((item, index) => { - try { - const titleSelectorValue = '{{TITLE_SELECTOR}}'; - const titleEl = titleSelectorValue ? item.querySelector(titleSelectorValue) : null; - - const linkSelectorValue = '{{LINK_SELECTOR}}'; - const linkEl = linkSelectorValue ? item.querySelector(linkSelectorValue) : null; - - const descSelectorValue = '{{DESC_SELECTOR}}'; - const descEl = descSelectorValue ? item.querySelector(descSelectorValue) : null; - - const faviconSelectorValue = '{{FAVICON_SELECTOR}}'; - const faviconEl = faviconSelectorValue ? item.querySelector(faviconSelectorValue) : null; - - if (titleEl && linkEl) { - results.push({ - title: {{TITLE_EXTRACT}}, - url: {{URL_EXTRACT}}, - rank: index + 1, - description: descEl ? {{DESC_EXTRACT}} : '', - icon: {{ICON_EXTRACT}} - }); - } - } catch (e) { - // 如果修改后仍然出现错误,那么可能是其他未预料到的问题 - console.error('Error processing item (unexpected with conditional selectors):', e); - } - }); - return results; -` - -// 定义选择器配置的接口 -interface SelectorConfig { - itemsSelector: string - titleSelector: string - linkSelector: string - descSelector: string - faviconSelector: string - titleExtract: string - urlExtract: string - descExtract: string - iconExtract: string -} - -const searchEngineSelectors: Record = { - sogou: { - itemsSelector: '.news-list li', - titleSelector: 'h3 a', - linkSelector: 'h3 a', - descSelector: 'p.txt-info', - faviconSelector: 'a[data-z="art"] img', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl ? faviconEl.src : ''" - }, - google: { - itemsSelector: '#search .MjjYud', - titleSelector: 'h3', - linkSelector: 'a', - descSelector: '.VwiC3b', - faviconSelector: 'img.XNo5Ab', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl ? faviconEl.src : ''" - }, - baidu: { - itemsSelector: '#content_left .result', - titleSelector: '.t', - linkSelector: 'a', - descSelector: '.c-abstract', - faviconSelector: '.c-img', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl ? faviconEl.getAttribute('src') : ''" - }, - bing: { - itemsSelector: '#b_results h2', - titleSelector: 'h2 a', - linkSelector: 'h2 a', - descSelector: '.b_caption p', - faviconSelector: '.wr_fav img', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl?.src ? faviconEl.src : ''" - }, - 'google-scholar': { - itemsSelector: '.gs_r', - titleSelector: '.gs_rt', - linkSelector: '.gs_rt a', - descSelector: '.gs_rs', - faviconSelector: '.gs_rt img', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl ? faviconEl.src : ''" - }, - 'baidu-xueshu': { - itemsSelector: '#bdxs_result_lists .sc_default_result', - titleSelector: '.sc_content .t', - linkSelector: '.sc_content a', - descSelector: '.c_abstract', - faviconSelector: '', - titleExtract: 'titleEl.textContent?.trim()', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent?.trim()', - iconExtract: "''" - }, - duckduckgo: { - itemsSelector: 'article.yQDlj3B5DI5YO8c8Ulio', - titleSelector: '.EKtkFWMYpwzMKOYr0GYm.LQVY1Jpkk8nyJ6HBWKAk', - linkSelector: 'a', - descSelector: '.E2eLOJr8HctVnDOTM8fs', - faviconSelector: '.DpVR46dTZaePK29PDkz8 img', - titleExtract: 'titleEl.textContent', - urlExtract: 'linkEl.href', - descExtract: 'descEl.textContent', - iconExtract: "faviconEl ? faviconEl.src : ''" - } -} - -// 根据选择器配置生成提取脚本 -function generateExtractorScript(selectorConfig: SelectorConfig): string { - return EXTRACTOR_SCRIPT_TEMPLATE.replace('{{ITEMS_SELECTOR}}', selectorConfig.itemsSelector) - .replace('{{TITLE_SELECTOR}}', selectorConfig.titleSelector) - .replace('{{LINK_SELECTOR}}', selectorConfig.linkSelector) - .replace('{{DESC_SELECTOR}}', selectorConfig.descSelector) - .replace('{{FAVICON_SELECTOR}}', selectorConfig.faviconSelector) - .replace('{{TITLE_EXTRACT}}', selectorConfig.titleExtract) - .replace('{{URL_EXTRACT}}', selectorConfig.urlExtract) - .replace('{{DESC_EXTRACT}}', selectorConfig.descExtract) - .replace('{{ICON_EXTRACT}}', selectorConfig.iconExtract) -} - -// 生成完整的搜索引擎配置 -function generateDefaultEngines() { - return [ - { - id: 'sogou', - name: 'sogou', - selector: '.news-list', - searchUrl: - 'https://weixin.sogou.com/weixin?ie=utf8&s_from=input&_sug_=y&_sug_type_=&type=2&query={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['sogou']) - }, - { - id: 'google', - name: 'google', - selector: '#search', - searchUrl: 'https://www.google.com/search?q={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['google']) - }, - { - id: 'baidu', - name: 'baidu', - selector: '#content_left', - searchUrl: 'https://www.baidu.com/s?wd={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['baidu']) - }, - { - id: 'bing', - name: 'bing', - selector: '', - searchUrl: 'https://www.bing.com/search?q={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['bing']) - }, - { - id: 'google-scholar', - name: 'google-scholar', - selector: '#gs_res_ccl', - searchUrl: 'https://scholar.google.com/scholar?q={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['google-scholar']) - }, - { - id: 'baidu-xueshu', - name: 'baidu-xueshu', - selector: '#bdxs_result_lists', - searchUrl: 'https://xueshu.baidu.com/s?wd={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['baidu-xueshu']) - }, - { - id: 'duckduckgo', - name: 'duckduckgo', - selector: 'button.cxQwADb9kt3UnKwcXKat', - searchUrl: 'https://duckduckgo.com/?q={query}', - extractorScript: generateExtractorScript(searchEngineSelectors['duckduckgo']) - } - ] -} - -// 初始化默认搜索引擎 -const defaultEngines = generateDefaultEngines() - -// 格式化搜索结果的函数 -export function formatSearchResults(results: SearchResult[]): string { - const formattedResults = results - .map( - (result, index) => `[webpage ${index + 1} begin] -title: ${result.title} -URL: ${result.url} -content:${result.content || ''} -[webpage ${index + 1} end]` - ) - .join('\n\n') - // 记录格式化后的搜索结果 - // console.log('formattedResults:', formattedResults) - return formattedResults -} -// 生成带搜索结果的提示词 -export function generateSearchPrompt(query: string, results: SearchResult[]): string { - if (results.length > 0) { - const searchPrompt = SEARCH_PROMPT_TEMPLATE.replace( - '{{SEARCH_RESULTS}}', - formatSearchResults(results) - ) - .replace('{{USER_QUERY}}', query) - .replace('{{CUR_DATE}}', new Date().toLocaleDateString()) - - // 记录最终生成的提示词 - console.log('generateSearchPrompt', searchPrompt) - - return searchPrompt - } else { - return query - } -} - -export class SearchManager { - private readonly configPresenter: IConfigPresenter - private readonly windowPresenter: IWindowPresenter - private readonly llmProviderPresenter: ILlmProviderPresenter - private readonly getSearchAssistantModel: () => MODEL_META | null - private readonly getSearchAssistantProviderId: () => string | null - private searchWindows: Map = new Map() - private maxConcurrentSearches = 3 - private engines: SearchEngineTemplate[] = defaultEngines - private activeEngine: SearchEngineTemplate = this.engines[0] - private originalWindowSizes: Map = new Map() - private originalWindowPositions: Map = new Map() - private wasFullScreen: Map = new Map() - private searchWindowWidth = 800 - private abortControllers: Map = new Map() - private lastEnginesUpdateTime = 0 - // 保存当前正在使用的选择器配置 - private currentSelectors = { ...searchEngineSelectors } - - constructor(options: { - configPresenter: IConfigPresenter - windowPresenter: IWindowPresenter - llmProviderPresenter: ILlmProviderPresenter - getSearchAssistantModel?: () => MODEL_META | null - getSearchAssistantProviderId?: () => string | null - }) { - this.configPresenter = options.configPresenter - this.windowPresenter = options.windowPresenter - this.llmProviderPresenter = options.llmProviderPresenter - this.getSearchAssistantModel = options.getSearchAssistantModel ?? (() => null) - this.getSearchAssistantProviderId = options.getSearchAssistantProviderId ?? (() => null) - // 初始化搜索管理器 - this.setupEventListeners() - } - - /** - * 设置事件监听器,监听搜索引擎更新事件 - */ - private setupEventListeners(): void { - // 监听搜索引擎更新事件 - eventBus.on(CONFIG_EVENTS.SEARCH_ENGINES_UPDATED, () => { - // 标记需要刷新引擎列表 - this.lastEnginesUpdateTime = 0 - }) - } - - /** - * 获取搜索引擎列表,包括默认引擎和自定义引擎 - */ - async getEngines(): Promise { - await this.ensureEnginesUpdated() - return this.engines - } - - /** - * 获取当前活跃的搜索引擎 - */ - getActiveEngine(): SearchEngineTemplate { - return this.activeEngine - } - - /** - * 设置活跃搜索引擎 - * @param engineId 搜索引擎ID - */ - async setActiveEngine(engineId: string): Promise { - console.log('setActiveEngine', engineId) - const engine = this.engines.find((e) => e.id === engineId) - if (engine) { - this.activeEngine = engine - // 保存搜索引擎选择到配置中 - await this.configPresenter.setSetting('searchEngine', engineId) - return true - } - return false - } - - /** - * 更新搜索引擎列表 - * @param newEngines 新的搜索引擎列表 - */ - async updateEngines(newEngines: SearchEngineTemplate[]): Promise { - // 保存当前活跃引擎ID - const activeEngineId = this.activeEngine.id - - // 更新引擎列表 - this.engines = newEngines - - // 尝试保持当前活跃引擎 - const engine = this.engines.find((e) => e.id === activeEngineId) - if (engine) { - this.activeEngine = engine - } else { - // 如果当前活跃引擎不在新列表中,选择第一个引擎 - this.activeEngine = this.engines[0] - } - - // 更新自定义引擎到配置 - await this.updateCustomEnginesToConfig() - - // 更新时间戳 - this.lastEnginesUpdateTime = Date.now() - } - - async addCustomEngine(engine: SearchEngineTemplate): Promise { - await this.ensureEnginesUpdated() - const updated = this.engines.filter((item) => item.id !== engine.id) - updated.push({ ...engine, isCustom: true }) - await this.updateEngines(updated) - } - - async removeCustomEngine(engineId: string): Promise { - await this.ensureEnginesUpdated() - const updated = this.engines.filter((engine) => engine.id !== engineId) - await this.updateEngines(updated) - } - - /** - * 确保引擎列表是最新的,如果需要就更新 - */ - private async ensureEnginesUpdated(): Promise { - console.log('ensureEnginesUpdated', this.lastEnginesUpdateTime) - // 如果上次更新时间是0或者距离现在超过24小时,则更新引擎列表 - const currentTime = Date.now() - if ( - this.lastEnginesUpdateTime === 0 || - currentTime - this.lastEnginesUpdateTime > 24 * 60 * 60 * 1000 - ) { - await this.refreshEngines() - } - } - - /** - * 刷新引擎列表,合并默认引擎和自定义引擎 - */ - private async refreshEngines(): Promise { - try { - const configPresenter = this.configPresenter - - // 获取自定义搜索引擎 - const customEngines = await configPresenter.getCustomSearchEngines() - - // 尝试获取云端选择器配置,预留接口,方便二次开发的时候下发配置 - this.refreshSelectorsFromCloud() - - // 重新生成默认引擎 - const updatedDefaultEngines = this.regenerateDefaultEngines() - - if (customEngines && customEngines.length > 0) { - // 记住当前活跃引擎ID - const activeEngineId = this.activeEngine.id - - // 合并更新后的默认引擎和自定义引擎 - this.engines = [...updatedDefaultEngines, ...customEngines] - - // 尝试保持当前活跃引擎 - const engine = this.engines.find((e) => e.id === activeEngineId) - if (engine) { - this.activeEngine = engine - } - } else { - // 没有自定义引擎,使用更新后的默认引擎 - this.engines = updatedDefaultEngines - } - - // 更新时间戳 - this.lastEnginesUpdateTime = Date.now() - } catch (error) { - console.error('刷新搜索引擎列表失败:', error) - } - } - - /** - * 从云端获取最新的选择器配置 - */ - private async refreshSelectorsFromCloud(): Promise { - try { - // 这里添加从云端获取选择器配置的逻辑 - // 例如通过API调用或配置服务获取 - const cloudSelectors = await this.fetchSelectorsFromCloud() - - if (cloudSelectors) { - // 安全地合并云端选择器和本地默认选择器 - this.updateSelectorsConfig(cloudSelectors) - } - } catch (error) { - console.error('从云端获取选择器配置失败:', error) - // 出错时继续使用当前选择器配置 - } - } - - /** - * 模拟从云端获取选择器配置的方法 - * 实际实现时,这里应该是一个真正的API调用 - */ - private async fetchSelectorsFromCloud(): Promise> | null> { - try { - // 这里只是一个示例,方便二次开发的用户需要下发配置的情况 - // 例如: - // const response = await fetch('https://your-api.com/search-selectors') - // if (response.ok) { - // return await response.json() - // } - - // 目前返回null,表示没有云端配置 - return null - } catch (error) { - console.error('获取云端选择器配置失败:', error) - return null - } - } - - /** - * 安全地更新选择器配置 - * 确保云端下发的选择器不会包含恶意代码 - */ - private updateSelectorsConfig(cloudSelectors: Record>): void { - // 创建一个新的选择器配置对象 - const updatedSelectors = { ...this.currentSelectors } - - // 遍历云端选择器 - for (const [engineId, cloudSelector] of Object.entries(cloudSelectors)) { - // 检查是否已有此引擎的本地配置 - if (updatedSelectors[engineId]) { - // 安全地更新字段,只允许特定字段 - const safeFields = [ - 'itemsSelector', - 'titleSelector', - 'linkSelector', - 'descSelector', - 'faviconSelector' - ] as const - - // 只更新安全字段,忽略其他字段 - for (const field of safeFields) { - if (typeof cloudSelector[field] === 'string') { - // 进行必要的安全检查,例如检查是否包含脚本标签或危险属性 - const sanitizedValue = this.sanitizeSelector(cloudSelector[field] as string) - updatedSelectors[engineId][field] = sanitizedValue - } - } - - // 对于提取逻辑的字段,采用更严格的安全措施 - const extractFields = ['titleExtract', 'urlExtract', 'descExtract', 'iconExtract'] as const - - for (const field of extractFields) { - if (typeof cloudSelector[field] === 'string') { - // 验证提取表达式是否安全 - const safeExtract = this.validateExtractExpression(cloudSelector[field] as string) - if (safeExtract) { - updatedSelectors[engineId][field] = safeExtract - } - } - } - } else if (this.isValidSelectorConfig(cloudSelector)) { - // 如果是新的引擎配置,验证完整性和安全性后添加 - updatedSelectors[engineId] = this.sanitizeSelectorConfig(cloudSelector) - } - } - - // 更新当前选择器配置 - this.currentSelectors = updatedSelectors - } - - /** - * 验证一个完整的选择器配置是否有效 - */ - private isValidSelectorConfig(config: Partial): boolean { - // 检查所有必需字段是否存在且类型正确 - const requiredFields = [ - 'itemsSelector', - 'titleSelector', - 'linkSelector', - 'titleExtract', - 'urlExtract' - ] as const - - for (const field of requiredFields) { - if (typeof config[field] !== 'string' || !config[field]) { - return false - } - } - - // 验证提取表达式是否安全 - return ( - this.validateExtractExpression(config.titleExtract as string) !== null && - this.validateExtractExpression(config.urlExtract as string) !== null - ) - } - - /** - * 对一个完整的选择器配置进行安全处理 - */ - private sanitizeSelectorConfig(config: Partial): SelectorConfig { - const safeConfig: SelectorConfig = { - itemsSelector: '', - titleSelector: '', - linkSelector: '', - descSelector: '', - faviconSelector: '', - titleExtract: 'null', - urlExtract: 'null', - descExtract: 'null', - iconExtract: 'null' - } - - // 安全处理选择器字段 - const selectorFields = [ - 'itemsSelector', - 'titleSelector', - 'linkSelector', - 'descSelector', - 'faviconSelector' - ] as const - - for (const field of selectorFields) { - safeConfig[field] = - typeof config[field] === 'string' ? this.sanitizeSelector(config[field] as string) : '' - } - - // 安全处理提取表达式字段 - const extractFields = ['titleExtract', 'urlExtract', 'descExtract', 'iconExtract'] as const - - for (const field of extractFields) { - const safeExtract = - typeof config[field] === 'string' - ? this.validateExtractExpression(config[field] as string) - : null - - safeConfig[field] = safeExtract || 'null' - } - - return safeConfig - } - - /** - * 清理选择器字符串,防止XSS攻击 - */ - private sanitizeSelector(selector: string): string { - // 移除可能的JavaScript代码或事件处理器 - const sanitized = selector - .replace(/javascript:|data:| { - try { - // 提取所有标记为自定义的引擎 - const customEngines = this.engines.filter((engine) => engine.isCustom) - - // 更新到配置 - if (customEngines.length > 0) { - const configPresenter = this.configPresenter - await configPresenter.setCustomSearchEngines(customEngines) - } - } catch (error) { - console.error('更新自定义搜索引擎到配置失败:', error) - } - } - - private async initSearchWindow(conversationId: string): Promise { - // 直接从 ConfigPresenter 获取搜索预览设置状态 - const searchPreviewEnabled = await this.configPresenter.getSearchPreviewEnabled() - - // 如果搜索预览关闭,创建一个隐藏的窗口 - if (!searchPreviewEnabled) { - const searchWindow = new BrowserWindow({ - width: this.searchWindowWidth, - height: 800, - show: false, // 窗口不显示 - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - devTools: is.dev - } - }) - - searchWindow.webContents.session.webRequest.onBeforeSendHeaders( - { urls: ['*://*/*'] }, - (details, callback) => { - const headers = { - ...details.requestHeaders, - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - callback({ requestHeaders: headers }) - } - ) - - this.searchWindows.set(conversationId, searchWindow) - return searchWindow - } - - // 下面是原始代码,当预览启用时执行 - if (this.searchWindows.size >= this.maxConcurrentSearches) { - // 找到最早创建的窗口并销毁 - const [oldestConversationId] = this.searchWindows.keys() - this.destroySearchWindow(oldestConversationId) - } - const mainWindow = this.windowPresenter.mainWindow - - // 确保mainWindow存在 - if (!mainWindow) { - console.error('主窗口不存在,无法创建搜索窗口') - throw new Error('主窗口不存在') - } - - // 检查是否处于全屏状态 - const isFullScreen = mainWindow.isFullScreen() - this.wasFullScreen.set(conversationId, isFullScreen) - - // 如果是全屏,先退出全屏 - if (isFullScreen) { - // 保存全屏前的位置和大小(如果可能的话) - this.originalWindowSizes.set(conversationId, { - width: mainWindow.getBounds().width, - height: mainWindow.getBounds().height - }) - this.originalWindowPositions.set(conversationId, { - x: mainWindow.getBounds().x, - y: mainWindow.getBounds().y - }) - - // 退出全屏并等待完成 - mainWindow.setFullScreen(false) - - // 等待退出全屏完成 - await new Promise((resolve) => { - const checkFullScreenState = () => { - if (!mainWindow.isFullScreen()) { - resolve() - } else { - setTimeout(checkFullScreenState, 100) - } - } - checkFullScreenState() - }) - - // 给界面一些时间来重新布局 - await new Promise((resolve) => setTimeout(resolve, 200)) - } else { - // 不是全屏模式,正常保存当前主窗口位置和大小信息 - this.originalWindowPositions.set(conversationId, { - x: mainWindow.getBounds().x, - y: mainWindow.getBounds().y - }) - this.originalWindowSizes.set(conversationId, { - width: mainWindow.getBounds().width, - height: mainWindow.getBounds().height - }) - } - - // 获取当前屏幕可用空间 - const mainWindowBounds = mainWindow.getBounds() - const displayBounds = screen.getDisplayMatching(mainWindowBounds).workArea - - // 检查是否右侧有足够空间 - const rightSpace = - displayBounds.x + displayBounds.width - (mainWindowBounds.x + mainWindowBounds.width) - const needsAdjustment = rightSpace < this.searchWindowWidth + 20 // 加20px作为间隔 - - // 如果需要调整窗口 - if (needsAdjustment) { - // 在全屏模式下退出全屏后,优先采用两个窗口铺满屏幕的方式 - if (isFullScreen) { - const totalWidth = displayBounds.width - const mainWindowWidth = Math.floor(totalWidth * 0.6) // 主窗口占60% - const searchWindowWidth = Math.floor(totalWidth * 0.4) // 搜索窗口占40% - this.searchWindowWidth = searchWindowWidth - - // 设置主窗口尺寸和位置(使用Electron内置动画) - mainWindow.setBounds( - { - x: displayBounds.x, - y: displayBounds.y, - width: mainWindowWidth, - height: displayBounds.height - }, - true - ) // 添加true启用动画 - } else { - // 非全屏模式下的调整逻辑 - // 计算左移窗口所需的空间 - const neededSpace = this.searchWindowWidth + 20 - rightSpace - - // 检查左侧是否有足够空间移动窗口 - const availableLeftSpace = mainWindowBounds.x - displayBounds.x - - // 优先移动窗口位置 - if (availableLeftSpace >= neededSpace) { - // 有足够空间移动窗口位置 - const newX = Math.max(displayBounds.x, mainWindowBounds.x - neededSpace) - // 使用Electron内置动画 - mainWindow.setPosition(newX, mainWindowBounds.y, true) // 添加true启用动画 - } else { - // 左侧空间不足,结合移动和缩放 - // 先尽可能地移动窗口 - if (availableLeftSpace > 0) { - mainWindow.setPosition(displayBounds.x, mainWindowBounds.y, true) // 添加true启用动画 - } - - // 计算需要缩放的大小 - const remainingNeededSpace = neededSpace - availableLeftSpace - if (remainingNeededSpace > 0) { - // 还需要缩放窗口 - const newWidth = Math.max( - 400, // 最小主窗口宽度 - mainWindowBounds.width - remainingNeededSpace - ) - // 使用Electron内置动画 - mainWindow.setSize(newWidth, mainWindowBounds.height, true) // 添加true启用动画 - } - } - } - - // 给窗口一些时间来完成动画 - await new Promise((resolve) => setTimeout(resolve, 300)) - } - - console.log('creating search window') - // 创建搜索窗口 - const searchWindow = new BrowserWindow({ - width: this.searchWindowWidth, - height: isFullScreen ? displayBounds.height : mainWindowBounds.height, - parent: mainWindow, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - devTools: is.dev - } - }) - - // 获取调整后的主窗口位置 - const updatedMainBounds = mainWindow.getBounds() - - // 设置搜索窗口位置在主窗口右侧 - searchWindow.setPosition(updatedMainBounds.x + updatedMainBounds.width, updatedMainBounds.y) - - searchWindow.webContents.session.webRequest.onBeforeSendHeaders( - { urls: ['*://*/*'] }, - (details, callback) => { - const headers = { - ...details.requestHeaders, - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - callback({ requestHeaders: headers }) - } - ) - if (is.dev) { - searchWindow.webContents.openDevTools({ mode: 'detach' }) - } - this.searchWindows.set(conversationId, searchWindow) - return searchWindow - } - - private async destroySearchWindow(conversationId: string) { - const window = this.searchWindows.get(conversationId) - if (window) { - window.destroy() - this.searchWindows.delete(conversationId) - - // 直接从 ConfigPresenter 获取搜索预览设置状态 - const searchPreviewEnabled = await this.configPresenter.getSearchPreviewEnabled() - - // 如果搜索预览未启用,不需要恢复主窗口状态 - if (!searchPreviewEnabled) { - return - } - - // 恢复主窗口原始位置和大小 - const originalSize = this.originalWindowSizes.get(conversationId) - const originalPosition = this.originalWindowPositions.get(conversationId) - const wasFullScreen = this.wasFullScreen.get(conversationId) - - if (originalSize && originalPosition) { - const mainWindow = this.windowPresenter.mainWindow - if (mainWindow) { - if (wasFullScreen) { - // 如果原来是全屏,先恢复原始尺寸和位置,再进入全屏 - mainWindow.setBounds( - { - x: originalPosition.x, - y: originalPosition.y, - width: originalSize.width, - height: originalSize.height - }, - true - ) // 添加true启用动画 - - // 给UI一些时间来适应新尺寸 - await new Promise((resolve) => setTimeout(resolve, 300)) - - // 重新进入全屏 - mainWindow.setFullScreen(true) - } else { - // 非全屏模式下平滑恢复 - mainWindow.setBounds( - { - x: originalPosition.x, - y: originalPosition.y, - width: originalSize.width, - height: originalSize.height - }, - true - ) // 添加true启用动画 - } - } - - this.originalWindowSizes.delete(conversationId) - this.originalWindowPositions.delete(conversationId) - this.wasFullScreen.delete(conversationId) - } - } - } - - async search(conversationId: string, query: string): Promise { - // 确保引擎列表是最新的 - // await this.ensureEnginesUpdated() - - // 创建用于可能中断搜索的 AbortController - const abortController = new AbortController() - this.abortControllers.set(conversationId, abortController) - - let searchWindow = this.searchWindows.get(conversationId) - if (!searchWindow) { - searchWindow = await this.initSearchWindow(conversationId) - } - - const searchUrl = this.activeEngine.searchUrl.replace('{query}', encodeURIComponent(query)) - console.log('开始加载搜索URL:', searchUrl) - - const loadTimeout = setTimeout(() => { - searchWindow?.webContents.stop() - }, 8000) - - try { - // 检查是否已经被中止 - if (abortController.signal.aborted) { - throw new Error('搜索已被用户取消') - } - - await searchWindow.loadURL(searchUrl) - console.log('搜索URL加载成功') - } catch (error) { - console.error('加载URL失败:', error) - if (abortController.signal.aborted) { - // 如果是用户取消导致的错误,直接返回空结果 - this.destroySearchWindow(conversationId) - this.abortControllers.delete(conversationId) - return [] - } - } finally { - clearTimeout(loadTimeout) - } - - // 检查是否已经被中止 - if (abortController.signal.aborted) { - this.destroySearchWindow(conversationId) - this.abortControllers.delete(conversationId) - return [] - } - if (this.activeEngine.selector) { - await this.waitForSelector(searchWindow, this.activeEngine.selector) - } else { - await new Promise((resolve) => setTimeout(resolve, 1000)) - } - console.log('搜索结果加载完成') - - // 检查是否已经被中止 - if (abortController.signal.aborted) { - this.destroySearchWindow(conversationId) - this.abortControllers.delete(conversationId) - return [] - } - - const results = await this.extractSearchResults(searchWindow) - console.log('搜索结果提取完成:', results?.length) - - // 检查是否已经被中止 - if (abortController.signal.aborted) { - this.destroySearchWindow(conversationId) - this.abortControllers.delete(conversationId) - return [] - } - - const enrichedResults = await this.enrichResults(results.slice(0, 5)) - console.log('详细内容获取完成') - - // 清理资源 - this.abortControllers.delete(conversationId) - - searchWindow - .loadFile(helperPage) - .then(() => { - this.destroySearchWindow(conversationId) - }) - .catch((error) => { - console.error('加载空白页失败:', error) - this.destroySearchWindow(conversationId) - }) - const remainingResults = results.slice(5) // 获取剩余的结果 - const combinedResults = [...enrichedResults, ...remainingResults] // 合并enrichedResults和剩余的results - return combinedResults - } - - private async waitForSelector(window: BrowserWindow, selector: string): Promise { - return new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve() // 12秒后自动返回 - }, 12000) - // 如果selector不为空,就等待selector出现 - if (selector) { - window.webContents - .executeJavaScript( - ` - new Promise((innerResolve) => { - if (document.querySelector('${selector}')) { - innerResolve(); - } else { - const observer = new MutationObserver(() => { - if (document.querySelector('${selector}')) { - observer.disconnect(); - innerResolve(); - } - }); - observer.observe(document.body, { childList: true, subtree: true }); - } - }) - ` - ) - .then(() => { - resolve() - }) - .catch(() => { - resolve() - }) - - clearTimeout(timeout) - resolve() - } - }) - } - - private async extractSearchResults(window: BrowserWindow): Promise { - try { - // 0. 模拟页面滚动,模拟真实阅读体验 - this.simulatePageScrolling(window) - console.log('extraing', this.activeEngine.id) - const results = await window.webContents.executeJavaScript(` - (function() { - ${this.activeEngine.extractorScript} - })() - `) - // 如果结果为空或长度为0,尝试使用备用方法 - if (!results || results.length === 0) { - console.log('常规提取方法未返回结果,尝试使用备用方法') - return await this.fallbackExtractSearchResults(window) - } - - return results - } catch (error) { - console.error('提取搜索结果失败:', error) - // 出错时也使用备用方法 - return [] - } - } - - /** - * 备用的搜索结果提取方法,当标准提取方法失败时使用 - * 使用AI模型分析页面内容提取搜索结果 - */ - private async fallbackExtractSearchResults(window: BrowserWindow): Promise { - try { - // 1. 执行JS提取当前页面的body内容并清理不相关元素 - const cleanedHtml = await window.webContents.executeJavaScript(` - (function() { - // 克隆body以避免直接修改页面 - const tempDiv = document.createElement('div') - tempDiv.innerHTML = document.body.innerHTML - - // 移除不需要的元素 - const elementsToRemove = tempDiv.querySelectorAll('script, style, svg, iframe, nav, footer, header') - elementsToRemove.forEach(el => el.parentNode.removeChild(el)) - - // 移除广告相关元素 - const adElements = tempDiv.querySelectorAll('[class*="ad"], [id*="ad"], [class*="banner"], [class*="popup"]') - adElements.forEach(el => el.parentNode.removeChild(el)) - - // 移除隐藏元素 - const hiddenElements = tempDiv.querySelectorAll('[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"]') - hiddenElements.forEach(el => el.parentNode.removeChild(el)) - // 移除footer元素 - const footerElements = tempDiv.querySelectorAll('footer') - footerElements.forEach(el => el.parentNode.removeChild(el)) - // 移除header元素 - const headerElements = tempDiv.querySelectorAll('header') - headerElements.forEach(el => el.parentNode.removeChild(el)) - // 移除footer的class - const footerClassElements = tempDiv.querySelectorAll('.footer') - footerClassElements.forEach(el => el.parentNode.removeChild(el)) - // 移除可能是sidebar的元素 - const sidebarElements = tempDiv.querySelectorAll('.side-bar, .sidebar, [class*="sidebar"]') - sidebarElements.forEach(el => el.parentNode.removeChild(el)) - // 返回清理后的HTML - return tempDiv.innerHTML - })() - `) - - // 获取页面URL(用于转换相对链接为绝对链接) - const pageUrl = await window.webContents.getURL() - const pageTitle = await window.webContents.executeJavaScript(`document.title`) - - console.log('转换前的HTML长度:', cleanedHtml.length) - // 2. 使用ContentEnricher将HTML转换为Markdown - let markdownContent = ContentEnricher.convertHtmlToMarkdown(cleanedHtml, pageUrl) - console.log('转换后的Markdown长度:', markdownContent.length) - - // 限制markdown长度,避免过大 - const maxMarkdownLength = 10000 - if (markdownContent.length > maxMarkdownLength) { - markdownContent = markdownContent.substring(0, maxMarkdownLength) - } - - // 3. 构建提示词,使用AI模型提取搜索结果 - const prompt = ` - 请分析标签中的搜索引擎返回的markdown内容,并提取出所有搜索结果。每个搜索结果应包含以下字段: - - title: 结果标题 - - url: 结果链接URL - - rank: 结果的序号(从1开始) - - description: 结果描述或摘要 - - icon: 结果的图标URL(如果存在) - - 搜索页面URL: ${pageUrl} - 搜索页面标题: ${pageTitle} - - 请使用以下JSON数组格式返回结果: - [ - { - "title": "结果标题", - "url": "结果链接", - "rank": 1, - "description": "结果描述", - "icon": "图标URL" - }, - { - "title": "结果标题2", - "url": "结果链接2", - "rank": 2, - "description": "结果描述2", - "icon": "图标URL2" - }, - ... - ] - - 重要提示: - 1. 仅返回有效的搜索结果,忽略广告、推荐内容等。 - 2. 确保返回的是有效的JSON格式。 - 3. 请尽可能提取出完整的URL,如果链接是相对路径,请基于搜索页面URL构建完整URL。 - 4. 如果无法找到某个字段,请使用空字符串代替。 - 5. 请只返回JSON数组,不要返回其他说明文字。 - 6. 如果提取不到结果,请返回空数组[]。 - - - ${markdownContent} - - ` - - // 4. 使用AI模型进行分析 - const searchAssistantModel = this.getSearchAssistantModel() - const searchAssistantProviderId = this.getSearchAssistantProviderId() - if (!searchAssistantModel || !searchAssistantProviderId) { - throw new Error('搜索助手模型或提供商ID未设置') - } - const modelResponse = await this.llmProviderPresenter.generateCompletion( - searchAssistantProviderId, - [ - { - role: 'user', - content: prompt - } - ], - searchAssistantModel.id || '', - 0.4 - ) - console.log('模型返回的内容:', modelResponse?.length) - - // 5. 解析模型返回的内容 - try { - // 尝试解析JSON - const jsonStart = modelResponse.indexOf('[') - const jsonEnd = modelResponse.lastIndexOf(']') + 1 - - if (jsonStart >= 0 && jsonEnd > jsonStart) { - const jsonStr = modelResponse.substring(jsonStart, jsonEnd) - const results = JSON.parse(jsonStr) - - // 验证结果格式 - if (Array.isArray(results) && results.length > 0) { - console.log('AI模型成功提取到搜索结果:', results.length) - return results - } - } else if (jsonStart >= 0) { - // 找到了开始的 '[' 但没有找到匹配的结束 ']' - // 这种情况下尝试逐个解析JSON对象 - - // 从jsonStart开始的子字符串 - const incompleteJsonStr = modelResponse.substring(jsonStart) - - // 结果数组 - let results: SearchResult[] = [] - try { - console.log('try to repair json') - results = JSON.parse(jsonrepair(incompleteJsonStr)) - } catch (e: unknown) { - console.error('Error parsing AI model response:', e) - results = [] - } - - if (results.length > 0) { - console.log('成功从不完整JSON中提取到搜索结果:', results.length) - return results - } - } - - // 如果无法解析为JSON或格式不正确 - console.warn('AI模型返回的内容无法解析为有效的搜索结果') - return [] - } catch (error) { - console.error('解析AI模型返回内容失败:', error) - return [] - } - } catch (error) { - console.error('备用提取方法失败:', error) - return [] - } - } - - /** - * 模拟页面滚动,增强用户体验 - * @param window 浏览器窗口 - */ - private async simulatePageScrolling(window: BrowserWindow): Promise { - try { - // 获取页面高度 - const pageHeight = await window.webContents.executeJavaScript(` - document.body.scrollHeight - `) - - // 获取视窗高度 - const viewportHeight = await window.webContents.executeJavaScript(` - window.innerHeight - `) - - // 页面总高度 - const totalHeight = Math.max(pageHeight, 1000) - - // 计算滚动次数和每次滚动的距离 - const scrollIterations = 3 // 滚动3次 - const scrollDistance = Math.min(totalHeight / scrollIterations, viewportHeight * 0.8) - - // 平滑滚动 - for (let i = 0; i < scrollIterations; i++) { - await window.webContents.executeJavaScript(` - new Promise((resolve) => { - // 获取当前滚动位置 - const currentScroll = window.scrollY || document.documentElement.scrollTop; - // 计算目标位置 - const targetScroll = currentScroll + ${scrollDistance}; - - // 使用平滑滚动 - window.scrollTo({ - top: targetScroll, - behavior: 'smooth' - }); - - // 等待滚动完成 - setTimeout(resolve, 300); - }) - `) - - // 给浏览器一点时间来加载潜在的懒加载内容 - await new Promise((resolve) => setTimeout(resolve, 500)) - } - - // 等待一下,让页面完全加载 - await new Promise((resolve) => setTimeout(resolve, 500)) - - console.log('页面滚动完成') - } catch (error) { - console.error('模拟页面滚动失败:', error) - // 失败也继续处理 - } - } - - private async enrichResults(results: SearchResult[]): Promise { - return await ContentEnricher.enrichResults(results) - } - - /** - * 测试搜索引擎功能 - * 打开一个窗口进行测试搜索,窗口将保持在前台直到用户关闭 - * @param query 搜索关键词,默认为"天气" - * @returns 是否成功打开测试窗口 - */ - async testSearch(query: string = '天气'): Promise { - try { - // 确保引擎列表是最新的 - // await this.ensureEnginesUpdated() - - // 创建一个独立的测试窗口 - const testWindow = new BrowserWindow({ - width: 800, - height: 600, - title: `测试搜索 - ${this.activeEngine.name}`, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - devTools: is.dev - } - }) - - // 配置User-Agent - testWindow.webContents.session.webRequest.onBeforeSendHeaders( - { urls: ['*://*/*'] }, - (details, callback) => { - const headers = { - ...details.requestHeaders, - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - callback({ requestHeaders: headers }) - } - ) - - // 生成搜索URL - const searchUrl = this.activeEngine.searchUrl.replace('{query}', encodeURIComponent(query)) - console.log('测试搜索URL:', searchUrl) - - // 加载URL - await testWindow.loadURL(searchUrl) - - // 保持窗口在前台 - testWindow.focus() - - // 在窗口关闭时清理资源 - testWindow.on('closed', () => { - console.log('测试搜索窗口已关闭') - }) - - return true - } catch (error) { - console.error('测试搜索失败:', error) - return false - } - } - - /** - * 停止特定会话的搜索操作 - * @param conversationId 会话ID - */ - async stopSearch(conversationId: string): Promise { - console.log('停止搜索, conversationId:', conversationId) - - // 中止搜索操作 - const abortController = this.abortControllers.get(conversationId) - if (abortController) { - abortController.abort() - this.abortControllers.delete(conversationId) - } - - // 关闭搜索窗口 - await this.destroySearchWindow(conversationId) - } - - destroy() { - // 中止所有搜索操作 - for (const controller of this.abortControllers.values()) { - controller.abort() - } - this.abortControllers.clear() - - for (const [conversationId] of this.searchWindows) { - this.destroySearchWindow(conversationId) - } - this.originalWindowSizes.clear() - this.originalWindowPositions.clear() - this.wasFullScreen.clear() - } -} diff --git a/src/main/presenter/sessionPresenter/types.ts b/src/main/presenter/sessionPresenter/types.ts index 0873dcb1d..6d96bb7e2 100644 --- a/src/main/presenter/sessionPresenter/types.ts +++ b/src/main/presenter/sessionPresenter/types.ts @@ -11,7 +11,7 @@ export type SessionConfig = { title: string providerId: string modelId: string - chatMode: 'chat' | 'agent' | 'acp agent' + chatMode: 'agent' | 'acp agent' systemPrompt: string maxTokens?: number temperature?: number @@ -39,7 +39,7 @@ export type SessionBindings = { } export type WorkspaceContext = { - resolvedChatMode: 'chat' | 'agent' | 'acp agent' + resolvedChatMode: 'agent' | 'acp agent' agentWorkspacePath: string | null acpWorkdirMap?: Record } diff --git a/src/main/presenter/toolPresenter/index.ts b/src/main/presenter/toolPresenter/index.ts index 49f4695f8..bd887eb17 100644 --- a/src/main/presenter/toolPresenter/index.ts +++ b/src/main/presenter/toolPresenter/index.ts @@ -42,7 +42,7 @@ interface PreCheckedPermissionResult { export interface IToolPresenter { getAllToolDefinitions(context: { enabledMcpTools?: string[] - chatMode?: 'chat' | 'agent' | 'acp agent' + chatMode?: 'agent' | 'acp agent' supportsVision?: boolean agentWorkspacePath?: string | null conversationId?: string @@ -79,7 +79,7 @@ export class ToolPresenter implements IToolPresenter { */ async getAllToolDefinitions(context: { enabledMcpTools?: string[] - chatMode?: 'chat' | 'agent' | 'acp agent' + chatMode?: 'agent' | 'acp agent' supportsVision?: boolean agentWorkspacePath?: string | null conversationId?: string @@ -87,7 +87,7 @@ export class ToolPresenter implements IToolPresenter { const defs: MCPToolDefinition[] = [] this.mapper.clear() - const chatMode = context.chatMode || 'chat' + const chatMode = context.chatMode || 'agent' const supportsVision = context.supportsVision || false const agentWorkspacePath = context.agentWorkspacePath || null @@ -96,36 +96,34 @@ export class ToolPresenter implements IToolPresenter { defs.push(...mcpDefs) this.mapper.registerTools(mcpDefs, 'mcp') - // 2. Get Agent tools (only in agent or acp agent mode) - if (chatMode !== 'chat') { - // Initialize or update AgentToolManager if workspace path changed - if (!this.agentToolManager) { - this.agentToolManager = new AgentToolManager({ - agentWorkspacePath, - configPresenter: this.options.configPresenter, - commandPermissionHandler: this.options.commandPermissionHandler - }) - } + // 2. Get Agent tools (always load in agent or acp agent mode) + // Initialize or update AgentToolManager if workspace path changed + if (!this.agentToolManager) { + this.agentToolManager = new AgentToolManager({ + agentWorkspacePath, + configPresenter: this.options.configPresenter, + commandPermissionHandler: this.options.commandPermissionHandler + }) + } - try { - const agentDefs = await this.agentToolManager.getAllToolDefinitions({ - chatMode, - supportsVision, - agentWorkspacePath, - conversationId: context.conversationId - }) - const filteredAgentDefs = agentDefs.filter((tool) => { - if (!this.mapper.hasTool(tool.function.name)) return true - console.warn( - `[ToolPresenter] Tool name conflict for '${tool.function.name}', preferring MCP tool.` - ) - return false - }) - defs.push(...filteredAgentDefs) - this.mapper.registerTools(filteredAgentDefs, 'agent') - } catch (error) { - console.warn('[ToolPresenter] Failed to load Agent tool definitions', error) - } + try { + const agentDefs = await this.agentToolManager.getAllToolDefinitions({ + chatMode, + supportsVision, + agentWorkspacePath, + conversationId: context.conversationId + }) + const filteredAgentDefs = agentDefs.filter((tool) => { + if (!this.mapper.hasTool(tool.function.name)) return true + console.warn( + `[ToolPresenter] Tool name conflict for '${tool.function.name}', preferring MCP tool.` + ) + return false + }) + defs.push(...filteredAgentDefs) + this.mapper.registerTools(filteredAgentDefs, 'agent') + } catch (error) { + console.warn('[ToolPresenter] Failed to load Agent tool definitions', error) } return defs diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index 26de3fa75..4988aca60 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -71,8 +71,6 @@ import { useThemeStore } from '@/stores/theme' import { useProviderStore } from '@/stores/providerStore' import { useModelStore } from '@/stores/modelStore' import { useOllamaStore } from '@/stores/ollamaStore' -import { useSearchAssistantStore } from '@/stores/searchAssistantStore' -import { useSearchEngineStore } from '@/stores/searchEngineStore' import { useMcpStore } from '@/stores/mcp' import { useMcpInstallDeeplinkHandler } from '../src/lib/storeInitializer' import { useFontManager } from '../src/composables/useFontManager' @@ -93,8 +91,6 @@ const themeStore = useThemeStore() const providerStore = useProviderStore() const modelStore = useModelStore() const ollamaStore = useOllamaStore() -const searchAssistantStore = useSearchAssistantStore() -const searchEngineStore = useSearchEngineStore() const mcpStore = useMcpStore() const { setup: setupMcpDeeplink, cleanup: cleanupMcpDeeplink } = useMcpInstallDeeplinkHandler() // Register MCP deeplink listener immediately to avoid race with incoming IPC @@ -174,9 +170,6 @@ const initializeSettingsStores = async () => { await providerStore.initialize() await modelStore.initialize() await ollamaStore.initialize?.() - await searchAssistantStore.initOrUpdateSearchAssistantModel() - await searchEngineStore.refreshSearchEngines() - searchEngineStore.setupSearchEnginesListener() } catch (error) { console.error('Failed to initialize settings stores', error) } diff --git a/src/renderer/settings/components/CommonSettings.vue b/src/renderer/settings/components/CommonSettings.vue index 70e0e143d..9e64ffa6d 100644 --- a/src/renderer/settings/components/CommonSettings.vue +++ b/src/renderer/settings/components/CommonSettings.vue @@ -1,18 +1,8 @@