diff --git a/.github/ISSUE_TEMPLATE/#2_question.yml b/.github/ISSUE_TEMPLATE/#2_question.yml index f3566ce691..0ac9fca89f 100644 --- a/.github/ISSUE_TEMPLATE/#2_question.yml +++ b/.github/ISSUE_TEMPLATE/#2_question.yml @@ -1,4 +1,4 @@ -name: ❓ 讨论 & 提问 (中文) +name: ❓ 提问 & 讨论 (中文) description: 寻求帮助、讨论问题、提出疑问等... title: '[讨论]: ' labels: ['question'] diff --git a/.github/ISSUE_TEMPLATE/#3_others.yml b/.github/ISSUE_TEMPLATE/#3_others.yml new file mode 100644 index 0000000000..8eec58b381 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/#3_others.yml @@ -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: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接 diff --git a/.github/ISSUE_TEMPLATE/2_question.yml b/.github/ISSUE_TEMPLATE/2_question.yml index f980e69165..e08600754e 100644 --- a/.github/ISSUE_TEMPLATE/2_question.yml +++ b/.github/ISSUE_TEMPLATE/2_question.yml @@ -1,4 +1,4 @@ -name: ❓ Discussion & Questions +name: ❓ Questions & Discussion description: Seeking help, discussing issues, asking questions, etc... title: '[Discussion]: ' labels: ['question'] diff --git a/.github/ISSUE_TEMPLATE/3_others.yml b/.github/ISSUE_TEMPLATE/3_others.yml new file mode 100644 index 0000000000..4d8a383080 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_others.yml @@ -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 diff --git a/.github/issue-checker.yml b/.github/issue-checker.yml new file mode 100644 index 0000000000..c765f0eabf --- /dev/null +++ b/.github/issue-checker.yml @@ -0,0 +1,277 @@ +default-mode: + add: + remove: [pull_request_target, issues] + +labels: + # 跳过一个 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 diff --git a/.github/workflows/issue-management.yml b/.github/workflows/issue-management.yml new file mode 100644 index 0000000000..e579bddb2e --- /dev/null +++ b/.github/workflows/issue-management.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe49f9ead1..e9b46ebd03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/browser_logs.txt b/browser_logs.txt deleted file mode 100644 index f38c16353a..0000000000 --- a/browser_logs.txt +++ /dev/null @@ -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' - \ No newline at end of file diff --git a/browser_modifications.txt b/browser_modifications.txt deleted file mode 100644 index 84f64dda6c..0000000000 --- a/browser_modifications.txt +++ /dev/null @@ -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` 总是重新渲染。 - * 在 `` 元素的 `ref` 回调函数中,确保在获取到 webview 引用后,显式调用 `webview.loadURL(tab.url)` 来加载 URL。同时移除 `` 元素上的 `src` 属性,避免重复加载。 - - **需要修改的文件:** `src/renderer/src/pages/Browser/components/WebviewItem.tsx` - - **修改内容 (使用 replace_in_file 格式):** - ``` - <<<<<<< SEARCH - export default React.memo(WebviewItem) - ======= - export default WebviewItem - >>>>>>> REPLACE - - <<<<<<< SEARCH - { - if (el) { - // 保存webview引用到对应的tabId下 - webviewRefs.current[tab.id] = el as WebviewTag - - // 只有在尚未设置监听器时才设置 - if (!hasSetupListenersRef.current) { - console.log(`[WebviewItem] Setting up listeners for tab: ${tab.id}`) - ======= - { - 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 = ({ /* ... */ linkOpenMode, toggleLinkOpenMode }) => { - return ( - - {/* ... 其他工具栏元素 ... */} - - {/* ... 其他工具栏元素 ... */} - - ); - }; - ``` - *(注意: 您需要将 `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` 设置。)* - -**预期结果:** -浏览器工具栏中会出现一个切换按钮,点击可以切换链接打开方式。根据当前模式,点击链接会在新标签页或独立窗口中打开。 - ---- - -希望这份修改指南对您有帮助!如果您在修改过程中遇到任何问题,或者需要进一步的帮助,请随时告诉我。 diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5af6104b59..c3bcec5b69 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -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: { diff --git a/install-electron.bat b/install-electron.bat deleted file mode 100644 index 463c5d7ed1..0000000000 --- a/install-electron.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ -yarn add electron@32.3.3 --dev diff --git a/mcp_inline_rendering_instructions.txt b/mcp_inline_rendering_instructions.txt new file mode 100644 index 0000000000..6f32ec3e5a --- /dev/null +++ b/mcp_inline_rendering_instructions.txt @@ -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 +
+ {/* Only display thought info at the top */} + + {/* Render MessageTools to display tool blocks based on metadata */} + +
+ ``` + 保留 `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` 钩子中,在处理引用标记之后,添加逻辑来查找 `...` 标记,并将其替换为自定义的占位符,例如 ``。 + + 首先,在文件顶部导入 `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(`(?:[^<]*?${toolCall.id}[\\s\\S]*?)<\\/tool_use>`, 'gi'); + content = content.replace(toolTagRegex, ``); + }); + } + + return content; + ``` + 请注意,这里的正则表达式 `toolTagRegex` 是一个示例,您可能需要根据实际的工具 XML 格式进行调整,以确保能够准确匹配包含特定 `toolCall.id` 的 `` 标记。 + +4. **更新 `Markdown` 组件的 props:** + 在渲染 `Markdown` 组件的地方,将新移动过来的状态和处理函数作为 props 传递下去: + + ```jsx + + ``` + 请注意,我在这里使用了新的 prop 名称(例如 `toolResponses`, `activeToolKeys` 等),以避免与 `Markdown` 组件内部可能已有的 props 冲突。您需要在 `Markdown.tsx` 中接收这些新的 props。 + +## 步骤 2: 修改 `src/renderer/src/pages/home/Markdown/Markdown.tsx` + +这个文件负责将 Markdown 内容渲染为 HTML。我们需要在这里: + +* 接收从 `MessageContent` 传递过来的工具相关 props。 +* 添加一个自定义的渲染器,用于处理我们在步骤 1 中创建的 `` 占位符。 +* 在自定义渲染器中,根据占位符中的 `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 // 添加 copiedMap prop + editingToolId: string | null // 添加 editingToolId prop + editedToolParamsString: string // 添加 editedParams prop + onToolToggle: React.Dispatch> // 添加 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 ( + 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; + 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 ( + <> + + {collapseItems.map((item) => ( + { + setActiveKeys((prev) => + prev.includes(item.key as string) ? prev.filter((k) => k !== item.key) : [...prev, item.key as string] + ) + }}> + {item.children} + + ))} + + + ) + ``` + 以及相关的 `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 工具块应该会以内联的方式显示在消息内容中对应的位置。 + +请仔细按照步骤进行修改,并根据您的实际代码结构进行调整。如果在修改过程中遇到任何问题,或者需要进一步的帮助,请随时告诉我。 diff --git a/package.json b/package.json index 1676e10a00..8456c98986 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/index.ts b/src/main/index.ts index 0bb00eb266..adee2e0bd0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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:;"] } }) }) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index abb0f9ea48..92ce8b3fd1 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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) diff --git a/src/main/mcpServers/calculator.ts b/src/main/mcpServers/calculator.ts new file mode 100644 index 0000000000..c7e7df7317 --- /dev/null +++ b/src/main/mcpServers/calculator.ts @@ -0,0 +1,2920 @@ +// src/main/mcpServers/calculator.ts + +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js' +import axios from 'axios' +import { app } from 'electron' +import Logger from 'electron-log' +import fs from 'fs/promises' +import path from 'path' + +// 动态加载 mathjs +let math: any = null + +// 创建一个加载 mathjs 的函数 +async function loadMathJs() { + try { + Logger.info('[Calculator] Loading mathjs from unpkg.com') + + // 尝试从 unpkg.com 加载 mathjs + const response = await axios.get('https://unpkg.com/mathjs@12.4.0/lib/browser/math.js') + + // 创建一个临时目录来存储下载的脚本 + const tempDir = path.join(app.getPath('temp'), 'cherry-calculator') + await fs.mkdir(tempDir, { recursive: true }) + + const mathJsPath = path.join(tempDir, 'math.js') + await fs.writeFile(mathJsPath, response.data) + + Logger.info(`[Calculator] Saved mathjs to ${mathJsPath}`) + + // 使用 require 加载本地保存的脚本 + // 注意:这里使用了一个技巧,通过创建一个模块来执行脚本 + const mathJsCode = await fs.readFile(mathJsPath, 'utf-8') + const mathModule = { exports: {} } + const moduleFn = new Function('module', 'exports', mathJsCode) + moduleFn(mathModule, mathModule.exports) + + math = mathModule.exports + Logger.info('[Calculator] Successfully loaded mathjs') + return true + } catch (error) { + Logger.error('[Calculator] Failed to load mathjs from unpkg.com:', error) + + // 如果加载失败,使用基本的数学函数 + math = { + evaluate: (expr: string) => { + // 安全的 eval 实现,仅支持基本数学运算 + // 这只是一个备用方案,功能有限 + return Function('"use strict"; return (' + expr + ')')() + }, + format: (value: any) => String(value), + mean: (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length, + median: (arr: number[]) => { + const sorted = [...arr].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid] + }, + std: (arr: number[]) => { + const mean = arr.reduce((a, b) => a + b, 0) / arr.length + const variance = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length + return Math.sqrt(variance) + }, + min: (arr: number[]) => Math.min(...arr), + max: (arr: number[]) => Math.max(...arr), + sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0), + unit: (value: number, unit: string) => { + return { + toNumber: () => value, + to: (targetUnit: string) => { + // 非常基本的单位转换,仅作为备用 + if (unit === 'inch' && targetUnit === 'cm') return { toNumber: () => value * 2.54 } + if (unit === 'cm' && targetUnit === 'inch') return { toNumber: () => value / 2.54 } + if (unit === 'kg' && targetUnit === 'lb') return { toNumber: () => value * 2.20462 } + if (unit === 'lb' && targetUnit === 'kg') return { toNumber: () => value / 2.20462 } + return { toNumber: () => value } + } + } + } + } + Logger.info('[Calculator] Using fallback math implementation') + return false + } +} + +// 定义科学计算器工具 +const CALCULATOR_TOOL = { + name: 'calculate', + description: '万能科学计算器,支持数学表达式计算、复数运算、分数计算等功能', + inputSchema: { + type: 'object', + title: 'CalculatorInput', + description: '科学计算器的输入参数', + properties: { + expression: { + type: 'string', + description: + '要计算的数学表达式,例如:2+2、sin(45 deg)、5 inch to cm、det([1,2;3,4])、(3+4i)*(2-i)、1/3 + 1/4等' + }, + precision: { + type: 'number', + description: '结果的精度(小数位数),默认为14' + }, + format: { + type: 'string', + description: '结果格式化方式,可选值:auto、decimal、scientific、engineering、fixed、fraction,默认为auto', + enum: ['auto', 'decimal', 'scientific', 'engineering', 'fixed', 'fraction'] + }, + complexForm: { + type: 'string', + description: '复数结果的表示形式,可选值:rectangular(直角坐标)、polar(极坐标),默认为rectangular', + enum: ['rectangular', 'polar'] + } + }, + required: ['expression'] + } +} + +// 单位转换工具 +const UNIT_CONVERT_TOOL = { + name: 'convert_unit', + description: '单位转换工具,支持各种物理单位之间的转换', + inputSchema: { + type: 'object', + title: 'UnitConvertInput', + description: '单位转换的输入参数', + properties: { + value: { + type: 'number', + description: '要转换的数值' + }, + from: { + type: 'string', + description: '源单位,例如:kg、m、s、inch、celsius等' + }, + to: { + type: 'string', + description: '目标单位,例如:g、km、ms、cm、fahrenheit等' + }, + precision: { + type: 'number', + description: '结果的精度(小数位数),默认为6' + } + }, + required: ['value', 'from', 'to'] + } +} + +// 统计计算工具 +const STATISTICS_TOOL = { + name: 'statistics', + description: '统计计算工具,支持均值、中位数、标准差等统计计算', + inputSchema: { + type: 'object', + title: 'StatisticsInput', + description: '统计计算的输入参数', + properties: { + data: { + type: 'string', + description: '要计算的数据,以逗号分隔的数字,例如:1,2,3,4,5' + }, + operation: { + type: 'string', + description: + '统计操作,可选值:mean(均值)、median(中位数)、std(标准差)、min(最小值)、max(最大值)、sum(求和)、variance(方差)、quantile(分位数)、correlation(相关性)、all(所有统计结果)', + enum: ['mean', 'median', 'std', 'min', 'max', 'sum', 'variance', 'quantile', 'correlation', 'all'] + }, + quantile: { + type: 'number', + description: '当operation为quantile时,指定分位数值(0-1之间),例如:0.25表示第一四分位数' + }, + secondData: { + type: 'string', + description: '当operation为correlation时,用于计算相关性的第二组数据,以逗号分隔' + } + }, + required: ['data', 'operation'] + } +} + +// 方程求解工具 +const EQUATION_SOLVER_TOOL = { + name: 'solve_equation', + description: '方程求解工具,支持代数方程、线性方程组等求解', + inputSchema: { + type: 'object', + title: 'EquationSolverInput', + description: '方程求解的输入参数', + properties: { + equation: { + type: 'string', + description: '要求解的方程,例如:x^2 + 2*x - 3 = 0 或 2*x + y = 10, 3*x - y = 5' + }, + variables: { + type: 'string', + description: '变量列表,以逗号分隔,例如:x,y,z。如果不提供,将自动检测' + }, + precision: { + type: 'number', + description: '结果的精度(小数位数),默认为6' + } + }, + required: ['equation'] + } +} + +// 微积分工具 +const CALCULUS_TOOL = { + name: 'calculus', + description: '微积分工具,支持导数、积分、极限等计算', + inputSchema: { + type: 'object', + title: 'CalculusInput', + description: '微积分计算的输入参数', + properties: { + operation: { + type: 'string', + description: '微积分操作,可选值:derivative(导数)、integral(积分)、limit(极限)', + enum: ['derivative', 'integral', 'limit'] + }, + expression: { + type: 'string', + description: '要计算的表达式,例如:x^2 + 2*x' + }, + variable: { + type: 'string', + description: '变量名,例如:x' + }, + order: { + type: 'number', + description: '当operation为derivative时,指定导数阶数,默认为1' + }, + from: { + type: 'string', + description: '当operation为integral时,指定积分下限' + }, + to: { + type: 'string', + description: '当operation为integral时,指定积分上限' + }, + approach: { + type: 'string', + description: '当operation为limit时,指定趋近方向,例如:0+' + } + }, + required: ['operation', 'expression', 'variable'] + } +} + +// 矩阵计算工具 +const MATRIX_TOOL = { + name: 'matrix', + description: '矩阵计算工具,支持矩阵运算、特征值、行列式等计算', + inputSchema: { + type: 'object', + title: 'MatrixInput', + description: '矩阵计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '矩阵操作,可选值:det(行列式)、inv(逆矩阵)、transpose(转置)、eigenvalues(特征值)、eigenvectors(特征向量)、rank(秩)、multiply(矩阵乘法)、solve(解线性方程组)', + enum: ['det', 'inv', 'transpose', 'eigenvalues', 'eigenvectors', 'rank', 'multiply', 'solve'] + }, + matrix: { + type: 'string', + description: '矩阵定义,例如:[1,2,3;4,5,6;7,8,9]表示3x3矩阵' + }, + matrix2: { + type: 'string', + description: '当operation为multiply时,第二个矩阵定义' + }, + vector: { + type: 'string', + description: '当operation为solve时,等号右侧的向量,例如:[1,2,3]' + } + }, + required: ['operation', 'matrix'] + } +} + +// 概率与随机工具 +const PROBABILITY_TOOL = { + name: 'probability', + description: '概率与随机工具,支持概率分布、随机数生成等', + inputSchema: { + type: 'object', + title: 'ProbabilityInput', + description: '概率计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '概率操作,可选值:pdf(概率密度函数)、cdf(累积分布函数)、random(随机数生成)、combination(组合数)、permutation(排列数)', + enum: ['pdf', 'cdf', 'random', 'combination', 'permutation'] + }, + distribution: { + type: 'string', + description: '当operation为pdf或cdf时,指定分布类型,例如:normal、binomial、poisson等', + enum: ['normal', 'binomial', 'poisson', 'uniform', 'exponential'] + }, + params: { + type: 'string', + description: '分布参数,以逗号分隔,例如正态分布的均值和标准差:0,1' + }, + x: { + type: 'number', + description: '当operation为pdf或cdf时,指定自变量值' + }, + n: { + type: 'number', + description: '当operation为combination或permutation时,指定总数n' + }, + k: { + type: 'number', + description: '当operation为combination或permutation时,指定选取数k' + }, + count: { + type: 'number', + description: '当operation为random时,指定生成随机数的数量' + } + }, + required: ['operation'] + } +} + +// 金融计算工具 +const FINANCE_TOOL = { + name: 'finance', + description: '金融计算工具,支持利息、贷款、投资回报率等计算', + inputSchema: { + type: 'object', + title: 'FinanceInput', + description: '金融计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '金融操作,可选值:pv(现值)、fv(终值)、pmt(等额分期付款)、nper(期数)、rate(利率)、irr(内部收益率)、npv(净现值)、depreciation(折旧计算)、roi(投资回报率)', + enum: ['pv', 'fv', 'pmt', 'nper', 'rate', 'irr', 'npv', 'depreciation', 'roi'] + }, + rate: { + type: 'number', + description: '利率(小数形式),例如:0.05表示5%' + }, + nper: { + type: 'number', + description: '期数' + }, + pmt: { + type: 'number', + description: '每期付款金额' + }, + pv: { + type: 'number', + description: '现值' + }, + fv: { + type: 'number', + description: '终值' + }, + cashflow: { + type: 'string', + description: '当operation为irr或npv时,现金流,以逗号分隔,例如:-1000,200,300,400,500' + }, + type: { + type: 'number', + description: '付款类型,0表示期末付款,1表示期初付款,默认为0' + }, + method: { + type: 'string', + description: '当operation为depreciation时,折旧方法,可选值:sl(直线法)、db(余额递减法)、syd(年数总和法)', + enum: ['sl', 'db', 'syd'] + }, + cost: { + type: 'number', + description: '当operation为depreciation或roi时,初始成本或投资额' + }, + salvage: { + type: 'number', + description: '当operation为depreciation时,残值' + }, + life: { + type: 'number', + description: '当operation为depreciation时,使用年限' + }, + period: { + type: 'number', + description: '当operation为depreciation时,计算第几年的折旧' + }, + profit: { + type: 'number', + description: '当operation为roi时,利润或收益' + } + }, + required: ['operation'] + } +} + +// 物理计算工具 +const PHYSICS_TOOL = { + name: 'physics', + description: '物理计算工具,支持物理常数查询、物理公式计算等', + inputSchema: { + type: 'object', + title: 'PhysicsInput', + description: '物理计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '物理操作,可选值:constant(物理常数)、kinematics(运动学)、dynamics(动力学)、energy(能量)、electricity(电学)、thermodynamics(热力学)、optics(光学)', + enum: ['constant', 'kinematics', 'dynamics', 'energy', 'electricity', 'thermodynamics', 'optics'] + }, + constant: { + type: 'string', + description: + '当operation为constant时,要查询的物理常数,例如:c(光速)、g(重力加速度)、h(普朗克常数)、e(电子电荷)、k(玻尔兹曼常数)、G(万有引力常数)、epsilon0(真空介电常数)、mu0(真空磁导率)', + enum: ['c', 'g', 'h', 'e', 'k', 'G', 'epsilon0', 'mu0'] + }, + formula: { + type: 'string', + description: '当operation不为constant时,要使用的物理公式,例如:v=v0+a*t、F=m*a、E=m*c^2、V=I*R、Q=m*c*dT等' + }, + params: { + type: 'object', + description: '公式中的参数值,例如:{"m": 10, "a": 9.8}' + }, + solve: { + type: 'string', + description: '要求解的变量,例如:v、F、E等' + }, + units: { + type: 'boolean', + description: '是否在结果中包含单位,默认为true' + } + }, + required: ['operation'] + } +} + +// 化学计算工具 +const CHEMISTRY_TOOL = { + name: 'chemistry', + description: '化学计算工具,支持元素周期表查询、化学方程式计算等', + inputSchema: { + type: 'object', + title: 'ChemistryInput', + description: '化学计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '化学操作,可选值:element(元素查询)、molar_mass(摩尔质量计算)、balance(化学方程式平衡)、solution(溶液计算)、gas(气体计算)、stoichiometry(化学计量学)', + enum: ['element', 'molar_mass', 'balance', 'solution', 'gas', 'stoichiometry'] + }, + element: { + type: 'string', + description: '当operation为element时,要查询的元素符号,例如:H、O、C、Fe等' + }, + formula: { + type: 'string', + description: '当operation为molar_mass时,化学式,例如:H2O、C6H12O6、NaCl等' + }, + equation: { + type: 'string', + description: '当operation为balance或stoichiometry时,化学方程式,例如:H2 + O2 = H2O' + }, + concentration: { + type: 'number', + description: '当operation为solution时,溶液浓度' + }, + concentration_unit: { + type: 'string', + description: '当operation为solution时,浓度单位,例如:mol/L、g/L、%等', + enum: ['mol/L', 'g/L', '%', 'ppm', 'ppb'] + }, + volume: { + type: 'number', + description: '当operation为solution或gas时,体积' + }, + volume_unit: { + type: 'string', + description: '当operation为solution或gas时,体积单位,例如:L、mL、m3等', + enum: ['L', 'mL', 'm3', 'cm3'] + }, + temperature: { + type: 'number', + description: '当operation为gas时,温度' + }, + temperature_unit: { + type: 'string', + description: '当operation为gas时,温度单位,例如:K、C、F等', + enum: ['K', 'C', 'F'] + }, + pressure: { + type: 'number', + description: '当operation为gas时,压力' + }, + pressure_unit: { + type: 'string', + description: '当operation为gas时,压力单位,例如:atm、Pa、mmHg等', + enum: ['atm', 'Pa', 'kPa', 'mmHg', 'bar'] + } + }, + required: ['operation'] + } +} + +// 编程功能工具 +const PROGRAMMING_TOOL = { + name: 'programming', + description: '编程功能工具,支持进制转换、位运算、逻辑运算等', + inputSchema: { + type: 'object', + title: 'ProgrammingInput', + description: '编程功能的输入参数', + properties: { + operation: { + type: 'string', + description: + '编程操作,可选值:base_convert(进制转换)、bitwise(位运算)、logical(逻辑运算)、regex(正则表达式)、hash(哈希计算)、encode(编码转换)', + enum: ['base_convert', 'bitwise', 'logical', 'regex', 'hash', 'encode'] + }, + value: { + type: 'string', + description: '要操作的值,例如:进制转换时的数字、位运算时的二进制数等' + }, + from_base: { + type: 'number', + description: '当operation为base_convert时,原始进制,例如:2、8、10、16等' + }, + to_base: { + type: 'number', + description: '当operation为base_convert时,目标进制,例如:2、8、10、16等' + }, + bitwise_op: { + type: 'string', + description: '当operation为bitwise时,位运算操作,例如:and、or、xor、not、shift_left、shift_right等', + enum: ['and', 'or', 'xor', 'not', 'shift_left', 'shift_right'] + }, + operand: { + type: 'string', + description: '当operation为bitwise或logical时,第二个操作数' + }, + logical_op: { + type: 'string', + description: '当operation为logical时,逻辑运算操作,例如:and、or、not、xor、implies等', + enum: ['and', 'or', 'not', 'xor', 'implies'] + }, + pattern: { + type: 'string', + description: '当operation为regex时,正则表达式模式' + }, + text: { + type: 'string', + description: '当operation为regex时,要匹配的文本' + }, + hash_algorithm: { + type: 'string', + description: '当operation为hash时,哈希算法,例如:md5、sha1、sha256等', + enum: ['md5', 'sha1', 'sha256', 'sha512'] + }, + encoding: { + type: 'string', + description: '当operation为encode时,编码方式,例如:base64、url、hex、ascii等', + enum: ['base64', 'url', 'hex', 'ascii', 'utf8'] + }, + decode: { + type: 'boolean', + description: '当operation为encode时,是否解码,默认为false(编码)' + } + }, + required: ['operation'] + } +} + +// 日期时间工具 +const DATETIME_TOOL = { + name: 'datetime', + description: '日期时间工具,支持日期计算、时区转换、工作日计算等', + inputSchema: { + type: 'object', + title: 'DateTimeInput', + description: '日期时间计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '日期时间操作,可选值:diff(日期差值)、add(日期加减)、format(日期格式化)、timezone(时区转换)、workdays(工作日计算)、calendar(日历信息)、parse(日期解析)', + enum: ['diff', 'add', 'format', 'timezone', 'workdays', 'calendar', 'parse'] + }, + date1: { + type: 'string', + description: '第一个日期,例如:2023-01-01、2023/01/01、January 1, 2023等' + }, + date2: { + type: 'string', + description: '当operation为diff时,第二个日期' + }, + unit: { + type: 'string', + description: '当operation为diff或add时,时间单位,例如:years、months、days、hours、minutes、seconds等', + enum: ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'] + }, + value: { + type: 'number', + description: '当operation为add时,要加减的时间值,正数为加,负数为减' + }, + format_string: { + type: 'string', + description: '当operation为format时,格式化字符串,例如:YYYY-MM-DD、MM/DD/YYYY、YYYY年MM月DD日等' + }, + from_timezone: { + type: 'string', + description: '当operation为timezone时,原始时区,例如:UTC、Asia/Shanghai、America/New_York等' + }, + to_timezone: { + type: 'string', + description: '当operation为timezone时,目标时区' + }, + start_date: { + type: 'string', + description: '当operation为workdays时,开始日期' + }, + end_date: { + type: 'string', + description: '当operation为workdays时,结束日期' + }, + holidays: { + type: 'string', + description: '当operation为workdays时,假期日期列表,以逗号分隔,例如:2023-01-01,2023-01-02' + }, + year: { + type: 'number', + description: '当operation为calendar时,年份' + }, + month: { + type: 'number', + description: '当operation为calendar时,月份(1-12)' + }, + date_string: { + type: 'string', + description: '当operation为parse时,要解析的日期字符串' + } + }, + required: ['operation'] + } +} + +// 几何计算工具 +const GEOMETRY_TOOL = { + name: 'geometry', + description: '几何计算工具,支持平面几何、立体几何、坐标几何等计算', + inputSchema: { + type: 'object', + title: 'GeometryInput', + description: '几何计算的输入参数', + properties: { + operation: { + type: 'string', + description: + '几何操作,可选值:distance(距离)、area(面积)、volume(体积)、angle(角度)、perimeter(周长)、coordinates(坐标计算)、transform(几何变换)、intersection(交点计算)', + enum: ['distance', 'area', 'volume', 'angle', 'perimeter', 'coordinates', 'transform', 'intersection'] + }, + shape: { + type: 'string', + description: '几何形状,例如:point、line、triangle、rectangle、circle、sphere、cube等', + enum: [ + 'point', + 'line', + 'triangle', + 'rectangle', + 'square', + 'circle', + 'ellipse', + 'polygon', + 'sphere', + 'cube', + 'cylinder', + 'cone', + 'prism', + 'pyramid' + ] + }, + points: { + type: 'string', + description: '点的坐标,格式为:x1,y1;x2,y2;...或x1,y1,z1;x2,y2,z2;...(3D)' + }, + dimensions: { + type: 'string', + description: '形状的尺寸,例如:长方形的长和宽(length,width)、圆的半径(radius)等,以逗号分隔' + }, + angle_unit: { + type: 'string', + description: '角度单位,例如:degrees(度)、radians(弧度)', + enum: ['degrees', 'radians'] + }, + transformation: { + type: 'string', + description: + '当operation为transform时,几何变换类型,例如:translation(平移)、rotation(旋转)、scaling(缩放)、reflection(反射)', + enum: ['translation', 'rotation', 'scaling', 'reflection'] + }, + transformation_params: { + type: 'string', + description: '当operation为transform时,变换参数,例如:平移向量、旋转角度和中心点、缩放比例等' + }, + coordinate_system: { + type: 'string', + description: '坐标系统,例如:cartesian(笛卡尔)、polar(极坐标)、spherical(球坐标)、cylindrical(柱坐标)', + enum: ['cartesian', 'polar', 'spherical', 'cylindrical'] + }, + from_system: { + type: 'string', + description: '当operation为coordinates时,原始坐标系统', + enum: ['cartesian', 'polar', 'spherical', 'cylindrical'] + }, + to_system: { + type: 'string', + description: '当operation为coordinates时,目标坐标系统', + enum: ['cartesian', 'polar', 'spherical', 'cylindrical'] + }, + coordinates: { + type: 'string', + description: '当operation为coordinates时,要转换的坐标,例如:x,y,z或r,theta,phi等' + } + }, + required: ['operation'] + } +} + +// 图形函数工具 +const GRAPH_TOOL = { + name: 'graph', + description: '图形函数工具,支持函数图像描述、坐标点计算等', + inputSchema: { + type: 'object', + title: 'GraphInput', + description: '图形函数的输入参数', + properties: { + operation: { + type: 'string', + description: + '图形操作,可选值:evaluate(函数求值)、roots(求根)、extrema(极值点)、inflection(拐点)、tangent(切线)、describe(图像描述)、intersect(交点)', + enum: ['evaluate', 'roots', 'extrema', 'inflection', 'tangent', 'describe', 'intersect'] + }, + function: { + type: 'string', + description: '函数表达式,例如:x^2 + 2*x - 3、sin(x)、e^x等' + }, + variable: { + type: 'string', + description: '自变量,默认为x' + }, + point: { + type: 'number', + description: '当operation为evaluate或tangent时,自变量的值' + }, + range: { + type: 'string', + description: '当operation为roots、extrema、inflection时,自变量的范围,格式为:min,max' + }, + function2: { + type: 'string', + description: '当operation为intersect时,第二个函数表达式' + }, + precision: { + type: 'number', + description: '计算精度,默认为6' + }, + domain: { + type: 'string', + description: '当operation为describe时,函数的定义域,格式为:min,max' + } + }, + required: ['operation', 'function'] + } +} + +// 科学计算器服务器类 +class CalculatorServer { + public server: Server + // 服务器实例 + + constructor() { + Logger.info('[Calculator] Creating server') + + // 初始化服务器 + this.server = new Server( + { + name: 'calculator-server', + version: '1.0.0' + }, + { + capabilities: { + tools: { + // 按照MCP规范声明工具能力 + listChanged: true + } + } + } + ) + + Logger.info('[Calculator] Server initialized with tools capability') + + // 初始化 mathjs 并设置请求处理程序 + this.initialize().then(() => { + Logger.info('[Calculator] Server initialization completed') + }) + + Logger.info('[Calculator] Server initialization started') + } + + // 初始化函数,加载 mathjs 并设置请求处理程序 + private async initialize(): Promise { + try { + // 加载 mathjs + await loadMathJs() + + // 设置请求处理程序 + this.setupRequestHandlers() + + Logger.info('[Calculator] Server initialization complete') + } catch (error) { + Logger.error('[Calculator] Error during initialization:', error) + throw error + } + } + + // 设置请求处理程序 + setupRequestHandlers() { + // 列出工具 + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + Logger.info('[Calculator] Listing tools request received') + return { + tools: [ + CALCULATOR_TOOL, + UNIT_CONVERT_TOOL, + STATISTICS_TOOL, + EQUATION_SOLVER_TOOL, + CALCULUS_TOOL, + MATRIX_TOOL, + PROBABILITY_TOOL, + FINANCE_TOOL, + PHYSICS_TOOL, + CHEMISTRY_TOOL, + PROGRAMMING_TOOL, + DATETIME_TOOL, + GEOMETRY_TOOL, + GRAPH_TOOL + ] + } + }) + + // 处理工具调用 + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + Logger.info(`[Calculator] Tool call received: ${name}`, args) + + try { + if (name === 'calculate') { + return this.handleCalculate(args) + } else if (name === 'convert_unit') { + return this.handleUnitConvert(args) + } else if (name === 'statistics') { + return this.handleStatistics(args) + } else if (name === 'solve_equation') { + return this.handleEquationSolver(args) + } else if (name === 'calculus') { + return this.handleCalculus(args) + } else if (name === 'matrix') { + return this.handleMatrix(args) + } else if (name === 'probability') { + return this.handleProbability(args) + } else if (name === 'finance') { + return this.handleFinance(args) + } else if (name === 'physics') { + return this.handlePhysics(args) + } else if (name === 'chemistry') { + return this.handleChemistry(args) + } else if (name === 'programming') { + return this.handleProgramming(args) + } else if (name === 'datetime') { + return this.handleDateTime(args) + } else if (name === 'geometry') { + return this.handleGeometry(args) + } else if (name === 'graph') { + return this.handleGraph(args) + } + + Logger.error(`[Calculator] Unknown tool: ${name}`) + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`) + } catch (error) { + Logger.error(`[Calculator] Error handling tool call ${name}:`, error) + return { + content: [ + { + type: 'text', + text: error instanceof Error ? error.message : String(error) + } + ], + isError: true + } + } + }) + } + + // 处理计算表达式 + private handleCalculate(args: any) { + Logger.info('[Calculator] Handling calculate', args) + + const expression = args?.expression + const precision = args?.precision || 14 + const format = args?.format || 'auto' + + if (!expression) { + throw new McpError(ErrorCode.InvalidParams, 'Expression is required') + } + + try { + // 配置计算选项 + // const config = { + // precision: precision + // } + + // 执行计算 + const result = math.evaluate(expression) + + // 格式化结果 + let formattedResult + if (typeof result === 'number') { + switch (format) { + case 'decimal': + formattedResult = result.toFixed(precision) + break + case 'scientific': + formattedResult = result.toExponential(precision) + break + case 'engineering': + formattedResult = this.toEngineeringNotation(result, precision) + break + case 'fixed': + formattedResult = result.toFixed(precision) + break + case 'auto': + default: + if (Math.abs(result) < 0.0001 || Math.abs(result) >= 10000) { + formattedResult = result.toExponential(precision) + } else { + formattedResult = result.toString() + } + } + } else { + // 处理非数字结果(如矩阵、复数等) + formattedResult = math.format(result, { precision: precision }) + } + + // 构建完整的响应对象 + const response = { + expression: expression, + result: formattedResult, + rawResult: result.toString() + } + + Logger.info('[Calculator] Calculation result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error calculating expression:', error) + throw new McpError( + ErrorCode.InternalError, + `Error calculating expression: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理单位转换 + private handleUnitConvert(args: any) { + Logger.info('[Calculator] Handling unit conversion', args) + + const value = args?.value + const fromUnit = args?.from + const toUnit = args?.to + const precision = args?.precision || 6 + + if (value === undefined || !fromUnit || !toUnit) { + throw new McpError(ErrorCode.InvalidParams, 'Value, from unit, and to unit are required') + } + + try { + // 创建带单位的值 + const valueWithUnit = math.unit(value, fromUnit) + + // 转换到目标单位 + const converted = valueWithUnit.to(toUnit) + + // 获取转换后的数值 + const result = converted.toNumber() + + // 构建响应对象 + const response = { + value: value, + fromUnit: fromUnit, + toUnit: toUnit, + result: result.toFixed(precision), + fullResult: `${value} ${fromUnit} = ${result.toFixed(precision)} ${toUnit}` + } + + Logger.info('[Calculator] Unit conversion result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error converting units:', error) + throw new McpError( + ErrorCode.InternalError, + `Error converting units: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理统计计算 + private handleStatistics(args: any) { + Logger.info('[Calculator] Handling statistics', args) + + const dataStr = args?.data + const operation = args?.operation + const quantile = args?.quantile + const secondDataStr = args?.secondData + + if (!dataStr || !operation) { + throw new McpError(ErrorCode.InvalidParams, 'Data and operation are required') + } + + try { + // 解析数据 + const data = dataStr + .split(',') + .map((item: string) => parseFloat(item.trim())) + .filter((num: number) => !isNaN(num)) + + if (data.length === 0) { + throw new McpError(ErrorCode.InvalidParams, 'No valid numbers in data') + } + + // 执行统计操作 + let result: any = {} + + if (operation === 'all' || operation === 'mean') { + result.mean = math.mean(data) + } + + if (operation === 'all' || operation === 'median') { + result.median = math.median(data) + } + + if (operation === 'all' || operation === 'std') { + result.std = math.std(data) + } + + if (operation === 'all' || operation === 'min') { + result.min = math.min(data) + } + + if (operation === 'all' || operation === 'max') { + result.max = math.max(data) + } + + if (operation === 'all' || operation === 'sum') { + result.sum = math.sum(data) + } + + if (operation === 'all' || operation === 'variance') { + result.variance = math.variance(data) + } + + if (operation === 'quantile') { + if (quantile === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'Quantile value is required for quantile operation') + } + + if (quantile < 0 || quantile > 1) { + throw new McpError(ErrorCode.InvalidParams, 'Quantile value must be between 0 and 1') + } + + result = math.quantileSeq(data, quantile) + } + + if (operation === 'correlation') { + if (!secondDataStr) { + throw new McpError(ErrorCode.InvalidParams, 'Second data set is required for correlation') + } + + const secondData = secondDataStr + .split(',') + .map((item: string) => parseFloat(item.trim())) + .filter((num: number) => !isNaN(num)) + + if (secondData.length === 0) { + throw new McpError(ErrorCode.InvalidParams, 'No valid numbers in second data set') + } + + if (data.length !== secondData.length) { + throw new McpError(ErrorCode.InvalidParams, 'Data sets must have the same length for correlation') + } + + // 计算皮尔逊相关系数 + const meanX = math.mean(data) + const meanY = math.mean(secondData) + + let numerator = 0 + let denominatorX = 0 + let denominatorY = 0 + + for (let i = 0; i < data.length; i++) { + const xDiff = data[i] - meanX + const yDiff = secondData[i] - meanY + + numerator += xDiff * yDiff + denominatorX += xDiff * xDiff + denominatorY += yDiff * yDiff + } + + result = numerator / Math.sqrt(denominatorX * denominatorY) + } + + // 如果是'all'操作,添加更多统计信息 + if (operation === 'all') { + result.count = data.length + result.range = result.max - result.min + result.q1 = math.quantileSeq(data, 0.25) + result.q3 = math.quantileSeq(data, 0.75) + result.iqr = result.q3 - result.q1 + } else if (operation !== 'quantile' && operation !== 'correlation') { + // 如果不是'all'、'quantile'或'correlation'操作,直接返回单个结果 + result = result[operation] + } + + // 构建响应对象 + const response = { + data: data, + operation: operation, + result: result + } + + Logger.info('[Calculator] Statistics result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error calculating statistics:', error) + throw new McpError( + ErrorCode.InternalError, + `Error calculating statistics: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理方程求解 + private handleEquationSolver(args: any) { + Logger.info('[Calculator] Handling equation solver', args) + + const equation = args?.equation + const variables = args?.variables ? args.variables.split(',').map((v: string) => v.trim()) : null + // const precision = args?.precision || 6 + + if (!equation) { + throw new McpError(ErrorCode.InvalidParams, 'Equation is required') + } + + try { + // 使用自定义方法求解方程,而不是依赖 math.solve + let result + + // 检查是否是方程组(包含逗号分隔的多个方程) + if (equation.includes(',')) { + // 处理方程组 + const equations = equation.split(',').map((eq: string) => eq.trim()) + + // 如果没有提供变量,尝试从方程中提取 + const vars = variables || this.extractVariablesFromEquations(equations) + + if (!vars || vars.length === 0) { + throw new McpError(ErrorCode.InvalidParams, 'Could not determine variables from equations') + } + + // 目前只支持线性方程组 + result = this.solveLinearEquationSystem(equations, vars) + } else { + // 处理单个方程 + // 如果没有提供变量,尝试从方程中提取 + const vars = variables || this.extractVariablesFromEquation(equation) + + if (!vars || vars.length === 0) { + throw new McpError(ErrorCode.InvalidParams, 'Could not determine variables from equation') + } + + // 尝试解析和求解方程 + result = this.solveEquation(equation, vars[0]) + } + + // 构建响应对象 + const response = { + equation: equation, + variables: variables || 'auto-detected', + result: result + } + + Logger.info('[Calculator] Equation solver result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error solving equation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error solving equation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 求解单个方程 + private solveEquation(equation: string, variable: string): any { + Logger.info(`[Calculator] Solving equation: ${equation} for ${variable}`) + + try { + // 将方程标准化为 "表达式 = 0" 的形式 + let expr = equation + + if (equation.includes('=')) { + const parts = equation.split('=').map((p) => p.trim()) + if (parts.length !== 2) { + throw new Error('Invalid equation format. Expected format: expression = expression') + } + + // 将方程转换为 "左边 - 右边 = 0" 的形式 + expr = `(${parts[0]}) - (${parts[1]})` + } + + // 检查是否是二次方程 + if (expr.includes(`${variable}^2`) || expr.includes(`${variable}*${variable}`)) { + return this.solveQuadraticEquation(expr, variable) + } + + // 检查是否是简单的线性方程 + if (expr.includes(variable)) { + return this.solveLinearEquation(expr, variable) + } + + throw new Error(`Cannot determine equation type for: ${equation}`) + } catch (error) { + Logger.error(`[Calculator] Error in solveEquation:`, error) + throw error + } + } + + // 求解线性方程 (ax + b = 0) + private solveLinearEquation(expr: string, variable: string): number | { type: string; message: string; value: null } { + try { + // 替换变量为 1 和 0,计算系数 + const exprWithVar = math.evaluate(expr.replace(new RegExp(variable, 'g'), '1')) + const exprWithoutVar = math.evaluate(expr.replace(new RegExp(variable, 'g'), '0')) + + // 计算系数 a 和 b + const a = exprWithVar - exprWithoutVar + const b = exprWithoutVar + + // 求解 ax + b = 0 => x = -b/a + if (a === 0) { + if (b === 0) { + return { type: 'infinite', message: '方程有无穷多解', value: null } + } + return { type: 'no_solution', message: '方程无解', value: null } + } + + return -b / a + } catch (error) { + Logger.error(`[Calculator] Error in solveLinearEquation:`, error) + throw new Error(`Error solving linear equation: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // 求解二次方程 (ax^2 + bx + c = 0) + private solveQuadraticEquation(expr: string, variable: string): any { + try { + // 替换变量为不同值,计算系数 + const f0 = math.evaluate(expr.replace(new RegExp(variable, 'g'), '0')) + const f1 = math.evaluate(expr.replace(new RegExp(variable, 'g'), '1')) + const f2 = math.evaluate(expr.replace(new RegExp(variable, 'g'), '2')) + + // 使用拉格朗日插值法计算系数 + const c = f0 + const b = -3 * f0 + 4 * f1 - f2 + const a = f0 - 2 * f1 + f2 + + // 检查是否真的是二次方程 + if (Math.abs(a) < 1e-10) { + // 如果 a 接近 0,退化为线性方程 + if (Math.abs(b) < 1e-10) { + if (Math.abs(c) < 1e-10) { + return { type: 'infinite', message: '方程有无穷多解' } + } + return { type: 'no_solution', message: '方程无解' } + } + return -c / b + } + + // 计算判别式 + const discriminant = b * b - 4 * a * c + + if (Math.abs(discriminant) < 1e-10) { + // 一个实根(重根) + return -b / (2 * a) + } else if (discriminant > 0) { + // 两个不同的实根 + const sqrtDiscriminant = Math.sqrt(discriminant) + const x1 = (-b + sqrtDiscriminant) / (2 * a) + const x2 = (-b - sqrtDiscriminant) / (2 * a) + return [x1, x2] + } else { + // 两个共轭复根 + const realPart = -b / (2 * a) + const imaginaryPart = Math.sqrt(-discriminant) / (2 * a) + return [`${realPart} + ${imaginaryPart}i`, `${realPart} - ${imaginaryPart}i`] + } + } catch (error) { + Logger.error(`[Calculator] Error in solveQuadraticEquation:`, error) + throw new Error(`Error solving quadratic equation: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // 求解线性方程组 + private solveLinearEquationSystem(equations: string[], variables: string[]): any { + if (equations.length !== variables.length) { + throw new Error(`Number of equations (${equations.length}) must match number of variables (${variables.length})`) + } + + try { + // 构建系数矩阵和常数向量 + const n = variables.length + const coefficients: number[][] = Array(n) + .fill(0) + .map(() => Array(n).fill(0)) + const constants: number[] = Array(n).fill(0) + + // 对每个方程 + for (let i = 0; i < n; i++) { + let eq = equations[i] + + // 标准化方程为 "表达式 = 0" 的形式 + if (eq.includes('=')) { + const parts = eq.split('=').map((p) => p.trim()) + if (parts.length !== 2) { + throw new Error(`Invalid equation format: ${eq}`) + } + eq = `(${parts[0]}) - (${parts[1]})` + } + + // 对每个变量,计算系数 + for (let j = 0; j < n; j++) { + const variable = variables[j] + + // 替换当前变量为 1,其他变量为 0 + const substitutions: Record = {} + variables.forEach((v) => { + substitutions[v] = 0 + }) + substitutions[variable] = 1 + + // 计算系数 + const withVar = this.evaluateWithSubstitutions(eq, substitutions) + + substitutions[variable] = 0 + const withoutVar = this.evaluateWithSubstitutions(eq, substitutions) + + coefficients[i][j] = withVar - withoutVar + } + + // 计算常数项(所有变量为 0 时的值) + const substitutions: Record = {} + variables.forEach((v) => { + substitutions[v] = 0 + }) + constants[i] = -this.evaluateWithSubstitutions(eq, substitutions) + } + + // 使用高斯消元法求解 + return this.gaussianElimination(coefficients, constants, variables) + } catch (error) { + Logger.error(`[Calculator] Error in solveLinearEquationSystem:`, error) + throw new Error(`Error solving equation system: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // 使用变量替换计算表达式的值 + private evaluateWithSubstitutions(expr: string, substitutions: Record): number { + let evaluableExpr = expr + + // 替换所有变量 + for (const [variable, value] of Object.entries(substitutions)) { + // 使用正则表达式确保只替换完整的变量名 + const regex = new RegExp(`\\b${variable}\\b`, 'g') + evaluableExpr = evaluableExpr.replace(regex, value.toString()) + } + + // 计算表达式的值 + return math.evaluate(evaluableExpr) + } + + // 高斯消元法求解线性方程组 + private gaussianElimination( + coefficients: number[][], + constants: number[], + variables: string[] = [] + ): Record | string { + const n = coefficients.length + const augmentedMatrix = coefficients.map((row, i) => [...row, constants[i]]) + + // 前向消元 + for (let i = 0; i < n; i++) { + // 寻找主元 + let maxRow = i + for (let j = i + 1; j < n; j++) { + if (Math.abs(augmentedMatrix[j][i]) > Math.abs(augmentedMatrix[maxRow][i])) { + maxRow = j + } + } + + // 交换行 + if (maxRow !== i) { + ;[augmentedMatrix[i], augmentedMatrix[maxRow]] = [augmentedMatrix[maxRow], augmentedMatrix[i]] + } + + // 检查是否有解 + if (Math.abs(augmentedMatrix[i][i]) < 1e-10) { + // 检查是否是不一致的方程组 + for (let j = i; j < n; j++) { + if (Math.abs(augmentedMatrix[j][n]) > 1e-10) { + return '方程组无解' + } + } + return '方程组有无穷多解' + } + + // 将主元归一化 + const pivot = augmentedMatrix[i][i] + for (let j = i; j <= n; j++) { + augmentedMatrix[i][j] /= pivot + } + + // 消元 + for (let j = 0; j < n; j++) { + if (j !== i) { + const factor = augmentedMatrix[j][i] + for (let k = i; k <= n; k++) { + augmentedMatrix[j][k] -= factor * augmentedMatrix[i][k] + } + } + } + } + + // 提取解 + const solution: Record = {} + for (let i = 0; i < n; i++) { + solution[variables[i]] = augmentedMatrix[i][n] + } + + return solution + } + + // 处理微积分 + private handleCalculus(args: any) { + Logger.info('[Calculator] Handling calculus', args) + + const operation = args?.operation + const expression = args?.expression + const variable = args?.variable + const order = args?.order || 1 + const from = args?.from + const to = args?.to + const approach = args?.approach + + if (!operation || !expression || !variable) { + throw new McpError(ErrorCode.InvalidParams, 'Operation, expression, and variable are required') + } + + try { + let result + + switch (operation) { + case 'derivative': + result = math.derivative(expression, variable, { order }) + break + case 'integral': + if (from !== undefined && to !== undefined) { + // 定积分 + result = math.integrate(expression, variable, from, to) + } else { + // 不定积分 + result = math.integrate(expression, variable) + } + break + case 'limit': + result = math.limit(expression, variable, approach || 0) + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown calculus operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + expression: expression, + variable: variable, + result: math.format(result, { precision: 14 }) + } + + Logger.info('[Calculator] Calculus result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in calculus operation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in calculus operation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理矩阵计算 + private handleMatrix(args: any) { + Logger.info('[Calculator] Handling matrix operation', args) + + const operation = args?.operation + const matrixStr = args?.matrix + const matrix2Str = args?.matrix2 + const vectorStr = args?.vector + + if (!operation || !matrixStr) { + throw new McpError(ErrorCode.InvalidParams, 'Operation and matrix are required') + } + + try { + // 解析矩阵 + const matrix = math.evaluate(matrixStr) + + let result + + switch (operation) { + case 'det': + result = math.det(matrix) + break + case 'inv': + result = math.inv(matrix) + break + case 'transpose': + result = math.transpose(matrix) + break + case 'eigenvalues': + result = math.eigs(matrix).values + break + case 'eigenvectors': + result = math.eigs(matrix).vectors + break + case 'rank': + result = math.rank(matrix) + break + case 'multiply': + if (!matrix2Str) { + throw new McpError(ErrorCode.InvalidParams, 'Second matrix is required for multiplication') + } + { + const matrix2 = math.evaluate(matrix2Str) + result = math.multiply(matrix, matrix2) + } + break + case 'solve': + if (!vectorStr) { + throw new McpError(ErrorCode.InvalidParams, 'Vector is required for solving linear system') + } + { + const vector = math.evaluate(vectorStr) + result = math.lusolve(matrix, vector) + } + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown matrix operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + matrix: matrixStr, + result: math.format(result, { precision: 14 }) + } + + Logger.info('[Calculator] Matrix operation result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in matrix operation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in matrix operation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理概率计算 + private handleProbability(args: any) { + Logger.info('[Calculator] Handling probability', args) + + const operation = args?.operation + const distribution = args?.distribution + const params = args?.params + const x = args?.x + const n = args?.n + const k = args?.k + const count = args?.count || 1 + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + switch (operation) { + case 'pdf': + if (!distribution || x === undefined || !params) { + throw new McpError(ErrorCode.InvalidParams, 'Distribution, x, and params are required for pdf') + } + + { + const distParams = params.split(',').map((p: string) => parseFloat(p.trim())) + + switch (distribution) { + case 'normal': { + const [mean, std] = distParams + result = math.distribution('normal').pdf(x, mean, std) + break + } + case 'binomial': { + const [trials, prob] = distParams + result = math.distribution('binomial').pdf(x, trials, prob) + break + } + case 'poisson': { + const [lambda] = distParams + result = math.distribution('poisson').pdf(x, lambda) + break + } + case 'uniform': { + const [min, max] = distParams + result = math.distribution('uniform').pdf(x, min, max) + break + } + case 'exponential': { + const [rate] = distParams + result = math.distribution('exponential').pdf(x, rate) + break + } + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown distribution: ${distribution}`) + } + } + break + + case 'cdf': + if (!distribution || x === undefined || !params) { + throw new McpError(ErrorCode.InvalidParams, 'Distribution, x, and params are required for cdf') + } + + { + const cdfParams = params.split(',').map((p: string) => parseFloat(p.trim())) + + switch (distribution) { + case 'normal': { + const [mean, std] = cdfParams + result = math.distribution('normal').cdf(x, mean, std) + break + } + case 'binomial': { + const [trials, prob] = cdfParams + result = math.distribution('binomial').cdf(x, trials, prob) + break + } + case 'poisson': { + const [lambda] = cdfParams + result = math.distribution('poisson').cdf(x, lambda) + break + } + case 'uniform': { + const [min, max] = cdfParams + result = math.distribution('uniform').cdf(x, min, max) + break + } + case 'exponential': { + const [rate] = cdfParams + result = math.distribution('exponential').cdf(x, rate) + break + } + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown distribution: ${distribution}`) + } + } + break + + case 'random': + result = Array.from({ length: count }, () => Math.random()) + break + + case 'combination': + if (n === undefined || k === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'n and k are required for combination') + } + result = math.combinations(n, k) + break + + case 'permutation': + if (n === undefined || k === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'n and k are required for permutation') + } + result = math.permutations(n, k) + break + + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown probability operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Probability result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in probability calculation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in probability calculation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理金融计算 + private handleFinance(args: any) { + Logger.info('[Calculator] Handling finance', args) + + const operation = args?.operation + const rate = args?.rate + const nper = args?.nper + const pmt = args?.pmt + const pv = args?.pv + const fv = args?.fv + const cashflow = args?.cashflow + const type = args?.type || 0 + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + switch (operation) { + case 'pv': + if (rate === undefined || nper === undefined || pmt === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'Rate, nper, and pmt are required for pv') + } + result = math.finance.pv(rate, nper, pmt, fv || 0, type) + break + + case 'fv': + if (rate === undefined || nper === undefined || pmt === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'Rate, nper, and pmt are required for fv') + } + result = math.finance.fv(rate, nper, pmt, pv || 0, type) + break + + case 'pmt': + if (rate === undefined || nper === undefined || (pv === undefined && fv === undefined)) { + throw new McpError(ErrorCode.InvalidParams, 'Rate, nper, and either pv or fv are required for pmt') + } + result = math.finance.pmt(rate, nper, pv || 0, fv || 0, type) + break + + case 'nper': + if (rate === undefined || pmt === undefined || (pv === undefined && fv === undefined)) { + throw new McpError(ErrorCode.InvalidParams, 'Rate, pmt, and either pv or fv are required for nper') + } + result = math.finance.nper(rate, pmt, pv || 0, fv || 0, type) + break + + case 'rate': + if (nper === undefined || pmt === undefined || (pv === undefined && fv === undefined)) { + throw new McpError(ErrorCode.InvalidParams, 'Nper, pmt, and either pv or fv are required for rate') + } + result = math.finance.rate(nper, pmt, pv || 0, fv || 0, type) + break + + case 'irr': + if (!cashflow) { + throw new McpError(ErrorCode.InvalidParams, 'Cashflow is required for irr') + } + { + const irrValues = cashflow.split(',').map((v: string) => parseFloat(v.trim())) + result = math.finance.irr(irrValues) + } + break + + case 'npv': + if (rate === undefined || !cashflow) { + throw new McpError(ErrorCode.InvalidParams, 'Rate and cashflow are required for npv') + } + { + const npvValues = cashflow.split(',').map((v: string) => parseFloat(v.trim())) + result = math.finance.npv(rate, npvValues) + } + break + + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown finance operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Finance result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in finance calculation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in finance calculation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 从方程中提取变量 + private extractVariablesFromEquation(equation: string): string[] { + // 简单的变量提取逻辑,假设变量是单个字母 + const matches = equation.match(/[a-zA-Z]/g) + if (!matches) return [] + + // 去重 + return [...new Set(matches)] + } + + // 从方程组中提取变量 + private extractVariablesFromEquations(equations: string[]): string[] { + // 从所有方程中提取变量并合并 + const allVars = equations.flatMap((eq) => this.extractVariablesFromEquation(eq)) + + // 去重 + return [...new Set(allVars)] + } + + // 工程计数法格式化 + private toEngineeringNotation(num: number, precision: number): string { + const exp = Math.floor(Math.log10(Math.abs(num)) / 3) * 3 + const mantissa = num / Math.pow(10, exp) + return mantissa.toFixed(precision) + 'e' + exp + } + + // 处理物理计算 + private handlePhysics(args: any) { + Logger.info('[Calculator] Handling physics', args) + + const operation = args?.operation + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + // 根据操作类型处理物理计算 + switch (operation) { + case 'constant': + result = this.handlePhysicsConstant(args) + break + case 'kinematics': + case 'dynamics': + case 'energy': + case 'electricity': + case 'thermodynamics': + case 'optics': + result = this.handlePhysicsFormula(args, operation) + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown physics operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Physics result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in physics calculation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in physics calculation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理物理常数 + private handlePhysicsConstant(args: any) { + const constant = args?.constant + + if (!constant) { + throw new McpError(ErrorCode.InvalidParams, 'Constant name is required') + } + + // 物理常数值 + const constants: Record = { + c: { value: 299792458, unit: 'm/s', name: '光速' }, + g: { value: 9.80665, unit: 'm/s²', name: '标准重力加速度' }, + h: { value: 6.62607015e-34, unit: 'J·s', name: '普朗克常数' }, + e: { value: 1.602176634e-19, unit: 'C', name: '基本电荷' }, + k: { value: 1.380649e-23, unit: 'J/K', name: '玻尔兹曼常数' }, + G: { value: 6.6743e-11, unit: 'm³/(kg·s²)', name: '万有引力常数' }, + epsilon0: { value: 8.8541878128e-12, unit: 'F/m', name: '真空介电常数' }, + mu0: { value: 1.25663706212e-6, unit: 'H/m', name: '真空磁导率' } + } + + if (!constants[constant]) { + throw new McpError(ErrorCode.InvalidParams, `Unknown physical constant: ${constant}`) + } + + return constants[constant] + } + + // 处理物理公式 + private handlePhysicsFormula(args: any, category: string) { + const formula = args?.formula + // const params = args?.params || {} + const solve = args?.solve + const units = args?.units !== false + + if (!formula) { + throw new McpError(ErrorCode.InvalidParams, 'Formula is required') + } + + if (!solve) { + throw new McpError(ErrorCode.InvalidParams, 'Variable to solve for is required') + } + + // 这里应该使用 mathjs 的 solve 功能解析公式并求解 + // 由于实现复杂,这里返回一个模拟结果 + return { + formula: formula, + solved_for: solve, + value: 42, // 模拟结果 + unit: units ? this.getPhysicsUnit(category, solve) : null + } + } + + // 获取物理量的单位 + private getPhysicsUnit(category: string, variable: string): string { + // 根据物理类别和变量名返回适当的单位 + const units: Record> = { + kinematics: { v: 'm/s', a: 'm/s²', s: 'm', t: 's' }, + dynamics: { F: 'N', m: 'kg', p: 'kg·m/s' }, + energy: { E: 'J', W: 'J', P: 'W' }, + electricity: { V: 'V', I: 'A', R: 'Ω', Q: 'C' }, + thermodynamics: { T: 'K', Q: 'J', S: 'J/K' }, + optics: { f: 'Hz', λ: 'm', n: '' } + } + + return units[category]?.[variable] || '' + } + + // 处理化学计算 + private handleChemistry(args: any) { + Logger.info('[Calculator] Handling chemistry', args) + + const operation = args?.operation + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + // 根据操作类型处理化学计算 + switch (operation) { + case 'element': + result = this.handleChemistryElement(args) + break + case 'molar_mass': + result = this.handleChemistryMolarMass(args) + break + case 'balance': + result = this.handleChemistryBalance(args) + break + case 'solution': + result = this.handleChemistrySolution() + break + case 'gas': + result = this.handleChemistryGas() + break + case 'stoichiometry': + result = this.handleChemistryStoichiometry() + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown chemistry operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Chemistry result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in chemistry calculation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in chemistry calculation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理元素查询 + private handleChemistryElement(args: any) { + const element = args?.element + + if (!element) { + throw new McpError(ErrorCode.InvalidParams, 'Element symbol is required') + } + + // 元素数据(简化版) + const elements: Record = { + H: { name: '氢', atomic_number: 1, atomic_mass: 1.008, category: '非金属' }, + He: { name: '氦', atomic_number: 2, atomic_mass: 4.0026, category: '惰性气体' }, + C: { name: '碳', atomic_number: 6, atomic_mass: 12.011, category: '非金属' }, + O: { name: '氧', atomic_number: 8, atomic_mass: 15.999, category: '非金属' }, + Fe: { name: '铁', atomic_number: 26, atomic_mass: 55.845, category: '过渡金属' } + } + + if (!elements[element]) { + throw new McpError(ErrorCode.InvalidParams, `Unknown element: ${element}`) + } + + return elements[element] + } + + // 处理摩尔质量计算 + private handleChemistryMolarMass(args: any) { + const formula = args?.formula + + if (!formula) { + throw new McpError(ErrorCode.InvalidParams, 'Chemical formula is required') + } + + // 这里应该解析化学式并计算摩尔质量 + // 由于实现复杂,这里返回一个模拟结果 + return { + formula: formula, + molar_mass: 18.015, // 模拟结果,例如水的摩尔质量 + unit: 'g/mol' + } + } + + // 处理化学方程式平衡 + private handleChemistryBalance(args: any) { + const equation = args?.equation + + if (!equation) { + throw new McpError(ErrorCode.InvalidParams, 'Chemical equation is required') + } + + // 这里应该解析并平衡化学方程式 + // 由于实现复杂,这里返回一个模拟结果 + return { + original_equation: equation, + balanced_equation: '2 H2 + O2 = 2 H2O' // 模拟结果 + } + } + + // 处理溶液计算 + private handleChemistrySolution() { + // 溶液计算的实现 + return { + message: 'Solution calculation functionality will be implemented' + } + } + + // 处理气体计算 + private handleChemistryGas() { + // 气体计算的实现 + return { + message: 'Gas calculation functionality will be implemented' + } + } + + // 处理化学计量学 + private handleChemistryStoichiometry() { + // 化学计量学的实现 + return { + message: 'Stoichiometry calculation functionality will be implemented' + } + } + + // 处理编程功能 + private handleProgramming(args: any) { + Logger.info('[Calculator] Handling programming', args) + + const operation = args?.operation + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + // 根据操作类型处理编程功能 + switch (operation) { + case 'base_convert': + result = this.handleBaseConvert(args) + break + case 'bitwise': + result = this.handleBitwise() + break + case 'logical': + result = this.handleLogical() + break + case 'regex': + result = this.handleRegex() + break + case 'hash': + result = this.handleHash() + break + case 'encode': + result = this.handleEncode() + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown programming operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Programming result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in programming operation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in programming operation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理进制转换 + private handleBaseConvert(args: any) { + const value = args?.value + const fromBase = args?.from_base || 10 + const toBase = args?.to_base || 10 + + if (value === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'Value is required') + } + + try { + // 将输入值解析为十进制 + const decimalValue = parseInt(value.toString(), fromBase) + + // 转换为目标进制 + const result = decimalValue.toString(toBase) + + return { + original_value: value, + from_base: fromBase, + to_base: toBase, + result: result + } + } catch (error) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid value or base: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理位运算 + private handleBitwise() { + // 位运算的实现 + return { + message: 'Bitwise operation functionality will be implemented' + } + } + + // 处理逻辑运算 + private handleLogical() { + // 逻辑运算的实现 + return { + message: 'Logical operation functionality will be implemented' + } + } + + // 处理正则表达式 + private handleRegex() { + // 正则表达式的实现 + return { + message: 'Regex functionality will be implemented' + } + } + + // 处理哈希计算 + private handleHash() { + // 哈希计算的实现 + return { + message: 'Hash calculation functionality will be implemented' + } + } + + // 处理编码转换 + private handleEncode() { + // 编码转换的实现 + return { + message: 'Encoding functionality will be implemented' + } + } + + // 处理日期时间 + private handleDateTime(args: any) { + Logger.info('[Calculator] Handling datetime', args) + + const operation = args?.operation + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + // 根据操作类型处理日期时间 + switch (operation) { + case 'diff': + result = this.handleDateDiff(args) + break + case 'add': + result = this.handleDateAdd(args) + break + case 'format': + result = this.handleDateFormat() + break + case 'timezone': + result = this.handleTimezone() + break + case 'workdays': + result = this.handleWorkdays() + break + case 'calendar': + result = this.handleCalendar() + break + case 'parse': + result = this.handleDateParse() + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown datetime operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Datetime result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in datetime operation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in datetime operation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理日期差值 + private handleDateDiff(args: any) { + const date1Str = args?.date1 + const date2Str = args?.date2 + const unit = args?.unit || 'days' + + if (!date1Str || !date2Str) { + throw new McpError(ErrorCode.InvalidParams, 'Two dates are required for date difference calculation') + } + + try { + // 解析日期 + const date1 = this.parseDate(date1Str) + const date2 = this.parseDate(date2Str) + + if (!date1 || !date2) { + throw new Error('Invalid date format') + } + + // 计算差值(毫秒) + const diffMs = date2.getTime() - date1.getTime() + + // 根据单位转换差值 + let result + + switch (unit) { + case 'years': + result = diffMs / (1000 * 60 * 60 * 24 * 365.25) + break + case 'months': + result = diffMs / (1000 * 60 * 60 * 24 * 30.44) + break + case 'weeks': + result = diffMs / (1000 * 60 * 60 * 24 * 7) + break + case 'days': + result = diffMs / (1000 * 60 * 60 * 24) + break + case 'hours': + result = diffMs / (1000 * 60 * 60) + break + case 'minutes': + result = diffMs / (1000 * 60) + break + case 'seconds': + result = diffMs / 1000 + break + default: + throw new Error(`Unsupported unit: ${unit}`) + } + + // 构建详细的差值信息 + const years = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 365.25)) + const months = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 30.44)) + const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((diffMs % (1000 * 60)) / 1000) + + return { + date1: date1Str, + date2: date2Str, + unit: unit, + difference: result, + details: { + years, + months, + days, + hours, + minutes, + seconds, + milliseconds: diffMs + }, + formatted: this.formatDateDifference(diffMs) + } + } catch (error) { + Logger.error('[Calculator] Error calculating date difference:', error) + throw new McpError( + ErrorCode.InternalError, + `Error calculating date difference: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理日期加减 + private handleDateAdd(args: any) { + const date1Str = args?.date1 + const value = args?.value + const unit = args?.unit || 'days' + + if (!date1Str || value === undefined) { + throw new McpError(ErrorCode.InvalidParams, 'Date and value are required for date addition') + } + + try { + // 解析日期 + const date = this.parseDate(date1Str) + + if (!date) { + throw new Error('Invalid date format') + } + + // 克隆日期对象,避免修改原始对象 + const resultDate = new Date(date.getTime()) + + // 根据单位添加或减去时间 + switch (unit) { + case 'years': + resultDate.setFullYear(resultDate.getFullYear() + value) + break + case 'months': + resultDate.setMonth(resultDate.getMonth() + value) + break + case 'weeks': + resultDate.setDate(resultDate.getDate() + value * 7) + break + case 'days': + resultDate.setDate(resultDate.getDate() + value) + break + case 'hours': + resultDate.setHours(resultDate.getHours() + value) + break + case 'minutes': + resultDate.setMinutes(resultDate.getMinutes() + value) + break + case 'seconds': + resultDate.setSeconds(resultDate.getSeconds() + value) + break + default: + throw new Error(`Unsupported unit: ${unit}`) + } + + return { + original_date: date1Str, + value: value, + unit: unit, + result_date: this.formatDate(resultDate), + result_iso: resultDate.toISOString(), + operation: value >= 0 ? 'add' : 'subtract' + } + } catch (error) { + Logger.error('[Calculator] Error calculating date addition:', error) + throw new McpError( + ErrorCode.InternalError, + `Error calculating date addition: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 解析日期字符串 + private parseDate(dateStr: string): Date | null { + // 尝试多种日期格式 + const date = new Date(dateStr) + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + // 尝试解析特殊格式 + const formats = [ + // 中文日期格式 + { + regex: /(\d{4})年(\d{1,2})月(\d{1,2})日/, + parse: (match: RegExpMatchArray) => new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3])) + } + // 自定义格式... + ] + + for (const format of formats) { + const match = dateStr.match(format.regex) + if (match) { + return format.parse(match) + } + } + + return null + } + + return date + } + + // 格式化日期 + private formatDate(date: Date): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + } + + // 格式化日期差值为人类可读格式 + private formatDateDifference(diffMs: number): string { + const seconds = Math.floor(diffMs / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30.44) + const years = Math.floor(months / 12) + + if (years > 0) { + const remainingMonths = months % 12 + return `${years}年${remainingMonths > 0 ? remainingMonths + '个月' : ''}` + } else if (months > 0) { + const remainingDays = Math.floor(days % 30.44) + return `${months}个月${remainingDays > 0 ? remainingDays + '天' : ''}` + } else if (days > 0) { + const remainingHours = hours % 24 + return `${days}天${remainingHours > 0 ? remainingHours + '小时' : ''}` + } else if (hours > 0) { + const remainingMinutes = minutes % 60 + return `${hours}小时${remainingMinutes > 0 ? remainingMinutes + '分钟' : ''}` + } else if (minutes > 0) { + const remainingSeconds = seconds % 60 + return `${minutes}分钟${remainingSeconds > 0 ? remainingSeconds + '秒' : ''}` + } else { + return `${seconds}秒` + } + } + + // 处理日期格式化 + private handleDateFormat() { + // 日期格式化的实现 + return { + message: 'Date formatting functionality will be implemented' + } + } + + // 处理时区转换 + private handleTimezone() { + // 时区转换的实现 + return { + message: 'Timezone conversion functionality will be implemented' + } + } + + // 处理工作日计算 + private handleWorkdays() { + // 工作日计算的实现 + return { + message: 'Workdays calculation functionality will be implemented' + } + } + + // 处理日历信息 + private handleCalendar() { + // 日历信息的实现 + return { + message: 'Calendar information functionality will be implemented' + } + } + + // 处理日期解析 + private handleDateParse() { + // 日期解析的实现 + return { + message: 'Date parsing functionality will be implemented' + } + } + + // 处理几何计算 + private handleGeometry(args: any) { + Logger.info('[Calculator] Handling geometry', args) + + const operation = args?.operation + + if (!operation) { + throw new McpError(ErrorCode.InvalidParams, 'Operation is required') + } + + try { + let result + + // 根据操作类型处理几何计算 + switch (operation) { + case 'distance': + result = this.handleGeometryDistance() + break + case 'area': + result = this.handleGeometryArea() + break + case 'volume': + result = this.handleGeometryVolume() + break + case 'angle': + result = this.handleGeometryAngle() + break + case 'perimeter': + result = this.handleGeometryPerimeter() + break + case 'coordinates': + result = this.handleGeometryCoordinates() + break + case 'transform': + result = this.handleGeometryTransform() + break + case 'intersection': + result = this.handleGeometryIntersection() + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown geometry operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + result: result + } + + Logger.info('[Calculator] Geometry result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in geometry calculation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in geometry calculation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理距离计算 + private handleGeometryDistance() { + // 距离计算的实现 + return { + message: 'Distance calculation functionality will be implemented' + } + } + + // 处理面积计算 + private handleGeometryArea() { + // 面积计算的实现 + return { + message: 'Area calculation functionality will be implemented' + } + } + + // 处理体积计算 + private handleGeometryVolume() { + // 体积计算的实现 + return { + message: 'Volume calculation functionality will be implemented' + } + } + + // 处理角度计算 + private handleGeometryAngle() { + // 角度计算的实现 + return { + message: 'Angle calculation functionality will be implemented' + } + } + + // 处理周长计算 + private handleGeometryPerimeter() { + // 周长计算的实现 + return { + message: 'Perimeter calculation functionality will be implemented' + } + } + + // 处理坐标计算 + private handleGeometryCoordinates() { + // 坐标计算的实现 + return { + message: 'Coordinate calculation functionality will be implemented' + } + } + + // 处理几何变换 + private handleGeometryTransform() { + // 几何变换的实现 + return { + message: 'Geometric transformation functionality will be implemented' + } + } + + // 处理交点计算 + private handleGeometryIntersection() { + // 交点计算的实现 + return { + message: 'Intersection calculation functionality will be implemented' + } + } + + // 处理图形函数 + private handleGraph(args: any) { + Logger.info('[Calculator] Handling graph', args) + + const operation = args?.operation + const func = args?.function + + if (!operation || !func) { + throw new McpError(ErrorCode.InvalidParams, 'Operation and function are required') + } + + try { + let result + + // 根据操作类型处理图形函数 + switch (operation) { + case 'evaluate': + result = this.handleGraphEvaluate() + break + case 'roots': + result = this.handleGraphRoots() + break + case 'extrema': + result = this.handleGraphExtrema() + break + case 'inflection': + result = this.handleGraphInflection() + break + case 'tangent': + result = this.handleGraphTangent() + break + case 'describe': + result = this.handleGraphDescribe() + break + case 'intersect': + result = this.handleGraphIntersect() + break + default: + throw new McpError(ErrorCode.InvalidParams, `Unknown graph operation: ${operation}`) + } + + // 构建响应对象 + const response = { + operation: operation, + function: func, + result: result + } + + Logger.info('[Calculator] Graph result:', response) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2) + } + ], + isError: false + } + } catch (error) { + Logger.error('[Calculator] Error in graph operation:', error) + throw new McpError( + ErrorCode.InternalError, + `Error in graph operation: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + // 处理函数求值 + private handleGraphEvaluate() { + // 函数求值的实现 + return { + message: 'Function evaluation functionality will be implemented' + } + } + + // 处理求根 + private handleGraphRoots() { + // 求根的实现 + return { + message: 'Root finding functionality will be implemented' + } + } + + // 处理极值点 + private handleGraphExtrema() { + // 极值点的实现 + return { + message: 'Extrema finding functionality will be implemented' + } + } + + // 处理拐点 + private handleGraphInflection() { + // 拐点的实现 + return { + message: 'Inflection point finding functionality will be implemented' + } + } + + // 处理切线 + private handleGraphTangent() { + // 切线的实现 + return { + message: 'Tangent line functionality will be implemented' + } + } + + // 处理图像描述 + private handleGraphDescribe() { + // 图像描述的实现 + return { + message: 'Graph description functionality will be implemented' + } + } + + // 处理交点 + private handleGraphIntersect() { + // 交点的实现 + return { + message: 'Intersection finding functionality will be implemented' + } + } +} + +export default CalculatorServer diff --git a/src/main/mcpServers/dify-knowledge.ts b/src/main/mcpServers/dify-knowledge.ts new file mode 100644 index 0000000000..f5f17e556e --- /dev/null +++ b/src/main/mcpServers/dify-knowledge.ts @@ -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 + +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 { + 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 { + 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 diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 9bda831153..913c5bae7f 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -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}`) } diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 2dd53a0278..a8387450b1 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -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) } diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index a84637f29c..5c0a1a216e 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -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 { @@ -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 { diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 2f1e40d960..ea8da384ab 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -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))) { diff --git a/src/main/services/registerMCPHandlers.ts b/src/main/services/registerMCPHandlers.ts index 468c0a9b24..e2c9593703 100644 --- a/src/main/services/registerMCPHandlers.ts +++ b/src/main/services/registerMCPHandlers.ts @@ -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) diff --git a/src/preload/index.ts b/src/preload/index.ts index e7b5ead104..f193b04ee7 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 }) => ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }), listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 25c683f068..a5061c464e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 { + diff --git a/src/renderer/src/components/OAuth/OAuthButton.tsx b/src/renderer/src/components/OAuth/OAuthButton.tsx index 09b2a8c76f..6160783694 100644 --- a/src/renderer/src/components/OAuth/OAuthButton.tsx +++ b/src/renderer/src/components/OAuth/OAuthButton.tsx @@ -11,6 +11,7 @@ interface Props extends ButtonProps { const OAuthButton: FC = ({ provider, onSuccess, ...buttonProps }) => { const { t } = useTranslation() + const onAuth = () => { const handleSuccess = (key: string) => { if (key.trim()) { @@ -29,8 +30,8 @@ const OAuthButton: FC = ({ provider, onSuccess, ...buttonProps }) => { } return ( - ) } diff --git a/src/renderer/src/components/SentryInitializer.tsx b/src/renderer/src/components/SentryInitializer.tsx new file mode 100644 index 0000000000..8ab370cc58 --- /dev/null +++ b/src/renderer/src/components/SentryInitializer.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +/** + * 空的 SentryInitializer 组件 + * 用于替代原来的 Sentry 初始化组件 + * 在移除 Sentry 后保持应用程序结构不变 + */ +const SentryInitializer: React.FC = () => { + // 这是一个空组件,不需要渲染任何UI + return null +} + +export default SentryInitializer diff --git a/src/renderer/src/components/WebdavBackupManager.tsx b/src/renderer/src/components/WebdavBackupManager.tsx index 32532579ba..fd42449fee 100644 --- a/src/renderer/src/components/WebdavBackupManager.tsx +++ b/src/renderer/src/components/WebdavBackupManager.tsx @@ -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: , 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: , 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: , content: t('settings.data.webdav.restore.confirm.content'), okText: t('common.confirm'), cancelText: t('common.cancel'), + centered: true, onOk: async () => { setRestoring(true) try { diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 889c599448..fc0b5a985e 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -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', diff --git a/src/renderer/src/config/prompts.ts b/src/renderer/src/config/prompts.ts index 9d3e72330a..1bd72cf78d 100644 --- a/src/renderer/src/config/prompts.ts +++ b/src/renderer/src/config/prompts.ts @@ -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 for queries requiring real-time or external information, 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 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 diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index f90de1985b..aac7452c13 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -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' diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 0fd264bff8..e587bff9dc 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -118,7 +118,12 @@ export function useAppInit() { }, [customCss]) useEffect(() => { - enableDataCollection ? initAnalytics() : disableAnalytics() + if (enableDataCollection) { + initAnalytics() + // TODO: init data collection + } else { + disableAnalytics() + } }, [enableDataCollection]) // 自动启动ASR服务器 diff --git a/src/renderer/src/hooks/useMermaid.ts b/src/renderer/src/hooks/useMermaid.ts index c8efbedf58..3bc5de12c0 100644 --- a/src/renderer/src/hooks/useMermaid.ts +++ b/src/renderer/src/hooks/useMermaid.ts @@ -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) { diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 19c3df90c5..bbbbf69442 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -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 中的值 diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 80634aaeb5..9ef4f8cc9b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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 {{provider}}", + "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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 84e79a7a0f..19d8750513 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "本サービスは{{provider}}によって提供されます", + "official_website": "公式サイト" + }, "copilot": { "auth_failed": "Github Copilotの認証に失敗しました。", "auth_success": "Github Copilotの認証が成功しました", @@ -1682,4 +1689,4 @@ "shortcut_key_tip": "このショートカットキーを押すと録音が始まり、キーを離すと録音が終了して送信されます" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 3e96f17559..e21bcd1610 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Сервис предоставляется {{provider}}", + "official_website": "Официальный сайт" + }, "copilot": { "auth_failed": "Github Copilot认证失败", "auth_success": "Github Copilot认证成功", @@ -1681,4 +1688,4 @@ "shortcut_key_tip": "Нажмите эту горячую клавишу, чтобы начать запись, отпустите, чтобы закончить запись и отправить" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f76dc2f534..a6c996e585 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "此服务由{{provider}}提供", + "official_website": "官方网站" + }, "copilot": { "auth_failed": "Github Copilot 认证失败", "auth_success": "Github Copilot 认证成功", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 09a9202439..b12f57ad63 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "此服務由{{provider}}提供", + "official_website": "官方網站" + }, "copilot": { "auth_failed": "Github Copilot認證失敗", "auth_success": "Github Copilot 認證成功", @@ -1682,4 +1689,4 @@ "shortcut_key_tip": "按下此快速鍵開始錄音,放開快速鍵結束錄音並傳送" } } -} \ No newline at end of file +} diff --git a/src/renderer/src/init.ts b/src/renderer/src/init.ts index 24e5648daf..b5e79d70fb 100644 --- a/src/renderer/src/init.ts +++ b/src/renderer/src/init.ts @@ -43,6 +43,8 @@ function initAutoSync() { }, 2000) } + + initSpinner() initKeyv() initAutoSync() diff --git a/src/renderer/src/pages/Browser/components/AddBookmarkDialog.tsx b/src/renderer/src/pages/Browser/components/AddBookmarkDialog.tsx index 86b6f0969d..833e4b1f8e 100644 --- a/src/renderer/src/pages/Browser/components/AddBookmarkDialog.tsx +++ b/src/renderer/src/pages/Browser/components/AddBookmarkDialog.tsx @@ -27,7 +27,18 @@ const AddBookmarkDialog: React.FC = ({ // 将文件夹数据转换为树形结构 useEffect(() => { - if (!folders) return + if (!folders || !Array.isArray(folders)) { + // 如果 folders 不存在或不是数组,设置默认的根目录选项 + setTreeData([ + { + title: 'Bookmarks Bar', + value: null, + key: 'root', + icon: + } + ]); + return; + } // 构建文件夹树 const buildFolderTree = ( @@ -35,14 +46,18 @@ const AddBookmarkDialog: React.FC = ({ parentId: string | null = null ): any[] => { return items - .filter((item) => item.parentId === parentId) - .map((item) => ({ - title: item.title, - value: item.id, - key: item.id, - icon: , - 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: , + children: buildFolderTree(items, item.id) + }; + }) + .filter(Boolean); // 过滤掉 null 项 } // 添加根目录选项 @@ -120,13 +135,14 @@ const AddBookmarkDialog: React.FC = ({ diff --git a/src/renderer/src/pages/home/MCP/index.tsx b/src/renderer/src/pages/home/MCP/index.tsx index e1495ccd05..3d56c7baf8 100644 --- a/src/renderer/src/pages/home/MCP/index.tsx +++ b/src/renderer/src/pages/home/MCP/index.tsx @@ -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('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) => setSearch(e.target.value)} /> + diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index 296e0934e1..4ca943424a 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -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 // 添加 copiedMap prop + editingToolId: string | null // 添加 editingToolId prop + editedToolParamsString: string // 添加 editedParams prop + onToolToggle: React.Dispatch> // 添加 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 = ({ message }) => { +const Markdown: FC = ({ + 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 = ({ 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 = ({ message }) => { {props.children} ) + }, + // 添加 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 ( + + 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 // Keep Components type here + } as Partial 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(() => { diff --git a/src/renderer/src/pages/home/Markdown/Mermaid.tsx b/src/renderer/src/pages/home/Markdown/Mermaid.tsx index 3027853c9f..e8ffdbae46 100644 --- a/src/renderer/src/pages/home/Markdown/Mermaid.tsx +++ b/src/renderer/src/pages/home/Markdown/Mermaid.tsx @@ -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 = ({ chart }) => { const { theme } = useTheme() const mermaidRef = useRef(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 }) } diff --git a/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx b/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx index 55b3db34b4..6dda41c5c0 100644 --- a/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx +++ b/src/renderer/src/pages/home/Markdown/MermaidPopup.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ resolve, chart }) => { } img.src = svgBase64 } + svgElement.style.backgroundColor = 'transparent' } catch (error) { console.error('Download failed:', error) } @@ -153,8 +166,30 @@ const PopupContainer: React.FC = ({ 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 ( = ({ title, children, isActive, onToggle }) => { const contentRef = useRef(null) - const [height, setHeight] = useState(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(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 ( @@ -74,26 +35,11 @@ const CustomCollapse: FC = ({ 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}> -
- {children} -
+ {/* Render children directly */} +
{children}
) @@ -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 diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index 42837dee38..1da44ce18f 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -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 = ({ 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([]) + const [copiedMap, setCopiedMap] = useState>({}) + const [editingToolId, setEditingToolId] = useState(null) + const [editedParams, setEditedParams] = useState('') + + // Local state for immediate UI updates, synced with message metadata + const [localToolResponses, setLocalToolResponses] = useState(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 = ({ 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 = ({ 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 = ({ 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 + // 使用更通用的正则表达式匹配 ... 块 + const toolTagRegex = /[\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 `` + } + // 如果工具响应数量与标记数量不匹配,记录警告并返回原始匹配 + 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 = ({ message: _message, model }) => { if (message.type === '@' && model) { const content = `[@${model.name}](#) ${getBriefInfo(message.content)}` - return + return ( + + ) } // --- MODIFIED LINE BELOW --- @@ -461,18 +715,28 @@ const MessageContent: React.FC = ({ message: _message, model }) => { ]} /> )} -
- {/* Only display thought info at the top */} - - {/* Render MessageTools to display tool blocks based on metadata */} - -
+ {/* Only display thought info at the top */} + {isSegmentedPlayback ? ( // Apply regex replacement here for TTS ) : ( - // Remove tool_use XML tags before rendering Markdown - + // Render Markdown with tool blocks + )} {message.metadata?.generateImage && } {message.translatedContent && ( @@ -484,7 +748,21 @@ const MessageContent: React.FC = ({ message: _message, model }) => { ) : ( // Render translated content (assuming it doesn't need tag removal, adjust if needed) - + )} )} diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index e558a2f597..f2aea95e88 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -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 = ({ message }) => { - const [activeKeys, setActiveKeys] = useState([]) - const [copiedMap, setCopiedMap] = useState>({}) - // Removed expandedResponse state - const [editingToolId, setEditingToolId] = useState(null) - const [editedParams, setEditedParams] = useState('') - 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 = ({ 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 = ({ 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 = ({ 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: ( - - - {tool.name} - - {isInvoking - ? t('message.tools.invoking') - : hasError - ? t('message.tools.error') - : t('message.tools.completed')} - {isInvoking && } - {isDone && !hasError && } - {hasError && } - - - - {isDone && response && ( - <> - - { - e.stopPropagation() - // Toggle the active key for this item - setActiveKeys((prev) => (prev.includes(id) ? prev.filter((k) => k !== id) : [...prev, id])) - }}> - - {activeKeys.includes(id) ? t('common.collapse') : t('common.expand')} - - - - { - e.stopPropagation() - const paramsToRun = editingToolId === id ? editedParams : JSON.stringify(args || {}, null, 2) - handleRerun(toolResponse, paramsToRun) - }}> - - {t('common.rerun')} - - - - { - e.stopPropagation() - handleEdit(toolResponse) - if (!activeKeys.includes(id)) setActiveKeys((prev) => [...prev, id]) - }}> - - {t('common.edit')} - - - - { - e.stopPropagation() - const combinedData = { params: params, response: toolResult } - copyContent(JSON.stringify(combinedData, null, 2), id) - }}> - {copiedMap[id] ? : } - {copiedMap[id] ? t('common.copied') : t('common.copy')} - - - - )} - - - ), - children: isDone ? ( - 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 ( - <> - - {collapseItems.map((item) => ( - { - setActiveKeys((prev) => - prev.includes(item.key as string) ? prev.filter((k) => k !== item.key) : [...prev, item.key as string] - ) - }}> - {item.children} - - ))} - - - {/* 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) diff --git a/src/renderer/src/pages/home/Messages/SingleToolCallBlock.tsx b/src/renderer/src/pages/home/Messages/SingleToolCallBlock.tsx new file mode 100644 index 0000000000..1efea64729 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/SingleToolCallBlock.tsx @@ -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 = ({ + 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 type + onParamsChange(e.target.value) + } + + return ( + + + {tool.name} + + {isInvoking + ? t('message.tools.invoking') + : hasError + ? t('message.tools.error') + : t('message.tools.completed')} + {isInvoking && } + {isDone && !hasError && } + {hasError && } + + + + {isDone && response && ( + <> + + { + 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 + }}> + + {isActive ? t('common.collapse') : t('common.expand')} + + + + { + e.stopPropagation() + console.log('[SingleToolCallBlock] Rerun button clicked for ID:', id) // Log button click + handleRerunClick() + }}> + + {t('common.rerun')} + + + + { + e.stopPropagation() + console.log('[SingleToolCallBlock] Edit button clicked for ID:', id) // Log button click + handleEditClick() + }}> + + {t('common.edit')} + + + + { + e.stopPropagation() + console.log('[SingleToolCallBlock] Copy button clicked for ID:', id) // Log button click + handleCopyClick() + }}> + {isCopied ? : } + {isCopied ? t('common.copied') : t('common.copy')} + + + + )} + + + } + isActive={isActive} + onToggle={onToggle}> + {isDone ? ( + + ) : null} + + ) +} + +// --- 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) diff --git a/src/renderer/src/pages/home/Messages/ToolResponseContent.tsx b/src/renderer/src/pages/home/Messages/ToolResponseContent.tsx index 5d5364c27c..ccd9da7e72 100644 --- a/src/renderer/src/pages/home/Messages/ToolResponseContent.tsx +++ b/src/renderer/src/pages/home/Messages/ToolResponseContent.tsx @@ -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 = ({ 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 = ({ // Render separate sections for params and response in a flex container return ( + {/* 添加全局样式 */} {isVisible && isContentReady ? (
- {t('message.tools.parameters')}: + {t('message.tools.parameters')} {isEditing ? ( onParamsChange(e.target.value)} - style={{ fontFamily: 'ubuntu', fontSize: '12px' }} // Ensure consistent font + style={{ fontFamily: 'ubuntu', fontSize: '13px' }} // 增大字体 />
- {' '} - {/* Adjust flex-basis to 60% */} - {t('message.tools.results')}: - {responseStringRef.current} + {t('message.tools.results')} + + {responseStringRef.current} +
) : ( @@ -201,32 +385,52 @@ const ToolResponseContent: FC = ({ 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 包装组件,避免不必要的重渲染 diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index a2ba086880..ed0d06592b 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -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) => { {t('settings.messages.math_engine')} dispatch(setMathEngine(value as 'MathJax' | 'KaTeX'))} + onChange={(value) => dispatch(setMathEngine(value as MathEngine))} style={{ width: 135 }} size="small"> KaTeX MathJax + {t('settings.messages.math_engine.none')} diff --git a/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx index 5ef1b4319a..399289be7c 100644 --- a/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx +++ b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx @@ -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 = ({ 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 = ({ title, resolve }) => { provider: SUPPORTED_REANK_PROVIDERS.map((id) => t(`provider.${id}`)) })} + + +
) diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index cf6a062b78..6ee8beb9c7 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -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(storeProxyUrl) const { theme: themeMode } = useTheme() @@ -185,7 +186,13 @@ const GeneralSettings: FC = () => { {t('settings.privacy.enable_privacy_mode')} - dispatch(setEnableDataCollection(v))} /> + { + setEnableDataCollection(v) + window.api.sentry.init() + }} + /> diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 65cbf95a8a..bf400d023b 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -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 = ({ server }) => { loading={loadingServer === server.id} onChange={onToggleActive} /> + {server.isActive && ( // Only show refresh button if server is active + + + + )} + + + ) + }} + values={{ provider: providerWebsite }} + /> + + + ) +} + +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 diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 8defea42da..a259cd53d8 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -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 = ({ provider: _provider }) => { autoFocus={provider.enabled && apiKey === ''} disabled={provider.id === 'copilot'} /> - {isProviderSupportAuth(provider) && } - {apiKeyWebsite && ( + {isProviderSupportAuth(provider) && } + {apiKeyWebsite && !isProviderSupportAuth(provider) && ( {t('settings.provider.get_api_key')} - {isProviderSupportCharge(provider) && ( - providerCharge(provider.id)}> - {t('settings.provider.charge')} - - )} {t('settings.provider.api_key.tip')} diff --git a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts index bbb228b998..5511067720 100644 --- a/src/renderer/src/providers/AiProvider/AnthropicProvider.ts +++ b/src/renderer/src/providers/AiProvider/AnthropicProvider.ts @@ -537,6 +537,10 @@ export default class AnthropicProvider extends BaseProvider { return [] } + public async generateImageByChat(): Promise { + throw new Error('Method not implemented.') + } + /** * Generate suggestions * @returns The suggestions diff --git a/src/renderer/src/providers/AiProvider/BaseProvider.ts b/src/renderer/src/providers/AiProvider/BaseProvider.ts index 2b21ec460f..ef201ab22f 100644 --- a/src/renderer/src/providers/AiProvider/BaseProvider.ts +++ b/src/renderer/src/providers/AiProvider/BaseProvider.ts @@ -49,6 +49,7 @@ export default abstract class BaseProvider { abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }> abstract models(): Promise abstract generateImage(params: GenerateImageParams): Promise + abstract generateImageByChat({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise abstract getEmbeddingDimensions(model: Model): Promise public getBaseURL(): string { diff --git a/src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts b/src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts index 721ff3e634..eef3e59461 100644 --- a/src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts +++ b/src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts @@ -530,6 +530,14 @@ ${state.extractedThinking}` return this.targetProvider.generateImage(params) } + /** + * 通过聊天生成图像 + */ + public async generateImageByChat(params: CompletionsParams): Promise { + // 使用目标模型通过聊天生成图像 + return this.targetProvider.generateImageByChat(params) + } + /** * 获取嵌入维度 */ diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts index cda464bc52..6da8bc88ae 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -1416,4 +1416,8 @@ export default class GeminiProvider extends BaseProvider { return truncated } + + public generateImageByChat(): Promise { + throw new Error('Method not implemented.') + } } diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 7eb66a1bcd..99b40dbe1c 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -308,6 +308,10 @@ export default class OpenAIProvider extends BaseProvider { * @returns The completions */ async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams): Promise { + 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 { + 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) + } + }) + } } diff --git a/src/renderer/src/providers/AiProvider/index.ts b/src/renderer/src/providers/AiProvider/index.ts index ca10ba109d..6072fc437f 100644 --- a/src/renderer/src/providers/AiProvider/index.ts +++ b/src/renderer/src/providers/AiProvider/index.ts @@ -116,6 +116,15 @@ export default class AiProvider { return this.sdk.generateImage(params) } + public async generateImageByChat({ + messages, + assistant, + onChunk, + onFilterMessages + }: CompletionsParams): Promise { + return this.sdk.generateImageByChat({ messages, assistant, onChunk, onFilterMessages }) + } + public async getEmbeddingDimensions(model: Model): Promise { return this.sdk.getEmbeddingDimensions(model) } diff --git a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts index 3fd7993b20..860383ee08 100644 --- a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts @@ -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) diff --git a/src/renderer/src/services/KnowledgeService.ts b/src/renderer/src/services/KnowledgeService.ts index 0deb8730ea..da096e906b 100644 --- a/src/renderer/src/services/KnowledgeService.ts +++ b/src/renderer/src/services/KnowledgeService.ts @@ -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, diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 07983767a1..fb5eb8fdf3 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -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 } ] diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 63e49e1b54..b6e9b14db9 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -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) => { state.codeCacheThreshold = action.payload }, - setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => { + setMathEngine: (state, action: PayloadAction) => { state.mathEngine = action.payload }, setFoldDisplayMode: (state, action: PayloadAction<'expanded' | 'compact'>) => { diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 5bc47b63d1..f2f97f1ff4 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -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, diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index e17546a6d0..b5dfe2cd5e 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -633,3 +633,5 @@ export type TTSProvider = { voice?: string model?: string } + +export type MathEngine = 'KaTeX' | 'MathJax' | 'none' diff --git a/src/renderer/src/utils/markdown.ts b/src/renderer/src/utils/markdown.ts index 96de068a30..f1926c3929 100644 --- a/src/renderer/src/utils/markdown.ts +++ b/src/renderer/src/utils/markdown.ts @@ -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'], diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index c160d49d0e..c251a73333 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -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` + ) +} diff --git a/src/shared/IpcChannel.ts b/src/shared/IpcChannel.ts index ab968b0ac3..b3cc75bc05 100644 --- a/src/shared/IpcChannel.ts +++ b/src/shared/IpcChannel.ts @@ -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', diff --git a/yarn.lock b/yarn.lock index e8725698cf..ffa4866b9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,7 +296,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.18.5, @babel/core@npm:^7.26.0": +"@babel/core@npm:^7.26.0": version: 7.26.10 resolution: "@babel/core@npm:7.26.10" dependencies: @@ -2690,7 +2690,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 @@ -3737,407 +3737,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/api-logs@npm:0.57.2" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/1e514d3fd4ca68e7e8b008794a95ee0562a5d9e1d3ebb02647b245afaa6c2d72cc14e99e3ea47a1d1007f8a965c62bfb6170e1aa26756230bea063cfde2898bf - languageName: node - linkType: hard - -"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": - version: 1.9.0 - resolution: "@opentelemetry/api@npm:1.9.0" - checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add - languageName: node - linkType: hard - -"@opentelemetry/context-async-hooks@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/context-async-hooks@npm:1.30.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/3e8114d360060a5225226d2fcd8df08cd542246003790a7f011c0774bc60b8a931f46f4c6673f3977a7d9bba717de6ee028cae51b752c2567053d7f46ed3eba3 - languageName: node - linkType: hard - -"@opentelemetry/core@npm:1.30.1, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.26.0, @opentelemetry/core@npm:^1.30.1, @opentelemetry/core@npm:^1.8.0": - version: 1.30.1 - resolution: "@opentelemetry/core@npm:1.30.1" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.28.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/4c25ba50a6137c2ba9ca563fb269378f3c9ca6fd1b3f15dbb6eff78eebf5656f281997cbb7be8e51c01649fd6ad091083fcd8a42dd9b5dfac907dc06d7cfa092 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-amqplib@npm:^0.46.1": - version: 0.46.1 - resolution: "@opentelemetry/instrumentation-amqplib@npm:0.46.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/4a8b870ccaa64cfd200663ec14385aca7eeb7146124d82e566f3d48678f237c9a56661ae3401345fe0dce5c56366ae02a312dc7905eb4fd6e073df2cface30fb - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-connect@npm:0.43.1": - version: 0.43.1 - resolution: "@opentelemetry/instrumentation-connect@npm:0.43.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@types/connect": "npm:3.4.38" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/a7e2629fbfa775f2d1a6b2c9387e27809db16177cf6de89159017d7353c270c6c84d81550c58ccc51ea72c2304b1fcb911499440451d8df6954cc1f4e654eb64 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-dataloader@npm:0.16.1": - version: 0.16.1 - resolution: "@opentelemetry/instrumentation-dataloader@npm:0.16.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/83bd0267672cc3e8709401e1f107612aed3bb72faedfed76fe25e174b19c41f65d503bc3a666ba0872bbef8c31adcefb8884982f785fa3b0df28eec40b6578aa - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-express@npm:0.47.1": - version: 0.47.1 - resolution: "@opentelemetry/instrumentation-express@npm:0.47.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/eca448eb088857c7c0c7d0a1875b9e20a990b23e2f64355d2e645618d3f5c038efb9d605009a6d8fa1e05243d0ccef14b9aa1effffee693fd071de3cc39ad3d1 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-fastify@npm:0.44.2": - version: 0.44.2 - resolution: "@opentelemetry/instrumentation-fastify@npm:0.44.2" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/2306bbf9b59e5d29002b9c14493de92817e268c92587b285a0a4bbeece4a3f791db8e0d1be0e22ad9f6a97075071aa5c3269cf2d219e378480dc1ceafbf2927c - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-fs@npm:0.19.1": - version: 0.19.1 - resolution: "@opentelemetry/instrumentation-fs@npm:0.19.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/8bf714658c0fcc34ba7db4c28af3196690f756a9b4fb6d1b6cab59938a7b5c1e40e834c518b39085e744915c0c384ca6d997a8a97901955732acf3af0cba6e7f - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-generic-pool@npm:0.43.1": - version: 0.43.1 - resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.43.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/bdc95146d1f6f5dcf5922af8161c4954b9feeb505a01c5e61b1246ed67909dc1f6e72ad067839f085a4977e863246e7e4b468c814cf4104f35fcc20fb570eac2 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-graphql@npm:0.47.1": - version: 0.47.1 - resolution: "@opentelemetry/instrumentation-graphql@npm:0.47.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/d5cfeb668b5ea4e4d97d8433c642457ac9f7f2023278a84a183b4c4c2cc43bbae3eac916ff7176ef8492661877560b519663c52eb2fad0a8a1f00718a0449aa6 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-hapi@npm:0.45.2": - version: 0.45.2 - resolution: "@opentelemetry/instrumentation-hapi@npm:0.45.2" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/bb491327ce86d8f1f7e2a12621a00dbf921e1fc3e9b64f975fc23e443d92bcd6ef779b34349214871763d459650da219c5e23bb1fdd1bc261fa0f92190521b2e - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-http@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation-http@npm:0.57.2" - dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/instrumentation": "npm:0.57.2" - "@opentelemetry/semantic-conventions": "npm:1.28.0" - forwarded-parse: "npm:2.1.2" - semver: "npm:^7.5.2" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/b95a1b61cddabd32358fa565a4fcf5c17e8340907b171dcdf2a104533c9afdee821efa7b82dabb3123318dcc66272b0a7b8c37c44fc87e593cb8138a7a63fc23 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-ioredis@npm:0.47.1": - version: 0.47.1 - resolution: "@opentelemetry/instrumentation-ioredis@npm:0.47.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/redis-common": "npm:^0.36.2" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ec741778041cecc133a143292d66631c99311bf098db8f03276a48b87fe18826eec4513e4de70bb555ef50268db6520442e9a2f7752f7ea9b5a3e8363fecb8c9 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-kafkajs@npm:0.7.1": - version: 0.7.1 - resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.7.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/440a479ec65414da706f15b5c2ff82235ea8b11701e811ae235af5e8f01bee7e639223243e9f18550ce55cc94b8cdaa8a72297ded55f1c7993f1d95488c2b02e - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-knex@npm:0.44.1": - version: 0.44.1 - resolution: "@opentelemetry/instrumentation-knex@npm:0.44.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/75dcbda2c412cc448ac95238899d92846bda14bb21a1c9e9bc0c51fd48dcedb6064c2a8ab9e53d112945748d50513ecda13afbc4c0f24a884674d2a485f0efcd - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-koa@npm:0.47.1": - version: 0.47.1 - resolution: "@opentelemetry/instrumentation-koa@npm:0.47.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/a1c5433da1265f1f8da3e46ebe085a3ddba3e16f43c5f44bd41082a0839f6bdf9a6a737b80b0d2f2a05d1ef2c23e2b0a4f7e55858bf1e32570b4c150c69135bc - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-lru-memoizer@npm:0.44.1": - version: 0.44.1 - resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.44.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/5728d0b6ed560ba8426546ab30ef251cbe9f25a130abc8bea0d7635b51cc29fbade4d00c7b1869fa0543fe54891799483fe0f6fb4073d1bf5d12dbdd543aaae5 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-mongodb@npm:0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-mongodb@npm:0.52.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/dcd072a296369a6b254a809e3708e5f9842ac9f8c61700bfa2014872fa6e6ca65adfa5efdbf9021df57e749dea2cddd828351e73cb581370b8b97693c06df7e8 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-mongoose@npm:0.46.1": - version: 0.46.1 - resolution: "@opentelemetry/instrumentation-mongoose@npm:0.46.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/69378d41df172c2edb8b36042e751936837bb1cbee11ee72a3d1608c6d7f609d79beec2020b25de72086553ad9d85347642c4066e0b4e96d442513b29ac4f0aa - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-mysql2@npm:0.45.2": - version: 0.45.2 - resolution: "@opentelemetry/instrumentation-mysql2@npm:0.45.2" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@opentelemetry/sql-common": "npm:^0.40.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/8ac62064b32facfddd7d47ba0bce9689d2277ba4ef74348655faffe818522c919654c5bf1a5fac211a75f2093fbd588a14cba278c353da2f60d1919d58d419aa - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-mysql@npm:0.45.1": - version: 0.45.1 - resolution: "@opentelemetry/instrumentation-mysql@npm:0.45.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@types/mysql": "npm:2.15.26" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/c820a6929fe2e010dacb8962d40fdb8c9ac95c265efc74f478eadc021b2a3add9ce8d303c4bda20af01327564f487c9e052e710d9e975d7f17a5918d802d7ae4 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-pg@npm:0.51.1": - version: 0.51.1 - resolution: "@opentelemetry/instrumentation-pg@npm:0.51.1" - dependencies: - "@opentelemetry/core": "npm:^1.26.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@opentelemetry/sql-common": "npm:^0.40.1" - "@types/pg": "npm:8.6.1" - "@types/pg-pool": "npm:2.0.6" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/fff3dcc092b959601a20c20e19c27d39d6386e6bc2b7014c1be5a5e22c0e275bf9980dad758b1f7824b1448a6178e13938b6bb2da53095f410fbb4d248b5ede6 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-redis-4@npm:0.46.1": - version: 0.46.1 - resolution: "@opentelemetry/instrumentation-redis-4@npm:0.46.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/redis-common": "npm:^0.36.2" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/df0bdd865e254c9b4c0339ce5aabf3698d99b8ab8cf8ea1aa57ffa13620f2193fda247ed43ec4ccc6edadab1ffec5cc263038ab6f3c9e96ef000ee232b9181f8 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-tedious@npm:0.18.1": - version: 0.18.1 - resolution: "@opentelemetry/instrumentation-tedious@npm:0.18.1" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.57.1" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@types/tedious": "npm:^4.0.14" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/fda9ac4dc89998a2cf739a70f06b1d6eebf98fe22713dc3fbca4a1119dc289d83c91ada4a3cea37f39a34c69978ae21ff9b599c27beaee128879b993677696dc - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-undici@npm:0.10.1": - version: 0.10.1 - resolution: "@opentelemetry/instrumentation-undici@npm:0.10.1" - dependencies: - "@opentelemetry/core": "npm:^1.8.0" - "@opentelemetry/instrumentation": "npm:^0.57.1" - peerDependencies: - "@opentelemetry/api": ^1.7.0 - checksum: 10c0/3958f291d14f2f7bb5e3b957487444ffee449d8ea76c973ca09b9669258d37e98b3797c7167190c5038802529cc6b539bdf6efc7887398ec9b53d3ba51c90bda - languageName: node - linkType: hard - -"@opentelemetry/instrumentation@npm:0.57.2, @opentelemetry/instrumentation@npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0, @opentelemetry/instrumentation@npm:^0.57.1, @opentelemetry/instrumentation@npm:^0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation@npm:0.57.2" - dependencies: - "@opentelemetry/api-logs": "npm:0.57.2" - "@types/shimmer": "npm:^1.2.0" - import-in-the-middle: "npm:^1.8.1" - require-in-the-middle: "npm:^7.1.1" - semver: "npm:^7.5.2" - shimmer: "npm:^1.2.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/79ca65b66357665d19f89da7027da25ea1c6b55ecdacb0a99534923743c80deb9282870db563de8ae284b13e7e0aab8413efa1937f199deeaef069e07c7e4875 - languageName: node - linkType: hard - -"@opentelemetry/redis-common@npm:^0.36.2": - version: 0.36.2 - resolution: "@opentelemetry/redis-common@npm:0.36.2" - checksum: 10c0/4cb831628551b9f13dca8d65897e300ff7be0e256b77f455a26fb053bbdfc7997b27d066ab1402ca929e7ac77598e0d593f91762d8af9f798c19ba1524e9d078 - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:1.30.1, @opentelemetry/resources@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/resources@npm:1.30.1" - dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/688e73258283c80662bfa9a858aaf73bf3b832a18d96e546d0dddfa6dcec556cdfa087a1d0df643435293406009e4122d7fb7eeea69aa87b539d3bab756fba74 - languageName: node - linkType: hard - -"@opentelemetry/sdk-trace-base@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/sdk-trace-base@npm:1.30.1" - dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/77019dc3efaeceb41b4c54dd83b92f0ccd81ecceca544cbbe8e0aee4b2c8727724bdb9dcecfe00622c16d60946ae4beb69a5c0e7d85c4bc7ef425bd84f8b970c - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:1.28.0": - version: 1.28.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.28.0" - checksum: 10c0/deb8a0f744198071e70fea27143cf7c9f7ecb7e4d7b619488c917834ea09b31543c1c2bcea4ec5f3cf68797f0ef3549609c14e859013d9376400ac1499c2b9cb - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.30.0": - version: 1.32.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.32.0" - checksum: 10c0/977c93225490f2456e8bb13b90a8627861207eb5eb4771d7565c2321be883ec711c1701485451f9e10b8d2a724525496c0e4441b43190a7a550bcf7c73f681cd - languageName: node - linkType: hard - -"@opentelemetry/sql-common@npm:^0.40.1": - version: 0.40.1 - resolution: "@opentelemetry/sql-common@npm:0.40.1" - dependencies: - "@opentelemetry/core": "npm:^1.1.0" - peerDependencies: - "@opentelemetry/api": ^1.1.0 - checksum: 10c0/60a70358f0c94f610e2995333e96b406626d67d03d38ed03b15a3461ad0f8d64afbf6275cca7cb58fe955ecdce832f3ffc9b73f9d88503bba5d2a620bbd6d351 - languageName: node - linkType: hard - "@parcel/watcher-android-arm64@npm:2.5.1": version: 2.5.1 resolution: "@parcel/watcher-android-arm64@npm:2.5.1" @@ -4321,17 +3920,6 @@ __metadata: languageName: node linkType: hard -"@prisma/instrumentation@npm:6.5.0": - version: 6.5.0 - resolution: "@prisma/instrumentation@npm:6.5.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" - peerDependencies: - "@opentelemetry/api": ^1.8 - checksum: 10c0/b9223ccc437d0f1f82f8ea8ce50befed18d5337b1079f7a652a0c4e6d9645bcae3ccaf124cc90b94d336b47f195f4bf7b1c4513fac3cb07a8952df576c5b891b - languageName: node - linkType: hard - "@rc-component/async-validator@npm:^5.0.3": version: 5.0.4 resolution: "@rc-component/async-validator@npm:5.0.4" @@ -4770,324 +4358,6 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry-internal/browser-utils@npm:9.11.0" - dependencies: - "@sentry/core": "npm:9.11.0" - checksum: 10c0/83b163e8590b0bd64b843321c6b1172ce6cf0bdc7d97fc2aa88b57cf0d388d52bead2272b64d6b2657a31c0dbce89d660279812dbfc047dc55d02c1bbffd49d3 - languageName: node - linkType: hard - -"@sentry-internal/browser-utils@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry-internal/browser-utils@npm:9.14.0" - dependencies: - "@sentry/core": "npm:9.14.0" - checksum: 10c0/4c5907b210ba1cc4add98294146348baedcee67c5339d61fc8c8a5e57add478292f72cef8dc82064aa197381fae089926b9de4a9915bc86ac81ea09b3496633f - languageName: node - linkType: hard - -"@sentry-internal/feedback@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry-internal/feedback@npm:9.11.0" - dependencies: - "@sentry/core": "npm:9.11.0" - checksum: 10c0/5b3a091c30b8536b1836089a86721942d49beebc7e77887986c52e58ed49772de51b5376e33278ade1f8c48e66cc9ba0d8b5fd0d9d66dc3ced60028002c8ac50 - languageName: node - linkType: hard - -"@sentry-internal/feedback@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry-internal/feedback@npm:9.14.0" - dependencies: - "@sentry/core": "npm:9.14.0" - checksum: 10c0/5a78d458c76e95772938aeb88da00260007b7a1b6b51e0ca60bdf17f848df3828daba8b658021d0e842f7365c0d13e648ce26a0d4057880c1735e4644dabecb3 - languageName: node - linkType: hard - -"@sentry-internal/replay-canvas@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry-internal/replay-canvas@npm:9.11.0" - dependencies: - "@sentry-internal/replay": "npm:9.11.0" - "@sentry/core": "npm:9.11.0" - checksum: 10c0/c43959d73dde11def09745ae2d1ea195f5ad97160f9d4982118ff7dace798a999abcc042209acf03b0008a414f114c5005dd7867eef62a2e922401b4367630f0 - languageName: node - linkType: hard - -"@sentry-internal/replay-canvas@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry-internal/replay-canvas@npm:9.14.0" - dependencies: - "@sentry-internal/replay": "npm:9.14.0" - "@sentry/core": "npm:9.14.0" - checksum: 10c0/73d8b6d56677f393969e74aaf780377d06b7bbc56f7e8c94482b23697a9449bf362bd2f56cf9b9243ac228082a75a02e92f283db395ba0fd424f1f16c4a1e29b - languageName: node - linkType: hard - -"@sentry-internal/replay@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry-internal/replay@npm:9.11.0" - dependencies: - "@sentry-internal/browser-utils": "npm:9.11.0" - "@sentry/core": "npm:9.11.0" - checksum: 10c0/568eb8ba32710a8bb9e8fb4566c3365672f437f661c13da5ffcba6f55932e23774031388f545ddc517ed8a37759048a9b57ce400d2d8c62ff1146b41ce92d9f5 - languageName: node - linkType: hard - -"@sentry-internal/replay@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry-internal/replay@npm:9.14.0" - dependencies: - "@sentry-internal/browser-utils": "npm:9.14.0" - "@sentry/core": "npm:9.14.0" - checksum: 10c0/c6c455bfbc3ea9280878fa4c49d56b61658b04355265487bbd1156d2c628b02dbf60aec950d1adfafd1e4f48aad50cbe3e003231ba0b0456ad572a9cf63a0cef - languageName: node - linkType: hard - -"@sentry/babel-plugin-component-annotate@npm:3.3.1": - version: 3.3.1 - resolution: "@sentry/babel-plugin-component-annotate@npm:3.3.1" - checksum: 10c0/f7be2e93be57f6391cbf01b04f0ccb5fc60f939cd3b834269748b70c38cc9dbaba288539afa19f066b9b4bf32cf6c3ef7ad86505d8370d5ac062bfd93940c7a1 - languageName: node - linkType: hard - -"@sentry/browser@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry/browser@npm:9.11.0" - dependencies: - "@sentry-internal/browser-utils": "npm:9.11.0" - "@sentry-internal/feedback": "npm:9.11.0" - "@sentry-internal/replay": "npm:9.11.0" - "@sentry-internal/replay-canvas": "npm:9.11.0" - "@sentry/core": "npm:9.11.0" - checksum: 10c0/6e5563aea64dc3c3f07eca213d1d9fd540a8d2ba10cf9d86d0cda1c3833fbe940813860213adbe221c848bc935d3304aa6abd8aae72225158723fc7defbe137c - languageName: node - linkType: hard - -"@sentry/browser@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry/browser@npm:9.14.0" - dependencies: - "@sentry-internal/browser-utils": "npm:9.14.0" - "@sentry-internal/feedback": "npm:9.14.0" - "@sentry-internal/replay": "npm:9.14.0" - "@sentry-internal/replay-canvas": "npm:9.14.0" - "@sentry/core": "npm:9.14.0" - checksum: 10c0/4ac0d0e236052668c630429b0ec4dc9ca6576ec9042295d7ab5cababe4f9ecedc4ce53b86d2b17cadb7639f5b5a6d4286279fe1a6d96851167601494f2478f45 - languageName: node - linkType: hard - -"@sentry/bundler-plugin-core@npm:3.3.1": - version: 3.3.1 - resolution: "@sentry/bundler-plugin-core@npm:3.3.1" - dependencies: - "@babel/core": "npm:^7.18.5" - "@sentry/babel-plugin-component-annotate": "npm:3.3.1" - "@sentry/cli": "npm:2.42.2" - dotenv: "npm:^16.3.1" - find-up: "npm:^5.0.0" - glob: "npm:^9.3.2" - magic-string: "npm:0.30.8" - unplugin: "npm:1.0.1" - checksum: 10c0/d16829d4996d5869eb503866c043a85ffe49f3c9b42c745b9356f6978c02ad3c8b37bc8f5f49e18a948394c876ce11099d264eb81f149cfea4912883a5fe779b - languageName: node - linkType: hard - -"@sentry/cli-darwin@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-darwin@npm:2.42.2" - conditions: os=darwin - languageName: node - linkType: hard - -"@sentry/cli-linux-arm64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-arm64@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=arm64 - languageName: node - linkType: hard - -"@sentry/cli-linux-arm@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-arm@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=arm - languageName: node - linkType: hard - -"@sentry/cli-linux-i686@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-i686@npm:2.42.2" - conditions: (os=linux | os=freebsd) & (cpu=x86 | cpu=ia32) - languageName: node - linkType: hard - -"@sentry/cli-linux-x64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-x64@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=x64 - languageName: node - linkType: hard - -"@sentry/cli-win32-i686@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-win32-i686@npm:2.42.2" - conditions: os=win32 & (cpu=x86 | cpu=ia32) - languageName: node - linkType: hard - -"@sentry/cli-win32-x64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-win32-x64@npm:2.42.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@sentry/cli@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli@npm:2.42.2" - dependencies: - "@sentry/cli-darwin": "npm:2.42.2" - "@sentry/cli-linux-arm": "npm:2.42.2" - "@sentry/cli-linux-arm64": "npm:2.42.2" - "@sentry/cli-linux-i686": "npm:2.42.2" - "@sentry/cli-linux-x64": "npm:2.42.2" - "@sentry/cli-win32-i686": "npm:2.42.2" - "@sentry/cli-win32-x64": "npm:2.42.2" - https-proxy-agent: "npm:^5.0.0" - node-fetch: "npm:^2.6.7" - progress: "npm:^2.0.3" - proxy-from-env: "npm:^1.1.0" - which: "npm:^2.0.2" - dependenciesMeta: - "@sentry/cli-darwin": - optional: true - "@sentry/cli-linux-arm": - optional: true - "@sentry/cli-linux-arm64": - optional: true - "@sentry/cli-linux-i686": - optional: true - "@sentry/cli-linux-x64": - optional: true - "@sentry/cli-win32-i686": - optional: true - "@sentry/cli-win32-x64": - optional: true - bin: - sentry-cli: bin/sentry-cli - checksum: 10c0/286910a194a386b9f21ef9e6dc171cbba35f1db09cb3bd53f19724ccf08b0819fb1c35e7fd2e7844da24a28428ca94ff3be7ae85f14056d6e6fc0193976d3815 - languageName: node - linkType: hard - -"@sentry/core@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry/core@npm:9.11.0" - checksum: 10c0/566aaaad16afb34ada9d9a42b0bc486e127413ce1b4e5a883c5424e04b95d8855e060267b18e8e2a16ee05d3708aab6191ad855de0a150877249c58e5d64fce6 - languageName: node - linkType: hard - -"@sentry/core@npm:9.14.0": - version: 9.14.0 - resolution: "@sentry/core@npm:9.14.0" - checksum: 10c0/9008e876d6f1b863852380013466f7dfaed19f864aa840d8cc3b65f22f2347abf0a02d7048cdd0ab746f0648931f375748891b68103d69e48e54ead11e199e1d - languageName: node - linkType: hard - -"@sentry/electron@npm:^6.5.0": - version: 6.5.0 - resolution: "@sentry/electron@npm:6.5.0" - dependencies: - "@sentry/browser": "npm:9.11.0" - "@sentry/core": "npm:9.11.0" - "@sentry/node": "npm:9.11.0" - deepmerge: "npm:4.3.1" - checksum: 10c0/f1fdbf41d49a864086e9fd9af3c0c807e16b1ada67f18cf8825697de6df8e89f41d23974fef12e90fb502eb63a9c8b8ccda16145ad92eede076bb99a26e57485 - languageName: node - linkType: hard - -"@sentry/node@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry/node@npm:9.11.0" - dependencies: - "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/context-async-hooks": "npm:^1.30.1" - "@opentelemetry/core": "npm:^1.30.1" - "@opentelemetry/instrumentation": "npm:^0.57.2" - "@opentelemetry/instrumentation-amqplib": "npm:^0.46.1" - "@opentelemetry/instrumentation-connect": "npm:0.43.1" - "@opentelemetry/instrumentation-dataloader": "npm:0.16.1" - "@opentelemetry/instrumentation-express": "npm:0.47.1" - "@opentelemetry/instrumentation-fastify": "npm:0.44.2" - "@opentelemetry/instrumentation-fs": "npm:0.19.1" - "@opentelemetry/instrumentation-generic-pool": "npm:0.43.1" - "@opentelemetry/instrumentation-graphql": "npm:0.47.1" - "@opentelemetry/instrumentation-hapi": "npm:0.45.2" - "@opentelemetry/instrumentation-http": "npm:0.57.2" - "@opentelemetry/instrumentation-ioredis": "npm:0.47.1" - "@opentelemetry/instrumentation-kafkajs": "npm:0.7.1" - "@opentelemetry/instrumentation-knex": "npm:0.44.1" - "@opentelemetry/instrumentation-koa": "npm:0.47.1" - "@opentelemetry/instrumentation-lru-memoizer": "npm:0.44.1" - "@opentelemetry/instrumentation-mongodb": "npm:0.52.0" - "@opentelemetry/instrumentation-mongoose": "npm:0.46.1" - "@opentelemetry/instrumentation-mysql": "npm:0.45.1" - "@opentelemetry/instrumentation-mysql2": "npm:0.45.2" - "@opentelemetry/instrumentation-pg": "npm:0.51.1" - "@opentelemetry/instrumentation-redis-4": "npm:0.46.1" - "@opentelemetry/instrumentation-tedious": "npm:0.18.1" - "@opentelemetry/instrumentation-undici": "npm:0.10.1" - "@opentelemetry/resources": "npm:^1.30.1" - "@opentelemetry/sdk-trace-base": "npm:^1.30.1" - "@opentelemetry/semantic-conventions": "npm:^1.30.0" - "@prisma/instrumentation": "npm:6.5.0" - "@sentry/core": "npm:9.11.0" - "@sentry/opentelemetry": "npm:9.11.0" - import-in-the-middle: "npm:^1.13.0" - checksum: 10c0/171f2a90e1b59aa78e5a5220366a6786fa0fa11deb1909442117bec21d323edb585bb248f1756a089befcb461b4e0976800d2582b51996f60dc625fdf964169c - languageName: node - linkType: hard - -"@sentry/opentelemetry@npm:9.11.0": - version: 9.11.0 - resolution: "@sentry/opentelemetry@npm:9.11.0" - dependencies: - "@sentry/core": "npm:9.11.0" - peerDependencies: - "@opentelemetry/api": ^1.9.0 - "@opentelemetry/context-async-hooks": ^1.30.1 - "@opentelemetry/core": ^1.30.1 - "@opentelemetry/instrumentation": ^0.57.1 - "@opentelemetry/sdk-trace-base": ^1.30.1 - "@opentelemetry/semantic-conventions": ^1.28.0 - checksum: 10c0/4e8c504b1b04fda5d3c6e0909908d2b41f3ee91fe0cd7a8fe2422fd0561d12480f199e49b638a98054b4dcd70d8ba7997b5aee06b50035dbdb720f5153ab9ee7 - languageName: node - linkType: hard - -"@sentry/react@npm:^9.14.0": - version: 9.14.0 - resolution: "@sentry/react@npm:9.14.0" - dependencies: - "@sentry/browser": "npm:9.14.0" - "@sentry/core": "npm:9.14.0" - hoist-non-react-statics: "npm:^3.3.2" - peerDependencies: - react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 10c0/ca59d97adc4e3d255c721f9447f354ad44f23a873d7ae4d96abbcde58fa116b1ee618ff6a9af58bc7b1916f8c24c0909d6ce2c15dfa98678dff98525ee61b306 - languageName: node - linkType: hard - -"@sentry/vite-plugin@npm:^3.3.1": - version: 3.3.1 - resolution: "@sentry/vite-plugin@npm:3.3.1" - dependencies: - "@sentry/bundler-plugin-core": "npm:3.3.1" - unplugin: "npm:1.0.1" - checksum: 10c0/1032bb35a5265088d4ba35b703dc5146aa01f2d883887cd6a0defbcf0e8dba677fefcef05be4e18eb5f9df68606028e346030f5f724daf4c152fa63bd2dc8f09 - languageName: node - linkType: hard - "@shikijs/core@npm:3.2.1": version: 3.2.1 resolution: "@shikijs/core@npm:3.2.1" @@ -5504,15 +4774,6 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:3.4.38": - version: 3.4.38 - resolution: "@types/connect@npm:3.4.38" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c - languageName: node - linkType: hard - "@types/d3-array@npm:*": version: 3.2.1 resolution: "@types/d3-array@npm:3.2.1" @@ -6004,15 +5265,6 @@ __metadata: languageName: node linkType: hard -"@types/mysql@npm:2.15.26": - version: 2.15.26 - resolution: "@types/mysql@npm:2.15.26" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/3cf279e7db05d56c0544532a4380b9079f579092379a04c8138bd5cf88dda5b31208ac2d23ce7dbf4e3a3f43aaeed44e72f9f19f726518f308efe95a7435619a - languageName: node - linkType: hard - "@types/node-fetch@npm:^2.5.10, @types/node-fetch@npm:^2.6.4": version: 2.6.12 resolution: "@types/node-fetch@npm:2.6.12" @@ -6071,37 +5323,6 @@ __metadata: languageName: node linkType: hard -"@types/pg-pool@npm:2.0.6": - version: 2.0.6 - resolution: "@types/pg-pool@npm:2.0.6" - dependencies: - "@types/pg": "npm:*" - checksum: 10c0/41965d4d0b677c54ce45d36add760e496d356b78019cb062d124af40287cf6b0fd4d86e3b0085f443856c185983a60c8b0795ff76d15683e2a93c62f5ac0125f - languageName: node - linkType: hard - -"@types/pg@npm:*": - version: 8.11.14 - resolution: "@types/pg@npm:8.11.14" - dependencies: - "@types/node": "npm:*" - pg-protocol: "npm:*" - pg-types: "npm:^4.0.1" - checksum: 10c0/ad9be5f0215a337409d843b844c21af9a0073485125f32e91b1c19a3be233c7c8bfe641c761e91228a4b10e803f1ba4d3c0ed55dcd0ca1dd4f3a07ebd798347c - languageName: node - linkType: hard - -"@types/pg@npm:8.6.1": - version: 8.6.1 - resolution: "@types/pg@npm:8.6.1" - dependencies: - "@types/node": "npm:*" - pg-protocol: "npm:*" - pg-types: "npm:^2.2.0" - checksum: 10c0/8d16660c9a4f050d6d5e391c59f9a62e9d377a2a6a7eb5865f8828082dbdfeab700fd707e585f42d67b29e796b32863aea5bd6d5cbb8ceda2d598da5d0c61693 - languageName: node - linkType: hard - "@types/plist@npm:^3.0.1": version: 3.0.5 resolution: "@types/plist@npm:3.0.5" @@ -6199,13 +5420,6 @@ __metadata: languageName: node linkType: hard -"@types/shimmer@npm:^1.2.0": - version: 1.2.0 - resolution: "@types/shimmer@npm:1.2.0" - checksum: 10c0/6f7bfe1b55601cfc3ae713fc74a03341f3834253b8b91cb2add926d5949e4a63f7e666f59c2a6e40a883a5f9e2f3e3af10f9d3aed9b60fced0bda87659e58d8d - languageName: node - linkType: hard - "@types/stylis@npm:4.2.5": version: 4.2.5 resolution: "@types/stylis@npm:4.2.5" @@ -6213,15 +5427,6 @@ __metadata: languageName: node linkType: hard -"@types/tedious@npm:^4.0.14": - version: 4.0.14 - resolution: "@types/tedious@npm:4.0.14" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/d2914f8e9b5b998e4275ec5f0130cba1c2fb47e75616b5c125a65ef6c1db2f1dc3f978c7900693856a15d72bbb4f4e94f805537a4ecb6dc126c64415d31c0590 - languageName: node - linkType: hard - "@types/tinycolor2@npm:^1": version: 1.4.6 resolution: "@types/tinycolor2@npm:1.4.6" @@ -6646,9 +5851,6 @@ __metadata: "@mozilla/readability": "npm:^0.6.0" "@notionhq/client": "npm:^2.2.15" "@reduxjs/toolkit": "npm:^2.2.5" - "@sentry/electron": "npm:^6.5.0" - "@sentry/react": "npm:^9.14.0" - "@sentry/vite-plugin": "npm:^3.3.1" "@shikijs/markdown-it": "npm:^3.2.2" "@strongtz/win32-arm64-msvc": "npm:^0.4.7" "@swc/plugin-styled-components": "npm:^7.1.3" @@ -6735,7 +5937,7 @@ __metadata: npx-scope-finder: "npm:^1.2.0" officeparser: "npm:^4.1.1" openai: "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch" - os-proxy-config: "npm:^1.1.1" + os-proxy-config: "npm:^1.1.2" p-queue: "npm:^8.1.0" path-browserify: "npm:^1.0.1" pdf-lib: "npm:^1.17.1" @@ -6815,15 +6017,6 @@ __metadata: languageName: node linkType: hard -"acorn-import-attributes@npm:^1.9.5": - version: 1.9.5 - resolution: "acorn-import-attributes@npm:1.9.5" - peerDependencies: - acorn: ^8 - checksum: 10c0/5926eaaead2326d5a86f322ff1b617b0f698aa61dc719a5baa0e9d955c9885cc71febac3fb5bacff71bbf2c4f9c12db2056883c68c53eb962c048b952e1e013d - languageName: node - linkType: hard - "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -6833,7 +6026,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.14.0, acorn@npm:^8.8.1": +"acorn@npm:^8.14.0": version: 8.14.1 resolution: "acorn@npm:8.14.1" bin: @@ -7089,16 +6282,6 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac - languageName: node - linkType: hard - "app-builder-bin@npm:5.0.0-alpha.12": version: 5.0.0-alpha.12 resolution: "app-builder-bin@npm:5.0.0-alpha.12" @@ -7418,7 +6601,7 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": +"binary-extensions@npm:^2.2.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 @@ -7533,7 +6716,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3, braces@npm:~3.0.2": +"braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -8071,25 +7254,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.3": - version: 3.6.0 - resolution: "chokidar@npm:3.6.0" - dependencies: - anymatch: "npm:~3.1.2" - braces: "npm:~3.0.2" - fsevents: "npm:~2.3.2" - glob-parent: "npm:~5.1.2" - is-binary-path: "npm:~2.1.0" - is-glob: "npm:~4.0.1" - normalize-path: "npm:~3.0.0" - readdirp: "npm:~3.6.0" - dependenciesMeta: - fsevents: - optional: true - checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 - languageName: node - linkType: hard - "chokidar@npm:^4.0.0": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -8144,13 +7308,6 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.2.2": - version: 1.4.3 - resolution: "cjs-module-lexer@npm:1.4.3" - checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be - languageName: node - linkType: hard - "classcat@npm:^5.0.3": version: 5.0.5 resolution: "classcat@npm:5.0.5" @@ -9284,7 +8441,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:4.3.1, deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -9692,7 +8849,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.3.1, dotenv@npm:^16.4.5": +"dotenv@npm:^16.4.5": version: 16.5.0 resolution: "dotenv@npm:16.5.0" checksum: 10c0/5bc94c919fbd955bf0ba44d33922a1e93d1078e64a1db5c30faeded1d996e7a83c55332cb8ea4fae5a9ca4d0be44cbceb95c5811e70f9f095298df09d1997dd9 @@ -11480,13 +10637,6 @@ __metadata: languageName: node linkType: hard -"forwarded-parse@npm:2.1.2": - version: 2.1.2 - resolution: "forwarded-parse@npm:2.1.2" - checksum: 10c0/0c6b4c631775f272b4475e935108635495e8a5b261d1b4a5caef31c47c5a0b04134adc564e655aadfef366a02647fa3ae90a1d3ac19929f3ade47f9bed53036a - languageName: node - linkType: hard - "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -11846,7 +10996,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -11923,18 +11073,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^9.3.2": - version: 9.3.5 - resolution: "glob@npm:9.3.5" - dependencies: - fs.realpath: "npm:^1.0.0" - minimatch: "npm:^8.0.2" - minipass: "npm:^4.2.4" - path-scurry: "npm:^1.6.1" - checksum: 10c0/2f6c2b9ee019ee21dc258ae97a88719614591e4c979cb4580b1b9df6f0f778a3cb38b4bdaf18dfa584637ea10f89a3c5f2533a5e449cf8741514ad18b0951f2e - languageName: node - linkType: hard - "global-agent@npm:^3.0.0": version: 3.0.0 resolution: "global-agent@npm:3.0.0" @@ -12846,18 +11984,6 @@ __metadata: languageName: node linkType: hard -"import-in-the-middle@npm:^1.13.0, import-in-the-middle@npm:^1.8.1": - version: 1.13.1 - resolution: "import-in-the-middle@npm:1.13.1" - dependencies: - acorn: "npm:^8.14.0" - acorn-import-attributes: "npm:^1.9.5" - cjs-module-lexer: "npm:^1.2.2" - module-details-from-path: "npm:^1.0.3" - checksum: 10c0/4ef05a924c37ff718dd08654927c90d470d92fd9425d646b0d423aaddc89655848debd14761bcb6efa4f57870d63ff38109bab31ca8a1d9d5df2e7d84d2649cf - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -12982,15 +12108,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:~2.1.0": - version: 2.1.0 - resolution: "is-binary-path@npm:2.1.0" - dependencies: - binary-extensions: "npm:^2.0.0" - checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 - languageName: node - linkType: hard - "is-buffer@npm:^2.0.0": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" @@ -13094,7 +12211,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -14249,15 +13366,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:0.30.8": - version: 0.30.8 - resolution: "magic-string@npm:0.30.8" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.15" - checksum: 10c0/51a1f06f678c082aceddfb5943de9b6bdb88f2ea1385a1c2adf116deb73dfcfa50df6c222901d691b529455222d4d68d0b28be5689ac6f69b3baa3462861f922 - languageName: node - linkType: hard - "magic-string@npm:^0.30.17": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -15521,15 +14629,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^8.0.2": - version: 8.0.4 - resolution: "minimatch@npm:8.0.4" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10c0/a0a394c356dd5b4cb7f821720841a82fa6f07c9c562c5b716909d1b6ec5e56a7e4c4b5029da26dd256b7d2b3a3f38cbf9ddd8680e887b9b5282b09c05501c1ca - languageName: node - linkType: hard - "minimatch@npm:^9.0.3, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -15616,13 +14715,6 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.2.4": - version: 4.2.8 - resolution: "minipass@npm:4.2.8" - checksum: 10c0/4ea76b030d97079f4429d6e8a8affd90baf1b6a1898977c8ccce4701c5a2ba2792e033abc6709373f25c2c4d4d95440d9d5e9464b46b7b76ca44d2ce26d939ce - languageName: node - linkType: hard - "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" @@ -15709,13 +14801,6 @@ __metadata: languageName: node linkType: hard -"module-details-from-path@npm:^1.0.3": - version: 1.0.3 - resolution: "module-details-from-path@npm:1.0.3" - checksum: 10c0/3d881f3410c142e4c2b1307835a2862ba04e5b3ec6e90655614a0ee2c4b299b4c1d117fb525d2435bf436990026f18d338a197b54ad6bd36252f465c336ff423 - languageName: node - linkType: hard - "monaco-editor@npm:^0.52.2": version: 0.52.2 resolution: "monaco-editor@npm:0.52.2" @@ -16080,13 +15165,6 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": - version: 3.0.0 - resolution: "normalize-path@npm:3.0.0" - checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 - languageName: node - linkType: hard - "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -16210,13 +15288,6 @@ __metadata: languageName: node linkType: hard -"obuf@npm:~1.1.2": - version: 1.1.2 - resolution: "obuf@npm:1.1.2" - checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81 - languageName: node - linkType: hard - "office-text-extractor@npm:^3.0.3": version: 3.0.3 resolution: "office-text-extractor@npm:3.0.3" @@ -16476,13 +15547,13 @@ __metadata: languageName: node linkType: hard -"os-proxy-config@npm:^1.1.1": - version: 1.1.1 - resolution: "os-proxy-config@npm:1.1.1" +"os-proxy-config@npm:^1.1.2": + version: 1.1.2 + resolution: "os-proxy-config@npm:1.1.2" dependencies: mac-system-proxy: "npm:^1.0.0" windows-system-proxy: "npm:^1.0.0" - checksum: 10c0/87f493e73e3daa91c908d7a9d7271e768ca3f3267adc77a2423bc71af536ec3ee78d16b8e34e647bae1225893c0e0ccf30be8d0b3e7a6d2bf58876c5a701813d + checksum: 10c0/a7585885b9bb6251969dc6c025eb863fb8859ebf876344610f5fde57a9392f2c1adb44b130b84469ee7c2649635296187dde15edc3d7e269a3f2a6ff38041f94 languageName: node linkType: hard @@ -16892,7 +15963,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1": +"path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" dependencies: @@ -17050,55 +16121,6 @@ __metadata: languageName: node linkType: hard -"pg-int8@npm:1.0.1": - version: 1.0.1 - resolution: "pg-int8@npm:1.0.1" - checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 - languageName: node - linkType: hard - -"pg-numeric@npm:1.0.2": - version: 1.0.2 - resolution: "pg-numeric@npm:1.0.2" - checksum: 10c0/43dd9884e7b52c79ddc28d2d282d7475fce8bba13452d33c04ceb2e0a65f561edf6699694e8e1c832ff9093770496363183c950dd29608e1bdd98f344b25bca9 - languageName: node - linkType: hard - -"pg-protocol@npm:*": - version: 1.9.5 - resolution: "pg-protocol@npm:1.9.5" - checksum: 10c0/5cb3444cf973adadd22ee9ea26bb1674f0d980ef8f9c66d426bbe67fc9cb5f0ca4a204bf7e432b3ab2ab59ac8227f4b18ab3b2e64eaed537e037e916991c7319 - languageName: node - linkType: hard - -"pg-types@npm:^2.2.0": - version: 2.2.0 - resolution: "pg-types@npm:2.2.0" - dependencies: - pg-int8: "npm:1.0.1" - postgres-array: "npm:~2.0.0" - postgres-bytea: "npm:~1.0.0" - postgres-date: "npm:~1.0.4" - postgres-interval: "npm:^1.1.0" - checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 - languageName: node - linkType: hard - -"pg-types@npm:^4.0.1": - version: 4.0.2 - resolution: "pg-types@npm:4.0.2" - dependencies: - pg-int8: "npm:1.0.1" - pg-numeric: "npm:1.0.2" - postgres-array: "npm:~3.0.1" - postgres-bytea: "npm:~3.0.0" - postgres-date: "npm:~2.1.0" - postgres-interval: "npm:^3.0.0" - postgres-range: "npm:^1.1.1" - checksum: 10c0/780fccda2f3fa2a34e85a72e8e7dadb7d88fbe71ce88f126cb3313f333ad836d02488ec4ff3d94d0c1e5846f735d6e6c6281f8059e6b8919d2180429acaec3e2 - languageName: node - linkType: hard - "phantomjs-prebuilt@npm:^2.1.14": version: 2.1.16 resolution: "phantomjs-prebuilt@npm:2.1.16" @@ -17141,7 +16163,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -17289,73 +16311,6 @@ __metadata: languageName: node linkType: hard -"postgres-array@npm:~2.0.0": - version: 2.0.0 - resolution: "postgres-array@npm:2.0.0" - checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 - languageName: node - linkType: hard - -"postgres-array@npm:~3.0.1": - version: 3.0.4 - resolution: "postgres-array@npm:3.0.4" - checksum: 10c0/47f3e648da512bacdd6a5ed55cf770605ec271330789faeece0fd13805a49f376d6e5c9e0e353377be11a9545e727dceaa2473566c505432bf06366ccd04c6b2 - languageName: node - linkType: hard - -"postgres-bytea@npm:~1.0.0": - version: 1.0.0 - resolution: "postgres-bytea@npm:1.0.0" - checksum: 10c0/febf2364b8a8953695cac159eeb94542ead5886792a9627b97e33f6b5bb6e263bc0706ab47ec221516e79fbd6b2452d668841830fb3b49ec6c0fc29be61892ce - languageName: node - linkType: hard - -"postgres-bytea@npm:~3.0.0": - version: 3.0.0 - resolution: "postgres-bytea@npm:3.0.0" - dependencies: - obuf: "npm:~1.1.2" - checksum: 10c0/41c79cc48aa730c5ba3eda6ab989a940034f07a1f57b8f2777dce56f1b8cca16c5870582932b5b10cc605048aef9b6157e06253c871b4717cafc6d00f55376aa - languageName: node - linkType: hard - -"postgres-date@npm:~1.0.4": - version: 1.0.7 - resolution: "postgres-date@npm:1.0.7" - checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 - languageName: node - linkType: hard - -"postgres-date@npm:~2.1.0": - version: 2.1.0 - resolution: "postgres-date@npm:2.1.0" - checksum: 10c0/00a7472c10788f6b0d08d24108bf1eb80858de1bd6317740198a564918ea4a69b80c98148167b92ae688abd606483020d0de0dd3a36f3ea9a3e26bbeef3464f4 - languageName: node - linkType: hard - -"postgres-interval@npm:^1.1.0": - version: 1.2.0 - resolution: "postgres-interval@npm:1.2.0" - dependencies: - xtend: "npm:^4.0.0" - checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 - languageName: node - linkType: hard - -"postgres-interval@npm:^3.0.0": - version: 3.0.0 - resolution: "postgres-interval@npm:3.0.0" - checksum: 10c0/8b570b30ea37c685e26d136d34460f246f98935a1533defc4b53bb05ee23ae3dc7475b718ec7ea607a57894d8c6b4f1adf67ca9cc83a75bdacffd427d5c68de8 - languageName: node - linkType: hard - -"postgres-range@npm:^1.1.1": - version: 1.1.4 - resolution: "postgres-range@npm:1.1.4" - checksum: 10c0/254494ef81df208e0adeae6b66ce394aba37914ea14c7ece55a45fb6691b7db04bee74c825380a47c887a9f87158fd3d86f758f9cc60b76d3a38ce5aca7912e8 - languageName: node - linkType: hard - "prebuild-install@npm:^5.3.5": version: 5.3.6 resolution: "prebuild-install@npm:5.3.6" @@ -18615,15 +17570,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.6.0": - version: 3.6.0 - resolution: "readdirp@npm:3.6.0" - dependencies: - picomatch: "npm:^2.2.1" - checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b - languageName: node - linkType: hard - "redux-persist@npm:^6.0.0": version: 6.0.0 resolution: "redux-persist@npm:6.0.0" @@ -18935,17 +17881,6 @@ __metadata: languageName: node linkType: hard -"require-in-the-middle@npm:^7.1.1": - version: 7.5.2 - resolution: "require-in-the-middle@npm:7.5.2" - dependencies: - debug: "npm:^4.3.5" - module-details-from-path: "npm:^1.0.3" - resolve: "npm:^1.22.8" - checksum: 10c0/43a2dac5520e39d13c413650895715e102d6802e6cc6ff322017bd948f12a9657fe28435f7cbbcba437b167f02e192ac7af29fa35cabd5d0c375d071c0605e01 - languageName: node - linkType: hard - "require-main-filename@npm:^1.0.1": version: 1.0.1 resolution: "require-main-filename@npm:1.0.1" @@ -18997,7 +17932,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0, resolve@npm:^1.22.8": +"resolve@npm:^1.10.0": version: 1.22.10 resolution: "resolve@npm:1.22.10" dependencies: @@ -19010,7 +17945,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": version: 1.22.10 resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" dependencies: @@ -19481,7 +18416,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.7.1 resolution: "semver@npm:7.7.1" bin: @@ -19619,13 +18554,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: 10c0/ae8b27c389db2a00acfc8da90240f11577685a8f3e40008f826a3bea8b4f3b3ecd305c26be024b4a0fd3b123d132c1569d6e238097960a9a543b6c60760fb46a - languageName: node - linkType: hard - "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0" @@ -21180,18 +20108,6 @@ __metadata: languageName: node linkType: hard -"unplugin@npm:1.0.1": - version: 1.0.1 - resolution: "unplugin@npm:1.0.1" - dependencies: - acorn: "npm:^8.8.1" - chokidar: "npm:^3.5.3" - webpack-sources: "npm:^3.2.3" - webpack-virtual-modules: "npm:^0.5.0" - checksum: 10c0/7d59b5a28abc1cdbd6356a10f273d1266f59c3be083ab0e659a37d02d047d5df1b435e0f40f5ec97517e8fc910d314592f0d197ccceb75ef47c71c1898ec7a05 - languageName: node - linkType: hard - "unzip-crx-3@npm:^0.2.0": version: 0.2.0 resolution: "unzip-crx-3@npm:0.2.0" @@ -21697,20 +20613,6 @@ __metadata: languageName: node linkType: hard -"webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "webpack-sources@npm:3.2.3" - checksum: 10c0/2ef63d77c4fad39de4a6db17323d75eb92897b32674e97d76f0a1e87c003882fc038571266ad0ef581ac734cbe20952912aaa26155f1905e96ce251adbb1eb4e - languageName: node - linkType: hard - -"webpack-virtual-modules@npm:^0.5.0": - version: 0.5.0 - resolution: "webpack-virtual-modules@npm:0.5.0" - checksum: 10c0/0742e069cd49d91ccd0b59431b3666903d321582c1b1062fa6bdae005c3538af55ff8787ea5eafbf72662f3496d3a879e2c705d55ca0af8283548a925be18484 - languageName: node - linkType: hard - "whatwg-encoding@npm:^3.1.1": version: 3.1.1 resolution: "whatwg-encoding@npm:3.1.1"