mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
修复
This commit is contained in:
parent
e2236b48a6
commit
567e54bd75
2
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
2
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
name: ❓ 提问 & 讨论 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['question']
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
name: 🤔 其他问题 (中文)
|
||||
description: 提交不属于错误报告或功能需求的问题
|
||||
title: '[其他]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出问题!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是"一个问题"、"求助"等。
|
||||
required: true
|
||||
- label: 我的问题不属于错误报告或功能需求类别。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 请详细描述您的问题或疑问
|
||||
placeholder: 我想了解有关...的更多信息
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供与您的问题相关的任何背景信息或上下文
|
||||
placeholder: 我尝试实现...时遇到了疑问
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempts
|
||||
attributes:
|
||||
label: 您已尝试的方法
|
||||
description: 请描述您为解决问题已经尝试过的方法(如果有)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
2
.github/ISSUE_TEMPLATE/2_question.yml
vendored
2
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: ❓ Discussion & Questions
|
||||
name: ❓ Questions & Discussion
|
||||
description: Seeking help, discussing issues, asking questions, etc...
|
||||
title: '[Discussion]: '
|
||||
labels: ['question']
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/3_others.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/3_others.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
name: 🤔 Other Questions (English)
|
||||
description: Submit questions that don't fit into bug reports or feature requests
|
||||
title: '[Other]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to ask a question!
|
||||
Before submitting this issue, please make sure you've reviewed the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Base](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Pre-submission Checklist
|
||||
description: |
|
||||
Please ensure you've completed all the steps below before submitting your issue
|
||||
options:
|
||||
- label: I understand that Issues are for feedback and problem-solving, not for complaints, and I will provide as much information as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have checked the pinned Issues and searched through existing [open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20) and didn't find similar questions.
|
||||
required: true
|
||||
- label: I have written a short and clear title that helps developers quickly understand the nature of my question, rather than vague titles like "A question" or "Help needed".
|
||||
required: true
|
||||
- label: My question doesn't fall under bug reports or feature requests categories.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Cherry Studio are you running?
|
||||
placeholder: e.g., v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question Description
|
||||
description: Please describe your question or inquiry in detail
|
||||
placeholder: I would like to know more about...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Relevant Context
|
||||
description: Please provide any background information or context related to your question
|
||||
placeholder: I encountered this question while trying to implement...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempts
|
||||
attributes:
|
||||
label: Attempted Solutions
|
||||
description: Please describe any methods you've already tried to resolve your question (if applicable)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other information that could help us better understand your question, including screenshots or relevant links
|
||||
277
.github/issue-checker.yml
vendored
Normal file
277
.github/issue-checker.yml
vendored
Normal file
@ -0,0 +1,277 @@
|
||||
default-mode:
|
||||
add:
|
||||
remove: [pull_request_target, issues]
|
||||
|
||||
labels:
|
||||
# <!-- [Ss]kip `LABEL` --> 跳过一个 label
|
||||
# <!-- [Rr]emove `LABEL` --> 去掉一个 label
|
||||
|
||||
# skips and removes
|
||||
- name: skip all
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
|
||||
- name: remove all
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
|
||||
|
||||
- name: skip kind/bug
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
- name: remove kind/bug
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
|
||||
- name: skip kind/enhancement
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
- name: remove kind/enhancement
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
|
||||
- name: skip kind/question
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
- name: remove kind/question
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
|
||||
- name: skip area/Connectivity
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
- name: remove area/Connectivity
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
|
||||
- name: skip area/UI/UX
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
- name: remove area/UI/UX
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
|
||||
- name: skip kind/documentation
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
- name: remove kind/documentation
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
|
||||
- name: skip client:linux
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
- name: remove client:linux
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
|
||||
- name: skip client:mac
|
||||
content:
|
||||
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:mac
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:mac
|
||||
|
||||
- name: skip client:win
|
||||
content:
|
||||
regexes: "(?:[Ww]in|[Ww]indows)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:win
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:win
|
||||
|
||||
- name: skip sig/Assistant
|
||||
content:
|
||||
regexes: "快捷助手|[Aa]ssistant"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Assistant
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Assistant
|
||||
|
||||
- name: skip sig/Data
|
||||
content:
|
||||
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Data
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Data
|
||||
|
||||
- name: skip sig/MCP
|
||||
content:
|
||||
regexes: "[Mm][Cc][Pp]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/MCP
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/MCP
|
||||
|
||||
- name: skip sig/RAG
|
||||
content:
|
||||
regexes: "知识库|[Rr][Aa][Gg]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/RAG
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/RAG
|
||||
|
||||
# Other labels
|
||||
- name: lgtm
|
||||
content: lgtm
|
||||
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip lgtm
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove lgtm
|
||||
|
||||
- name: License
|
||||
content: License
|
||||
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip License
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove License
|
||||
|
||||
# `Dev Team`
|
||||
- name: Dev Team
|
||||
mode:
|
||||
add: [pull_request_target, issues]
|
||||
author_association:
|
||||
- COLLABORATOR
|
||||
|
||||
# Area labels
|
||||
- name: area/Connectivity
|
||||
content: area/Connectivity
|
||||
regexes: "代理|[Pp]roxy"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/Connectivity
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove area/Connectivity
|
||||
|
||||
- name: area/UI/UX
|
||||
content: area/UI/UX
|
||||
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/UI/UX
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove area/UI/UX
|
||||
|
||||
# Kind labels
|
||||
- name: kind/documentation
|
||||
content: kind/documentation
|
||||
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip kind/documentation
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove kind/documentation
|
||||
|
||||
# Client labels
|
||||
- name: client:linux
|
||||
content: client:linux
|
||||
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:linux
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:linux
|
||||
|
||||
- name: client:mac
|
||||
content: client:mac
|
||||
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:mac
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:mac
|
||||
|
||||
- name: client:win
|
||||
content: client:win
|
||||
regexes: "(?:[Ww]in|[Ww]indows)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:win
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:win
|
||||
|
||||
# SIG labels
|
||||
- name: sig/Assistant
|
||||
content: sig/Assistant
|
||||
regexes: "快捷助手|[Aa]ssistant"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Assistant
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Assistant
|
||||
|
||||
- name: sig/Data
|
||||
content: sig/Data
|
||||
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Data
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Data
|
||||
|
||||
- name: sig/MCP
|
||||
content: sig/MCP
|
||||
regexes: "[Mm][Cc][Pp]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/MCP
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/MCP
|
||||
|
||||
- name: sig/RAG
|
||||
content: sig/RAG
|
||||
regexes: "知识库|[Rr][Aa][Gg]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/RAG
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/RAG
|
||||
|
||||
# Other labels
|
||||
- name: lgtm
|
||||
content: lgtm
|
||||
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip lgtm
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove lgtm
|
||||
|
||||
- name: License
|
||||
content: License
|
||||
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip License
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove License
|
||||
50
.github/workflows/issue-management.yml
vendored
Normal file
50
.github/workflows/issue-management.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: "Issue Management"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
env:
|
||||
daysBeforeStale: 30 # Number of days of inactivity before marking as stale
|
||||
daysBeforeClose: 10 # Number of days to wait after marking as stale before closing
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: none
|
||||
steps:
|
||||
- name: Close needs-more-info issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-info"
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: "inactive"
|
||||
close-issue-label: "closed:no-response"
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
It will be closed now due to lack of additional information.
|
||||
|
||||
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
|
||||
operations-per-run: 50
|
||||
exempt-issue-labels: "pending, Dev Team"
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
||||
- name: Close inactive issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: ${{ env.daysBeforeClose }}
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
exempt-issue-labels: "pending, Dev Team, kind/enhancement"
|
||||
days-before-pr-stale: -1 # Completely disable stalling for PRs
|
||||
days-before-pr-close: -1 # Completely disable closing for PRs
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -71,6 +71,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
@ -85,6 +86,7 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@ -94,6 +96,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Replace spaces in filenames
|
||||
run: node scripts/replace-spaces.js
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=lgZ0TYM3KRXU…l1cvzRgzjWM-1745729582-1.0.1.1-_Q9WkYircZ9.sJjbmssTZZ1o3mYuv4Ow2wIMeuvsSCo'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM'
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM'
|
||||
3
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=INZqycV.To1s…ncOZ60qZVcE-1745729582-1.0.1.1-UL04FD3iguMiqRuBPofPqfu9U_q7Odl1ms9vmH6gl9c, title: 请稍候…
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: 请稍候…
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=INZqycV.To1s…ncOZ60qZVcE-1745729582-1.0.1.1-UL04FD3iguMiqRuBPofPqfu9U_q7Odl1ms9vmH6gl9c'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:382 [Tab tab-1745729188027] New window request: undefined, frameName: 未指定, linkOpenMode: newTab
|
||||
useAnimatedTabs.ts:37 [openUrlInTab] Called with url: undefined, inNewTab: true, title: 加载中...
|
||||
useAnimatedTabs.ts:41 [openUrlInTab] Called with undefined or empty URL, ignoring.
|
||||
useWebviewEvents.ts:87 [Tab tab-1745729188027] Navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM, title: Just a moment...
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: Just a moment...
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y, title: Just a moment...
|
||||
2
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
2
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM, title: 请稍候…
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: 请稍候…
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM'
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=GCNUZYaTSrE.…pVJUudhRU9E-1745729582-1.0.1.1-cOI6OFfdInZDh1SbcsdmR0TUDz5vRX_9ObN3mdFwDIM'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:382 [Tab tab-1745729188027] New window request: undefined, frameName: 未指定, linkOpenMode: newTab
|
||||
useAnimatedTabs.ts:37 [openUrlInTab] Called with url: undefined, inNewTab: true, title: 加载中...
|
||||
useAnimatedTabs.ts:41 [openUrlInTab] Called with undefined or empty URL, ignoring.
|
||||
useWebviewEvents.ts:87 [Tab tab-1745729188027] Navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y, title: Just a moment...
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: Just a moment...
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM, title: Just a moment...
|
||||
useWebviewEvents.ts:382 [Tab tab-1745721367810] New window request: undefined, frameName: 未指定, linkOpenMode: newTab
|
||||
useAnimatedTabs.ts:37 [openUrlInTab] Called with url: undefined, inNewTab: true, title: 加载中...
|
||||
useAnimatedTabs.ts:41 [openUrlInTab] Called with undefined or empty URL, ignoring.
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM'
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y, title: 请稍候…
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: 请稍候…
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=5VL.95t2wGT8…dw3BSxspj4s-1745729583-1.0.1.1-RaroI4FRyeRaDPaLSSEIpan8Rc0zq4drA9ul2Vc1.2Y'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:382 [Tab tab-1745729188027] New window request: undefined, frameName: 未指定, linkOpenMode: newTab
|
||||
useAnimatedTabs.ts:37 [openUrlInTab] Called with url: undefined, inNewTab: true, title: 加载中...
|
||||
useAnimatedTabs.ts:41 [openUrlInTab] Called with undefined or empty URL, ignoring.
|
||||
useWebviewEvents.ts:87 [Tab tab-1745729188027] Navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM, title: Just a moment...
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: Unable to load preload script: J:\Cherry\cherry-studioTTS\out\preload\index.js
|
||||
useWebviewEvents.ts:313 [Tab tab-1745729188027] handleConsoleMessage called with message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:315 [Tab tab-1745729188027] Console message: TypeError: Cannot read properties of undefined (reading 'openExternal')
|
||||
useWebviewEvents.ts:140 [Tab tab-1745729188027] Title updated: Just a moment...
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
useWebviewEvents.ts:115 [Tab tab-1745729188027] In-page navigation: https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=HspNR8z7A2YV…r7S37QRYDJc-1745729583-1.0.1.1-A2E.8KxOq117yDjAtJ1ROmjVdhUD9cjPqi3v4pq8c7M, title: Just a moment...
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM'
|
||||
storage.ts:24 Loaded tabs from storage: 5 tabs, active tab: tab-1745729188027
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=7WYUVBYXysh_…E4CM2E6FmZQ-1745729583-1.0.1.1-.NKbkenFPZsGeZTPRzYu0iV0KxQaTqvXihK.HA.OGVM'
|
||||
node:electron/js2c/isolated_bundle:2 Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://accounts.x.ai/sign-in?redirect=grok-com&__cf_chl_rt_tk=HspNR8z7A2YV…r7S37QRYDJc-1745729583-1.0.1.1-A2E.8KxOq117yDjAtJ1ROmjVdhUD9cjPqi3v4pq8c7M'
|
||||
|
||||
@ -1,385 +0,0 @@
|
||||
# 内置浏览器功能修改指南
|
||||
|
||||
本文档包含对内置浏览器功能的修改说明,包括:
|
||||
1. 修复新标签页无法打开的问题
|
||||
2. 处理登录弹窗 (HTTP 认证)
|
||||
3. 增加切换链接打开方式 (新标签页/独立窗口) 的按钮和逻辑
|
||||
|
||||
---
|
||||
|
||||
## 1. 修复新标签页无法打开的问题
|
||||
|
||||
**问题原因:**
|
||||
初步诊断发现,新创建的 webview 元素没有正确加载 URL,并且 `useWebviewEvents.ts` 文件中存在语法错误,导致事件监听器和链接点击拦截脚本未能正确执行。
|
||||
|
||||
**修改步骤:**
|
||||
|
||||
1. **修改 `src/renderer/src/pages/Browser/components/WebviewItem.tsx`:**
|
||||
* 移除 `React.memo` 包裹,确保在父组件状态变化时 `WebviewItem` 总是重新渲染。
|
||||
* 在 `<webview>` 元素的 `ref` 回调函数中,确保在获取到 webview 引用后,显式调用 `webview.loadURL(tab.url)` 来加载 URL。同时移除 `<webview>` 元素上的 `src` 属性,避免重复加载。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/components/WebviewItem.tsx`
|
||||
|
||||
**修改内容 (使用 replace_in_file 格式):**
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
export default React.memo(WebviewItem)
|
||||
=======
|
||||
export default WebviewItem
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
<webview
|
||||
src={tab.url}
|
||||
ref={(el: any) => {
|
||||
if (el) {
|
||||
// 保存webview引用到对应的tabId下
|
||||
webviewRefs.current[tab.id] = el as WebviewTag
|
||||
|
||||
// 只有在尚未设置监听器时才设置
|
||||
if (!hasSetupListenersRef.current) {
|
||||
console.log(`[WebviewItem] Setting up listeners for tab: ${tab.id}`)
|
||||
=======
|
||||
<webview
|
||||
ref={(el: any) => {
|
||||
if (el) {
|
||||
// 保存webview引用到对应的tabId下
|
||||
webviewRefs.current[tab.id] = el as WebviewTag
|
||||
|
||||
// 只有在尚未设置监听器时才设置
|
||||
if (!hasSetupListenersRef.current) {
|
||||
console.log(`[WebviewItem] Setting up listeners for tab: ${tab.id}`)
|
||||
|
||||
// 显式加载URL
|
||||
el.loadURL(tab.url);
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
*(注意: 上述 diff 仅为示例,实际修改时请参考您当前文件的最新内容和格式进行调整。特别是移除 `src={tab.url}` 和添加 `el.loadURL(tab.url);`)*
|
||||
|
||||
2. **修改 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`:**
|
||||
* 修复 `handleDomReady` 函数中注入的链接点击拦截脚本末尾多余的 `})();` 语法错误。
|
||||
* 将本地变量 `ENABLE_BROWSER_EMULATION` 传递到注入的浏览器模拟脚本中,解决 ESLint 警告。
|
||||
* 在 `handleNewWindow` 和 `handleConsoleMessage` 中添加日志,用于调试(可选,调试完成后可移除)。
|
||||
* 在注入的链接点击拦截脚本中添加日志,用于调试(可选,调试完成后可移除)。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||||
|
||||
**修改内容 (使用 replace_in_file 格式):**
|
||||
```
|
||||
<<<<<<< SEARCH
|
||||
console.log('Link interceptor script injected successfully');
|
||||
})();
|
||||
console.log('Link interceptor script injected successfully');
|
||||
})();
|
||||
`)
|
||||
|
||||
// 注入浏览器模拟脚本 (在脚本内部检查 ENABLE_BROWSER_EMULATION)
|
||||
webview.executeJavaScript(`
|
||||
if (window.ENABLE_BROWSER_EMULATION) {
|
||||
try {
|
||||
// 覆盖navigator.userAgent
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: '${userAgent}',
|
||||
writable: false
|
||||
});
|
||||
=======
|
||||
console.log('Link interceptor script injected successfully');
|
||||
})();
|
||||
`)
|
||||
|
||||
// 注入浏览器模拟脚本
|
||||
webview.executeJavaScript(`
|
||||
(function() {
|
||||
// 检查是否启用浏览器模拟
|
||||
const ENABLE_BROWSER_EMULATION = ${ENABLE_BROWSER_EMULATION};
|
||||
|
||||
if (ENABLE_BROWSER_EMULATION) {
|
||||
try {
|
||||
// 覆盖navigator.userAgent
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: '${userAgent}',
|
||||
writable: false
|
||||
});
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
*(注意: 上述 diff 仅为示例,实际修改时请参考您当前文件的最新内容和格式进行调整。特别是移除多余的 `})();` 和传递 `ENABLE_BROWSER_EMULATION` 变量)*
|
||||
|
||||
**预期结果:**
|
||||
完成上述修改后,点击需要新标签页打开的链接应该能够成功创建一个新的标签页并加载对应的 URL。控制台应该能看到链接拦截脚本的日志。
|
||||
|
||||
---
|
||||
|
||||
## 2. 处理登录弹窗 (HTTP 认证)
|
||||
|
||||
**问题描述:**
|
||||
当访问需要 HTTP 认证的网站时,内置浏览器没有弹出登录对话框。
|
||||
|
||||
**实现方案:**
|
||||
Electron 的 `webview` 标签会触发 `show-login` 事件,当需要进行 HTTP 认证时。我们可以在 `useWebviewEvents.ts` 中监听这个事件,并通过 IPC 通道将认证请求发送到主进程。主进程可以显示一个原生的认证对话框,获取用户输入的用户名和密码,然后通过 IPC 将凭据返回给渲染进程,由渲染进程将凭据发送给 webview 进行认证。
|
||||
|
||||
**修改步骤:**
|
||||
|
||||
1. **在主进程中添加 IPC 处理:**
|
||||
* 在主进程 (`src/main/index.ts` 或相关的 IPC 处理文件) 中,添加一个 IPC 监听器,例如 `ipcMain.handle('show-login-dialog', ...)`。
|
||||
* 在这个处理函数中,使用 Electron 的 `dialog.showLoginDialog()` 方法显示认证对话框。
|
||||
* 将对话框的结果(用户名和密码)通过 IPC 返回给渲染进程。
|
||||
|
||||
**需要修改的文件:** `src/main/index.ts` 或 IPC 处理文件
|
||||
|
||||
**示例代码 (主进程):**
|
||||
```typescript
|
||||
// src/main/index.ts 或 src/main/ipc.ts
|
||||
import { ipcMain, dialog } from 'electron';
|
||||
|
||||
ipcMain.handle('show-login-dialog', async (event, args) => {
|
||||
const { url, realm } = args;
|
||||
const result = await dialog.showLoginDialog({
|
||||
title: 'Authentication Required',
|
||||
text: `Enter credentials for ${url}`,
|
||||
message: `Realm: ${realm}`,
|
||||
});
|
||||
return result; // { username, password, response }
|
||||
});
|
||||
```
|
||||
|
||||
2. **在渲染进程中添加 IPC 调用和 `show-login` 事件处理:**
|
||||
* 在 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts` 中,添加 `show-login` 事件监听器。
|
||||
* 在 `handleShowLogin` 函数中,通过 `window.api.invoke('show-login-dialog', { url: e.url, realm: e.realm })` 调用主进程的认证对话框。
|
||||
* 获取对话框结果后,使用 `e.login(username, password)` 将凭据发送给 webview。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||||
|
||||
**示例代码 (渲染进程 - `useWebviewEvents.ts`):**
|
||||
```typescript
|
||||
// 在 setupWebviewListeners 函数内部添加
|
||||
const handleShowLogin = async (e: any) => {
|
||||
console.log(`[Tab ${tabId}] Show login dialog for url: ${e.url}, realm: ${e.realm}`);
|
||||
e.preventDefault(); // 阻止默认行为
|
||||
|
||||
try {
|
||||
// 调用主进程显示认证对话框
|
||||
const result = await window.api.invoke('show-login-dialog', { url: e.url, realm: e.realm });
|
||||
|
||||
if (result && result.response === 0) { // 0 表示用户点击了登录
|
||||
// 将凭据发送给webview
|
||||
e.login(result.username, result.password);
|
||||
} else {
|
||||
// 用户取消或关闭对话框
|
||||
e.cancel();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to show login dialog:', error);
|
||||
e.cancel(); // 发生错误时取消认证
|
||||
}
|
||||
};
|
||||
|
||||
// 在添加事件监听器的部分添加
|
||||
webview.addEventListener('show-login', handleShowLogin);
|
||||
|
||||
// 在清理函数中添加移除监听器
|
||||
return () => {
|
||||
// ... 其他移除监听器 ...
|
||||
webview.removeEventListener('show-login', handleShowLogin);
|
||||
};
|
||||
```
|
||||
*(注意: 上述示例代码假设您已经设置了 Electron 的 Context Bridge,并且在预加载脚本中将 `ipcRenderer.invoke` 暴露给了 `window.api`。如果您的 IPC 设置不同,请根据实际情况调整。)*
|
||||
|
||||
**预期结果:**
|
||||
当访问需要 HTTP 认证的网站时,应该会弹出一个原生的登录对话框,用户输入凭据后可以进行认证。
|
||||
|
||||
---
|
||||
|
||||
## 3. 增加切换链接打开方式 (新标签页/独立窗口) 的按钮和逻辑
|
||||
|
||||
**问题描述:**
|
||||
目前点击链接默认在新标签页打开,用户希望能够切换为在独立窗口中打开。
|
||||
|
||||
**实现方案:**
|
||||
1. 在浏览器界面的工具栏中添加一个按钮,用于切换链接打开方式的状态。
|
||||
2. 在状态管理中维护一个状态,记录当前的链接打开方式(例如 'newTab' 或 'newWindow')。
|
||||
3. 修改链接点击拦截脚本和 `handleNewWindow` 函数,根据当前状态决定是调用 `openUrlInTab` 还是通过 IPC 调用主进程打开新窗口。
|
||||
4. 在主进程中添加一个 IPC 处理函数,用于创建新的浏览器窗口并加载指定的 URL。
|
||||
|
||||
**修改步骤:**
|
||||
|
||||
1. **在状态管理中添加链接打开方式状态:**
|
||||
* 在 `src/renderer/src/pages/Browser/hooks/useAnimatedTabs.ts` 或创建一个新的 Context 中,添加一个状态来存储当前的链接打开方式,例如 `linkOpenMode`,默认值为 `'newTab'`。
|
||||
* 添加一个函数来切换这个状态,例如 `toggleLinkOpenMode`。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useAnimatedTabs.ts` (或新文件)
|
||||
|
||||
**示例代码 (useAnimatedTabs.ts):**
|
||||
```typescript
|
||||
// 在 useAnimatedTabs 钩子内部添加状态和切换函数
|
||||
const [linkOpenMode, setLinkOpenMode] = useState<'newTab' | 'newWindow'>('newTab');
|
||||
|
||||
const toggleLinkOpenMode = useCallback(() => {
|
||||
setLinkOpenMode(prevMode => prevMode === 'newTab' ? 'newWindow' : 'newTab');
|
||||
}, []);
|
||||
|
||||
// 在返回的对象中暴露 linkOpenMode 和 toggleLinkOpenMode
|
||||
return {
|
||||
// ... 其他状态和函数 ...
|
||||
linkOpenMode,
|
||||
toggleLinkOpenMode,
|
||||
};
|
||||
```
|
||||
|
||||
2. **在 UI 中添加切换按钮:**
|
||||
* 在浏览器工具栏组件 (`src/renderer/src/pages/Browser/components/NavBar.tsx`) 中,添加一个按钮。
|
||||
* 按钮的文本或图标可以根据 `linkOpenMode` 状态变化。
|
||||
* 按钮的点击事件调用 `toggleLinkOpenMode` 函数。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/components/NavBar.tsx`
|
||||
|
||||
**示例代码 (NavBar.tsx):**
|
||||
```typescript
|
||||
// 假设 NavBar 组件接收 linkOpenMode 和 toggleLinkOpenMode 作为 props
|
||||
interface NavBarProps {
|
||||
// ... 其他 props ...
|
||||
linkOpenMode: 'newTab' | 'newWindow';
|
||||
toggleLinkOpenMode: () => void;
|
||||
}
|
||||
|
||||
const NavBar: React.FC<NavBarProps> = ({ /* ... */ linkOpenMode, toggleLinkOpenMode }) => {
|
||||
return (
|
||||
<NavBarContainer>
|
||||
{/* ... 其他工具栏元素 ... */}
|
||||
<button onClick={toggleLinkOpenMode}>
|
||||
{linkOpenMode === 'newTab' ? '新标签页模式' : '独立窗口模式'}
|
||||
</button>
|
||||
{/* ... 其他工具栏元素 ... */}
|
||||
</NavBarContainer>
|
||||
);
|
||||
};
|
||||
```
|
||||
*(注意: 您需要将 `linkOpenMode` 和 `toggleLinkOpenMode` 从 `useAnimatedTabs` (或新 Context) 传递到 `NavBar` 组件。)*
|
||||
|
||||
3. **修改链接点击处理逻辑:**
|
||||
* 在 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts` 的 `handleConsoleMessage` 函数中,当处理 `LINK_CLICKED:` 消息时,根据当前的 `linkOpenMode` 状态决定是调用 `openUrlInTab` 还是触发 IPC 调用打开新窗口。
|
||||
* 在 `handleNewWindow` 函数中,也需要根据 `linkOpenMode` 状态决定行为。
|
||||
|
||||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||||
|
||||
**示例代码 (useWebviewEvents.ts - handleConsoleMessage):**
|
||||
```typescript
|
||||
// 在 setupWebviewListeners 函数签名中添加 linkOpenMode 参数
|
||||
const setupWebviewListeners = (
|
||||
// ... 其他参数 ...
|
||||
linkOpenMode: 'newTab' | 'newWindow', // 添加 linkOpenMode 参数
|
||||
// ... 其他参数 ...
|
||||
) => {
|
||||
// ...
|
||||
|
||||
const handleConsoleMessage = (event: any) => {
|
||||
// ... (现有日志和 LINK_CLICKED 处理逻辑) ...
|
||||
|
||||
if (event.message && event.message.startsWith('LINK_CLICKED:')) {
|
||||
try {
|
||||
const dataStr = event.message.replace('LINK_CLICKED:', '')
|
||||
const data = JSON.parse(dataStr)
|
||||
|
||||
console.log(`[Tab ${tabId}] Link clicked:`, data)
|
||||
|
||||
// 根据 linkOpenMode 决定打开方式
|
||||
if (linkOpenMode === 'newTab' && data.url && data.inNewTab) {
|
||||
console.log(`[Tab ${tabId}] Opening link in new tab:`, data.url)
|
||||
openUrlInTab(data.url, true, data.title || data.url)
|
||||
} else if (linkOpenMode === 'newWindow' && data.url) {
|
||||
console.log(`[Tab ${tabId}] Opening link in new window:`, data.url)
|
||||
// 调用主进程打开新窗口 (需要实现 IPC)
|
||||
window.api.invoke('open-new-browser-window', { url: data.url, title: data.title || data.url });
|
||||
} else if (data.url && !data.inNewTab) {
|
||||
// 在当前标签页打开 (如果不是新标签页模式且链接没有 target="_blank")
|
||||
// 这个逻辑已经在注入的脚本中处理了 window.location.href = target.href;
|
||||
// 这里可以根据需要添加额外的处理或日志
|
||||
console.log(`[Tab ${tabId}] Link clicked, navigating in current tab:`, data.url);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to parse link data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ... (保留对旧消息格式的支持) ...
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**示例代码 (useWebviewEvents.ts - handleNewWindow):**
|
||||
```typescript
|
||||
// 在 setupWebviewListeners 函数签名中添加 linkOpenMode 参数 (如果尚未添加)
|
||||
const setupWebviewListeners = (
|
||||
// ... 其他参数 ...
|
||||
linkOpenMode: 'newTab' | 'newWindow', // 确保 linkOpenMode 参数存在
|
||||
// ... 其他参数 ...
|
||||
) => {
|
||||
// ...
|
||||
|
||||
// 处理新窗口打开请求
|
||||
const handleNewWindow = (e: any) => {
|
||||
console.log(`[Tab ${tabId}] handleNewWindow called for url: ${e.url}`)
|
||||
e.preventDefault() // 阻止默认行为
|
||||
|
||||
console.log(`[Tab ${tabId}] New window request: ${e.url}, frameName: ${e.frameName || '未指定'}`)
|
||||
|
||||
// 根据 linkOpenMode 决定打开方式
|
||||
if (linkOpenMode === 'newTab') {
|
||||
// 始终在新标签页中打开
|
||||
openUrlInTab(e.url, true, e.frameName || '加载中...')
|
||||
} else if (linkOpenMode === 'newWindow') {
|
||||
// 调用主进程打开新窗口 (需要实现 IPC)
|
||||
window.api.invoke('open-new-browser-window', { url: e.url, title: e.frameName || e.url });
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
*(注意: 您需要将 `linkOpenMode` 从使用 `useAnimatedTabs` 的组件传递到 `setupWebviewListeners` 函数中。)*
|
||||
|
||||
4. **在主进程中添加打开新窗口的 IPC 处理:**
|
||||
* 在主进程 (`src/main/index.ts` 或相关的 IPC 处理文件) 中,添加一个 IPC 监听器,例如 `ipcMain.handle('open-new-browser-window', ...)`。
|
||||
* 在这个处理函数中,创建一个新的 Electron 浏览器窗口 (`new BrowserWindow(...)`) 并加载指定的 URL。
|
||||
|
||||
**需要修改的文件:** `src/main/index.ts` 或 IPC 处理文件
|
||||
|
||||
**示例代码 (主进程):**
|
||||
```typescript
|
||||
// src/main/index.ts 或 src/main/ipc.ts
|
||||
import { ipcMain, BrowserWindow } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
ipcMain.handle('open-new-browser-window', async (event, args) => {
|
||||
const { url, title } = args;
|
||||
|
||||
// 创建新的浏览器窗口
|
||||
const newWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
title: title || 'New Window',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'), // 根据您的项目结构调整预加载脚本路径
|
||||
sandbox: false, // 根据您的安全需求调整
|
||||
nodeIntegration: false, // 根据您的安全需求调整
|
||||
contextIsolation: true, // 根据您的安全需求调整
|
||||
},
|
||||
});
|
||||
|
||||
// 加载URL
|
||||
newWindow.loadURL(url);
|
||||
|
||||
// 可选: 打开开发者工具
|
||||
// newWindow.webContents.openDevTools();
|
||||
});
|
||||
```
|
||||
*(注意: 您需要根据您的项目结构调整 `preload` 路径和 `webPreferences` 设置。)*
|
||||
|
||||
**预期结果:**
|
||||
浏览器工具栏中会出现一个切换按钮,点击可以切换链接打开方式。根据当前模式,点击链接会在新标签页或独立窗口中打开。
|
||||
|
||||
---
|
||||
|
||||
希望这份修改指南对您有帮助!如果您在修改过程中遇到任何问题,或者需要进一步的帮助,请随时告诉我。
|
||||
@ -1,4 +1,3 @@
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
@ -65,9 +64,6 @@ export default defineConfig({
|
||||
]
|
||||
]
|
||||
}),
|
||||
sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN
|
||||
}),
|
||||
...visualizerPlugin('renderer')
|
||||
],
|
||||
resolve: {
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
yarn add electron@32.3.3 --dev
|
||||
239
mcp_inline_rendering_instructions.txt
Normal file
239
mcp_inline_rendering_instructions.txt
Normal file
@ -0,0 +1,239 @@
|
||||
# MCP 工具内联渲染修改说明
|
||||
|
||||
本文件提供了将 MCP 工具调用从消息顶部固定位置改为内联渲染的修改步骤。您需要修改以下两个文件:
|
||||
|
||||
1. `src/renderer/src/pages/home/Messages/MessageContent.tsx`
|
||||
2. `src/renderer/src/pages/home/Markdown/Markdown.tsx`
|
||||
|
||||
**重要提示:** 在进行修改之前,请确保您已经备份了这两个文件。
|
||||
|
||||
## 步骤 1: 修改 `src/renderer/src/pages/home/Messages/MessageContent.tsx`
|
||||
|
||||
这个文件主要负责处理消息内容的整体布局和数据准备。我们需要在这里:
|
||||
|
||||
* 移除 MCP 工具块的顶部容器。
|
||||
* 将 MCP 工具的相关状态(如 `activeKeys`, `copiedMap`, `editingToolId`, `editedParams`)和处理函数(如 `copyContent`, `handleRerun`, `handleEdit`, `handleCancelEdit`, `handleSaveEdit`, `handleParamsChange`)移动或传递给 `Markdown` 组件。
|
||||
* 修改 `processedContent` 的生成逻辑,将工具 XML 标记替换为自定义的占位符,以便 `Markdown` 组件能够识别并在正确的位置渲染工具块。
|
||||
|
||||
以下是需要修改的部分:
|
||||
|
||||
1. **移除 `MessageTools` 导入和相关的 JSX:**
|
||||
找到并删除以下导入:
|
||||
```typescript
|
||||
import { default as MessageTools } from './MessageTools' // Change to named import (using default alias)
|
||||
```
|
||||
找到并删除以下 JSX 结构:
|
||||
```jsx
|
||||
<div className="message-content-tools">
|
||||
{/* Only display thought info at the top */}
|
||||
<MessageThought message={message} />
|
||||
{/* Render MessageTools to display tool blocks based on metadata */}
|
||||
<MessageTools message={message} />
|
||||
</div>
|
||||
```
|
||||
保留 `MessageThought` 组件,它应该独立于工具块渲染。
|
||||
|
||||
2. **将工具相关的状态和处理函数移动或传递:**
|
||||
将 `MessageTools.tsx` 中的以下状态和处理函数定义复制到 `MessageContent.tsx` 中:
|
||||
* `activeKeys` state 和 `setActiveKeys`
|
||||
* `copiedMap` state 和 `setCopiedMap`
|
||||
* `editingToolId` state 和 `setEditingToolId`
|
||||
* `editedParams` state 和 `setEditedParams`
|
||||
* `localToolResponses` state 和 `setLocalToolResponses`
|
||||
* `useEffect` 钩子,用于同步 `localToolResponses`
|
||||
* `copyContent` useCallback
|
||||
* `handleRerun` useCallback
|
||||
* `handleEdit` useCallback
|
||||
* `handleCancelEdit` useCallback
|
||||
* `handleSaveEdit` useCallback
|
||||
* `handleParamsChange` useCallback
|
||||
* 监听 `onToolRerunUpdate` 的 `useEffect` 钩子
|
||||
|
||||
这些状态和函数需要作为 props 传递给 `Markdown` 组件。
|
||||
|
||||
3. **修改 `processedContent` 逻辑:**
|
||||
在 `processedContent` 的 `useMemo` 钩子中,在处理引用标记之后,添加逻辑来查找 `<tool_use>...</tool_use>` 标记,并将其替换为自定义的占位符,例如 `<tool-block id="[tool_call_id]"></tool-block>`。
|
||||
|
||||
首先,在文件顶部导入 `MCPToolResponse` 类型:
|
||||
```typescript
|
||||
import { Message, Model, MCPToolResponse } from '@renderer/types'
|
||||
```
|
||||
|
||||
然后,在 `processedContent` 的 `useMemo` 内部,在处理完引用标记后,添加以下代码:
|
||||
|
||||
```typescript
|
||||
// ... (之前的引用标记处理逻辑)
|
||||
|
||||
// 处理 MCP 工具调用标记
|
||||
const toolResponses = message.metadata?.mcpTools || [];
|
||||
if (toolResponses.length > 0) {
|
||||
toolResponses.forEach(toolCall => {
|
||||
const toolTagRegex = new RegExp(`<tool_use>(?:[^<]*?${toolCall.id}[\\s\\S]*?)<\\/tool_use>`, 'gi');
|
||||
content = content.replace(toolTagRegex, `<tool-block id="${toolCall.id}"></tool-block>`);
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
```
|
||||
请注意,这里的正则表达式 `toolTagRegex` 是一个示例,您可能需要根据实际的工具 XML 格式进行调整,以确保能够准确匹配包含特定 `toolCall.id` 的 `<tool_use>` 标记。
|
||||
|
||||
4. **更新 `Markdown` 组件的 props:**
|
||||
在渲染 `Markdown` 组件的地方,将新移动过来的状态和处理函数作为 props 传递下去:
|
||||
|
||||
```jsx
|
||||
<Markdown
|
||||
message={{ ...message, content: processedContent }} // 传递包含占位符的内容
|
||||
toolResponses={message.metadata?.mcpTools || []} // 传递工具响应数据
|
||||
activeToolKeys={activeKeys} // 传递 activeKeys 状态
|
||||
copiedToolMap={copiedMap} // 传递 copiedMap 状态
|
||||
editingToolId={editingToolId} // 传递 editingToolId 状态
|
||||
editedToolParamsString={editedParams} // 传递 editedParams 状态
|
||||
onToolToggle={setActiveKeys} // 传递 setActiveKeys 函数
|
||||
onToolCopy={copyContent} // 传递 copyContent 函数
|
||||
onToolRerun={handleRerun} // 传递 handleRerun 函数
|
||||
onToolEdit={handleEdit} // 传递 handleEdit 函数
|
||||
onToolSave={handleSaveEdit} // 传递 handleSaveEdit 函数
|
||||
onToolCancel={handleCancelEdit} // 传递 handleCancelEdit 函数
|
||||
onToolParamsChange={handleParamsChange} // 传递 handleParamsChange 函数
|
||||
/>
|
||||
```
|
||||
请注意,我在这里使用了新的 prop 名称(例如 `toolResponses`, `activeToolKeys` 等),以避免与 `Markdown` 组件内部可能已有的 props 冲突。您需要在 `Markdown.tsx` 中接收这些新的 props。
|
||||
|
||||
## 步骤 2: 修改 `src/renderer/src/pages/home/Markdown/Markdown.tsx`
|
||||
|
||||
这个文件负责将 Markdown 内容渲染为 HTML。我们需要在这里:
|
||||
|
||||
* 接收从 `MessageContent` 传递过来的工具相关 props。
|
||||
* 添加一个自定义的渲染器,用于处理我们在步骤 1 中创建的 `<tool-block>` 占位符。
|
||||
* 在自定义渲染器中,根据占位符中的 `id` 查找对应的工具响应数据,并渲染 `SingleToolCallBlock` 组件。
|
||||
|
||||
以下是需要修改的部分:
|
||||
|
||||
1. **更新 Props 接口:**
|
||||
修改 `Props` 接口,添加从 `MessageContent` 传递过来的新 props:
|
||||
|
||||
```typescript
|
||||
import { MCPToolResponse, Message } from '@renderer/types' // 导入 MCPToolResponse
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
toolResponses: MCPToolResponse[] // 添加工具响应数据 prop
|
||||
activeToolKeys: string[] // 添加 activeKeys prop
|
||||
copiedToolMap: Record<string, boolean> // 添加 copiedMap prop
|
||||
editingToolId: string | null // 添加 editingToolId prop
|
||||
editedToolParamsString: string // 添加 editedParams prop
|
||||
onToolToggle: React.Dispatch<React.SetStateAction<string[]>> // 添加 onToolToggle prop
|
||||
onToolCopy: (content: string, toolId: string) => void // 添加 onToolCopy prop
|
||||
onToolRerun: (toolCall: MCPToolResponse, currentParamsString: string) => void // 添加 onToolRerun prop
|
||||
onToolEdit: (toolCall: MCPToolResponse) => void // 添加 onToolEdit prop
|
||||
onToolSave: (toolCall: MCPToolResponse) => void // 添加 onToolSave prop
|
||||
onToolCancel: () => void // 添加 onToolCancel prop
|
||||
onToolParamsChange: (newParams: string) => void // 添加 onToolParamsChange prop
|
||||
}
|
||||
```
|
||||
|
||||
2. **导入 `SingleToolCallBlock` 组件:**
|
||||
在文件顶部导入 `SingleToolCallBlock` 组件:
|
||||
```typescript
|
||||
import SingleToolCallBlock from '../Messages/SingleToolCallBlock' // 导入 SingleToolCallBlock
|
||||
```
|
||||
|
||||
3. **添加自定义渲染器:**
|
||||
在 `components` 的 `useMemo` 钩子中,添加一个针对 `tool-block` 标签的渲染器:
|
||||
|
||||
```typescript
|
||||
const components = useMemo(() => {
|
||||
const baseComponents = {
|
||||
// ... (其他现有渲染器)
|
||||
|
||||
// 添加 tool-block 渲染器
|
||||
'tool-block': (props: any) => {
|
||||
const toolCallId = props.id; // 获取占位符中的 id
|
||||
const toolResponse = toolResponses.find(tr => tr.id === toolCallId); // 查找对应的工具响应数据
|
||||
|
||||
if (!toolResponse) {
|
||||
return null; // 如果找不到对应的工具响应,则不渲染
|
||||
}
|
||||
|
||||
// 渲染 SingleToolCallBlock 组件,并传递必要的 props
|
||||
return (
|
||||
<SingleToolCallBlock
|
||||
toolResponse={toolResponse}
|
||||
isActive={activeToolKeys.includes(toolCallId)}
|
||||
isCopied={copiedToolMap[toolCallId] || false}
|
||||
isEditing={editingToolId === toolCallId}
|
||||
editedParamsString={editedToolParamsString}
|
||||
fontFamily={messageFont === 'serif' ? 'serif' : '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'} // 传递字体样式
|
||||
t={t} // 传递翻译函数
|
||||
onToggle={() => onToolToggle(prev => prev.includes(toolCallId) ? prev.filter(k => k !== toolCallId) : [...prev, toolCallId])} // 传递 onToolToggle 函数
|
||||
onCopy={onToolCopy} // 传递 onToolCopy 函数
|
||||
onRerun={onToolRerun} // 传递 onToolRerun 函数
|
||||
onEdit={onToolEdit} // 传递 onToolEdit 函数
|
||||
onSave={onToolSave} // 传递 onToolSave 函数
|
||||
onCancel={onToolCancel} // 传递 onToolCancel 函数
|
||||
onParamsChange={onToolParamsChange} // 传递 onToolParamsChange 函数
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as Partial<Components>;
|
||||
return baseComponents;
|
||||
}, [
|
||||
// ... (其他现有依赖)
|
||||
toolResponses, // 添加 toolResponses 依赖
|
||||
activeToolKeys, // 添加 activeToolKeys 依赖
|
||||
copiedToolMap, // 添加 copiedToolMap 依赖
|
||||
editingToolId, // 添加 editingToolId 依赖
|
||||
editedToolParamsString, // 添加 editedToolParamsString 依赖
|
||||
onToolToggle, // 添加 onToolToggle 依赖
|
||||
onToolCopy, // 添加 onToolCopy 依赖
|
||||
onToolRerun, // 添加 onToolRerun 依赖
|
||||
onToolEdit, // 添加 onToolEdit 依赖
|
||||
onToolSave, // 添加 onToolSave 依赖
|
||||
onToolCancel, // 添加 onToolCancel 依赖
|
||||
onToolParamsChange, // 添加 onToolParamsChange 依赖
|
||||
t, // 添加 t 依赖
|
||||
messageFont // 添加 messageFont 依赖
|
||||
]);
|
||||
```
|
||||
请确保在 `useMemo` 的依赖数组中包含了所有使用到的外部变量和函数。
|
||||
|
||||
## 步骤 3: 修改 `src/renderer/src/pages/home/Messages/MessageTools.tsx`
|
||||
|
||||
这个文件将不再负责渲染工具块,它的主要作用将变为处理工具相关的状态和逻辑。
|
||||
|
||||
1. **移除渲染工具块的 JSX:**
|
||||
找到并删除 `MessageTools` 组件中的以下 JSX 结构:
|
||||
|
||||
```jsx
|
||||
return (
|
||||
<>
|
||||
<ToolsContainer className="message-tools-container">
|
||||
{collapseItems.map((item) => (
|
||||
<CustomCollapse
|
||||
key={item.key}
|
||||
id={item.key as string}
|
||||
title={item.label}
|
||||
isActive={activeKeys.includes(item.key as string)}
|
||||
onToggle={() => {
|
||||
setActiveKeys((prev) =>
|
||||
prev.includes(item.key as string) ? prev.filter((k) => k !== item.key) : [...prev, item.key as string]
|
||||
)
|
||||
}}>
|
||||
{item.children}
|
||||
</CustomCollapse>
|
||||
))}
|
||||
</ToolsContainer>
|
||||
</>
|
||||
)
|
||||
```
|
||||
以及相关的 `ToolsContainer` 样式定义。
|
||||
|
||||
2. **保留工具相关的状态和逻辑:**
|
||||
保留 `MessageTools` 组件中所有与工具相关的状态(`activeKeys`, `copiedMap`, `editingToolId`, `editedParams`, `localToolResponses`)和处理函数(`copyContent`, `handleRerun`, `handleEdit`, `handleCancelEdit`, `handleSaveEdit`, `handleParamsChange`),以及监听 `onToolRerunUpdate` 的 `useEffect` 钩子。这些状态和逻辑将需要被提升到 `MessageContent.tsx` 中。
|
||||
|
||||
3. **修改 `MessageTools` 组件的用途:**
|
||||
`MessageTools` 组件可能不再需要作为一个 React 组件存在,或者可以修改其用途,例如只包含工具相关的逻辑和状态管理,并通过钩子或上下文提供给其他组件使用。考虑到您希望自己修改,您可以选择将这些逻辑完全移动到 `MessageContent.tsx` 中,然后删除 `MessageTools.tsx` 文件。
|
||||
|
||||
完成以上修改后,重新运行您的应用程序,MCP 工具块应该会以内联的方式显示在消息内容中对应的位置。
|
||||
|
||||
请仔细按照步骤进行修改,并根据您的实际代码结构进行调整。如果在修改过程中遇到任何问题,或者需要进一步的帮助,请随时告诉我。
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.2.6-bate",
|
||||
"version": "2.0.1-vludi",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@ -101,8 +101,6 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@sentry/electron": "^6.5.0",
|
||||
"@sentry/react": "^9.14.0",
|
||||
"@shikijs/markdown-it": "^3.2.2",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
@ -135,7 +133,7 @@
|
||||
"monaco-editor": "^0.52.2",
|
||||
"node-edge-tts": "^1.2.8",
|
||||
"officeparser": "^4.1.1",
|
||||
"os-proxy-config": "^1.1.1",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.1.91",
|
||||
@ -173,7 +171,6 @@
|
||||
"@modelcontextprotocol/sdk": "^1.10.1",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@sentry/vite-plugin": "^3.3.1",
|
||||
"@swc/plugin-styled-components": "^7.1.3",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
|
||||
@ -126,7 +126,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': ["default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"]
|
||||
'Content-Security-Policy': ["default-src * 'unsafe-inline' 'unsafe-eval' data: blob: sentry-ipc:;"]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -110,6 +110,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
configManager.setTrayOnClose(isActive)
|
||||
})
|
||||
|
||||
// 设置数据收集
|
||||
ipcMain.handle('app:setEnableDataCollection', (_, isActive: boolean) => {
|
||||
configManager.setEnableDataCollection(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
|
||||
@ -419,6 +424,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
|
||||
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
||||
ipcMain.handle(IpcChannel.Mcp_ResetToolsList, mcpService.resetToolsList)
|
||||
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
||||
|
||||
2920
src/main/mcpServers/calculator.ts
Normal file
2920
src/main/mcpServers/calculator.ts
Normal file
File diff suppressed because it is too large
Load Diff
263
src/main/mcpServers/dify-knowledge.ts
Normal file
263
src/main/mcpServers/dify-knowledge.ts
Normal file
@ -0,0 +1,263 @@
|
||||
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { z } from 'zod'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
interface DifyKnowledgeServerConfig {
|
||||
difyKey: string
|
||||
apiHost: string
|
||||
}
|
||||
|
||||
interface DifyListKnowledgeResponse {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface DifySearchKnowledgeResponse {
|
||||
query: {
|
||||
content: string
|
||||
}
|
||||
records: Array<{
|
||||
segment: {
|
||||
id: string
|
||||
position: number
|
||||
document_id: string
|
||||
content: string
|
||||
keywords: string[]
|
||||
document?: {
|
||||
id: string
|
||||
data_source_type: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
|
||||
const SearchKnowledgeArgsSchema = z.object({
|
||||
id: z.string().describe('Knowledge ID'),
|
||||
query: z.string().describe('Query string'),
|
||||
topK: z.number().optional().describe('Number of top results to return')
|
||||
})
|
||||
|
||||
type McpResponse = {
|
||||
content: Array<{ type: 'text'; text: string }>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
class DifyKnowledgeServer {
|
||||
public server: Server
|
||||
private config: DifyKnowledgeServerConfig
|
||||
|
||||
constructor(difyKey: string, args: string[]) {
|
||||
console.log('DifyKnowledgeServer args', args)
|
||||
if (args.length === 0) {
|
||||
throw new Error('DifyKnowledgeServer requires at least one argument')
|
||||
}
|
||||
this.config = {
|
||||
difyKey: difyKey,
|
||||
apiHost: args[0]
|
||||
}
|
||||
this.server = new Server(
|
||||
{
|
||||
name: '@cherry/dify-knowledge-server',
|
||||
version: '0.1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'list_knowledges',
|
||||
description: 'List all knowledges',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_knowledge',
|
||||
description: 'Search knowledge by id and query',
|
||||
inputSchema: zodToJsonSchema(SearchKnowledgeArgsSchema) as ToolInput
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params
|
||||
switch (name) {
|
||||
case 'list_knowledges': {
|
||||
return await this.performListKnowledges(this.config.difyKey, this.config.apiHost)
|
||||
}
|
||||
case 'search_knowledge': {
|
||||
const parsed = SearchKnowledgeArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorDetails = JSON.stringify(parsed.error.format(), null, 2)
|
||||
throw new Error(`无效的参数:\n${errorDetails}`)
|
||||
}
|
||||
|
||||
console.log('DifyKnowledgeServer search_knowledge parsed', parsed.data)
|
||||
return await this.performSearchKnowledge(
|
||||
parsed.data.id,
|
||||
parsed.data.query,
|
||||
parsed.data.topK || 6,
|
||||
this.config.difyKey,
|
||||
this.config.apiHost
|
||||
)
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const apiResponse = await response.json()
|
||||
|
||||
const knowledges: DifyListKnowledgeResponse[] =
|
||||
apiResponse?.data?.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || ''
|
||||
})) || []
|
||||
|
||||
const listText =
|
||||
knowledges.length > 0
|
||||
? knowledges.map((k) => `- **${k.name}** (ID: ${k.id})\n ${k.description || 'No Description'}`).join('\n')
|
||||
: '- No knowledges found.'
|
||||
|
||||
const formattedText = `### 可用知识库:\n\n${listText}`
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库列表时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
// 返回包含错误信息的 MCP 响应
|
||||
return {
|
||||
content: [{ type: 'text', text: `Accessing Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async performSearchKnowledge(
|
||||
id: string,
|
||||
query: string,
|
||||
topK: number,
|
||||
difyKey: string,
|
||||
apiHost: string
|
||||
): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
retrieval_model: {
|
||||
top_k: topK,
|
||||
// will be error if not set
|
||||
reranking_enable: null,
|
||||
score_threshold_enabled: null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const searchResponse: DifySearchKnowledgeResponse = await response.json()
|
||||
|
||||
if (!searchResponse || !Array.isArray(searchResponse.records)) {
|
||||
throw new Error(`从 Dify API 收到的响应格式无效: ${JSON.stringify(searchResponse)}`)
|
||||
}
|
||||
|
||||
const header = `### Query: ${query}\n\n`
|
||||
let body: string
|
||||
|
||||
if (searchResponse.records.length === 0) {
|
||||
body = 'No results found.'
|
||||
} else {
|
||||
const resultsText = searchResponse.records
|
||||
.map((record, index) => {
|
||||
const docName = record.segment.document?.name || 'Unknown Document'
|
||||
const content = record.segment.content.trim()
|
||||
const score = record.score
|
||||
const keywords = record.segment.keywords || []
|
||||
|
||||
let resultEntry = `#### ${index + 1}. ${docName} (Relevant Score: ${(score * 100).toFixed(1)}%)`
|
||||
resultEntry += `\n${content}`
|
||||
if (keywords.length > 0) {
|
||||
resultEntry += `\n*Keywords: ${keywords.join(', ')}*`
|
||||
}
|
||||
return resultEntry
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
body = `Found ${searchResponse.records.length} results:\n\n${resultsText}`
|
||||
}
|
||||
|
||||
const formattedText = header + body
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索知识库时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DifyKnowledgeServer
|
||||
@ -2,6 +2,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import BraveSearchServer from './brave-search'
|
||||
import CalculatorServer from './calculator'
|
||||
import DifyKnowledgeServer from './dify-knowledge'
|
||||
import FetchServer from './fetch'
|
||||
import FileSystemServer from './filesystem'
|
||||
import MemoryServer from './memory'
|
||||
@ -33,6 +35,10 @@ export async function createInMemoryMCPServer(
|
||||
case '@cherry/filesystem': {
|
||||
return new FileSystemServer(args).server
|
||||
}
|
||||
case '@cherry/dify-knowledge': {
|
||||
const difyKey = envs.DIFY_KEY
|
||||
return new DifyKnowledgeServer(difyKey, args).server
|
||||
}
|
||||
case '@cherry/simpleremember': {
|
||||
const envPath = envs.SIMPLEREMEMBER_FILE_PATH
|
||||
return new SimpleRememberServer(envPath).server
|
||||
@ -74,6 +80,21 @@ export async function createInMemoryMCPServer(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
case '@cherry/calculator': {
|
||||
Logger.info('[MCP] Creating CalculatorServer instance')
|
||||
try {
|
||||
// 创建计算器服务器实例
|
||||
const calculatorServer = new CalculatorServer()
|
||||
|
||||
// 返回服务器实例
|
||||
// 注意:初始化过程已经在构造函数中启动,会异步完成
|
||||
Logger.info('[MCP] CalculatorServer instance created successfully')
|
||||
return calculatorServer.server
|
||||
} catch (error) {
|
||||
Logger.error('[MCP] Error creating CalculatorServer instance:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||
}
|
||||
|
||||
@ -14,7 +14,8 @@ enum ConfigKeys {
|
||||
ZoomFactor = 'ZoomFactor',
|
||||
Shortcuts = 'shortcuts',
|
||||
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
|
||||
EnableQuickAssistant = 'enableQuickAssistant'
|
||||
EnableQuickAssistant = 'enableQuickAssistant',
|
||||
EnableDataCollection = 'enableDataCollection'
|
||||
}
|
||||
|
||||
export class ConfigManager {
|
||||
@ -128,6 +129,14 @@ export class ConfigManager {
|
||||
this.set(ConfigKeys.EnableQuickAssistant, value)
|
||||
}
|
||||
|
||||
getEnableDataCollection(): boolean {
|
||||
return this.get(ConfigKeys.EnableDataCollection, false)
|
||||
}
|
||||
|
||||
setEnableDataCollection(value: boolean) {
|
||||
this.set(ConfigKeys.EnableDataCollection, value)
|
||||
}
|
||||
|
||||
set(key: string, value: unknown) {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ class McpService {
|
||||
this.restartServer = this.restartServer.bind(this)
|
||||
this.stopServer = this.stopServer.bind(this)
|
||||
this.cleanup = this.cleanup.bind(this)
|
||||
this.resetToolsList = this.resetToolsList.bind(this)
|
||||
}
|
||||
|
||||
async initClient(server: MCPServer): Promise<Client> {
|
||||
@ -341,6 +342,21 @@ class McpService {
|
||||
await this.initClient(server)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置工具列表缓存,强制刷新工具列表
|
||||
*/
|
||||
async resetToolsList(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
Logger.info(`[MCP] Resetting tools list for server: ${server.name}`)
|
||||
const serverKey = this.getServerKey(server)
|
||||
|
||||
// 清除工具列表缓存
|
||||
CacheService.remove(`mcp:list_tool:${serverKey}`)
|
||||
Logger.info(`[MCP] Cleared tools list cache for server: ${serverKey}`)
|
||||
|
||||
// 重新获取工具列表
|
||||
return this.listToolsImpl(server)
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (const [key] of this.clients) {
|
||||
try {
|
||||
|
||||
@ -68,7 +68,9 @@ export class WindowService {
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true,
|
||||
allowRunningInsecureContent: true
|
||||
allowRunningInsecureContent: true,
|
||||
// 添加 Sentry IPC 协议到 CSP
|
||||
additionalArguments: ['--disable-features=OutOfBlinkCors', '--allow-file-access-from-files']
|
||||
}
|
||||
})
|
||||
|
||||
@ -191,9 +193,11 @@ export class WindowService {
|
||||
|
||||
const oauthProviderUrls = [
|
||||
'https://account.siliconflow.cn/oauth',
|
||||
'https://cloud.siliconflow.cn/bills',
|
||||
'https://cloud.siliconflow.cn/expensebill',
|
||||
'https://aihubmix.com/token',
|
||||
'https://aihubmix.com/topup'
|
||||
'https://aihubmix.com/topup',
|
||||
'https://aihubmix.com/statistics'
|
||||
]
|
||||
|
||||
if (oauthProviderUrls.some((link) => url.startsWith(link))) {
|
||||
|
||||
@ -12,6 +12,7 @@ export function registerMCPHandlers(): void {
|
||||
ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
|
||||
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
||||
ipcMain.handle(IpcChannel.Mcp_ResetToolsList, mcpService.resetToolsList)
|
||||
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
||||
@ -25,6 +26,7 @@ export function registerMCPHandlers(): void {
|
||||
ipcMain.handle('mcp:remove-server', mcpService.removeServer)
|
||||
ipcMain.handle('mcp:stop-server', mcpService.stopServer)
|
||||
ipcMain.handle('mcp:list-tools', mcpService.listTools)
|
||||
ipcMain.handle('mcp:reset-tools-list', mcpService.resetToolsList)
|
||||
ipcMain.handle('mcp:call-tool', mcpService.callTool)
|
||||
ipcMain.handle('mcp:list-prompts', mcpService.listPrompts)
|
||||
ipcMain.handle('mcp:get-prompt', mcpService.getPrompt)
|
||||
|
||||
@ -26,6 +26,7 @@ const api = {
|
||||
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
|
||||
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
|
||||
setEnableDataCollection: (isActive: boolean) => ipcRenderer.invoke('app:setEnableDataCollection', isActive),
|
||||
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
|
||||
@ -149,6 +150,7 @@ const api = {
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
||||
resetToolsList: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ResetToolsList, server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
|
||||
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
|
||||
|
||||
@ -8,6 +8,7 @@ import DeepClaudeProvider from './components/DeepClaudeProvider'
|
||||
import GeminiInitializer from './components/GeminiInitializer'
|
||||
import MemoryProvider from './components/MemoryProvider'
|
||||
import PDFSettingsInitializer from './components/PDFSettingsInitializer'
|
||||
import SentryInitializer from './components/SentryInitializer'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import WebSearchInitializer from './components/WebSearchInitializer'
|
||||
import WorkspaceInitializer from './components/WorkspaceInitializer'
|
||||
@ -29,6 +30,7 @@ function App(): React.ReactElement {
|
||||
<DeepClaudeProvider />
|
||||
<GeminiInitializer />
|
||||
<PDFSettingsInitializer />
|
||||
<SentryInitializer />
|
||||
<WebSearchInitializer />
|
||||
<WorkspaceInitializer />
|
||||
<TopViewContainer>
|
||||
|
||||
@ -11,6 +11,7 @@ interface Props extends ButtonProps {
|
||||
|
||||
const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onAuth = () => {
|
||||
const handleSuccess = (key: string) => {
|
||||
if (key.trim()) {
|
||||
@ -29,8 +30,8 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onAuth} {...buttonProps}>
|
||||
{t('auth.get_key')}
|
||||
<Button type="primary" onClick={onAuth} shape="round" {...buttonProps}>
|
||||
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
13
src/renderer/src/components/SentryInitializer.tsx
Normal file
13
src/renderer/src/components/SentryInitializer.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 空的 SentryInitializer 组件
|
||||
* 用于替代原来的 Sentry 初始化组件
|
||||
* 在移除 Sentry 后保持应用程序结构不变
|
||||
*/
|
||||
const SentryInitializer: React.FC = () => {
|
||||
// 这是一个空组件,不需要渲染任何UI
|
||||
return null
|
||||
}
|
||||
|
||||
export default SentryInitializer
|
||||
@ -98,12 +98,13 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.webdav.backup.manager.delete.confirm.title'),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('settings.data.webdav.backup.manager.delete.confirm.multiple', { count: selectedRowKeys.length }),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
setDeleting(true)
|
||||
try {
|
||||
@ -136,12 +137,13 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.webdav.backup.manager.delete.confirm.title'),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('settings.data.webdav.backup.manager.delete.confirm.single', { fileName }),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
setDeleting(true)
|
||||
try {
|
||||
@ -168,12 +170,13 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.webdav.restore.confirm.title'),
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('settings.data.webdav.restore.confirm.content'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
setRestoring(true)
|
||||
try {
|
||||
|
||||
@ -2277,7 +2277,13 @@ export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
|
||||
'stabilityai/stable-diffusion-xl-base-1.0'
|
||||
]
|
||||
|
||||
export const GENERATE_IMAGE_MODELS = ['gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-exp']
|
||||
export const GENERATE_IMAGE_MODELS = [
|
||||
'gemini-2.0-flash-exp-image-generation',
|
||||
'gemini-2.0-flash-exp',
|
||||
'grok-2-image-1212',
|
||||
'gpt-4o-image',
|
||||
'gpt-image-1'
|
||||
]
|
||||
|
||||
export const GEMINI_SEARCH_MODELS = [
|
||||
'gemini-2.0-flash',
|
||||
|
||||
@ -120,7 +120,7 @@ export const SEARCH_SUMMARY_PROMPT = `
|
||||
4. Websearch: Always return the rephrased question inside the 'question' XML block. If there are no links in the follow-up question, do not insert a 'links' XML block in your response.
|
||||
5. Knowledge: Always return the rephrased question inside the 'question' XML block.
|
||||
6. Always wrap the rephrased question in the appropriate XML blocks to specify the tool(s) for retrieving information: use <websearch></websearch> for queries requiring real-time or external information, <knowledge></knowledge> for queries that can be answered from a pre-existing knowledge base, or both if the question could be applicable to either tool. Ensure that the rephrased question is always contained within a <question></question> block inside these wrappers.
|
||||
7. If you are not sure to use knowledge or websearch, you need use both of them.
|
||||
7. *use {tools} to rephrase the question*
|
||||
|
||||
There are several examples attached for your reference inside the below \`examples\` XML block
|
||||
|
||||
|
||||
@ -100,13 +100,13 @@ export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina', 'voyageai']
|
||||
export const PROVIDER_CONFIG = {
|
||||
openai: {
|
||||
api: {
|
||||
url: 'https://api.openai.com'
|
||||
url: 'https://api.siliconflow.cn'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://openai.com/',
|
||||
apiKey: 'https://platform.openai.com/api-keys',
|
||||
docs: 'https://platform.openai.com/docs',
|
||||
models: 'https://platform.openai.com/docs/models'
|
||||
official: 'https://www.siliconflow.cn',
|
||||
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
|
||||
docs: 'https://docs.siliconflow.cn/',
|
||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||
}
|
||||
},
|
||||
o3: {
|
||||
@ -148,7 +148,7 @@ export const PROVIDER_CONFIG = {
|
||||
url: 'https://api.siliconflow.cn'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://www.siliconflow.cn/',
|
||||
official: 'https://www.siliconflow.cn',
|
||||
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
|
||||
docs: 'https://docs.siliconflow.cn/',
|
||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||
|
||||
@ -118,7 +118,12 @@ export function useAppInit() {
|
||||
}, [customCss])
|
||||
|
||||
useEffect(() => {
|
||||
enableDataCollection ? initAnalytics() : disableAnalytics()
|
||||
if (enableDataCollection) {
|
||||
initAnalytics()
|
||||
// TODO: init data collection
|
||||
} else {
|
||||
disableAnalytics()
|
||||
}
|
||||
}, [enableDataCollection])
|
||||
|
||||
// 自动启动ASR服务器
|
||||
|
||||
@ -1,42 +1,30 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { loadScript, runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useRuntime } from './useRuntime'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const useMermaid = () => {
|
||||
const { theme } = useTheme()
|
||||
const { generating } = useRuntime()
|
||||
const mermaidLoaded = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@11.4.0/dist/mermaid.min.js')
|
||||
await loadScript('https://unpkg.com/mermaid@11.6.0/dist/mermaid.min.js')
|
||||
}
|
||||
|
||||
if (!mermaidLoaded.current) {
|
||||
await window.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
mermaidLoaded.current = true
|
||||
EventEmitter.emit('mermaid-loaded')
|
||||
}
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
})
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.mermaid || generating) return
|
||||
|
||||
const renderMermaid = () => {
|
||||
const mermaidElements = document.querySelectorAll('.mermaid')
|
||||
mermaidElements.forEach((element) => {
|
||||
if (!element.querySelector('svg')) {
|
||||
element.removeAttribute('data-processed')
|
||||
}
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
|
||||
setTimeout(renderMermaid, 100)
|
||||
}, [generating])
|
||||
|
||||
useEffect(() => {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
AssistantIconType,
|
||||
SendMessageShortcut,
|
||||
setAssistantIconType,
|
||||
setEnableDataCollection as _setEnableDataCollection,
|
||||
setLaunchOnBoot,
|
||||
setLaunchToTray,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
@ -30,6 +31,7 @@ export function useSettings(): SettingsState & {
|
||||
updateSidebarVisibleIcons: (icons: SidebarIcon[]) => void
|
||||
updateSidebarDisabledIcons: (icons: SidebarIcon[]) => void
|
||||
setAssistantIconType: (assistantIconType: AssistantIconType) => void
|
||||
setEnableDataCollection: (enabled: boolean) => void
|
||||
showAgentTaskList: boolean
|
||||
agentAutoExecutionCount: number
|
||||
setShowAgentTaskList: (show: boolean) => void
|
||||
@ -91,6 +93,12 @@ export function useSettings(): SettingsState & {
|
||||
setAssistantIconType(assistantIconType: AssistantIconType) {
|
||||
dispatch(setAssistantIconType(assistantIconType))
|
||||
},
|
||||
|
||||
setEnableDataCollection(enabled: boolean) {
|
||||
dispatch(_setEnableDataCollection(enabled))
|
||||
// 同步到主进程
|
||||
window.api.invoke('app:setEnableDataCollection', enabled)
|
||||
},
|
||||
// 添加 showAgentTaskList 和 agentAutoExecutionCount 属性
|
||||
showAgentTaskList: settings.showAgentTaskList, // 使用 Redux store 中的值
|
||||
agentAutoExecutionCount: settings.agentAutoExecutionCount, // 使用 Redux store 中的值
|
||||
|
||||
@ -1428,7 +1428,8 @@
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available",
|
||||
"loadError": "Get tools Error"
|
||||
"loadError": "Get tools Error",
|
||||
"resetToolsList": "Reset Tools List"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Available Prompts",
|
||||
@ -1483,6 +1484,7 @@
|
||||
"messages.input.enable_delete_model": "Enable the backspace key to delete models/attachments.",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input message",
|
||||
"messages.math_engine": "Math engine",
|
||||
"messages.math_engine.none": "None",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.navigation": "Message Navigation",
|
||||
@ -1588,10 +1590,16 @@
|
||||
"api_key": "API Key",
|
||||
"api_key.tip": "Multiple keys separated by commas",
|
||||
"api_version": "API Version",
|
||||
"charge": "Charge",
|
||||
"charge": "Balance Recharge",
|
||||
"bills": "Fee Bills",
|
||||
"check": "Check",
|
||||
"check_all_keys": "Check All Keys",
|
||||
"check_multiple_keys": "Check Multiple API Keys",
|
||||
"oauth": {
|
||||
"button": "Login with {{provider}}",
|
||||
"description": "This service is provided by <website>{{provider}}</website>",
|
||||
"official_website": "Official Website"
|
||||
},
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot authentication failed.",
|
||||
"auth_success": "GitHub Copilot authentication successful.",
|
||||
@ -1688,7 +1696,7 @@
|
||||
"websearch": {
|
||||
"blacklist": "Blacklist",
|
||||
"blacklist_description": "Results from the following websites will not appear in search results",
|
||||
"blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"blacklist_tooltip": "Please use the following format (separated by newlines)\nPattern matching: *://*.example.com/*\nRegular expression: /example\\.(net|org)/",
|
||||
"check": "Check",
|
||||
"check_failed": "Verification failed",
|
||||
"check_success": "Verification successful",
|
||||
|
||||
@ -1173,6 +1173,7 @@
|
||||
"messages.input.enable_delete_model": "バックスペースキーでモデル/添付ファイルを削除します。",
|
||||
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
||||
"messages.math_engine": "数式エンジン",
|
||||
"messages.math_engine.none": "なし",
|
||||
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
||||
"messages.model.title": "モデル設定",
|
||||
"messages.navigation": "メッセージナビゲーション",
|
||||
@ -1237,10 +1238,16 @@
|
||||
"api_key": "APIキー",
|
||||
"api_key.tip": "複数のキーはカンマで区切ります",
|
||||
"api_version": "APIバージョン",
|
||||
"charge": "充電",
|
||||
"charge": "残高充電",
|
||||
"bills": "費用帳單",
|
||||
"check": "チェック",
|
||||
"check_all_keys": "すべてのキーをチェック",
|
||||
"check_multiple_keys": "複数のAPIキーをチェック",
|
||||
"oauth": {
|
||||
"button": "{{provider}} アカウントでログイン",
|
||||
"description": "本サービスは<website>{{provider}}</website>によって提供されます",
|
||||
"official_website": "公式サイト"
|
||||
},
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilotの認証に失敗しました。",
|
||||
"auth_success": "Github Copilotの認証が成功しました",
|
||||
@ -1682,4 +1689,4 @@
|
||||
"shortcut_key_tip": "このショートカットキーを押すと録音が始まり、キーを離すと録音が終了して送信されます"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1176,6 +1176,7 @@
|
||||
"messages.input.enable_delete_model": "Включите удаление модели/вложения с помощью клавиши Backspace",
|
||||
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
||||
"messages.math_engine": "Математический движок",
|
||||
"messages.math_engine.none": "Нет",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Настройки модели",
|
||||
"messages.navigation": "Навигация сообщений",
|
||||
@ -1240,10 +1241,16 @@
|
||||
"api_key": "Ключ API",
|
||||
"api_key.tip": "Несколько ключей, разделенных запятыми",
|
||||
"api_version": "Версия API",
|
||||
"charge": "Пополнить",
|
||||
"charge": "Пополнить баланс",
|
||||
"bills": "Счета за услуги",
|
||||
"check": "Проверить",
|
||||
"check_all_keys": "Проверить все ключи",
|
||||
"check_multiple_keys": "Проверить несколько ключей API",
|
||||
"oauth": {
|
||||
"button": "Войти с {{provider}}",
|
||||
"description": "Сервис предоставляется <website>{{provider}}</website>",
|
||||
"official_website": "Официальный сайт"
|
||||
},
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot认证失败",
|
||||
"auth_success": "Github Copilot认证成功",
|
||||
@ -1681,4 +1688,4 @@
|
||||
"shortcut_key_tip": "Нажмите эту горячую клавишу, чтобы начать запись, отпустите, чтобы закончить запись и отправить"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1573,7 +1573,8 @@
|
||||
"inputSchema": "输入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "无可用工具",
|
||||
"loadError": "获取工具失败"
|
||||
"loadError": "获取工具失败",
|
||||
"resetToolsList": "重置工具列表"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
@ -1628,6 +1629,7 @@
|
||||
"messages.input.enable_delete_model": "启用删除键删除输入的模型/附件",
|
||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||
"messages.math_engine": "数学公式引擎",
|
||||
"messages.math_engine.none": "无",
|
||||
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型设置",
|
||||
"messages.navigation": "对话导航按钮",
|
||||
@ -1707,6 +1709,11 @@
|
||||
"check": "检查",
|
||||
"check_all_keys": "检查所有密钥",
|
||||
"check_multiple_keys": "检查多个 API 密钥",
|
||||
"oauth": {
|
||||
"button": "使用{{provider}}登录",
|
||||
"description": "此服务由<website>{{provider}}</website>提供",
|
||||
"official_website": "官方网站"
|
||||
},
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot 认证失败",
|
||||
"auth_success": "Github Copilot 认证成功",
|
||||
|
||||
@ -1130,7 +1130,8 @@
|
||||
"inputSchema": "輸入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "無可用工具",
|
||||
"loadError": "獲取工具失敗"
|
||||
"loadError": "獲取工具失敗",
|
||||
"resetToolsList": "重置工具列表"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
@ -1172,7 +1173,8 @@
|
||||
"messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單",
|
||||
"messages.input.enable_delete_model": "啟用刪除鍵刪除模型/附件",
|
||||
"messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息",
|
||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||
"messages.math_engine": "數學公式引擎",
|
||||
"messages.math_engine.none": "無",
|
||||
"messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型設定",
|
||||
"messages.navigation": "訊息導航",
|
||||
@ -1241,6 +1243,11 @@
|
||||
"check": "檢查",
|
||||
"check_all_keys": "檢查所有金鑰",
|
||||
"check_multiple_keys": "檢查多個 API 金鑰",
|
||||
"oauth": {
|
||||
"button": "使用{{provider}}登入",
|
||||
"description": "此服務由<website>{{provider}}</website>提供",
|
||||
"official_website": "官方網站"
|
||||
},
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot認證失敗",
|
||||
"auth_success": "Github Copilot 認證成功",
|
||||
@ -1682,4 +1689,4 @@
|
||||
"shortcut_key_tip": "按下此快速鍵開始錄音,放開快速鍵結束錄音並傳送"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,8 @@ function initAutoSync() {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
initSpinner()
|
||||
initKeyv()
|
||||
initAutoSync()
|
||||
|
||||
@ -27,7 +27,18 @@ const AddBookmarkDialog: React.FC<AddBookmarkDialogProps> = ({
|
||||
|
||||
// 将文件夹数据转换为树形结构
|
||||
useEffect(() => {
|
||||
if (!folders) return
|
||||
if (!folders || !Array.isArray(folders)) {
|
||||
// 如果 folders 不存在或不是数组,设置默认的根目录选项
|
||||
setTreeData([
|
||||
{
|
||||
title: 'Bookmarks Bar',
|
||||
value: null,
|
||||
key: 'root',
|
||||
icon: <FolderOutlined />
|
||||
}
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建文件夹树
|
||||
const buildFolderTree = (
|
||||
@ -35,14 +46,18 @@ const AddBookmarkDialog: React.FC<AddBookmarkDialogProps> = ({
|
||||
parentId: string | null = null
|
||||
): any[] => {
|
||||
return items
|
||||
.filter((item) => item.parentId === parentId)
|
||||
.map((item) => ({
|
||||
title: item.title,
|
||||
value: item.id,
|
||||
key: item.id,
|
||||
icon: <FolderOutlined />,
|
||||
children: buildFolderTree(items, item.id)
|
||||
}))
|
||||
.filter((item) => item && item.parentId === parentId)
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
return {
|
||||
title: item.title || 'Unnamed Folder',
|
||||
value: item.id,
|
||||
key: item.id,
|
||||
icon: <FolderOutlined />,
|
||||
children: buildFolderTree(items, item.id)
|
||||
};
|
||||
})
|
||||
.filter(Boolean); // 过滤掉 null 项
|
||||
}
|
||||
|
||||
// 添加根目录选项
|
||||
@ -120,13 +135,14 @@ const AddBookmarkDialog: React.FC<AddBookmarkDialogProps> = ({
|
||||
|
||||
<Form.Item name="folderId" label="Save to">
|
||||
<TreeSelect
|
||||
treeData={treeData}
|
||||
treeData={treeData || []}
|
||||
placeholder="Select a folder"
|
||||
loading={loading}
|
||||
treeDefaultExpandAll
|
||||
showSearch
|
||||
allowClear
|
||||
treeNodeFilterProp="title"
|
||||
fieldNames={{ label: 'title', value: 'value', children: 'children' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons'
|
||||
import { Card, Empty, Flex, Input, Radio, Tag, Typography } from 'antd'
|
||||
import { AppstoreOutlined, ReloadOutlined, UnorderedListOutlined } from '@ant-design/icons'
|
||||
import { Button, Card, Empty, Flex, Input, Radio, Tag, Typography } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { Search, SquareTerminal } from 'lucide-react'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
@ -23,23 +23,40 @@ const MCPPage: FC = () => {
|
||||
const [viewMode, setViewMode] = useState<MCPViewMode>('list')
|
||||
|
||||
// 获取所有可用工具
|
||||
useEffect(() => {
|
||||
const fetchTools = async () => {
|
||||
setLoading(true)
|
||||
const allTools: MCPTool[] = []
|
||||
for (const server of mcpServers.filter((s) => s.isActive)) {
|
||||
try {
|
||||
// @ts-ignore - window.api is defined in preload
|
||||
const serverTools = await window.api.mcp.listTools(server)
|
||||
allTools.push(...serverTools)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching tools for server ${server.name}:`, error)
|
||||
}
|
||||
const fetchTools = async () => {
|
||||
setLoading(true)
|
||||
const allTools: MCPTool[] = []
|
||||
for (const server of mcpServers.filter((s) => s.isActive)) {
|
||||
try {
|
||||
// @ts-ignore - window.api is defined in preload
|
||||
const serverTools = await window.api.mcp.listTools(server)
|
||||
allTools.push(...serverTools)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching tools for server ${server.name}:`, error)
|
||||
}
|
||||
setTools(allTools)
|
||||
setLoading(false)
|
||||
}
|
||||
setTools(allTools)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// 重置工具列表
|
||||
const resetToolsList = async () => {
|
||||
setLoading(true)
|
||||
const allTools: MCPTool[] = []
|
||||
for (const server of mcpServers.filter((s) => s.isActive)) {
|
||||
try {
|
||||
// @ts-ignore - window.api is defined in preload
|
||||
const serverTools = await window.api.mcp.resetToolsList(server)
|
||||
allTools.push(...serverTools)
|
||||
} catch (error) {
|
||||
console.error(`Error resetting tools for server ${server.name}:`, error)
|
||||
}
|
||||
}
|
||||
setTools(allTools)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (mcpServers.length > 0) {
|
||||
fetchTools()
|
||||
} else {
|
||||
@ -85,6 +102,15 @@ const MCPPage: FC = () => {
|
||||
value={search}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
size="small"
|
||||
onClick={resetToolsList}
|
||||
loading={loading}
|
||||
style={{ marginLeft: 8 }}>
|
||||
{t('settings.mcp.tools.resetToolsList') || '重置工具列表'}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Spacer />
|
||||
</StyledNavbarCenter>
|
||||
|
||||
@ -6,10 +6,10 @@ import '@renderer/styles/citation.css'
|
||||
|
||||
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import type { Message } from '@renderer/types'
|
||||
import type { MCPToolResponse, Message } from '@renderer/types' // Import MCPToolResponse
|
||||
import { parseJSON } from '@renderer/utils'
|
||||
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
|
||||
import { findCitationInChildren, sanitizeSchema } from '@renderer/utils/markdown'
|
||||
import { findCitationInChildren } from '@renderer/utils/markdown'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { type FC, memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -19,26 +19,57 @@ import rehypeKatex from 'rehype-katex'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
// @ts-ignore next-line
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import remarkCjkFriendly from 'remark-cjk-friendly'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import SingleToolCallBlock from '../Messages/SingleToolCallBlock' // 导入 SingleToolCallBlock
|
||||
import CitationTooltip from './CitationTooltip'
|
||||
// Removed InlineToolBlock import
|
||||
import EditableCodeBlock from './EditableCodeBlock'
|
||||
import ImagePreview from './ImagePreview'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
toolResponses: MCPToolResponse[] // 添加工具响应数据 prop
|
||||
activeToolKeys: string[] // 添加 activeKeys prop
|
||||
copiedToolMap: Record<string, boolean> // 添加 copiedMap prop
|
||||
editingToolId: string | null // 添加 editingToolId prop
|
||||
editedToolParamsString: string // 添加 editedParams prop
|
||||
onToolToggle: React.Dispatch<React.SetStateAction<string[]>> // 添加 onToolToggle prop
|
||||
onToolCopy: (content: string, toolId: string) => void // 添加 onToolCopy prop
|
||||
onToolRerun: (toolCall: MCPToolResponse, currentParamsString: string) => void // 添加 onToolRerun prop
|
||||
onToolEdit: (toolCall: MCPToolResponse) => void // 添加 onToolEdit prop
|
||||
onToolSave: (toolCall: MCPToolResponse) => void // 添加 onToolSave prop
|
||||
onToolCancel: () => void // 添加 onToolCancel prop
|
||||
onToolParamsChange: (newParams: string) => void // 添加 onToolParamsChange prop
|
||||
}
|
||||
|
||||
const remarkPlugins = [remarkMath, remarkGfm, remarkCjkFriendly]
|
||||
|
||||
const Markdown: FC<Props> = ({ message }) => {
|
||||
const Markdown: FC<Props> = ({
|
||||
message,
|
||||
toolResponses,
|
||||
activeToolKeys,
|
||||
copiedToolMap,
|
||||
editingToolId,
|
||||
editedToolParamsString,
|
||||
onToolToggle,
|
||||
onToolCopy,
|
||||
onToolRerun,
|
||||
onToolEdit,
|
||||
onToolSave,
|
||||
onToolCancel,
|
||||
onToolParamsChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { renderInputMessageAsMarkdown, mathEngine } = useSettings()
|
||||
const { renderInputMessageAsMarkdown, messageFont, mathEngine } = useSettings() // Add messageFont
|
||||
|
||||
const remarkPlugins = useMemo(() => {
|
||||
const plugins = [remarkGfm, remarkCjkFriendly]
|
||||
if (mathEngine && mathEngine !== 'none') {
|
||||
plugins.push(remarkMath)
|
||||
}
|
||||
return plugins
|
||||
}, [mathEngine])
|
||||
|
||||
const messageContent = useMemo(() => {
|
||||
// 检查消息内容是否为空或未定义
|
||||
@ -53,8 +84,19 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
}, [message, t])
|
||||
|
||||
const rehypePlugins = useMemo(() => {
|
||||
return [rehypeRaw, [rehypeSanitize, sanitizeSchema], mathEngine === 'KaTeX' ? rehypeKatex : rehypeMathjax]
|
||||
}, [mathEngine])
|
||||
const plugins: any[] = []
|
||||
// Check if messageContent contains any of the allowed raw HTML tags
|
||||
const rawTagsRegex = /<(style|translated|think|tool-block)[\s>]/i
|
||||
if (rawTagsRegex.test(messageContent)) {
|
||||
plugins.push(rehypeRaw)
|
||||
}
|
||||
if (mathEngine === 'KaTeX') {
|
||||
plugins.push(rehypeKatex as any)
|
||||
} else if (mathEngine === 'MathJax') {
|
||||
plugins.push(rehypeMathjax as any)
|
||||
}
|
||||
return plugins
|
||||
}, [mathEngine, messageContent])
|
||||
|
||||
// Remove processToolUse function as it's based on XML tags in content,
|
||||
// which won't exist with native function calling.
|
||||
@ -130,11 +172,71 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
// 添加 tool-block 渲染器
|
||||
'tool-block': (props: any) => {
|
||||
console.log('[Markdown] Tool block renderer called with props:', props) // Log renderer call and props
|
||||
const toolCallId = props.id // 获取占位符中的 id
|
||||
console.log('[Markdown] Extracted toolCallId:', toolCallId) // Log extracted ID
|
||||
const toolResponse = toolResponses.find((tr) => tr.id === toolCallId) // 查找对应的工具响应数据
|
||||
console.log('[Markdown] Found toolResponse:', toolResponse) // Log found tool response
|
||||
|
||||
if (!toolResponse) {
|
||||
return null // 如果找不到对应的工具响应,则不渲染
|
||||
}
|
||||
|
||||
if (!toolResponse) {
|
||||
console.warn('[Markdown] Tool response not found for id:', toolCallId) // Warn if not found
|
||||
return null // 如果找不到对应的工具响应,则不渲染
|
||||
}
|
||||
|
||||
console.log('[Markdown] Rendering SingleToolCallBlock for toolCallId:', toolCallId) // Log rendering SingleToolCallBlock
|
||||
// 渲染 SingleToolCallBlock 组件,并传递必要的 props
|
||||
return (
|
||||
<SingleToolCallBlock
|
||||
toolResponse={toolResponse}
|
||||
isActive={activeToolKeys.includes(toolCallId)}
|
||||
isCopied={copiedToolMap[toolCallId] || false}
|
||||
isEditing={editingToolId === toolCallId}
|
||||
editedParamsString={editedToolParamsString}
|
||||
fontFamily={
|
||||
messageFont === 'serif'
|
||||
? 'serif'
|
||||
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'
|
||||
} // 传递字体样式
|
||||
t={t} // 传递翻译函数
|
||||
onToggle={() =>
|
||||
onToolToggle((prev) =>
|
||||
prev.includes(toolCallId) ? prev.filter((k) => k !== toolCallId) : [...prev, toolCallId]
|
||||
)
|
||||
} // 传递 onToolToggle 函数
|
||||
onCopy={onToolCopy} // 传递 onToolCopy 函数
|
||||
onRerun={onToolRerun} // 传递 onToolRerun 函数
|
||||
onEdit={onToolEdit} // 传递 onToolEdit 函数
|
||||
onSave={() => toolResponse && onToolSave(toolResponse)} // 传递 onToolSave 函数
|
||||
onCancel={onToolCancel} // 传递 onToolCancel 函数
|
||||
onParamsChange={onToolParamsChange} // 传递 onToolParamsChange 函数
|
||||
/>
|
||||
)
|
||||
}
|
||||
// Removed custom div renderer for tool markers
|
||||
} as Partial<Components> // Keep Components type here
|
||||
} as Partial<Components>
|
||||
return baseComponents
|
||||
}, []) // Removed message.metadata dependency as it's no longer used here
|
||||
}, [
|
||||
toolResponses, // 添加 toolResponses 依赖
|
||||
activeToolKeys, // 添加 activeToolKeys 依赖
|
||||
copiedToolMap, // 添加 copiedToolMap 依赖
|
||||
editingToolId, // 添加 editingToolId 依赖
|
||||
editedToolParamsString, // 添加 editedToolParamsString 依赖
|
||||
onToolToggle, // 添加 onToolToggle 依赖
|
||||
onToolCopy, // 添加 onToolCopy 依赖
|
||||
onToolRerun, // 添加 onToolRerun 依赖
|
||||
onToolEdit, // 添加 onToolEdit 依赖
|
||||
onToolSave, // 添加 onToolSave 依赖
|
||||
onToolCancel, // 添加 onToolCancel 依赖
|
||||
onToolParamsChange, // 添加 onToolParamsChange 依赖
|
||||
t, // 添加 t 依赖
|
||||
messageFont // 添加 messageFont 依赖
|
||||
])
|
||||
|
||||
// 使用useEffect在渲染后添加事件处理
|
||||
React.useEffect(() => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import MermaidPopup from './MermaidPopup'
|
||||
|
||||
@ -12,20 +14,44 @@ const Mermaid: React.FC<Props> = ({ chart }) => {
|
||||
const { theme } = useTheme()
|
||||
const mermaidRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (mermaidRef.current && window.mermaid) {
|
||||
const renderMermaidBase = useCallback(async () => {
|
||||
if (!mermaidRef.current || !window.mermaid || isEmpty(chart)) return
|
||||
|
||||
try {
|
||||
mermaidRef.current.innerHTML = chart
|
||||
mermaidRef.current.removeAttribute('data-processed')
|
||||
if (window.mermaid.initialize) {
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
}
|
||||
window.mermaid.contentLoaded()
|
||||
|
||||
await window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
|
||||
await window.mermaid.run({ nodes: [mermaidRef.current] })
|
||||
} catch (error) {
|
||||
console.error('Failed to render mermaid chart:', error)
|
||||
}
|
||||
}, [chart, theme])
|
||||
|
||||
const renderMermaid = useCallback(debounce(renderMermaidBase, 1000), [renderMermaidBase])
|
||||
|
||||
useEffect(() => {
|
||||
renderMermaid()
|
||||
// Make sure to cancel any pending debounced calls when unmounting
|
||||
return () => renderMermaid.cancel()
|
||||
}, [renderMermaid])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(renderMermaidBase, 0)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = EventEmitter.on('mermaid-loaded', renderMermaid)
|
||||
return () => {
|
||||
removeListener()
|
||||
renderMermaid.cancel()
|
||||
}
|
||||
}, [renderMermaid])
|
||||
|
||||
const onPreview = () => {
|
||||
MermaidPopup.show({ chart })
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { download } from '@renderer/utils/download'
|
||||
import { Button, Modal, Space, Tabs } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -16,6 +19,7 @@ interface Props extends ShowParams {
|
||||
const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||
const [activeTab, setActiveTab] = useState('preview')
|
||||
const [scale, setScale] = useState(1)
|
||||
@ -97,19 +101,21 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
if (!element) return
|
||||
|
||||
const timestamp = Date.now()
|
||||
const backgroundColor = theme === ThemeMode.dark ? '#1F1F1F' : '#fff'
|
||||
const svgElement = element.querySelector('svg')
|
||||
|
||||
if (!svgElement) return
|
||||
|
||||
if (format === 'svg') {
|
||||
const svgElement = element.querySelector('svg')
|
||||
if (!svgElement) return
|
||||
// Add background color to SVG
|
||||
svgElement.style.backgroundColor = backgroundColor
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
download(url, `mermaid-diagram-${timestamp}.svg`)
|
||||
URL.revokeObjectURL(url)
|
||||
} else if (format === 'png') {
|
||||
const svgElement = element.querySelector('svg')
|
||||
if (!svgElement) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
@ -119,6 +125,9 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
const width = viewBox[2] || svgElement.clientWidth || svgElement.getBoundingClientRect().width
|
||||
const height = viewBox[3] || svgElement.clientHeight || svgElement.getBoundingClientRect().height
|
||||
|
||||
// Add background color to SVG before converting to image
|
||||
svgElement.style.backgroundColor = backgroundColor
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||
const svgBase64 = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`
|
||||
|
||||
@ -129,6 +138,9 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
|
||||
if (ctx) {
|
||||
ctx.scale(scale, scale)
|
||||
// Fill background
|
||||
ctx.fillStyle = backgroundColor
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
}
|
||||
|
||||
@ -142,6 +154,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
}
|
||||
img.src = svgBase64
|
||||
}
|
||||
svgElement.style.backgroundColor = 'transparent'
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
@ -153,8 +166,30 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window?.mermaid?.contentLoaded()
|
||||
}, [])
|
||||
runAsyncFunction(async () => {
|
||||
if (!window.mermaid) return
|
||||
|
||||
try {
|
||||
const element = document.getElementById(mermaidId)
|
||||
if (!element) return
|
||||
|
||||
// Clear previous content
|
||||
element.innerHTML = chart
|
||||
element.removeAttribute('data-processed')
|
||||
|
||||
await window.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
|
||||
await window.mermaid.run({
|
||||
nodes: [element]
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to render mermaid chart in popup:', error)
|
||||
}
|
||||
})
|
||||
}, [activeTab, theme, mermaidId, chart])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||
import React, { FC, useLayoutEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
@ -11,61 +11,22 @@ interface CustomCollapseProps {
|
||||
|
||||
const CustomCollapse: FC<CustomCollapseProps> = ({ title, children, isActive, onToggle }) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [height, setHeight] = useState<number | 'auto'>(0)
|
||||
const [isContentVisible, setIsContentVisible] = useState(false)
|
||||
const [isAnimating, setIsAnimating] = useState(false)
|
||||
const prevActiveRef = useRef(isActive)
|
||||
|
||||
// 使用 requestAnimationFrame 来优化动画性能
|
||||
const animateHeight = useCallback((from: number, to: number, duration: number = 250) => {
|
||||
setIsAnimating(true)
|
||||
const startTime = performance.now()
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsedTime = currentTime - startTime
|
||||
const progress = Math.min(elapsedTime / duration, 1)
|
||||
// 使用缓动函数使动画更平滑
|
||||
const easeProgress = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress)
|
||||
const currentHeight = from + (to - from) * easeProgress
|
||||
|
||||
setHeight(currentHeight)
|
||||
|
||||
if (progress < 1) {
|
||||
window.requestAnimationFrame(animate)
|
||||
} else {
|
||||
setHeight(to === 0 ? 0 : 'auto')
|
||||
setIsAnimating(false)
|
||||
setIsContentVisible(to !== 0)
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(animate)
|
||||
}, [])
|
||||
|
||||
// 使用 useLayoutEffect 来测量高度,避免闪烁
|
||||
const [height, setHeight] = useState<number | 'auto'>(isActive ? 'auto' : 0)
|
||||
// Use useLayoutEffect to update height based on isActive prop
|
||||
useLayoutEffect(() => {
|
||||
// 如果状态没有变化,不做任何处理
|
||||
if (prevActiveRef.current === isActive) return
|
||||
|
||||
// 如果正在动画中,不做处理
|
||||
if (isAnimating) return
|
||||
|
||||
prevActiveRef.current = isActive
|
||||
|
||||
console.log('[CustomCollapse] useLayoutEffect triggered. isActive:', isActive) // Log effect trigger
|
||||
if (isActive) {
|
||||
// 展开
|
||||
if (contentRef.current) {
|
||||
const contentHeight = contentRef.current.scrollHeight
|
||||
animateHeight(0, contentHeight)
|
||||
}
|
||||
// When expanding, set height to 'auto' to allow content to show
|
||||
setHeight('auto')
|
||||
console.log('[CustomCollapse] Expanding: setting height to auto.') // Log height set
|
||||
} else {
|
||||
// 折叠
|
||||
if (contentRef.current) {
|
||||
const currentHeight = contentRef.current.scrollHeight
|
||||
animateHeight(currentHeight, 0)
|
||||
}
|
||||
// When collapsing, set height to 0
|
||||
setHeight(0)
|
||||
console.log('[CustomCollapse] Collapsing: setting height to 0.') // Log height set
|
||||
}
|
||||
}, [isActive, animateHeight, isAnimating])
|
||||
}, [isActive])
|
||||
|
||||
console.log('[CustomCollapse] Rendering. isActive:', isActive, 'Current Height:', height) // Log rendering state
|
||||
|
||||
return (
|
||||
<CollapseWrapper>
|
||||
@ -74,26 +35,11 @@ const CustomCollapse: FC<CustomCollapseProps> = ({ title, children, isActive, on
|
||||
ref={contentRef}
|
||||
style={{
|
||||
height: height === 'auto' ? 'auto' : `${height}px`,
|
||||
// 使用 transform 而不是 height 来触发硬件加速
|
||||
transform: `translateZ(0)`,
|
||||
// 使用 will-change 提前告知浏览器将要发生变化
|
||||
willChange: isAnimating ? 'height' : 'auto',
|
||||
// 使用 contain 限制重绘范围
|
||||
contain: 'content',
|
||||
// 使用 GPU 加速
|
||||
backfaceVisibility: 'hidden'
|
||||
overflow: 'hidden' // Ensure content is hidden when collapsed
|
||||
}}
|
||||
$isActive={isActive}>
|
||||
<div
|
||||
style={{
|
||||
display: isContentVisible || isActive ? 'block' : 'none',
|
||||
// 使用 transform 触发硬件加速
|
||||
transform: 'translateZ(0)',
|
||||
// 使用 contain 限制重绘范围
|
||||
contain: 'content'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
{/* Render children directly */}
|
||||
<div>{children}</div>
|
||||
</CollapseContent>
|
||||
</CollapseWrapper>
|
||||
)
|
||||
@ -103,8 +49,6 @@ const CollapseWrapper = styled.div`
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-1);
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@ -116,7 +60,6 @@ const CollapseHeader = styled.div`
|
||||
cursor: pointer;
|
||||
background-color: var(--color-bg-2);
|
||||
transition: background-color 0.2s;
|
||||
will-change: transform, background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-3);
|
||||
@ -125,14 +68,13 @@ const CollapseHeader = styled.div`
|
||||
|
||||
const CollapseContent = styled.div<{ $isActive: boolean }>`
|
||||
overflow: hidden;
|
||||
/* 移除过渡效果,改用 requestAnimationFrame 手动控制动画 */
|
||||
/* 使用 GPU 加速 */
|
||||
transition: height 250ms ease-out; /* Add CSS transition for height */
|
||||
background-color: var(--color-bg-1); /* Add background color */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
perspective: 1000;
|
||||
-webkit-perspective: 1000;
|
||||
background-color: var(--color-bg-1); /* 添加背景色 */
|
||||
`
|
||||
|
||||
export default CustomCollapse
|
||||
|
||||
@ -2,13 +2,13 @@ import { SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import TTSHighlightedText from '@renderer/components/TTSHighlightedText'
|
||||
import { isOpenAIWebSearch } from '@renderer/config/models'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { MCPToolResponse, Message, Model } from '@renderer/types' // Import MCPToolResponse
|
||||
import { getBriefInfo } from '@renderer/utils'
|
||||
import { withMessageThought } from '@renderer/utils/formats'
|
||||
import { Collapse, Divider, Flex } from 'antd'
|
||||
import { clone } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' // Import useCallback
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
@ -20,7 +20,6 @@ import MessageAttachments from './MessageAttachments'
|
||||
import MessageError from './MessageError'
|
||||
import MessageImage from './MessageImage'
|
||||
import MessageThought from './MessageThought'
|
||||
import { default as MessageTools } from './MessageTools' // Change to named import (using default alias)
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@ -33,6 +32,214 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
const isWebCitation = model && (isOpenAIWebSearch(model) || model.provider === 'openrouter')
|
||||
const [isSegmentedPlayback, setIsSegmentedPlayback] = useState(false)
|
||||
|
||||
// MCP Tool related states and handlers moved from MessageTools.tsx
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>([])
|
||||
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
|
||||
const [editingToolId, setEditingToolId] = useState<string | null>(null)
|
||||
const [editedParams, setEditedParams] = useState<string>('')
|
||||
|
||||
// Local state for immediate UI updates, synced with message metadata
|
||||
const [localToolResponses, setLocalToolResponses] = useState<MCPToolResponse[]>(message.metadata?.mcpTools || [])
|
||||
|
||||
// Effect to sync local state when message metadata changes externally
|
||||
useEffect(() => {
|
||||
// Only update local state if the incoming metadata is actually different
|
||||
// This prevents unnecessary re-renders if the message object reference changes but content doesn't
|
||||
const incomingTools = message.metadata?.mcpTools || []
|
||||
if (JSON.stringify(incomingTools) !== JSON.stringify(localToolResponses)) {
|
||||
setLocalToolResponses(incomingTools)
|
||||
}
|
||||
}, [message.metadata?.mcpTools, localToolResponses])
|
||||
|
||||
const copyContent = useCallback(
|
||||
(content: string, toolId: string) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopiedMap((prev) => ({ ...prev, [toolId]: true }))
|
||||
setTimeout(() => setCopiedMap((prev) => ({ ...prev, [toolId]: false })), 2000)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
// --- Handlers for Edit/Rerun ---
|
||||
const handleRerun = useCallback(
|
||||
(toolCall: MCPToolResponse, currentParamsString: string) => {
|
||||
console.log('Rerunning tool:', toolCall.id, 'with params:', currentParamsString)
|
||||
try {
|
||||
const paramsToRun = JSON.parse(currentParamsString)
|
||||
|
||||
// Proactively update local state for immediate UI feedback
|
||||
setLocalToolResponses((prevResponses) =>
|
||||
prevResponses.map((tc) =>
|
||||
tc.id === toolCall.id ? { ...tc, args: paramsToRun, status: 'invoking', response: undefined } : tc
|
||||
)
|
||||
)
|
||||
|
||||
const serverConfig = message.enabledMCPs?.find((server) => server.id === toolCall.tool.serverId)
|
||||
if (!serverConfig) {
|
||||
console.error(`[MessageContent] Server config not found for ID ${toolCall.tool.serverId}`)
|
||||
window.message.error({ content: t('common.rerun_failed_server_not_found'), key: 'rerun-tool' })
|
||||
return
|
||||
}
|
||||
|
||||
window.api.mcp
|
||||
.rerunTool(message.id, toolCall.id, serverConfig, toolCall.tool.name, paramsToRun)
|
||||
.then(() => window.message.success({ content: t('common.rerun_started'), key: 'rerun-tool' }))
|
||||
.catch((err) => {
|
||||
console.error('Rerun failed:', err)
|
||||
window.message.error({ content: t('common.rerun_failed'), key: 'rerun-tool' })
|
||||
// Optionally revert local state on failure
|
||||
setLocalToolResponses(
|
||||
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
|
||||
)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON parameters for rerun:', e)
|
||||
window.message.error(t('common.invalid_json'))
|
||||
// Revert local state if JSON parsing fails
|
||||
setLocalToolResponses(
|
||||
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
|
||||
)
|
||||
}
|
||||
},
|
||||
[message.id, message.enabledMCPs, t]
|
||||
)
|
||||
|
||||
const handleEdit = useCallback((toolCall: MCPToolResponse) => {
|
||||
setEditingToolId(toolCall.id)
|
||||
setEditedParams(JSON.stringify(toolCall.args || {}, null, 2))
|
||||
}, [])
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setEditingToolId(null)
|
||||
setEditedParams('')
|
||||
}, [])
|
||||
|
||||
const handleSaveEdit = useCallback(
|
||||
(toolCall: MCPToolResponse) => {
|
||||
handleRerun(toolCall, editedParams)
|
||||
setEditingToolId(null)
|
||||
setEditedParams('')
|
||||
},
|
||||
[editedParams, handleRerun]
|
||||
)
|
||||
|
||||
const handleParamsChange = useCallback((newParams: string) => {
|
||||
setEditedParams(newParams)
|
||||
}, [])
|
||||
// --- End Handlers ---
|
||||
|
||||
// --- Listener for Rerun Updates & Persistence ---
|
||||
useEffect(() => {
|
||||
const cleanupListener = window.api.mcp.onToolRerunUpdate((update) => {
|
||||
if (update.messageId !== message.id) return // Ignore updates for other messages
|
||||
|
||||
console.log('[MessageContent] Received rerun update:', update)
|
||||
|
||||
// --- Update Local State for Immediate UI Feedback ---
|
||||
setLocalToolResponses((currentLocalResponses) => {
|
||||
return currentLocalResponses.map((toolCall) => {
|
||||
if (toolCall.id === update.toolCallId) {
|
||||
let updatedCall: MCPToolResponse
|
||||
switch (update.status) {
|
||||
case 'rerunning':
|
||||
// Note: 'rerunning' status from IPC translates to 'invoking' in UI
|
||||
updatedCall = { ...toolCall, status: 'invoking', args: update.args, response: undefined }
|
||||
break
|
||||
case 'done':
|
||||
updatedCall = {
|
||||
...toolCall,
|
||||
status: 'done',
|
||||
response: update.response,
|
||||
// Persist the args used for the successful rerun
|
||||
args: update.args !== undefined ? update.args : toolCall.args
|
||||
}
|
||||
break
|
||||
case 'error':
|
||||
updatedCall = {
|
||||
...toolCall,
|
||||
status: 'done', // Keep UI status as 'done' even on error
|
||||
response: { content: [{ type: 'text', text: update.error }], isError: true },
|
||||
// Persist the args used for the failed rerun
|
||||
args: update.args !== undefined ? update.args : toolCall.args
|
||||
}
|
||||
break
|
||||
default:
|
||||
updatedCall = toolCall // Should not happen
|
||||
}
|
||||
return updatedCall
|
||||
}
|
||||
return toolCall
|
||||
})
|
||||
})
|
||||
// --- End Local State Update ---
|
||||
|
||||
// --- Persist Changes to Global Store and DB (only on final states) ---
|
||||
if (update.status === 'done' || update.status === 'error') {
|
||||
// IMPORTANT: Use the message prop directly to get the state *before* this update cycle
|
||||
const previousMcpTools = message.metadata?.mcpTools || []
|
||||
console.log(
|
||||
'[MessageContent Persistence] Previous MCP Tools from message.metadata:',
|
||||
JSON.stringify(previousMcpTools, null, 2)
|
||||
) // Log previous state
|
||||
|
||||
const updatedMcpToolsForPersistence = previousMcpTools.map((toolCall) => {
|
||||
if (toolCall.id === update.toolCallId) {
|
||||
console.log(
|
||||
`[MessageContent Persistence] Updating tool ${toolCall.id} with status ${update.status}, args:`,
|
||||
update.args,
|
||||
'response:',
|
||||
update.response || update.error
|
||||
) // Log update details
|
||||
// Apply the final state directly from the update object
|
||||
return {
|
||||
...toolCall, // Keep existing id, tool info
|
||||
status: 'done', // Final status is always 'done' for persistence
|
||||
args: update.args !== undefined ? update.args : toolCall.args, // Persist the args used for the rerun
|
||||
response:
|
||||
update.status === 'error'
|
||||
? { content: [{ type: 'text', text: update.error }], isError: true } // Create error response object
|
||||
: update.response // Use the successful response
|
||||
}
|
||||
}
|
||||
return toolCall // Keep other tool calls as they were
|
||||
})
|
||||
|
||||
console.log(
|
||||
'[MessageContent Persistence] Calculated MCP Tools for Persistence:',
|
||||
JSON.stringify(updatedMcpToolsForPersistence, null, 2)
|
||||
) // Log calculated state
|
||||
|
||||
// Dispatch the thunk to update the message globally
|
||||
// Ensure we have the necessary IDs
|
||||
if (message.topicId && message.id) {
|
||||
console.log(
|
||||
`[MessageContent Persistence] Dispatching updateMessageThunk for message ${message.id} in topic ${message.topicId}`
|
||||
) // Log dispatch attempt
|
||||
window.api.store.dispatch(
|
||||
window.api.store.updateMessageThunk(message.topicId, message.id, {
|
||||
metadata: {
|
||||
...message.metadata, // Keep other metadata
|
||||
mcpTools: updatedMcpToolsForPersistence // Provide the correctly calculated final array
|
||||
}
|
||||
})
|
||||
)
|
||||
console.log(
|
||||
'[MessageContent] Dispatched updateMessageThunk with calculated persistence data for tool:',
|
||||
update.toolCallId
|
||||
)
|
||||
} else {
|
||||
console.error('[MessageContent] Missing topicId or messageId, cannot dispatch update.')
|
||||
}
|
||||
}
|
||||
// --- End Persistence Logic ---
|
||||
})
|
||||
|
||||
return () => cleanupListener()
|
||||
// Ensure all necessary dependencies are included
|
||||
}, [message.id, message.topicId, message.metadata]) // message.metadata is crucial here
|
||||
// --- End Listener ---
|
||||
|
||||
// 监听分段播放状态变化
|
||||
useEffect(() => {
|
||||
const handleSegmentedPlaybackUpdate = (event: CustomEvent) => {
|
||||
@ -101,12 +308,16 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
}, [message.metadata?.citations, message.metadata?.annotations, model])
|
||||
|
||||
// 获取引用数据
|
||||
// https://github.com/CherryHQ/cherry-studio/issues/5234#issuecomment-2824704499
|
||||
const citationsData = useMemo(() => {
|
||||
const citationUrls =
|
||||
Array.isArray(message.metadata?.citations) &&
|
||||
(message?.metadata?.annotations?.map((annotation) => annotation.url_citation) ?? [])
|
||||
const searchResults =
|
||||
message?.metadata?.webSearch?.results ||
|
||||
message?.metadata?.webSearchInfo ||
|
||||
message?.metadata?.groundingMetadata?.groundingChunks?.map((chunk) => chunk?.web) ||
|
||||
message?.metadata?.annotations?.map((annotation) => annotation.url_citation) ||
|
||||
citationUrls ||
|
||||
[]
|
||||
const citationsUrls = formattedCitations || []
|
||||
|
||||
@ -139,7 +350,8 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
message?.metadata?.annotations,
|
||||
message?.metadata?.groundingMetadata?.groundingChunks,
|
||||
message?.metadata?.webSearch?.results,
|
||||
message?.metadata?.webSearchInfo
|
||||
message?.metadata?.webSearchInfo,
|
||||
message.metadata?.citations // Added missing dependency
|
||||
// knowledge 依赖已移除,因为它在 useMemo 中没有被使用
|
||||
])
|
||||
|
||||
@ -320,14 +532,40 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 处理 MCP 工具调用标记
|
||||
// 处理 MCP 工具调用标记
|
||||
// 处理 MCP 工具调用标记
|
||||
console.log('[MessageContent] Original message content:', message.content) // Log original content
|
||||
const toolResponses = message.metadata?.mcpTools || []
|
||||
console.log('[MessageContent] Tool responses from metadata:', toolResponses) // Log tool responses
|
||||
if (toolResponses.length > 0) {
|
||||
let toolIndex = 0
|
||||
// 使用更通用的正则表达式匹配 <tool_use>...</tool_use> 块
|
||||
const toolTagRegex = /<tool_use>[\s\S]*?<\/tool_use>/gi
|
||||
const matches = Array.from(content.matchAll(toolTagRegex)) // Get all matches
|
||||
console.log('[MessageContent] Regex matches for tool tags:', matches) // Log matches
|
||||
|
||||
content = content.replace(toolTagRegex, (match) => {
|
||||
if (toolIndex < toolResponses.length) {
|
||||
const toolCall = toolResponses[toolIndex]
|
||||
toolIndex++
|
||||
console.log(`[MessageContent] Replacing match ${toolIndex} with tool-block id="${toolCall.id}"`) // Log replacement
|
||||
return `<tool-block id="${toolCall.id}"></tool-block>`
|
||||
}
|
||||
// 如果工具响应数量与标记数量不匹配,记录警告并返回原始匹配
|
||||
console.warn('[MessageContent] Mismatch between tool tags and tool responses. Returning original tag:', match)
|
||||
return match
|
||||
})
|
||||
console.log('[MessageContent] Content after tool tag replacement:', content) // Log content after replacement
|
||||
}
|
||||
|
||||
return content
|
||||
}, [
|
||||
message.metadata?.citations,
|
||||
message.metadata?.webSearch,
|
||||
message.metadata?.knowledge,
|
||||
// 移除不必要的依赖
|
||||
// message.metadata?.webSearchInfo,
|
||||
// message.metadata?.annotations,
|
||||
message.metadata?.mcpTools, // Add mcpTools as dependency
|
||||
message.content,
|
||||
citationsData
|
||||
])
|
||||
@ -356,7 +594,23 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
|
||||
if (message.type === '@' && model) {
|
||||
const content = `[@${model.name}](#) ${getBriefInfo(message.content)}`
|
||||
return <Markdown message={{ ...message, content }} />
|
||||
return (
|
||||
<Markdown
|
||||
message={{ ...message, content, metadata: message.metadata || {} }} // Ensure metadata is included
|
||||
toolResponses={localToolResponses} // 传递工具响应数据 (使用 local state)
|
||||
activeToolKeys={activeKeys} // 传递 activeKeys 状态
|
||||
copiedToolMap={copiedMap} // 传递 copiedMap 状态
|
||||
editingToolId={editingToolId} // 传递 editingToolId 状态
|
||||
editedToolParamsString={editedParams} // 传递 editedParams 状态
|
||||
onToolToggle={setActiveKeys} // 传递 setActiveKeys 函数
|
||||
onToolCopy={copyContent} // 传递 copyContent 函数
|
||||
onToolRerun={handleRerun} // 传递 handleRerun 函数
|
||||
onToolEdit={handleEdit} // 传递 handleEdit 函数
|
||||
onToolSave={handleSaveEdit} // 传递 handleSaveEdit 函数
|
||||
onToolCancel={handleCancelEdit} // 传递 handleCancelEdit 函数
|
||||
onToolParamsChange={handleParamsChange} // 传递 onToolParamsChange 函数
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// --- MODIFIED LINE BELOW ---
|
||||
@ -461,18 +715,28 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div className="message-content-tools">
|
||||
{/* Only display thought info at the top */}
|
||||
<MessageThought message={message} />
|
||||
{/* Render MessageTools to display tool blocks based on metadata */}
|
||||
<MessageTools message={message} />
|
||||
</div>
|
||||
{/* Only display thought info at the top */}
|
||||
<MessageThought message={message} />
|
||||
{isSegmentedPlayback ? (
|
||||
// Apply regex replacement here for TTS
|
||||
<TTSHighlightedText text={processedContent.replace(tagsToRemoveRegex, '')} />
|
||||
) : (
|
||||
// Remove tool_use XML tags before rendering Markdown
|
||||
<Markdown message={{ ...message, content: processedContent.replace(tagsToRemoveRegex, '') }} />
|
||||
// Render Markdown with tool blocks
|
||||
<Markdown
|
||||
message={{ ...message, content: processedContent }} // 传递包含占位符的内容
|
||||
toolResponses={localToolResponses} // 传递工具响应数据 (使用 local state)
|
||||
activeToolKeys={activeKeys} // 传递 activeKeys 状态
|
||||
copiedToolMap={copiedMap} // 传递 copiedMap 状态
|
||||
editingToolId={editingToolId} // 传递 editingToolId 状态
|
||||
editedToolParamsString={editedParams} // 传递 editedParams 状态
|
||||
onToolToggle={setActiveKeys} // 传递 setActiveKeys 函数
|
||||
onToolCopy={copyContent} // 传递 copyContent 函数
|
||||
onToolRerun={handleRerun} // 传递 handleRerun 函数
|
||||
onToolEdit={handleEdit} // 传递 handleEdit 函数
|
||||
onToolSave={handleSaveEdit} // 传递 handleSaveEdit 函数
|
||||
onToolCancel={handleCancelEdit} // 传递 handleCancelEdit 函数
|
||||
onToolParamsChange={handleParamsChange} // 传递 onToolParamsChange 函数
|
||||
/>
|
||||
)}
|
||||
{message.metadata?.generateImage && <MessageImage message={message} />}
|
||||
{message.translatedContent && (
|
||||
@ -484,7 +748,21 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginBottom: 5 }} />
|
||||
) : (
|
||||
// Render translated content (assuming it doesn't need tag removal, adjust if needed)
|
||||
<Markdown message={{ ...message, content: message.translatedContent }} />
|
||||
<Markdown
|
||||
message={{ ...message, content: message.translatedContent }}
|
||||
toolResponses={localToolResponses} // 传递工具响应数据 (使用 local state)
|
||||
activeToolKeys={activeKeys} // 传递 activeKeys 状态
|
||||
copiedToolMap={copiedMap} // 传递 copiedMap 状态
|
||||
editingToolId={editingToolId} // 传递 editingToolId 状态
|
||||
editedToolParamsString={editedParams} // 传递 editedParams 状态
|
||||
onToolToggle={setActiveKeys} // 传递 setActiveKeys 函数
|
||||
onToolCopy={copyContent} // 传递 copyContent 函数
|
||||
onToolRerun={handleRerun} // 传递 handleRerun 函数
|
||||
onToolEdit={handleEdit} // 传递 handleEdit 函数
|
||||
onToolSave={handleSaveEdit} // 传递 handleSaveEdit 函数
|
||||
onToolCancel={handleCancelEdit} // 传递 handleCancelEdit 函数
|
||||
onToolParamsChange={handleParamsChange} // 传递 onToolParamsChange 函数
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@ -1,37 +1,17 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
ExpandAltOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
WarningOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateMessageThunk } from '@renderer/store/messages'
|
||||
import { MCPToolResponse, Message } from '@renderer/types'
|
||||
import { message as antdMessage, Tooltip } from 'antd' // Removed Modal
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' // Removed useLayoutEffect, useRef
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
// Removed Modal
|
||||
import { FC, memo, useEffect, useState } from 'react' // Removed useLayoutEffect, useRef
|
||||
|
||||
import CustomCollapse from './CustomCollapse'
|
||||
// Removed ExpandedResponseContent import
|
||||
import ToolResponseContent from './ToolResponseContent'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageTools: FC<Props> = ({ message }) => {
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>([])
|
||||
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
|
||||
// Removed expandedResponse state
|
||||
const [editingToolId, setEditingToolId] = useState<string | null>(null)
|
||||
const [editedParams, setEditedParams] = useState<string>('')
|
||||
const { t } = useTranslation()
|
||||
const { messageFont } = useSettings() // Removed fontSize
|
||||
// Removed state and handlers related to tool block rendering
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Local state for immediate UI updates, synced with message metadata
|
||||
@ -47,91 +27,8 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
}
|
||||
}, [message.metadata?.mcpTools]) // Removed localToolResponses from dependency array
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif'
|
||||
? 'serif'
|
||||
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'
|
||||
}, [messageFont])
|
||||
|
||||
const copyContent = useCallback(
|
||||
(content: string, toolId: string) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopiedMap((prev) => ({ ...prev, [toolId]: true }))
|
||||
setTimeout(() => setCopiedMap((prev) => ({ ...prev, [toolId]: false })), 2000)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
// --- Handlers for Edit/Rerun ---
|
||||
const handleRerun = useCallback(
|
||||
(toolCall: MCPToolResponse, currentParamsString: string) => {
|
||||
console.log('Rerunning tool:', toolCall.id, 'with params:', currentParamsString)
|
||||
try {
|
||||
const paramsToRun = JSON.parse(currentParamsString)
|
||||
|
||||
// Proactively update local state for immediate UI feedback
|
||||
setLocalToolResponses((prevResponses) =>
|
||||
prevResponses.map((tc) =>
|
||||
tc.id === toolCall.id ? { ...tc, args: paramsToRun, status: 'invoking', response: undefined } : tc
|
||||
)
|
||||
)
|
||||
|
||||
const serverConfig = message.enabledMCPs?.find((server) => server.id === toolCall.tool.serverId)
|
||||
if (!serverConfig) {
|
||||
console.error(`[MessageTools] Server config not found for ID ${toolCall.tool.serverId}`)
|
||||
antdMessage.error({ content: t('common.rerun_failed_server_not_found'), key: 'rerun-tool' })
|
||||
return
|
||||
}
|
||||
|
||||
window.api.mcp
|
||||
.rerunTool(message.id, toolCall.id, serverConfig, toolCall.tool.name, paramsToRun)
|
||||
.then(() => antdMessage.success({ content: t('common.rerun_started'), key: 'rerun-tool' }))
|
||||
.catch((err) => {
|
||||
console.error('Rerun failed:', err)
|
||||
antdMessage.error({ content: t('common.rerun_failed'), key: 'rerun-tool' })
|
||||
// Optionally revert local state on failure
|
||||
setLocalToolResponses(
|
||||
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
|
||||
)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON parameters for rerun:', e)
|
||||
antdMessage.error(t('common.invalid_json'))
|
||||
// Revert local state if JSON parsing fails
|
||||
setLocalToolResponses(
|
||||
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
|
||||
)
|
||||
}
|
||||
},
|
||||
[message.id, message.enabledMCPs, t, dispatch] // Added dispatch
|
||||
)
|
||||
|
||||
const handleEdit = useCallback((toolCall: MCPToolResponse) => {
|
||||
setEditingToolId(toolCall.id)
|
||||
setEditedParams(JSON.stringify(toolCall.args || {}, null, 2))
|
||||
}, [])
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setEditingToolId(null)
|
||||
setEditedParams('')
|
||||
}, [])
|
||||
|
||||
const handleSaveEdit = useCallback(
|
||||
(toolCall: MCPToolResponse) => {
|
||||
handleRerun(toolCall, editedParams)
|
||||
setEditingToolId(null)
|
||||
setEditedParams('')
|
||||
},
|
||||
[editedParams, handleRerun]
|
||||
)
|
||||
|
||||
const handleParamsChange = useCallback((newParams: string) => {
|
||||
setEditedParams(newParams)
|
||||
}, [])
|
||||
// --- End Handlers ---
|
||||
|
||||
// --- Listener for Rerun Updates & Persistence ---
|
||||
// Keep the listener as it updates the message metadata in the store
|
||||
useEffect(() => {
|
||||
const cleanupListener = window.api.mcp.onToolRerunUpdate((update) => {
|
||||
if (update.messageId !== message.id) return // Ignore updates for other messages
|
||||
@ -139,42 +36,8 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
console.log('[MessageTools] Received rerun update:', update)
|
||||
|
||||
// --- Update Local State for Immediate UI Feedback ---
|
||||
setLocalToolResponses((currentLocalResponses) => {
|
||||
return currentLocalResponses.map((toolCall) => {
|
||||
if (toolCall.id === update.toolCallId) {
|
||||
let updatedCall: MCPToolResponse
|
||||
switch (update.status) {
|
||||
case 'rerunning':
|
||||
// Note: 'rerunning' status from IPC translates to 'invoking' in UI
|
||||
updatedCall = { ...toolCall, status: 'invoking', args: update.args, response: undefined }
|
||||
break
|
||||
case 'done':
|
||||
updatedCall = {
|
||||
...toolCall,
|
||||
status: 'done',
|
||||
response: update.response,
|
||||
// Persist the args used for the successful rerun
|
||||
args: update.args !== undefined ? update.args : toolCall.args
|
||||
}
|
||||
break
|
||||
case 'error':
|
||||
updatedCall = {
|
||||
...toolCall,
|
||||
status: 'done', // Keep UI status as 'done' even on error
|
||||
response: { content: [{ type: 'text', text: update.error }], isError: true },
|
||||
// Persist the args used for the failed rerun
|
||||
args: update.args !== undefined ? update.args : toolCall.args
|
||||
}
|
||||
break
|
||||
default:
|
||||
updatedCall = toolCall // Should not happen
|
||||
}
|
||||
return updatedCall
|
||||
}
|
||||
return toolCall
|
||||
})
|
||||
})
|
||||
// --- End Local State Update ---
|
||||
// This part is no longer needed in MessageTools as state is in MessageContent
|
||||
// setLocalToolResponses((currentLocalResponses) => { ... });
|
||||
|
||||
// --- Persist Changes to Global Store and DB (only on final states) ---
|
||||
if (update.status === 'done' || update.status === 'error') {
|
||||
@ -242,235 +105,12 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
}, [message.id, message.topicId, message.metadata, dispatch]) // message.metadata is crucial here
|
||||
// --- End Listener ---
|
||||
|
||||
// Use localToolResponses for rendering
|
||||
const toolResponses = localToolResponses
|
||||
|
||||
// Removed responseStringsRef and its useLayoutEffect
|
||||
|
||||
// Memoize collapse items
|
||||
const collapseItems = useMemo(() => {
|
||||
return toolResponses.map((toolResponse) => {
|
||||
const { id, tool, args, status, response } = toolResponse
|
||||
const isInvoking = status === 'invoking'
|
||||
const isDone = status === 'done'
|
||||
const hasError = isDone && response?.isError === true
|
||||
const params = args || {}
|
||||
const toolResult = response
|
||||
|
||||
return {
|
||||
key: id,
|
||||
label: (
|
||||
<MessageTitleLabel>
|
||||
<TitleContent>
|
||||
<ToolName>{tool.name}</ToolName>
|
||||
<StatusIndicator $isInvoking={isInvoking} $hasError={hasError}>
|
||||
{isInvoking
|
||||
? t('message.tools.invoking')
|
||||
: hasError
|
||||
? t('message.tools.error')
|
||||
: t('message.tools.completed')}
|
||||
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
||||
{isDone && !hasError && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||
{hasError && <WarningOutlined style={{ marginLeft: 6 }} />}
|
||||
</StatusIndicator>
|
||||
</TitleContent>
|
||||
<ActionButtonsContainer>
|
||||
{isDone && response && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={activeKeys.includes(id) ? t('common.collapse') : t('common.expand')}
|
||||
mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// Toggle the active key for this item
|
||||
setActiveKeys((prev) => (prev.includes(id) ? prev.filter((k) => k !== id) : [...prev, id]))
|
||||
}}>
|
||||
<ExpandAltOutlined />
|
||||
{activeKeys.includes(id) ? t('common.collapse') : t('common.expand')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.rerun')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const paramsToRun = editingToolId === id ? editedParams : JSON.stringify(args || {}, null, 2)
|
||||
handleRerun(toolResponse, paramsToRun)
|
||||
}}>
|
||||
<ReloadOutlined />
|
||||
{t('common.rerun')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleEdit(toolResponse)
|
||||
if (!activeKeys.includes(id)) setActiveKeys((prev) => [...prev, id])
|
||||
}}>
|
||||
<EditOutlined />
|
||||
{t('common.edit')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const combinedData = { params: params, response: toolResult }
|
||||
copyContent(JSON.stringify(combinedData, null, 2), id)
|
||||
}}>
|
||||
{copiedMap[id] ? <CheckOutlined /> : <CopyOutlined />}
|
||||
{copiedMap[id] ? t('common.copied') : t('common.copy')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</ActionButtonsContainer>
|
||||
</MessageTitleLabel>
|
||||
),
|
||||
children: isDone ? (
|
||||
<ToolResponseContent
|
||||
params={params} // Use derived params
|
||||
response={toolResult}
|
||||
fontFamily={fontFamily}
|
||||
fontSize="12px"
|
||||
isEditing={editingToolId === id}
|
||||
editedParamsString={editedParams}
|
||||
onParamsChange={handleParamsChange}
|
||||
onSave={() => handleSaveEdit(toolResponse)}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
})
|
||||
}, [
|
||||
toolResponses,
|
||||
t,
|
||||
copiedMap,
|
||||
copyContent,
|
||||
editingToolId,
|
||||
editedParams,
|
||||
handleEdit,
|
||||
handleRerun,
|
||||
handleSaveEdit,
|
||||
handleCancelEdit,
|
||||
handleParamsChange,
|
||||
activeKeys,
|
||||
fontFamily // Added fontFamily
|
||||
])
|
||||
|
||||
if (toolResponses.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolsContainer className="message-tools-container">
|
||||
{collapseItems.map((item) => (
|
||||
<CustomCollapse
|
||||
key={item.key}
|
||||
id={item.key as string}
|
||||
title={item.label}
|
||||
isActive={activeKeys.includes(item.key as string)}
|
||||
onToggle={() => {
|
||||
setActiveKeys((prev) =>
|
||||
prev.includes(item.key as string) ? prev.filter((k) => k !== item.key) : [...prev, item.key as string]
|
||||
)
|
||||
}}>
|
||||
{item.children}
|
||||
</CustomCollapse>
|
||||
))}
|
||||
</ToolsContainer>
|
||||
|
||||
{/* Removed Modal component */}
|
||||
</>
|
||||
)
|
||||
// MessageTools component no longer renders the tool blocks directly.
|
||||
// It only needs to keep the listener for persistence.
|
||||
// It returns null as it doesn't render any visible elements itself anymore.
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Styled Components --- (Keep existing styled components definitions)
|
||||
const MessageTitleLabel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const TitleContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ToolName = styled.span`
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.span<{ $isInvoking: boolean; $hasError?: boolean }>`
|
||||
color: ${(props) => {
|
||||
if (props.$hasError) return 'var(--color-error, #ff4d4f)'
|
||||
if (props.$isInvoking) return 'var(--color-primary)'
|
||||
return 'var(--color-success, #52c41a)'
|
||||
}};
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.85;
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding-left: 8px;
|
||||
`
|
||||
|
||||
const ActionButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
`
|
||||
|
||||
const ToolsContainer = styled.div`
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-1);
|
||||
border: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const ActionButton = styled.button`
|
||||
background: none;
|
||||
border: 1px solid var(--color-border); /* Add border */
|
||||
color: var(--color-text); /* Use primary text color for better contrast */
|
||||
cursor: pointer;
|
||||
padding: 1px 5px; /* Adjust padding for border */
|
||||
font-size: 12px; /* Smaller font size */
|
||||
font-weight: 500; /* Increase font weight */
|
||||
display: inline-flex; /* Use inline-flex for icon + text */
|
||||
align-items: center;
|
||||
gap: 4px; /* Add gap between icon and text */
|
||||
justify-content: center;
|
||||
user-select: none; /* Prevent text selection */
|
||||
opacity: 0.8; /* Slightly increase opacity */
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-1); /* Use a subtle background on hover */
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
// --- End Styled Components ---
|
||||
// --- Styled Components --- (Removed unused styled components)
|
||||
|
||||
export default memo(MessageTools)
|
||||
|
||||
251
src/renderer/src/pages/home/Messages/SingleToolCallBlock.tsx
Normal file
251
src/renderer/src/pages/home/Messages/SingleToolCallBlock.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
ExpandAltOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
WarningOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { MCPToolResponse } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import CustomCollapse from './CustomCollapse'
|
||||
import ToolResponseContent from './ToolResponseContent'
|
||||
|
||||
interface Props {
|
||||
toolResponse: MCPToolResponse
|
||||
isActive: boolean
|
||||
isCopied: boolean
|
||||
isEditing: boolean
|
||||
editedParamsString: string
|
||||
fontFamily: string
|
||||
t: any // Use any type for t to avoid TFunction error
|
||||
onToggle: () => void
|
||||
onCopy: (content: string, toolId: string) => void
|
||||
onRerun: (toolCall: MCPToolResponse, currentParamsString: string) => void
|
||||
onEdit: (toolCall: MCPToolResponse) => void
|
||||
onSave: () => void // Changed from (toolCall: MCPToolResponse) => void to match ToolResponseContent
|
||||
onCancel: () => void
|
||||
onParamsChange: (newParams: string) => void
|
||||
}
|
||||
|
||||
const SingleToolCallBlock: FC<Props> = ({
|
||||
toolResponse,
|
||||
isActive,
|
||||
isCopied,
|
||||
isEditing,
|
||||
editedParamsString,
|
||||
fontFamily,
|
||||
t,
|
||||
onToggle,
|
||||
onCopy,
|
||||
onRerun,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onParamsChange
|
||||
}) => {
|
||||
console.log('[SingleToolCallBlock] Rendering for ID:', toolResponse.id, 'isActive:', isActive) // Log isActive prop
|
||||
const { id, tool, args, status, response } = toolResponse
|
||||
const isInvoking = status === 'invoking'
|
||||
const isDone = status === 'done'
|
||||
const hasError = isDone && response?.isError === true
|
||||
const params = args || {}
|
||||
const toolResult = response
|
||||
|
||||
const handleCopyClick = () => {
|
||||
const combinedData = { params: params, response: toolResult }
|
||||
onCopy(JSON.stringify(combinedData, null, 2), id)
|
||||
}
|
||||
|
||||
const handleRerunClick = () => {
|
||||
const paramsToRun = isEditing ? editedParamsString : JSON.stringify(args || {}, null, 2)
|
||||
onRerun(toolResponse, paramsToRun)
|
||||
}
|
||||
|
||||
const handleEditClick = () => {
|
||||
onEdit(toolResponse)
|
||||
if (!isActive) onToggle() // Expand if not active
|
||||
}
|
||||
|
||||
const handleSaveClick = () => {
|
||||
onSave() // Changed from onSave(toolResponse) to onSave()
|
||||
}
|
||||
|
||||
const handleCancelClick = () => {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
const handleParamsChangeClick = (e: any) => {
|
||||
// Removed React.ChangeEvent<HTMLTextAreaElement> type
|
||||
onParamsChange(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomCollapse
|
||||
key={id}
|
||||
id={id}
|
||||
title={
|
||||
<MessageTitleLabel>
|
||||
<TitleContent>
|
||||
<ToolName>{tool.name}</ToolName>
|
||||
<StatusIndicator $isInvoking={isInvoking} $hasError={hasError}>
|
||||
{isInvoking
|
||||
? t('message.tools.invoking')
|
||||
: hasError
|
||||
? t('message.tools.error')
|
||||
: t('message.tools.completed')}
|
||||
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
||||
{isDone && !hasError && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||
{hasError && <WarningOutlined style={{ marginLeft: 6 }} />}
|
||||
</StatusIndicator>
|
||||
</TitleContent>
|
||||
<ActionButtonsContainer>
|
||||
{isDone && response && (
|
||||
<>
|
||||
<Tooltip title={isActive ? t('common.collapse') : t('common.expand')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
console.log('[SingleToolCallBlock] Expand/Collapse button clicked for ID:', id) // Log button click
|
||||
onToggle()
|
||||
console.log('[SingleToolCallBlock] onToggle prop called for ID:', id) // Log onToggle call
|
||||
}}>
|
||||
<ExpandAltOutlined />
|
||||
{isActive ? t('common.collapse') : t('common.expand')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.rerun')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
console.log('[SingleToolCallBlock] Rerun button clicked for ID:', id) // Log button click
|
||||
handleRerunClick()
|
||||
}}>
|
||||
<ReloadOutlined />
|
||||
{t('common.rerun')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
console.log('[SingleToolCallBlock] Edit button clicked for ID:', id) // Log button click
|
||||
handleEditClick()
|
||||
}}>
|
||||
<EditOutlined />
|
||||
{t('common.edit')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
console.log('[SingleToolCallBlock] Copy button clicked for ID:', id) // Log button click
|
||||
handleCopyClick()
|
||||
}}>
|
||||
{isCopied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
{isCopied ? t('common.copied') : t('common.copy')}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</ActionButtonsContainer>
|
||||
</MessageTitleLabel>
|
||||
}
|
||||
isActive={isActive}
|
||||
onToggle={onToggle}>
|
||||
{isDone ? (
|
||||
<ToolResponseContent
|
||||
params={params}
|
||||
response={toolResult}
|
||||
fontFamily={fontFamily}
|
||||
fontSize="12px"
|
||||
isEditing={isEditing}
|
||||
editedParamsString={editedParamsString}
|
||||
onParamsChange={handleParamsChangeClick}
|
||||
onSave={handleSaveClick}
|
||||
onCancel={handleCancelClick}
|
||||
/>
|
||||
) : null}
|
||||
</CustomCollapse>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Styled Components ---
|
||||
const MessageTitleLabel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 26px;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const TitleContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ToolName = styled.span`
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.span<{ $isInvoking?: boolean; $hasError?: boolean }>`
|
||||
font-size: 12px;
|
||||
color: ${(props) =>
|
||||
props.$isInvoking ? 'var(--color-warning)' : props.$hasError ? 'var(--color-error)' : 'var(--color-success)'};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ActionButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const ActionButton = styled.button`
|
||||
background: none;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(SingleToolCallBlock)
|
||||
@ -1,15 +1,61 @@
|
||||
import { Button, Input } from 'antd' // Import Button and Input
|
||||
import { FC, memo, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import React, { FC, memo, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs' // Choose a style
|
||||
import styled, { createGlobalStyle } from 'styled-components'
|
||||
|
||||
// 添加全局样式,在合适位置换行
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
/* 代码块容器样式 */
|
||||
.tool-response-syntax-highlighter {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* 代码块内容样式 */
|
||||
.tool-response-syntax-highlighter pre {
|
||||
white-space: pre-wrap !important; /* 允许在空白处换行 */
|
||||
word-break: normal !important; /* 使用normal而不是break-all,避免过度换行 */
|
||||
overflow-wrap: anywhere !important; /* 在必要时才换行 */
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 代码和span元素样式 */
|
||||
.tool-response-syntax-highlighter code,
|
||||
.tool-response-syntax-highlighter code span {
|
||||
white-space: inherit !important; /* 继承父元素的white-space */
|
||||
word-break: inherit !important; /* 继承父元素的word-break */
|
||||
overflow-wrap: inherit !important; /* 继承父元素的overflow-wrap */
|
||||
}
|
||||
|
||||
/* 只对超长字符串应用break-all */
|
||||
.tool-response-syntax-highlighter .token.string {
|
||||
word-break: break-word !important; /* 使用break-word而不是break-all */
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
/* 确保JSON结构保持完整 */
|
||||
.tool-response-syntax-highlighter .token.punctuation,
|
||||
.tool-response-syntax-highlighter .token.operator {
|
||||
white-space: pre !important; /* 保持标点符号不换行 */
|
||||
}
|
||||
`
|
||||
|
||||
// --- Styled Components Definitions ---
|
||||
|
||||
// Add FlexContainer style and modify Section style
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row; // 保持左右布局
|
||||
gap: 16px;
|
||||
align-items: stretch; /* Ensure items stretch to fill height */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column; // 在小屏幕上改为纵向布局
|
||||
}
|
||||
`
|
||||
|
||||
// Add Divider style
|
||||
@ -23,21 +69,50 @@ const Section = styled.div<{ flexBasis?: string }>`
|
||||
flex: 1; /* Allow sections to grow/shrink */
|
||||
flex-basis: ${(props) => props.flexBasis || 'auto'}; /* Set flex-basis if provided */
|
||||
min-width: 0; /* Prevent overflow issues with flex items */
|
||||
max-width: ${(props) => props.flexBasis || 'auto'}; /* 限制最大宽度,防止内容溢出 */
|
||||
border: 1px solid var(--color-border); /* 减小边框粗细 */
|
||||
border-radius: 6px; /* 增加圆角 */
|
||||
padding: 12px; /* 增加内边距 */
|
||||
background-color: var(--color-bg-2); /* 添加轻微的背景色区分 */
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* 添加轻微阴影 */
|
||||
transition: all 0.2s ease; /* 添加过渡效果 */
|
||||
overflow: hidden; /* 确保内容不会溢出 */
|
||||
|
||||
/* 确保Section内的内容适当换行 */
|
||||
* {
|
||||
max-width: 100%;
|
||||
overflow-wrap: normal;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); /* 悬停时增强阴影 */
|
||||
}
|
||||
`
|
||||
|
||||
const SectionLabel = styled.div`
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px; /* Slightly smaller label */
|
||||
font-weight: 600;
|
||||
color: var(--color-primary); /* 使用主题色 */
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px; /* 增大标签字体 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background-color: var(--color-primary);
|
||||
margin-right: 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const ToolResponseContainer = styled.div`
|
||||
background: var(--color-bg-1);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 12px 16px;
|
||||
/* overflow: auto; Remove overflow here, let sections handle scrolling if needed */
|
||||
/* max-height: 300px; Remove fixed max-height for the container */
|
||||
border-radius: 8px; /* 增加圆角 */
|
||||
padding: 16px; /* 增加内边距 */
|
||||
border-top: none;
|
||||
position: relative;
|
||||
will-change: transform; /* 优化渲染性能 */
|
||||
@ -48,32 +123,37 @@ const ToolResponseContainer = styled.div`
|
||||
-webkit-perspective: 1000;
|
||||
contain: content; /* 限制重绘范围 */
|
||||
background-color: var(--color-bg-1); /* 确保背景色 */
|
||||
max-width: 100%; /* 确保不超出父容器 */
|
||||
`
|
||||
|
||||
const CodeBlock = styled.pre`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--color-text);
|
||||
font-family: ubuntu;
|
||||
contain: content;
|
||||
max-height: 280px; /* Limit height of code block */
|
||||
overflow-y: auto; /* Add scrollbar if content exceeds max height */
|
||||
/* transform: translateZ(0); 移除硬件加速,可能导致编辑时闪烁 */
|
||||
backface-visibility: hidden; /* 使用 GPU 加速 */
|
||||
-webkit-backface-visibility: hidden;
|
||||
`
|
||||
// Removed CodeBlock styled component as we will use SyntaxHighlighter
|
||||
|
||||
const LoadingPlaceholder = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
height: 80px;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
transform: translateZ(0); /* 启用硬件加速 */
|
||||
backface-visibility: hidden; /* 使用 GPU 加速 */
|
||||
-webkit-backface-visibility: hidden;
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--color-border);
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// --- Component Definition ---
|
||||
@ -115,20 +195,63 @@ const ToolResponseContent: FC<ToolResponseContentProps> = ({
|
||||
useLayoutEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
// Stringify params, handle potential empty objects/null/undefined
|
||||
paramsStringRef.current =
|
||||
params && Object.keys(params).length > 0 ? JSON.stringify(params, null, 2) : t('message.tools.no_params') // Display message if no params
|
||||
// 处理参数
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
paramsStringRef.current = JSON.stringify(params, null, 2)
|
||||
} else {
|
||||
paramsStringRef.current = t('message.tools.no_params')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stringifying params:', error)
|
||||
paramsStringRef.current = String(params)
|
||||
}
|
||||
|
||||
// --- 核心修改部分:处理 responseStringRef ---
|
||||
let processedResponseString = String(response) // 默认回退到显示原始响应的字符串表示
|
||||
|
||||
try {
|
||||
// Stringify response
|
||||
responseStringRef.current = JSON.stringify(response, null, 2)
|
||||
if (response && typeof response === 'object') {
|
||||
// 尝试判断响应是否是 { content: [{ type: 'text', text: '...' }] } 的特定结构
|
||||
if (
|
||||
Array.isArray(response.content) &&
|
||||
response.content.length > 0 &&
|
||||
response.content[0].type === 'text' &&
|
||||
typeof response.content[0].text === 'string'
|
||||
) {
|
||||
const rawInnerText = response.content[0].text
|
||||
try {
|
||||
// ***尝试将提取到的内部字符串解析为 JSON 对象***
|
||||
const parsedInnerText = JSON.parse(rawInnerText)
|
||||
// ***如果解析成功,将解析后的对象重新格式化为带缩进的 JSON 字符串用于显示***
|
||||
processedResponseString = JSON.stringify(parsedInnerText, null, 2)
|
||||
console.log('Successfully parsed inner JSON:', processedResponseString) // 调试日志
|
||||
} catch (innerParseError) {
|
||||
// 如果内部字符串不是有效的 JSON 格式(或者解析失败)
|
||||
console.warn('Inner text is not valid JSON, displaying raw string literal:', rawInnerText) // 调试日志
|
||||
// 回退到显示原始内部字符串
|
||||
processedResponseString = rawInnerText
|
||||
}
|
||||
} else {
|
||||
// 如果响应结构不是预期的类型,则将整个响应对象格式化为 JSON 字符串显示
|
||||
processedResponseString = JSON.stringify(response, null, 2)
|
||||
}
|
||||
} else {
|
||||
// 如果响应不是对象类型,直接转换为字符串
|
||||
processedResponseString = String(response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stringifying response:', error)
|
||||
responseStringRef.current = String(response)
|
||||
console.error('Error processing response data structure:', error) // 调试日志
|
||||
// 在任何处理过程中出现错误,都尝试回退到将原始响应格式化为 JSON 字符串
|
||||
try {
|
||||
processedResponseString = JSON.stringify(response, null, 2)
|
||||
} catch (finalStringifyError) {
|
||||
// 如果连 JSON.stringify 都失败,回退到最原始的字符串表示
|
||||
processedResponseString = String(response)
|
||||
}
|
||||
}
|
||||
|
||||
// 将最终处理好的字符串赋值给 ref
|
||||
responseStringRef.current = processedResponseString
|
||||
setIsContentReady(true)
|
||||
}, 0)
|
||||
|
||||
@ -157,17 +280,18 @@ const ToolResponseContent: FC<ToolResponseContentProps> = ({
|
||||
// Render separate sections for params and response in a flex container
|
||||
return (
|
||||
<ToolResponseContainer ref={containerRef} style={{ fontFamily, fontSize }}>
|
||||
<GlobalStyle /> {/* 添加全局样式 */}
|
||||
{isVisible && isContentReady ? (
|
||||
<FlexContainer>
|
||||
<Section flexBasis="40%">
|
||||
<SectionLabel>{t('message.tools.parameters')}:</SectionLabel>
|
||||
<SectionLabel>{t('message.tools.parameters')}</SectionLabel>
|
||||
{isEditing ? (
|
||||
<EditContainer>
|
||||
<StyledTextArea
|
||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||
value={editedParamsString}
|
||||
onChange={(e) => onParamsChange(e.target.value)}
|
||||
style={{ fontFamily: 'ubuntu', fontSize: '12px' }} // Ensure consistent font
|
||||
style={{ fontFamily: 'ubuntu', fontSize: '13px' }} // 增大字体
|
||||
/>
|
||||
<EditActions>
|
||||
<Button size="small" onClick={onCancel}>
|
||||
@ -179,15 +303,75 @@ const ToolResponseContent: FC<ToolResponseContentProps> = ({
|
||||
</EditActions>
|
||||
</EditContainer>
|
||||
) : (
|
||||
<CodeBlock>{paramsStringRef.current}</CodeBlock>
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
className="tool-response-syntax-highlighter"
|
||||
style={atomOneDark} // Apply the chosen style
|
||||
customStyle={{
|
||||
margin: 0, // Remove margin
|
||||
whiteSpace: 'pre-wrap', // 允许在空白处换行
|
||||
wordBreak: 'normal', // 使用normal而不是break-all,避免过度换行
|
||||
maxHeight: '280px',
|
||||
overflowY: 'auto', // Add scrollbar
|
||||
overflowX: 'hidden', // 隐藏水平滚动条
|
||||
backgroundColor: 'transparent', // Use parent background
|
||||
borderRadius: '4px', // 添加圆角
|
||||
width: '100%', // 确保宽度100%
|
||||
maxWidth: '100%' // 限制最大宽度
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontFamily: fontFamily, // Use the provided font family
|
||||
fontSize: '14px', // Increase font size for better readability
|
||||
color: 'var(--color-text)', // Ensure text color is readable
|
||||
whiteSpace: 'pre-wrap', // 允许在空白处换行
|
||||
wordWrap: 'break-word', // 确保长单词换行
|
||||
wordBreak: 'normal' // 使用normal而不是break-all,避免过度换行
|
||||
}
|
||||
}}
|
||||
wrapLines={true} // 启用行包装
|
||||
wrapLongLines={true} // 包装长行
|
||||
lineProps={{ style: { whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }} // 为每一行添加样式
|
||||
>
|
||||
{paramsStringRef.current}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</Section>
|
||||
<Divider />
|
||||
<Section flexBasis="60%">
|
||||
{' '}
|
||||
{/* Adjust flex-basis to 60% */}
|
||||
<SectionLabel>{t('message.tools.results')}:</SectionLabel>
|
||||
<CodeBlock>{responseStringRef.current}</CodeBlock>
|
||||
<SectionLabel>{t('message.tools.results')}</SectionLabel>
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
className="tool-response-syntax-highlighter"
|
||||
style={atomOneDark} // Apply the chosen style
|
||||
customStyle={{
|
||||
margin: 0, // Remove margin
|
||||
whiteSpace: 'pre-wrap', // 允许在空白处换行
|
||||
wordBreak: 'normal', // 使用normal而不是break-all,避免过度换行
|
||||
maxHeight: '280px',
|
||||
overflowY: 'auto', // Add scrollbar
|
||||
overflowX: 'hidden', // 隐藏水平滚动条
|
||||
backgroundColor: 'transparent', // Use parent background
|
||||
borderRadius: '4px', // 添加圆角
|
||||
width: '100%', // 确保宽度100%
|
||||
maxWidth: '100%' // 限制最大宽度
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontFamily: fontFamily, // Use the provided font family
|
||||
fontSize: '14px', // Increase font size for better readability
|
||||
color: 'var(--color-text)', // Ensure text color is readable
|
||||
whiteSpace: 'pre-wrap', // 允许在空白处换行
|
||||
wordWrap: 'break-word', // 确保长单词换行
|
||||
wordBreak: 'normal' // 使用normal而不是break-all,避免过度换行
|
||||
}
|
||||
}}
|
||||
wrapLines={true} // 启用行包装
|
||||
wrapLongLines={true} // 包装长行
|
||||
lineProps={{ style: { whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }} // 为每一行添加样式
|
||||
>
|
||||
{responseStringRef.current}
|
||||
</SyntaxHighlighter>
|
||||
</Section>
|
||||
</FlexContainer>
|
||||
) : (
|
||||
@ -201,32 +385,52 @@ const ToolResponseContent: FC<ToolResponseContentProps> = ({
|
||||
const EditContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* height: 100%; Remove fixed height, let content determine height */
|
||||
background-color: var(--color-bg-1);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.05);
|
||||
`
|
||||
|
||||
const StyledTextArea = styled(Input.TextArea)`
|
||||
flex-grow: 1; /* Allow textarea to fill available space */
|
||||
resize: vertical; /* Allow vertical resize */
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-family: 'Ubuntu Mono', monospace !important; /* Ensure monospace font */
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.6;
|
||||
background-color: var(--color-bg-input); /* Use input background */
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-border);
|
||||
}
|
||||
|
||||
&:hover:not(:focus) {
|
||||
border-color: var(--color-primary-light, #40a9ff);
|
||||
}
|
||||
`
|
||||
|
||||
const EditActions = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: auto; /* Push actions to the bottom */
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
|
||||
button {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// 使用 memo 包装组件,避免不必要的重渲染
|
||||
|
||||
@ -42,7 +42,14 @@ import {
|
||||
setShowMessageDivider,
|
||||
setThoughtAutoCollapse
|
||||
} from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
import {
|
||||
Assistant,
|
||||
AssistantSettings,
|
||||
CodeStyleVarious,
|
||||
MathEngine,
|
||||
ThemeMode,
|
||||
TranslateLanguageVarious
|
||||
} from '@renderer/types'
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { Button, Col, InputNumber, Row, Segmented, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react'
|
||||
@ -512,11 +519,12 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
|
||||
<StyledSelect
|
||||
value={mathEngine}
|
||||
onChange={(value) => dispatch(setMathEngine(value as 'MathJax' | 'KaTeX'))}
|
||||
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
|
||||
style={{ width: 135 }}
|
||||
size="small">
|
||||
<Select.Option value="KaTeX">KaTeX</Select.Option>
|
||||
<Select.Option value="MathJax">MathJax</Select.Option>
|
||||
<Select.Option value="none">{t('settings.messages.math_engine.none')}</Select.Option>
|
||||
</StyledSelect>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT } from '@renderer/config/constant'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { SUPPORTED_REANK_PROVIDERS } from '@renderer/config/providers'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
@ -9,7 +10,7 @@ import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils/error'
|
||||
import { Form, Input, Modal, Select } from 'antd'
|
||||
import { Form, Input, Modal, Select, Slider } from 'antd'
|
||||
import { find, sortBy } from 'lodash'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useRef, useState } from 'react'
|
||||
@ -23,6 +24,7 @@ interface FormData {
|
||||
name: string
|
||||
model: string
|
||||
rerankModel: string | undefined
|
||||
documentCount: number | undefined
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
@ -113,6 +115,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
model: selectedModel,
|
||||
rerankModel: selectedRerankModel,
|
||||
dimensions,
|
||||
documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT,
|
||||
items: [],
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
@ -177,6 +180,19 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
provider: SUPPORTED_REANK_PROVIDERS.map((id) => t(`provider.${id}`))
|
||||
})}
|
||||
</SettingHelpText>
|
||||
<Form.Item
|
||||
name="documentCount"
|
||||
label={t('knowledge.document_count')}
|
||||
initialValue={DEFAULT_KNOWLEDGE_DOCUMENT_COUNT} // 设置初始值
|
||||
tooltip={{ title: t('knowledge.document_count_help') }}>
|
||||
<Slider
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setEnableDataCollection, setLanguage } from '@renderer/store/settings'
|
||||
import { setLanguage } from '@renderer/store/settings'
|
||||
import { setProxyMode, setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import { LanguageVarious } from '@renderer/types'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
@ -25,7 +25,8 @@ const GeneralSettings: FC = () => {
|
||||
trayOnClose,
|
||||
tray,
|
||||
proxyMode: storeProxyMode,
|
||||
enableDataCollection
|
||||
enableDataCollection,
|
||||
setEnableDataCollection
|
||||
} = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
const { theme: themeMode } = useTheme()
|
||||
@ -185,7 +186,13 @@ const GeneralSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.privacy.enable_privacy_mode')}</SettingRowTitle>
|
||||
<Switch value={enableDataCollection} onChange={(v) => dispatch(setEnableDataCollection(v))} />
|
||||
<Switch
|
||||
checked={enableDataCollection}
|
||||
onChange={(v) => {
|
||||
setEnableDataCollection(v)
|
||||
window.api.sentry.init()
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
|
||||
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
||||
@ -595,6 +595,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
loading={loadingServer === server.id}
|
||||
onChange={onToggleActive}
|
||||
/>
|
||||
{server.isActive && ( // Only show refresh button if server is active
|
||||
<Button
|
||||
icon={<ReloadOutlined />} // Add ReloadOutlined icon
|
||||
onClick={fetchTools} // Call fetchTools on click
|
||||
loading={loadingServer === server.id} // Use loadingServer state
|
||||
type="text"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
|
||||
@ -26,21 +26,21 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const LeftColumn = styled.div`
|
||||
width: 10%;
|
||||
border-right: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-background-soft);
|
||||
`
|
||||
|
||||
const MiddleColumn = styled.div`
|
||||
width: 20%;
|
||||
border-right: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-background-soft);
|
||||
`
|
||||
|
||||
const RightColumn = styled.div`
|
||||
const MiddleColumn = styled.div`
|
||||
width: 70%;
|
||||
border-right: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-background-soft);
|
||||
`
|
||||
|
||||
const RightColumn = styled.div`
|
||||
width: 10%;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background-color: var(--color-background-soft);
|
||||
|
||||
@ -126,7 +126,7 @@ const MCPSettings: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ThreeColumnLayout leftColumn={renderNavMenu()} middleColumn={renderServerList()} rightColumn={renderContent()} />
|
||||
<ThreeColumnLayout leftColumn={renderServerList()} middleColumn={renderContent()} rightColumn={renderNavMenu()} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { providerBills, providerCharge } from '@renderer/utils/oauth'
|
||||
import { Button } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { ReceiptText } from 'lucide-react'
|
||||
import { CircleDollarSign } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
setApiKey: (apiKey: string) => void
|
||||
}
|
||||
|
||||
const PROVIDER_LOGO_MAP = {
|
||||
silicon: SiliconFlowProviderLogo,
|
||||
aihubmix: AiHubMixProviderLogo
|
||||
}
|
||||
|
||||
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const providerWebsite =
|
||||
PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ProviderLogo src={PROVIDER_LOGO_MAP[provider.id]} />
|
||||
{isEmpty(provider.apiKey) ? (
|
||||
<OAuthButton provider={provider} onSuccess={setApiKey}>
|
||||
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
|
||||
</OAuthButton>
|
||||
) : (
|
||||
<HStack gap={10}>
|
||||
<Button shape="round" icon={<CircleDollarSign size={16} />} onClick={() => providerCharge(provider.id)}>
|
||||
{t('settings.provider.charge')}
|
||||
</Button>
|
||||
<Button shape="round" icon={<ReceiptText size={16} />} onClick={() => providerBills(provider.id)}>
|
||||
{t('settings.provider.bills')}
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
<Description>
|
||||
<Trans
|
||||
i18nKey="settings.provider.oauth.description"
|
||||
components={{
|
||||
website: (
|
||||
<OfficialWebsite href={PROVIDER_CONFIG[provider.id].websites.official} target="_blank" rel="noreferrer" />
|
||||
)
|
||||
}}
|
||||
values={{ provider: providerWebsite }}
|
||||
/>
|
||||
</Description>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled.img`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
const Description = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const OfficialWebsite = styled.a`
|
||||
text-decoration: none;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
export default ProviderOAuth
|
||||
@ -1,7 +1,6 @@
|
||||
import { CheckOutlined, CopyOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
@ -10,10 +9,9 @@ import i18n from '@renderer/i18n'
|
||||
import { isOpenAIProvider } from '@renderer/providers/AiProvider/ProviderFactory'
|
||||
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
|
||||
import { checkModelsHealth, ModelCheckStatus } from '@renderer/services/HealthCheckService'
|
||||
import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService'
|
||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { formatApiHost, maskApiKey } from '@renderer/utils/api'
|
||||
import { providerCharge } from '@renderer/utils/oauth'
|
||||
import { Button, Divider, Flex, Input, Space, Switch, Tooltip, Typography } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
@ -39,6 +37,7 @@ import LMStudioSettings from './LMStudioSettings'
|
||||
import ModelList, { ModelStatus } from './ModelList'
|
||||
import ModelListSearchBar from './ModelListSearchBar'
|
||||
import OllamSettings from './OllamaSettings'
|
||||
import ProviderOAuth from './ProviderOAuth'
|
||||
import ProviderSettingsPopup from './ProviderSettingsPopup'
|
||||
import SelectProviderModelPopup from './SelectProviderModelPopup'
|
||||
|
||||
@ -320,7 +319,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
autoFocus={provider.enabled && apiKey === ''}
|
||||
disabled={provider.id === 'copilot'}
|
||||
/>
|
||||
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
|
||||
<Button
|
||||
type={apiValid ? 'primary' : 'default'}
|
||||
ghost={apiValid}
|
||||
@ -329,17 +327,13 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{apiKeyWebsite && (
|
||||
{isProviderSupportAuth(provider) && <ProviderOAuth provider={provider} setApiKey={setApiKey} />}
|
||||
{apiKeyWebsite && !isProviderSupportAuth(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
{isProviderSupportCharge(provider) && (
|
||||
<SettingHelpLink onClick={() => providerCharge(provider.id)}>
|
||||
{t('settings.provider.charge')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
|
||||
@ -537,6 +537,10 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
return []
|
||||
}
|
||||
|
||||
public async generateImageByChat(): Promise<void> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate suggestions
|
||||
* @returns The suggestions
|
||||
|
||||
@ -49,6 +49,7 @@ export default abstract class BaseProvider {
|
||||
abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }>
|
||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||
abstract generateImage(params: GenerateImageParams): Promise<string[]>
|
||||
abstract generateImageByChat({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
|
||||
abstract getEmbeddingDimensions(model: Model): Promise<number>
|
||||
|
||||
public getBaseURL(): string {
|
||||
|
||||
@ -530,6 +530,14 @@ ${state.extractedThinking}`
|
||||
return this.targetProvider.generateImage(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过聊天生成图像
|
||||
*/
|
||||
public async generateImageByChat(params: CompletionsParams): Promise<void> {
|
||||
// 使用目标模型通过聊天生成图像
|
||||
return this.targetProvider.generateImageByChat(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌入维度
|
||||
*/
|
||||
|
||||
@ -1416,4 +1416,8 @@ export default class GeminiProvider extends BaseProvider {
|
||||
|
||||
return truncated
|
||||
}
|
||||
|
||||
public generateImageByChat(): Promise<void> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,6 +308,10 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
* @returns The completions
|
||||
*/
|
||||
async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
||||
if (assistant.enableGenerateImage) {
|
||||
await this.generateImageByChat({ messages, assistant, onChunk } as CompletionsParams)
|
||||
return
|
||||
}
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
||||
@ -1160,4 +1164,33 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
const { token } = await window.api.copilot.getToken(defaultHeaders)
|
||||
this.sdk.apiKey = token
|
||||
}
|
||||
|
||||
public async generateImageByChat({ messages, assistant, onChunk }: CompletionsParams): Promise<void> {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const lastUserMessage = messages.findLast((m) => m.role === 'user')
|
||||
const { abortController, signalPromise } = this.createAbortController(lastUserMessage?.id, true)
|
||||
const { signal } = abortController
|
||||
const response = await this.sdk.images.generate(
|
||||
{
|
||||
model: model.id,
|
||||
prompt: lastUserMessage?.content || ''
|
||||
},
|
||||
{
|
||||
signal
|
||||
}
|
||||
)
|
||||
|
||||
await signalPromise?.promise?.catch((error) => {
|
||||
throw error
|
||||
})
|
||||
|
||||
return onChunk({
|
||||
text: '',
|
||||
generateImage: {
|
||||
type: 'url',
|
||||
images: response.data.map((item) => item.url).filter((url): url is string => url !== undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +116,15 @@ export default class AiProvider {
|
||||
return this.sdk.generateImage(params)
|
||||
}
|
||||
|
||||
public async generateImageByChat({
|
||||
messages,
|
||||
assistant,
|
||||
onChunk,
|
||||
onFilterMessages
|
||||
}: CompletionsParams): Promise<void> {
|
||||
return this.sdk.generateImageByChat({ messages, assistant, onChunk, onFilterMessages })
|
||||
}
|
||||
|
||||
public async getEmbeddingDimensions(model: Model): Promise<number> {
|
||||
return this.sdk.getEmbeddingDimensions(model)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { SearxngClient } from '@agentic/searxng'
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
|
||||
import { fetchWebContent, noContent } from '@renderer/utils/fetch'
|
||||
import axios from 'axios'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
@ -89,15 +90,27 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
||||
if (!result || !Array.isArray(result.results)) {
|
||||
throw new Error('Invalid search results from SearxNG')
|
||||
}
|
||||
const validItems = result.results
|
||||
.filter((item) => item.url.startsWith('http') || item.url.startsWith('https'))
|
||||
.slice(0, websearch.maxResults)
|
||||
// console.log('Valid search items:', validItems)
|
||||
|
||||
// Fetch content for each URL concurrently
|
||||
const fetchPromises = validItems.map(async (item) => {
|
||||
// console.log(`Fetching content for ${item.url}...`)
|
||||
const result = await fetchWebContent(item.url, 'markdown', this.provider.usingBrowser)
|
||||
if (websearch.contentLimit && result.content.length > websearch.contentLimit) {
|
||||
result.content = result.content.slice(0, websearch.contentLimit) + '...'
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// Wait for all fetches to complete
|
||||
const results: WebSearchResult[] = await Promise.all(fetchPromises)
|
||||
|
||||
return {
|
||||
query: result.query,
|
||||
results: result.results.slice(0, websearch.maxResults).map((result) => {
|
||||
return {
|
||||
title: result.title || 'No title',
|
||||
content: result.content || '',
|
||||
url: result.url || ''
|
||||
}
|
||||
})
|
||||
query: query,
|
||||
results: results.filter((result) => result.content != noContent)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Searxng search failed:', error)
|
||||
|
||||
@ -4,7 +4,7 @@ import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import AiProvider from '@renderer/providers/AiProvider'
|
||||
import store from '@renderer/store'
|
||||
import { FileType, KnowledgeBase, KnowledgeBaseParams, KnowledgeReference, Message } from '@renderer/types'
|
||||
import { isEmpty, take } from 'lodash'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { getProviderByModel } from './AssistantService'
|
||||
import FileManager from './FileManager'
|
||||
@ -117,8 +117,11 @@ export const getKnowledgeBaseReference = async (base: KnowledgeBase, message: Me
|
||||
|
||||
const documentCount = base.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT
|
||||
|
||||
// 确保在处理后再截取结果,这样可以保证返回最相关的结果
|
||||
const limitedResults = processdResults.length > 0 ? processdResults.slice(0, documentCount) : processdResults
|
||||
|
||||
const references = await Promise.all(
|
||||
take(processdResults, documentCount).map(async (item, index) => {
|
||||
limitedResults.map(async (item, index) => {
|
||||
const baseItem = base.items.find((i) => i.uniqueId === item.metadata.uniqueLoaderId)
|
||||
return {
|
||||
id: index + 1,
|
||||
|
||||
@ -98,6 +98,13 @@ export const builtinMCPServers: MCPServer[] = [
|
||||
description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器',
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: '@cherry/dify-knowledge',
|
||||
type: 'inMemory',
|
||||
description: 'Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互',
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: '@cherry/simpleremember',
|
||||
@ -123,6 +130,13 @@ export const builtinMCPServers: MCPServer[] = [
|
||||
type: 'inMemory',
|
||||
description: '时间工具,提供获取当前系统时间的功能,允许AI随时知道当前日期和时间。',
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: '@cherry/calculator',
|
||||
type: 'inMemory',
|
||||
description: '万能科学计算器,提供数学表达式计算、单位转换、统计计算等功能,支持复杂的科学计算和数据分析。',
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||
import { CodeStyleVarious, LanguageVarious, Model, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
import { CodeStyleVarious, LanguageVarious, MathEngine, Model, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
|
||||
import { WebDAVSyncState } from './backup'
|
||||
@ -78,7 +78,7 @@ export interface SettingsState {
|
||||
codeCacheMaxSize: number
|
||||
codeCacheTTL: number
|
||||
codeCacheThreshold: number
|
||||
mathEngine: 'MathJax' | 'KaTeX'
|
||||
mathEngine: MathEngine
|
||||
messageStyle: 'plain' | 'bubble'
|
||||
codeStyle: CodeStyleVarious
|
||||
foldDisplayMode: 'expanded' | 'compact'
|
||||
@ -515,7 +515,7 @@ const settingsSlice = createSlice({
|
||||
setCodeCacheThreshold: (state, action: PayloadAction<number>) => {
|
||||
state.codeCacheThreshold = action.payload
|
||||
},
|
||||
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
||||
setMathEngine: (state, action: PayloadAction<MathEngine>) => {
|
||||
state.mathEngine = action.payload
|
||||
},
|
||||
setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => {
|
||||
|
||||
@ -16,6 +16,8 @@ export interface WebSearchState {
|
||||
searchWithTime: boolean
|
||||
// 搜索结果的最大数量
|
||||
maxResults: number
|
||||
// 搜索结果内容的最大长度
|
||||
contentLimit?: number
|
||||
// 要排除的域名列表
|
||||
excludeDomains: string[]
|
||||
// 订阅源列表
|
||||
@ -152,6 +154,7 @@ const initialState: WebSearchState = {
|
||||
],
|
||||
searchWithTime: true,
|
||||
maxResults: 100,
|
||||
contentLimit: 10000,
|
||||
excludeDomains: [],
|
||||
subscribeSources: [],
|
||||
enhanceMode: true,
|
||||
|
||||
@ -633,3 +633,5 @@ export type TTSProvider = {
|
||||
voice?: string
|
||||
model?: string
|
||||
}
|
||||
|
||||
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
|
||||
|
||||
@ -63,14 +63,16 @@ export const MARKDOWN_ALLOWED_TAGS = [
|
||||
'sub',
|
||||
'sup',
|
||||
'think',
|
||||
'translated' // 添加自定义翻译标签
|
||||
'translated', // 添加自定义翻译标签
|
||||
'tool-block' // 添加自定义工具块标签
|
||||
]
|
||||
|
||||
// rehype-sanitize配置
|
||||
export const sanitizeSchema = {
|
||||
tagNames: MARKDOWN_ALLOWED_TAGS,
|
||||
tagNames: [...MARKDOWN_ALLOWED_TAGS, 'tool-block'], // 确保 tool-block 在允许的标签中
|
||||
attributes: {
|
||||
'*': ['className', 'style', 'id', 'title', 'data-*'],
|
||||
'tool-block': ['id'], // 允许 tool-block 标签使用 id 属性
|
||||
svg: ['viewBox', 'width', 'height', 'xmlns', 'fill', 'stroke'],
|
||||
path: ['d', 'fill', 'stroke', 'strokeWidth', 'strokeLinecap', 'strokeLinejoin'],
|
||||
circle: ['cx', 'cy', 'r', 'fill', 'stroke'],
|
||||
|
||||
@ -80,3 +80,26 @@ export const providerCharge = async (provider: string) => {
|
||||
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
|
||||
)
|
||||
}
|
||||
|
||||
export const providerBills = async (provider: string) => {
|
||||
const billsUrlMap = {
|
||||
silicon: {
|
||||
url: 'https://cloud.siliconflow.cn/expensebill',
|
||||
width: 900,
|
||||
height: 700
|
||||
},
|
||||
aihubmix: {
|
||||
url: `https://aihubmix.com/bills?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh`,
|
||||
width: 720,
|
||||
height: 900
|
||||
}
|
||||
}
|
||||
|
||||
const { url, width, height } = billsUrlMap[provider]
|
||||
|
||||
window.open(
|
||||
url,
|
||||
'oauth',
|
||||
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
|
||||
)
|
||||
}
|
||||
|
||||
@ -106,6 +106,7 @@ export enum IpcChannel {
|
||||
Mcp_RestartServer = 'mcp:restartServer',
|
||||
Mcp_StopServer = 'mcp:stopServer',
|
||||
Mcp_ListTools = 'mcp:listTools',
|
||||
Mcp_ResetToolsList = 'mcp:resetToolsList',
|
||||
Mcp_CallTool = 'mcp:callTool',
|
||||
Mcp_ListPrompts = 'mcp:listPrompts',
|
||||
Mcp_GetPrompt = 'mcp:getPrompt',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user