Compare commits
5 Commits
feat/custo
...
feat/varia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370cfd6e9f | ||
|
|
d213bc1024 | ||
|
|
1187a47698 | ||
|
|
83d0eb07aa | ||
|
|
8f6bf11320 |
14
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['kind/bug']
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -18,9 +18,7 @@ body:
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我的问题不是 [常见问题](https://github.com/CherryHQ/cherry-studio/issues/3881) 中的内容。
|
||||
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),没有找到类似的问题。
|
||||
- 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
|
||||
@@ -50,8 +48,8 @@ body:
|
||||
id: description
|
||||
attributes:
|
||||
label: 错误描述
|
||||
description: 描述问题时请尽可能详细。请尽可能提供截图或屏幕录制,以帮助我们更好地理解问题。
|
||||
placeholder: 告诉我们发生了什么...(记得附上截图/录屏,如果适用)
|
||||
description: 描述问题时请尽可能详细
|
||||
placeholder: 告诉我们发生了什么...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -59,14 +57,12 @@ body:
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 提供详细的重现步骤,以便于我们的开发人员可以准确地重现问题。请尽可能为每个步骤提供截图或屏幕录制。
|
||||
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
|
||||
placeholder: |
|
||||
1. 转到 '...'
|
||||
2. 点击 '....'
|
||||
3. 向下滚动到 '....'
|
||||
4. 看到错误
|
||||
|
||||
记得尽可能为每个步骤附上截图/录屏!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['kind/enhancement']
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ❓ 提问 & 讨论 (中文)
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['kind/question']
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
@@ -1,76 +0,0 @@
|
||||
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: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
10
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -1,13 +1,13 @@
|
||||
name: 🐛 Bug Report (English)
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['kind/bug']
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out this bug report!
|
||||
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
Thank you for taking the time to fill out this bug report!
|
||||
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
@@ -18,9 +18,7 @@ body:
|
||||
options:
|
||||
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
|
||||
required: true
|
||||
- label: My issue is not listed in the [FAQ](https://github.com/CherryHQ/cherry-studio/issues/3881).
|
||||
required: true
|
||||
- label: I've looked at **pinned issues** and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
|
||||
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
|
||||
required: true
|
||||
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 💡 Feature Request (English)
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['kind/enhancement']
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ❓ Questions & Discussion
|
||||
name: ❓ Discussion & Questions
|
||||
description: Seeking help, discussing issues, asking questions, etc...
|
||||
title: '[Discussion]: '
|
||||
labels: ['kind/question']
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/3_others.yml
vendored
@@ -1,76 +0,0 @@
|
||||
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
|
||||
86
.github/dependabot.yml
vendored
@@ -1,86 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 7
|
||||
target-branch: "main"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
groups:
|
||||
# 核心框架
|
||||
core-framework:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "electron"
|
||||
- "typescript"
|
||||
- "@types/react*"
|
||||
- "@types/node"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Electron 生态和构建工具
|
||||
electron-build:
|
||||
patterns:
|
||||
- "electron-*"
|
||||
- "@electron*"
|
||||
- "vite"
|
||||
- "@vitejs/*"
|
||||
- "dotenv-cli"
|
||||
- "rollup-plugin-*"
|
||||
- "@swc/*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# 测试工具
|
||||
testing-tools:
|
||||
patterns:
|
||||
- "vitest"
|
||||
- "@vitest/*"
|
||||
- "playwright"
|
||||
- "@playwright/*"
|
||||
- "eslint*"
|
||||
- "@eslint*"
|
||||
- "prettier"
|
||||
- "husky"
|
||||
- "lint-staged"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# CherryStudio 自定义包
|
||||
cherrystudio-packages:
|
||||
patterns:
|
||||
- "@cherrystudio/*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# 兜底其他 dependencies
|
||||
other-dependencies:
|
||||
dependency-type: "production"
|
||||
|
||||
# 兜底其他 devDependencies
|
||||
other-dev-dependencies:
|
||||
dependency-type: "development"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 3
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
252
.github/issue-checker.yml
vendored
@@ -1,252 +0,0 @@
|
||||
default-mode:
|
||||
add:
|
||||
remove: [pull_request_target, issues]
|
||||
|
||||
labels:
|
||||
# <!-- [Ss]kip `LABEL` --> 跳过一个 label
|
||||
# <!-- [Rr]emove `LABEL` --> 去掉一个 label
|
||||
|
||||
# skips and removes
|
||||
- name: skip all
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
|
||||
- name: remove all
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
|
||||
|
||||
- name: skip kind/bug
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
- name: remove kind/bug
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
|
||||
- name: skip kind/enhancement
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
- name: remove kind/enhancement
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
|
||||
- name: skip kind/question
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
- name: remove kind/question
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
|
||||
- name: skip area/Connectivity
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
- name: remove area/Connectivity
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
|
||||
- name: skip area/UI/UX
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
- name: remove area/UI/UX
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
|
||||
- name: skip kind/documentation
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
- name: remove kind/documentation
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
|
||||
- name: skip client:linux
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
- name: remove client:linux
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
|
||||
- name: skip client:mac
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
- name: remove client:mac
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
|
||||
- name: skip client:win
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
- name: remove client:win
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
|
||||
- name: skip sig/Assistant
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
- name: remove sig/Assistant
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
|
||||
- name: skip sig/Data
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
- name: remove sig/Data
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
|
||||
- name: skip sig/MCP
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
- name: remove sig/MCP
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
|
||||
- name: skip sig/RAG
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
- name: remove sig/RAG
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
|
||||
- name: skip lgtm
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
- name: remove lgtm
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
|
||||
- name: skip License
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)"
|
||||
- name: remove License
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)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
|
||||
54
.github/pull_request_template.md
vendored
@@ -1,54 +0,0 @@
|
||||
<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
|
||||
<!-- Thanks for sending a pull request! Here are some tips for you:
|
||||
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
### What this PR does
|
||||
|
||||
Before this PR:
|
||||
|
||||
After this PR:
|
||||
|
||||
<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->
|
||||
|
||||
Fixes #
|
||||
|
||||
### Why we need it and why it was done in this way
|
||||
|
||||
The following tradeoffs were made:
|
||||
|
||||
The following alternatives were considered:
|
||||
|
||||
Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->
|
||||
|
||||
### Breaking changes
|
||||
|
||||
<!-- optional -->
|
||||
|
||||
If this PR introduces breaking changes, please describe the changes and the impact on users.
|
||||
|
||||
### Special notes for your reviewer
|
||||
|
||||
<!-- optional -->
|
||||
|
||||
### Checklist
|
||||
|
||||
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
|
||||
Approvers are expected to review this list.
|
||||
|
||||
- [ ] PR: The PR description is expressive enough and will help future contributors
|
||||
- [ ] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
|
||||
- [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
|
||||
- [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
|
||||
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature.
|
||||
|
||||
### Release note
|
||||
|
||||
<!-- Write your release note:
|
||||
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
|
||||
2. If no release note is required, just write "NONE".
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
25
.github/workflows/issue-checker.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: "Issue Checker"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: MaaAssistantArknights/issue-checker@v1.14
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: .github/issue-checker.yml
|
||||
not-before: 2022-08-05T00:00:00Z
|
||||
include-title: 1
|
||||
58
.github/workflows/issue-management.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: "Stale Issue Management"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
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:
|
||||
if: github.repository_owner == 'CherryHQ'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write # Workaround for https://github.com/actions/stale/issues/1090
|
||||
issues: write
|
||||
# Completely disable stalling for PRs
|
||||
pull-requests: none
|
||||
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-label: "inactive"
|
||||
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
|
||||
|
||||
# Temporary to reduce the huge issues number
|
||||
operations-per-run: 1000
|
||||
debug-only: false
|
||||
78
.github/workflows/nightly-build.yml
vendored
@@ -7,41 +7,9 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write # Required for deleting artifacts
|
||||
|
||||
jobs:
|
||||
cleanup-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
# Calculate the date 14 days ago
|
||||
cutoff_date=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
|
||||
# List and delete artifacts older than cutoff date
|
||||
gh api repos/$REPO/actions/artifacts --paginate | \
|
||||
jq -r '.artifacts[] | select(.name | startswith("cherry-studio-nightly-")) | select(.created_at < "'$cutoff_date'") | .id' | \
|
||||
while read artifact_id; do
|
||||
echo "Deleting artifact $artifact_id"
|
||||
gh api repos/$REPO/actions/artifacts/$artifact_id -X DELETE
|
||||
done
|
||||
|
||||
check-repository:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ github.repository == 'CherryHQ/cherry-studio' }}
|
||||
steps:
|
||||
- name: Check if running in main repository
|
||||
run: |
|
||||
echo "Running in repository: ${{ github.repository }}"
|
||||
echo "Should run: ${{ github.repository == 'CherryHQ/cherry-studio' }}"
|
||||
|
||||
nightly-build:
|
||||
needs: check-repository
|
||||
if: needs.check-repository.outputs.should_run == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
@@ -52,19 +20,12 @@ jobs:
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
|
||||
@@ -98,7 +59,6 @@ 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'
|
||||
@@ -113,17 +73,16 @@ 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'
|
||||
run: |
|
||||
yarn build:npm windows
|
||||
yarn build:win
|
||||
run: yarn build:win
|
||||
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
|
||||
|
||||
- name: Rename artifacts with nightly format
|
||||
shell: bash
|
||||
@@ -134,24 +93,39 @@ jobs:
|
||||
# Windows artifacts - based on actual file naming pattern
|
||||
if [ "${{ matrix.os }}" == "windows-latest" ]; then
|
||||
# Setup installer
|
||||
find dist -name "*-x64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-setup.exe \;
|
||||
find dist -name "*-arm64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-setup.exe \;
|
||||
|
||||
find dist -name "*setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe \;
|
||||
|
||||
# Portable exe
|
||||
find dist -name "*-x64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-portable.exe \;
|
||||
find dist -name "*-arm64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-portable.exe \;
|
||||
find dist -name "*portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-portable.exe \;
|
||||
|
||||
# Rename blockmap files to match the new exe names
|
||||
if [ -f "dist/*setup.exe.blockmap" ]; then
|
||||
cp dist/*setup.exe.blockmap renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe.blockmap || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# macOS artifacts
|
||||
if [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
# 处理arm64架构文件
|
||||
find dist -name "*-arm64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg \;
|
||||
find dist -name "*-arm64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg.blockmap \;
|
||||
find dist -name "*-arm64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip \;
|
||||
find dist -name "*-arm64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip.blockmap \;
|
||||
|
||||
# 处理x64架构文件
|
||||
find dist -name "*-x64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg \;
|
||||
find dist -name "*-x64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg.blockmap \;
|
||||
find dist -name "*-x64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip \;
|
||||
find dist -name "*-x64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip.blockmap \;
|
||||
fi
|
||||
|
||||
# Linux artifacts
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
find dist -name "*-x86_64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x86_64.AppImage \;
|
||||
find dist -name "*-arm64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.AppImage \;
|
||||
find dist -name "*.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.AppImage \;
|
||||
find dist -name "*.snap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.snap \;
|
||||
find dist -name "*.deb" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.deb \;
|
||||
find dist -name "*.rpm" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.rpm \;
|
||||
find dist -name "*.tar.gz" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.tar.gz \;
|
||||
fi
|
||||
|
||||
# Copy update files
|
||||
|
||||
1
.github/workflows/pr-ci.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
20
.github/workflows/release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
default: 'v0.9.18'
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -26,8 +26,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Get release tag
|
||||
id: get-tag
|
||||
@@ -44,11 +42,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
|
||||
@@ -78,12 +71,10 @@ 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'
|
||||
run: |
|
||||
sudo -H pip install setuptools
|
||||
yarn build:npm mac
|
||||
yarn build:mac
|
||||
env:
|
||||
@@ -94,17 +85,16 @@ 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'
|
||||
run: |
|
||||
yarn build:npm windows
|
||||
yarn build:win
|
||||
run: yarn build:win
|
||||
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
|
||||
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
|
||||
7
.gitignore
vendored
@@ -35,6 +35,7 @@ Thumbs.db
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
build/icons
|
||||
stats.html
|
||||
|
||||
# ENV
|
||||
@@ -46,9 +47,3 @@ local
|
||||
.aider*
|
||||
.cursorrules
|
||||
.cursor/rules
|
||||
|
||||
# test
|
||||
coverage
|
||||
.vitest-cache
|
||||
vitest.config.*.timestamp-*
|
||||
YOUR_MEMORY_FILE_PATH
|
||||
|
||||
1
.vscode/launch.json
vendored
@@ -7,7 +7,6 @@
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||
"runtimeVersion": "20",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||
},
|
||||
|
||||
10
.vscode/settings.json
vendored
@@ -31,13 +31,5 @@
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
|
||||
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
|
||||
"i18n-ally.keystyle": "nested", // 翻译路径格式
|
||||
"i18n-ally.sortKeys": true, // 排序
|
||||
"i18n-ally.namespace": true, // 开启命名空间
|
||||
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
|
||||
"i18n-ally.sourceLanguage": "en-us", // 翻译源语言
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.fullReloadOnChanged": true // 界面显示语言
|
||||
"i18n-ally.localesPaths": ["src/renderer/src/i18n"]
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
diff --git a/out/macPackager.js b/out/macPackager.js
|
||||
index 852f6c4d16f86a7bb8a78bf1ed5a14647a279aa1..60e7f5f16a844541eb1909b215fcda1811e924b8 100644
|
||||
--- a/out/macPackager.js
|
||||
+++ b/out/macPackager.js
|
||||
@@ -423,7 +423,7 @@ class MacPackager extends platformPackager_1.PlatformPackager {
|
||||
}
|
||||
appPlist.CFBundleName = appInfo.productName;
|
||||
appPlist.CFBundleDisplayName = appInfo.productName;
|
||||
- const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion;
|
||||
+ const minimumSystemVersion = this.platformSpecificBuildOptions.LSMinimumSystemVersion;
|
||||
if (minimumSystemVersion != null) {
|
||||
appPlist.LSMinimumSystemVersion = minimumSystemVersion;
|
||||
}
|
||||
diff --git a/out/publish/updateInfoBuilder.js b/out/publish/updateInfoBuilder.js
|
||||
index 7924c5b47d01f8dfccccb8f46658015fa66da1f7..1a1588923c3939ae1297b87931ba83f0ebc052d8 100644
|
||||
--- a/out/publish/updateInfoBuilder.js
|
||||
+++ b/out/publish/updateInfoBuilder.js
|
||||
@@ -133,6 +133,7 @@ async function createUpdateInfo(version, event, releaseInfo) {
|
||||
const customUpdateInfo = event.updateInfo;
|
||||
const url = path.basename(event.file);
|
||||
const sha512 = (customUpdateInfo == null ? null : customUpdateInfo.sha512) || (await (0, hash_1.hashFile)(event.file));
|
||||
+ const minimumSystemVersion = customUpdateInfo == null ? null : customUpdateInfo.minimumSystemVersion;
|
||||
const files = [{ url, sha512 }];
|
||||
const result = {
|
||||
// @ts-ignore
|
||||
@@ -143,9 +144,13 @@ async function createUpdateInfo(version, event, releaseInfo) {
|
||||
path: url /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
|
||||
// @ts-ignore
|
||||
sha512 /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
|
||||
+ minimumSystemVersion,
|
||||
...releaseInfo,
|
||||
};
|
||||
if (customUpdateInfo != null) {
|
||||
+ if (customUpdateInfo.minimumSystemVersion) {
|
||||
+ delete customUpdateInfo.minimumSystemVersion;
|
||||
+ }
|
||||
// file info or nsis web installer packages info
|
||||
Object.assign("sha512" in customUpdateInfo ? files[0] : result, customUpdateInfo);
|
||||
}
|
||||
diff --git a/out/targets/ArchiveTarget.js b/out/targets/ArchiveTarget.js
|
||||
index e1f52a5fa86fff6643b2e57eaf2af318d541f865..47cc347f154a24b365e70ae5e1f6d309f3582ed0 100644
|
||||
--- a/out/targets/ArchiveTarget.js
|
||||
+++ b/out/targets/ArchiveTarget.js
|
||||
@@ -69,6 +69,9 @@ class ArchiveTarget extends core_1.Target {
|
||||
}
|
||||
}
|
||||
}
|
||||
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
|
||||
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
|
||||
+ }
|
||||
await packager.info.emitArtifactBuildCompleted({
|
||||
updateInfo,
|
||||
file: artifactPath,
|
||||
diff --git a/out/targets/nsis/NsisTarget.js b/out/targets/nsis/NsisTarget.js
|
||||
index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01de4b89b1 100644
|
||||
--- a/out/targets/nsis/NsisTarget.js
|
||||
+++ b/out/targets/nsis/NsisTarget.js
|
||||
@@ -305,6 +305,9 @@ class NsisTarget extends core_1.Target {
|
||||
if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) {
|
||||
updateInfo.isAdminRightsRequired = true;
|
||||
}
|
||||
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
|
||||
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
|
||||
+ }
|
||||
await packager.info.emitArtifactBuildCompleted({
|
||||
file: installerPath,
|
||||
updateInfo,
|
||||
diff --git a/scheme.json b/scheme.json
|
||||
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43ebd0fa8b61 100644
|
||||
--- a/scheme.json
|
||||
+++ b/scheme.json
|
||||
@@ -1975,6 +1975,13 @@
|
||||
],
|
||||
"description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing."
|
||||
},
|
||||
+ "minimumSystemVersion": {
|
||||
+ "description": "The minimum os kernel version required to install the application.",
|
||||
+ "type": [
|
||||
+ "null",
|
||||
+ "string"
|
||||
+ ]
|
||||
+ },
|
||||
"packageCategory": {
|
||||
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
|
||||
"type": [
|
||||
@@ -2327,6 +2334,13 @@
|
||||
"MacConfiguration": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
+ "LSMinimumSystemVersion": {
|
||||
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
|
||||
+ "type": [
|
||||
+ "null",
|
||||
+ "string"
|
||||
+ ]
|
||||
+ },
|
||||
"additionalArguments": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2737,7 +2751,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimumSystemVersion": {
|
||||
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
|
||||
+ "description": "The minimum os kernel version required to install the application.",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
@@ -2959,6 +2973,13 @@
|
||||
"MasConfiguration": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
+ "LSMinimumSystemVersion": {
|
||||
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
|
||||
+ "type": [
|
||||
+ "null",
|
||||
+ "string"
|
||||
+ ]
|
||||
+ },
|
||||
"additionalArguments": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -3369,7 +3390,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimumSystemVersion": {
|
||||
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
|
||||
+ "description": "The minimum os kernel version required to install the application.",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
@@ -6507,6 +6528,13 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
+ "minimumSystemVersion": {
|
||||
+ "description": "The minimum os kernel version required to install the application.",
|
||||
+ "type": [
|
||||
+ "null",
|
||||
+ "string"
|
||||
+ ]
|
||||
+ },
|
||||
"protocols": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -7376,6 +7404,13 @@
|
||||
],
|
||||
"description": "MAS (Mac Application Store) development options (`mas-dev` target)."
|
||||
},
|
||||
+ "minimumSystemVersion": {
|
||||
+ "description": "The minimum os kernel version required to install the application.",
|
||||
+ "type": [
|
||||
+ "null",
|
||||
+ "string"
|
||||
+ ]
|
||||
+ },
|
||||
"msi": {
|
||||
"anyOf": [
|
||||
{
|
||||
17
.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch
vendored
@@ -1,17 +0,0 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index 4e8423491ab51a9eb9fee22182e4ea0fcc3d3d3b..2846c5d4354c130d478dc99565b3ecd6d85b7d2e 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -19,7 +19,11 @@ function requireNative() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
- return require(`@libsql/${target}`);
|
||||
+ if (target === "win32-arm64-msvc") {
|
||||
+ return require(`@strongtz/win32-arm64-msvc`);
|
||||
+ } else {
|
||||
+ return require(`@libsql/${target}`);
|
||||
+ }
|
||||
}
|
||||
|
||||
const {
|
||||
39
.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index e75a18281ce8f051990c5a50bc1076afdddf91a3..e62f796791a155f23d054e74a429516c14d6e11b 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -156,7 +156,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index fcef58eb502664c41a77483a00db8adaf29b2817..18c5d6ed4be86b3640931277bdc27700006764d7 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -149,7 +149,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/error.mjs b/error.mjs
|
||||
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
|
||||
--- a/error.mjs
|
||||
+++ b/error.mjs
|
||||
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
85
.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch
vendored
@@ -1,85 +0,0 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -159,7 +159,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -152,7 +152,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/error.mjs b/error.mjs
|
||||
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
|
||||
--- a/error.mjs
|
||||
+++ b/error.mjs
|
||||
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
diff --git a/resources/embeddings.js b/resources/embeddings.js
|
||||
index aae578404cb2d09a39ac33fc416f1c215c45eecd..25c54b05bdae64d5c3b36fbb30dc7c8221b14034 100644
|
||||
--- a/resources/embeddings.js
|
||||
+++ b/resources/embeddings.js
|
||||
@@ -36,6 +36,9 @@ class Embeddings extends resource_1.APIResource {
|
||||
// No encoding_format specified, defaulting to base64 for performance reasons
|
||||
// See https://github.com/openai/openai-node/pull/1312
|
||||
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
+ if (body.model.includes('jina')) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
if (hasUserProvidedEncodingFormat) {
|
||||
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
|
||||
}
|
||||
@@ -47,7 +50,7 @@ class Embeddings extends resource_1.APIResource {
|
||||
...options,
|
||||
});
|
||||
// if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
|
||||
return response;
|
||||
}
|
||||
// in this stage, we are sure the user did not specify an encoding_format
|
||||
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
|
||||
index 0df3c6cc79a520e54acb4c2b5f77c43b774035ff..aa488b8a11b2c413c0a663d9a6059d286d7b5faf 100644
|
||||
--- a/resources/embeddings.mjs
|
||||
+++ b/resources/embeddings.mjs
|
||||
@@ -10,6 +10,9 @@ export class Embeddings extends APIResource {
|
||||
// No encoding_format specified, defaulting to base64 for performance reasons
|
||||
// See https://github.com/openai/openai-node/pull/1312
|
||||
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
+ if (body.model.includes('jina')) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
if (hasUserProvidedEncodingFormat) {
|
||||
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
|
||||
}
|
||||
@@ -21,7 +24,7 @@ export class Embeddings extends APIResource {
|
||||
...options,
|
||||
});
|
||||
// if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
|
||||
return response;
|
||||
}
|
||||
// in this stage, we are sure the user did not specify an encoding_format
|
||||
934
.yarn/releases/yarn-4.6.0.cjs
vendored
Executable file
948
.yarn/releases/yarn-4.9.1.cjs
vendored
@@ -4,4 +4,4 @@ httpTimeout: 300000
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.6.0.cjs
|
||||
|
||||
@@ -1,73 +1,45 @@
|
||||
[中文](./docs/CONTRIBUTING.zh.md) | [English](./CONTRIBUTING.md)
|
||||
# Cherry Studio 贡献者指南
|
||||
|
||||
# Cherry Studio Contributor Guide
|
||||
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
|
||||
|
||||
Welcome to the Cherry Studio contributor community! We are committed to making Cherry Studio a project that provides long-term value and hope to invite more developers to join us. Whether you are an experienced developer or a beginner just starting out, your contributions will help us better serve users and improve software quality.
|
||||
## 如何贡献
|
||||
|
||||
## How to Contribute
|
||||
以下是您可以参与的几种方式:
|
||||
|
||||
Here are several ways you can participate:
|
||||
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
|
||||
|
||||
1. **Contribute Code**: Help us develop new features or optimize existing code. Please ensure your code adheres to our coding standards and passes all tests.
|
||||
2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。
|
||||
|
||||
2. **Fix Bugs**: If you find a bug, you are welcome to submit a fix. Please confirm the issue is resolved before submitting and include relevant tests.
|
||||
3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。
|
||||
|
||||
3. **Maintain Issues**: Help us manage issues on GitHub by assisting with tagging, classifying, and resolving problems.
|
||||
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
|
||||
|
||||
4. **Product Design**: Participate in product design discussions to help us improve user experience and interface design.
|
||||
5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。
|
||||
|
||||
5. **Write Documentation**: Help us improve the user manual, API documentation, and developer guides.
|
||||
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
|
||||
|
||||
6. **Community Maintenance**: Participate in community discussions, help answer user questions, and promote community activity.
|
||||
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。
|
||||
|
||||
7. **Promote Usage**: Promote Cherry Studio through blogs, social media, and other channels to attract more users and developers.
|
||||
## 开始贡献
|
||||
|
||||
## Before You Start
|
||||
1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。
|
||||
|
||||
Please make sure you have read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [LICENSE](LICENSE).
|
||||
2. **创建分支**:为您要进行的更改创建一个新的分支。
|
||||
|
||||
## Getting Started
|
||||
3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。
|
||||
|
||||
To help you get familiar with the codebase, we recommend tackling issues tagged with one or more of the following labels: [good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue), [help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted), or [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug). Any help is welcome.
|
||||
4. **发起 Pull Request**:将您的更改推送到 GitHub,并发起 Pull Request。请描述您的更改内容和原因。
|
||||
|
||||
### Testing
|
||||
### 其他建议
|
||||
|
||||
Features without tests are considered non-existent. To ensure code is truly effective, relevant processes should be covered by unit tests and functional tests. Therefore, when considering contributions, please also consider testability. All tests can be run locally without dependency on CI. Please refer to the "Testing" section in the [Developer Guide](docs/dev.md).
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。
|
||||
|
||||
### Automated Testing for Pull Requests
|
||||
## 联系我们
|
||||
|
||||
Automated tests are triggered on pull requests (PRs) opened by members of the Cherry Studio organization, except for draft PRs. PRs opened by new contributors will initially be marked with the `needs-ok-to-test` label and will not be automatically tested. Once a Cherry Studio organization member adds `/ok-to-test` to the PR, the test pipeline will be created.
|
||||
如果您有任何问题或建议,欢迎通过以下方式联系我们:
|
||||
|
||||
### Consider Opening Your Pull Request as a Draft
|
||||
|
||||
Not all pull requests are ready for review when created. This might be because the author wants to start a discussion, they are not entirely sure if the changes are heading in the right direction, or the changes are not yet complete. Please consider creating these PRs as [draft pull requests](https://github.blog/2019-02-14-introducing-draft-pull-requests/). Draft PRs are skipped by CI, thus saving CI resources. This also means reviewers will not be automatically assigned, and the community will understand that this PR is not yet ready for review.
|
||||
Reviewers will be assigned after you mark the draft pull request as ready for review.
|
||||
|
||||
### Contributor Compliance with Project Terms
|
||||
|
||||
We require every contributor to certify that they have the right to legally contribute to our project. Contributors express this by consciously signing their commits, thereby indicating their compliance with the [LICENSE](LICENSE).
|
||||
A signed commit is one where the commit message includes the following:
|
||||
|
||||
You can generate a signed commit using the following command [git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff):
|
||||
|
||||
```
|
||||
git commit --signoff -m "Your commit message"
|
||||
```
|
||||
|
||||
### Getting Code Reviewed/Merged
|
||||
|
||||
Maintainers are here to help you implement your use case within a reasonable timeframe. They will do their best to review your code and provide constructive feedback promptly. However, if you get stuck during the review process or feel your Pull Request is not receiving the attention it deserves, please contact us via comments in the Issue or through the [Community](README.md#-community).
|
||||
|
||||
### Other Suggestions
|
||||
|
||||
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
|
||||
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have any questions or suggestions, feel free to contact us through the following ways:
|
||||
|
||||
- WeChat: kangfenmao
|
||||
- 微信:kangfenmao
|
||||
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
|
||||
|
||||
Thank you for your support and contributions! We look forward to working with you to make Cherry Studio a better product.
|
||||
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
|
||||
|
||||
111
LICENSE
@@ -1,87 +1,62 @@
|
||||
**许可协议 (Licensing)**
|
||||
**许可协议**
|
||||
|
||||
本项目采用**区分用户的双重许可 (User-Segmented Dual Licensing)** 模式。
|
||||
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
|
||||
|
||||
**核心原则:**
|
||||
**一. 商用许可**
|
||||
|
||||
* **个人用户 和 10人及以下企业/组织:** 默认适用 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)**。
|
||||
* **超过10人的企业/组织:** **必须** 获取 **商业许可证 (Commercial License)**。
|
||||
在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:
|
||||
|
||||
定义:“10人及以下”
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
1. **修改与衍生**: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。
|
||||
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 CherryStudio 的服务,且该服务支持 10 人及以上累计用户使用。
|
||||
3. **硬件捆绑销售**: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
|
||||
4. **政府或教育机构大规模采购**: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
||||
5. **面向公众的公有云服务**:基于 Cherry Studio,提供面向公众的公有云服务。
|
||||
|
||||
**二. 贡献者协议**
|
||||
|
||||
作为 Cherry Studio 的贡献者,您应当同意以下条款:
|
||||
|
||||
1. **许可调整**:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。
|
||||
2. **商业用途**:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。
|
||||
|
||||
**三. 其他条款**
|
||||
|
||||
1. 本协议条款的解释权归 Cherry Studio 开发者所有。
|
||||
2. 本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。
|
||||
|
||||
如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。
|
||||
|
||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问 http://www.apache.org/licenses/LICENSE-2.0。
|
||||
|
||||
---
|
||||
|
||||
**1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织**
|
||||
|
||||
* 如果您是个人用户,或者您的组织满足上述“10人及以下”的定义,您可以在 **AGPLv3** 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
|
||||
* **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3 许可证向接收者提供相应的**完整源代码**。即使您符合“10人及以下”的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。
|
||||
* 使用前请务必仔细阅读并理解 AGPLv3 的所有条款。
|
||||
**License Agreement**
|
||||
|
||||
**2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3 义务的用户**
|
||||
This software is licensed under the Apache License 2.0. In addition to the terms stipulated by the Apache License 2.0, you must comply with the following supplementary terms when using Cherry Studio:
|
||||
|
||||
* **强制要求:** 如果您的组织**不**满足上述“10人及以下”的定义(即有11人或更多人可以访问、使用或受益于本软件),您**必须**联系我们获取并签署一份商业许可证才能使用 Cherry Studio。
|
||||
* **自愿选择:** 即使您的组织满足“10人及以下”的条件,但如果您的使用场景**无法满足 AGPLv3 的条款要求**(特别是关于**源代码公开**的义务),或者您需要 AGPLv3 **未提供**的特定商业条款(如保证、赔偿、无 Copyleft 限制等),您也**必须**联系我们获取并签署一份商业许可证。
|
||||
* **需要商业许可证的常见情况包括(但不限于):**
|
||||
* 您的组织规模超过10人。
|
||||
* (无论组织规模)您希望分发修改过的 Cherry Studio 版本,但**不希望**根据 AGPLv3 公开您修改部分的源代码。
|
||||
* (无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但**不希望**根据 AGPLv3 向服务使用者提供修改后的源代码。
|
||||
* (无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
|
||||
* 商业许可证将为您提供豁免 AGPLv3 义务(如源代码公开)的权利,并可能包含额外的商业保障条款。
|
||||
* **获取商业许可:** 请通过邮箱 **bd@cherry-ai.com** 联系 Cherry Studio 开发团队洽谈商业授权事宜。
|
||||
**I. Commercial Licensing**
|
||||
|
||||
**3. 贡献 (Contributions)**
|
||||
You must contact us and obtain explicit written commercial authorization to continue using Cherry Studio materials under any of the following circumstances:
|
||||
|
||||
* 我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 **AGPLv3** 许可证下提供。
|
||||
* 通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
||||
* 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。
|
||||
1. **Modifications and Derivatives:** You modify Cherry Studio materials or perform derivative development based on them (including but not limited to changing the application’s name, logo, code, functionality, user interface, data, etc.).
|
||||
2. **Enterprise Services:** You use Cherry Studio internally within your enterprise, or you provide Cherry Studio-based services for enterprise customers, and such services support cumulative usage by 10 or more users.
|
||||
3. **Hardware Bundling and Sales:** You pre-install or integrate Cherry Studio into hardware devices or products for bundled sale.
|
||||
4. **Large-scale Procurement by Government or Educational Institutions:** Your usage scenario involves large-scale procurement projects by government or educational institutions, especially in cases involving sensitive requirements such as security and data privacy.
|
||||
5. **Public Cloud Services:** You provide public cloud-based product services utilizing Cherry Studio.
|
||||
|
||||
**4. 其他条款 (Other Terms)**
|
||||
**II. Contributor Agreement**
|
||||
|
||||
* 关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。
|
||||
* 项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
||||
As a contributor to Cherry Studio, you must agree to the following terms:
|
||||
|
||||
---
|
||||
1. **License Adjustments:** The producer reserves the right to adjust the open-source license as necessary, making it more strict or permissive.
|
||||
2. **Commercial Usage:** Your contributed code may be used commercially, including but not limited to cloud business operations.
|
||||
|
||||
**Licensing**
|
||||
**III. Other Terms**
|
||||
|
||||
This project employs a **User-Segmented Dual Licensing** model.
|
||||
1. Cherry Studio developers reserve the right of final interpretation of these agreement terms.
|
||||
2. This agreement may be updated according to practical circumstances, and users will be notified of updates through this software.
|
||||
|
||||
**Core Principle:**
|
||||
If you have any questions or need to apply for commercial authorization, please contact the Cherry Studio development team.
|
||||
|
||||
* **Individual Users and Organizations with 10 or Fewer Individuals:** Governed by default under the **GNU Affero General Public License v3.0 (AGPLv3)**.
|
||||
* **Organizations with More Than 10 Individuals:** **Must** obtain a **Commercial License**.
|
||||
|
||||
Definition: "10 or Fewer Individuals"
|
||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions, etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||
|
||||
---
|
||||
|
||||
**1. Open Source License: AGPLv3 - For Individuals and Organizations of 10 or Fewer**
|
||||
|
||||
* If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition above, you are free to use, modify, and distribute Cherry Studio under the terms of the **AGPLv3**. The full text of the AGPLv3 can be found in the LICENSE file at [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html).
|
||||
* **Core Obligation:** A key requirement of the AGPLv3 is that if you modify Cherry Studio and make it available over a network, or distribute the modified version, you must provide the **complete corresponding source code** under the AGPLv3 license to the recipients. Even if you qualify under the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will need to obtain a Commercial License (see below).
|
||||
* Please read and understand the full terms of the AGPLv3 carefully before use.
|
||||
|
||||
**2. Commercial License - For Organizations with More Than 10 Individuals, or Users Needing to Avoid AGPLv3 Obligations**
|
||||
|
||||
* **Mandatory Requirement:** If your organization does **not** meet the "10 or Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the software), you **must** contact us to obtain and execute a Commercial License to use Cherry Studio.
|
||||
* **Voluntary Option:** Even if your organization meets the "10 or Fewer Individuals" condition, if your intended use case **cannot comply with the terms of the AGPLv3** (particularly the obligations regarding **source code disclosure**), or if you require specific commercial terms **not offered** by the AGPLv3 (such as warranties, indemnities, or freedom from copyleft restrictions), you also **must** contact us to obtain and execute a Commercial License.
|
||||
* **Common scenarios requiring a Commercial License include (but are not limited to):**
|
||||
* Your organization has more than 10 individuals who can access, use, or benefit from the software.
|
||||
* (Regardless of organization size) You wish to distribute a modified version of Cherry Studio but **do not want** to disclose the source code of your modifications under AGPLv3.
|
||||
* (Regardless of organization size) You wish to provide a network service (SaaS) based on a modified version of Cherry Studio but **do not want** to provide the modified source code to users of the service under AGPLv3.
|
||||
* (Regardless of organization size) Your corporate policies, client contracts, or project requirements prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
|
||||
* The Commercial License grants you rights exempting you from AGPLv3 obligations (like source code disclosure) and may include additional commercial assurances.
|
||||
* **Obtaining a Commercial License:** Please contact the Cherry Studio development team via email at **bd@cherry-ai.com** to discuss commercial licensing options.
|
||||
|
||||
**3. Contributions**
|
||||
|
||||
* We welcome community contributions to Cherry Studio. All contributions submitted to this project are considered to be offered under the **AGPLv3** license.
|
||||
* By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately operate under AGPLv3 or a Commercial License).
|
||||
* You also understand and agree that your contribution may be included in distributions of Cherry Studio offered under our commercial license.
|
||||
|
||||
**4. Other Terms**
|
||||
|
||||
* The specific terms and conditions of the Commercial License are governed by the formal commercial license agreement signed by both parties.
|
||||
* The project maintainers reserve the right to update this licensing policy (including the definition and threshold for user count) as needed. Updates will be communicated through official project channels (e.g., code repository, official website).
|
||||
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For more detailed information regarding Apache License 2.0, please visit http://www.apache.org/licenses/LICENSE-2.0.
|
||||
32
README.md
@@ -13,24 +13,24 @@
|
||||
|
||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||
|
||||
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||
|
||||
# 📖 Guide
|
||||
|
||||
<https://docs.cherry-ai.com>
|
||||
https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 Key Features
|
||||
|
||||

|
||||
|
||||
1. **Diverse LLM Provider Support**:
|
||||
|
||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||
@@ -84,11 +84,9 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
|
||||
# 🌈 Theme
|
||||
|
||||
- Theme Gallery: <https://cherrycss.com>
|
||||
- Aero Theme: <https://github.com/hakadao/CherryStudio-Aero>
|
||||
- PaperMaterial Theme: <https://github.com/rainoffallingstar/CherryStudio-PaperMaterial>
|
||||
- Claude dynamic-style: <https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic>
|
||||
- Maple Neon Theme: <https://github.com/BoningtonChen/CherryStudio_themes>
|
||||
- Theme Gallery: https://cherrycss.com
|
||||
- Aero Theme: https://github.com/hakadao/CherryStudio-Aero
|
||||
- PaperMaterial Theme: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
|
||||
|
||||
Welcome PR for more themes
|
||||
|
||||
@@ -96,10 +94,6 @@ Welcome PR for more themes
|
||||
|
||||
Refer to the [development documentation](docs/dev.md)
|
||||
|
||||
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
|
||||
|
||||
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
|
||||
|
||||
# 🤝 Contributing
|
||||
|
||||
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
|
||||
@@ -123,7 +117,7 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
|
||||
|
||||
Thank you for your support and contributions!
|
||||
|
||||
# 🔗 Related Projects
|
||||
## Related Projects
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
|
||||
|
||||
@@ -132,7 +126,7 @@ Thank you for your support and contributions!
|
||||
# 🚀 Contributors
|
||||
|
||||
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
|
||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
@@ -150,7 +144,7 @@ Thank you for your support and contributions!
|
||||
|
||||
# ✉️ Contact
|
||||
|
||||
<yinsenho@cherry-ai.com>
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ Star History
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 621 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,8 +1,8 @@
|
||||
# provider: generic
|
||||
# url: http://127.0.0.1:8080
|
||||
# updaterCacheDirName: cherry-studio-updater
|
||||
# provider: github
|
||||
# repo: cherry-studio
|
||||
# owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://releases.cherry-ai.com
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
# provider: generic
|
||||
# url: https://cherrystudio.ocool.online
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Cherry Studio 贡献者指南
|
||||
|
||||
[**English**](../CONTRIBUTING.md) | [**中文**](./CONTRIBUTING.zh.md)
|
||||
|
||||
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
|
||||
|
||||
## 如何贡献
|
||||
|
||||
以下是您可以参与的几种方式:
|
||||
|
||||
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
|
||||
|
||||
2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。
|
||||
|
||||
3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。
|
||||
|
||||
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
|
||||
|
||||
5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。
|
||||
|
||||
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
|
||||
|
||||
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。
|
||||
|
||||
## 开始之前
|
||||
|
||||
请确保阅读了[行为准则](CODE_OF_CONDUCT.md)和[LICENSE](LICENSE)。
|
||||
|
||||
## 开始贡献
|
||||
|
||||
为了让您更熟悉代码,建议您处理一些标记有以下标签之一或多个的问题:[good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue)、[help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted) 或 [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug)。任何帮助都会收到欢迎。
|
||||
|
||||
### 测试
|
||||
|
||||
未经测试的功能等同于不存在。为确保代码真正有效,应通过单元测试和功能测试覆盖相关流程。因此,在考虑贡献时,也请考虑可测试性。所有测试均可本地运行,无需依赖 CI。请参阅[开发者指南](docs/dev.md#test)中的“Test”部分。
|
||||
|
||||
### 拉取请求的自动化测试
|
||||
|
||||
自动化测试会在 Cherry Studio 组织成员开启的拉取请求(PR)上触发,草稿 PR 除外。新贡献者开启的 PR 最初会标记为 needs-ok-to-test 标签且不自动测试。待 Cherry Studio 组织成员在 PR 上添加 /ok-to-test 后,测试通道将被创建。
|
||||
|
||||
### 考虑将您的拉取请求作为草稿打开
|
||||
|
||||
并非所有拉取请求在创建时就准备好接受审查。这可能是因为作者想发起讨论,或者他们不完全确定更改是否朝着正确的方向发展,甚至可能是因为更改尚未完成。请考虑将这些 PR 创建为[草稿拉取请求](https://github.blog/2019-02-14-introducing-draft-pull-requests/)。草稿 PR 会被CI跳过,从而节省CI资源。这也意味着审阅者不会被自动分配,社区会理解此 PR 尚未准备好接受审阅。
|
||||
在您将草稿拉取请求标记为准备审核后,审核人员将被分配
|
||||
|
||||
### 贡献者遵守项目条款
|
||||
|
||||
我们要求每位贡献者证明他们有权合法地为我们的项目做出贡献。贡献者通过有意识地签署他们的提交来表达这一点,并通过这一行为表明他们遵守许可证[LICENSE](LICENSE)。
|
||||
签名提交是指提交信息中包含以下内容的提交:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your.email@example.com>
|
||||
```
|
||||
|
||||
您可以通过以下命令[git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff)生成签名提交:
|
||||
|
||||
```
|
||||
git commit --signoff -m "Your commit message"
|
||||
```
|
||||
|
||||
### 获取代码审查/合并
|
||||
|
||||
维护者在此帮助您在合理时间内实现您的用例。他们会尽力在合理时间内审查您的代码并提供建设性反馈。但如果您在审查过程中受阻,或认为您的 Pull Request 未得到应有的关注,请通过 Issue 中的评论或者[社群](README.md#-community)联系我们
|
||||
|
||||
### 其他建议
|
||||
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
|
||||
|
||||
## 联系我们
|
||||
|
||||
如果您有任何问题或建议,欢迎通过以下方式联系我们:
|
||||
|
||||
- 微信:kangfenmao
|
||||
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
|
||||
|
||||
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
|
||||
@@ -6,19 +6,17 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
|
||||
|
||||
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
|
||||
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||
|
||||
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
|
||||
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||
|
||||
# 📖 ガイド
|
||||
|
||||
@@ -26,14 +24,14 @@ https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 主な機能
|
||||
|
||||

|
||||
|
||||
1. **多様な LLM サービス対応**:
|
||||
|
||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||
@@ -60,7 +58,7 @@ https://docs.cherry-ai.com
|
||||
- 🔤 AI による翻訳機能
|
||||
- 🎯 ドラッグ&ドロップによる整理
|
||||
- 🔌 ミニプログラム対応
|
||||
- ⚙️ MCP(モデルコンテキストプロトコル)サービス
|
||||
- ⚙️ MCP(モデルコンテキストプロトコル) サービス
|
||||
|
||||
5. **優れたユーザー体験**:
|
||||
|
||||
@@ -74,78 +72,68 @@ https://docs.cherry-ai.com
|
||||
|
||||
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
|
||||
- [x] 複数モデルの回答の比較
|
||||
- [x] サービスプロバイダーが提供する SSO を使用したログイン対応
|
||||
- [x] すべてのモデルのネットワーク対応
|
||||
- [x] サービスプロバイダーが提供する SSO を使用したログインをサポート
|
||||
- [x] すべてのモデルがネットワークをサポート
|
||||
- [x] 最初の公式バージョンのリリース
|
||||
- [x] バグ修正と改善(進行中...)
|
||||
- [ ] 錯誤修復と改善 (開発中...)
|
||||
- [ ] プラグイン機能(JavaScript)
|
||||
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
|
||||
- [ ] iOS & Android クライアント
|
||||
- [ ] AI ノート
|
||||
- [ ] AIノート
|
||||
- [ ] 音声入出力(AI コール)
|
||||
- [ ] データバックアップのカスタマイズ対応
|
||||
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||
|
||||
# 🌈 テーマ
|
||||
|
||||
- テーマギャラリー:https://cherrycss.com
|
||||
- Aero テーマ:https://github.com/hakadao/CherryStudio-Aero
|
||||
- PaperMaterial テーマ:https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
|
||||
- Claude テーマ:https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
|
||||
- メープルネオンテーマ:https://github.com/BoningtonChen/CherryStudio_themes
|
||||
テーマギャラリー: https://cherrycss.com
|
||||
Aero テーマ: https://github.com/hakadao/CherryStudio-Aero
|
||||
|
||||
より多くのテーマの PR を歓迎します
|
||||
より多くのテーマのPRを歓迎します
|
||||
|
||||
# 🖥️ 開発
|
||||
|
||||
[開発ドキュメント](dev.md)を参照してください
|
||||
|
||||
[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
|
||||
|
||||
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
|
||||
参考[開発ドキュメント](dev.md)
|
||||
|
||||
# 🤝 貢献
|
||||
|
||||
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
|
||||
|
||||
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
|
||||
2. **バグの修正**:見つけたバグを修正します
|
||||
3. **問題の管理**:GitHub の問題を管理するのを手伝います
|
||||
4. **製品デザイン**:デザインの議論に参加します
|
||||
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
|
||||
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
|
||||
7. **使用の促進**:Cherry Studio を広めます
|
||||
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します。
|
||||
2. **バグの修正**:見つけたバグを修正します。
|
||||
3. **問題の管理**:GitHub の問題を管理するのを手伝います。
|
||||
4. **製品デザイン**:デザインの議論に参加します。
|
||||
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します。
|
||||
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します。
|
||||
7. **使用の促進**:Cherry Studio を広めます。
|
||||
|
||||
## 始め方
|
||||
|
||||
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
|
||||
2. **ブランチを作成**:変更のためのブランチを作成します
|
||||
3. **変更を提出**:変更をコミットしてプッシュします
|
||||
4. **プルリクエストを開く**:変更内容と理由を説明します
|
||||
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします。
|
||||
2. **ブランチを作成**:変更のためのブランチを作成します。
|
||||
3. **変更を提出**:変更をコミットしてプッシュします。
|
||||
4. **プルリクエストを開く**:変更内容と理由を説明します。
|
||||
|
||||
詳細なガイドラインについては、[貢献ガイド](../CONTRIBUTING.md)をご覧ください。
|
||||
|
||||
ご支援と貢献に感謝します!
|
||||
|
||||
# 🔗 関連プロジェクト
|
||||
## 関連頁版
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM API の管理・配信システム。OpenAI、Azure、Anthropic などの主要モデルに対応し、統一 API インターフェースを提供。API キー管理と再配布に利用可能。
|
||||
|
||||
- [ublacklist](https://github.com/iorate/ublacklist):Google 検索結果から特定のサイトを非表示にします
|
||||
|
||||
# 🚀 コントリビューター
|
||||
|
||||
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
|
||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
# 🌐 コミュニティ
|
||||
# コミュニティ
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ スポンサー
|
||||
# スポンサー
|
||||
|
||||
[開発者を支援する](sponsor.md)
|
||||
[Buy Me a Coffee](sponsor.md)
|
||||
|
||||
# 📃 ライセンス
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
</a>
|
||||
</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br>
|
||||
</p>
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
@@ -15,32 +14,24 @@
|
||||
|
||||
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
|
||||
|
||||
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||
|
||||
# GitCode✖️Cherry Studio【新源力】贡献挑战赛
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitcode.com/CherryHQ/cherry-studio/discussion/2">
|
||||
<img src="https://raw.gitcode.com/user-images/assets/5007375/8d8d7559-1141-4691-b90f-d154558c6896/cherry-studio-gitcode.jpg" width="100%" alt="banner" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 📖 使用教程
|
||||
|
||||
https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 界面
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 主要特性
|
||||
|
||||

|
||||
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
@@ -77,12 +68,12 @@ https://docs.cherry-ai.com
|
||||
- 📝 完整的 Markdown 渲染
|
||||
- 🤲 便捷的内容分享功能
|
||||
|
||||
# 📝 待办事项
|
||||
# 📝 待辦事項
|
||||
|
||||
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
|
||||
- [x] 多模型回答对比
|
||||
- [x] 支持使用服务供应商提供的 SSO 进行登录
|
||||
- [x] 所有模型支持联网
|
||||
- [x] 支持使用服务供应商提供的 SSO 进行登入
|
||||
- [x] 全部模型支持连网(开发中...)
|
||||
- [x] 推出第一个正式版
|
||||
- [x] 错误修复和改进(开发中...)
|
||||
- [ ] 插件功能(JavaScript)
|
||||
@@ -94,11 +85,8 @@ https://docs.cherry-ai.com
|
||||
|
||||
# 🌈 主题
|
||||
|
||||
- 主题库:https://cherrycss.com
|
||||
- Aero 主题:https://github.com/hakadao/CherryStudio-Aero
|
||||
- PaperMaterial 主题:https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
|
||||
- 仿 Claude 主题:https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
|
||||
- 霓虹枫叶主题:https://github.com/BoningtonChen/CherryStudio_themes
|
||||
主题库:https://cherrycss.com
|
||||
Aero 主题:https://github.com/hakadao/CherryStudio-Aero
|
||||
|
||||
欢迎 PR 更多主题
|
||||
|
||||
@@ -106,43 +94,37 @@ https://docs.cherry-ai.com
|
||||
|
||||
参考[开发文档](dev.md)
|
||||
|
||||
参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
|
||||
|
||||
参考[分支策略](branching-strategy-zh.md)了解贡献指南
|
||||
|
||||
# 🤝 贡献
|
||||
|
||||
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
|
||||
|
||||
1. **贡献代码**:开发新功能或优化现有代码
|
||||
2. **修复错误**:提交您发现的错误修复
|
||||
3. **维护问题**:帮助管理 GitHub 问题
|
||||
4. **产品设计**:参与设计讨论
|
||||
5. **撰写文档**:改进用户手册和指南
|
||||
6. **社区参与**:加入讨论并帮助用户
|
||||
7. **推广使用**:宣传 Cherry Studio
|
||||
1. **贡献代码**:开发新功能或优化现有代码。
|
||||
2. **修复错误**:提交您发现的错误修复。
|
||||
3. **维护问题**:帮助管理 GitHub 问题。
|
||||
4. **产品设计**:参与设计讨论。
|
||||
5. **撰写文档**:改进用户手册和指南。
|
||||
6. **社区参与**:加入讨论并帮助用户。
|
||||
7. **推广使用**:宣传 Cherry Studio。
|
||||
|
||||
## 入门
|
||||
|
||||
1. **Fork 仓库**:Fork 并克隆到您的本地机器
|
||||
2. **创建分支**:为您的更改创建分支
|
||||
3. **提交更改**:提交并推送您的更改
|
||||
4. **打开 Pull Request**:描述您的更改和原因
|
||||
1. **Fork 仓库**:Fork 并克隆到您的本地机器。
|
||||
2. **创建分支**:为您的更改创建分支。
|
||||
3. **提交更改**:提交并推送您的更改。
|
||||
4. **打开 Pull Request**:描述您的更改和原因。
|
||||
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
|
||||
|
||||
感谢您的支持和贡献!
|
||||
|
||||
# 🔗 相关项目
|
||||
## 相关项目
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api):LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
|
||||
|
||||
- [ublacklist](https://github.com/iorate/ublacklist):屏蔽特定网站在 Google 搜索结果中显示
|
||||
|
||||
# 🚀 贡献者
|
||||
|
||||
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
|
||||
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
@@ -152,7 +134,7 @@ https://docs.cherry-ai.com
|
||||
|
||||
# ☕ 赞助
|
||||
|
||||
[赞助开发者](sponsor.md)
|
||||
[微信赞赏码](sponsor.md)
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# 🌿 Branching Strategy
|
||||
|
||||
Cherry Studio implements a structured branching strategy to maintain code quality and streamline the development process.
|
||||
|
||||
## Main Branches
|
||||
|
||||
- `main`: Main development branch
|
||||
|
||||
- Contains the latest development code
|
||||
- Direct commits are not allowed - changes must come through pull requests
|
||||
- Code may contain features in development and might not be fully stable
|
||||
|
||||
- `release/*`: Release branches
|
||||
- Created from `main` branch
|
||||
- Contains stable code ready for release
|
||||
- Only accepts documentation updates and bug fixes
|
||||
- Thoroughly tested before production deployment
|
||||
|
||||
## Contributing Branches
|
||||
|
||||
When contributing to Cherry Studio, please follow these guidelines:
|
||||
|
||||
1. **Feature Branches:**
|
||||
|
||||
- Create from `main` branch
|
||||
- Naming format: `feature/issue-number-brief-description`
|
||||
- Submit PR back to `main`
|
||||
|
||||
2. **Bug Fix Branches:**
|
||||
|
||||
- Create from `main` branch
|
||||
- Naming format: `fix/issue-number-brief-description`
|
||||
- Submit PR back to `main`
|
||||
|
||||
3. **Documentation Branches:**
|
||||
|
||||
- Create from `main` branch
|
||||
- Naming format: `docs/brief-description`
|
||||
- Submit PR back to `main`
|
||||
|
||||
4. **Hotfix Branches:**
|
||||
|
||||
- Create from `main` branch
|
||||
- Naming format: `hotfix/issue-number-brief-description`
|
||||
- Submit PR to both `main` and relevant `release` branches
|
||||
|
||||
5. **Release Branches:**
|
||||
- Create from `main` branch
|
||||
- Naming format: `release/version-number`
|
||||
- Used for final preparation work before version release
|
||||
- Only accepts bug fixes and documentation updates
|
||||
- After testing and preparation, merge back to `main` and tag with version
|
||||
|
||||
## Workflow Diagram
|
||||
|
||||

|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- All PRs should be submitted to the `main` branch unless fixing a critical production issue
|
||||
- Ensure your branch is up to date with the latest `main` changes before submitting
|
||||
- Include relevant issue numbers in your PR description
|
||||
- Make sure all tests pass and code meets our quality standards
|
||||
- Add before/after screenshots if you add a new feature or modify a UI component
|
||||
|
||||
## Version Tag Management
|
||||
|
||||
- Major releases: v1.0.0, v2.0.0, etc.
|
||||
- Feature releases: v1.1.0, v1.2.0, etc.
|
||||
- Patch releases: v1.0.1, v1.0.2, etc.
|
||||
- Hotfix releases: v1.0.1-hotfix, etc.
|
||||
@@ -1,71 +0,0 @@
|
||||
# 🌿 分支策略
|
||||
|
||||
Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发流程。
|
||||
|
||||
## 主要分支
|
||||
|
||||
- `main`:主开发分支
|
||||
|
||||
- 包含最新的开发代码
|
||||
- 禁止直接提交 - 所有更改必须通过拉取请求(Pull Request)
|
||||
- 此分支上的代码可能包含正在开发的功能,不一定完全稳定
|
||||
|
||||
- `release/*`:发布分支
|
||||
- 从 `main` 分支创建
|
||||
- 包含准备发布的稳定代码
|
||||
- 只接受文档更新和 bug 修复
|
||||
- 经过完整测试后可以发布到生产环境
|
||||
|
||||
## 贡献分支
|
||||
|
||||
在为 Cherry Studio 贡献代码时,请遵循以下准则:
|
||||
|
||||
1. **功能开发分支:**
|
||||
|
||||
- 从 `main` 分支创建
|
||||
- 命名格式:`feature/issue-number-brief-description`
|
||||
- 完成后提交 PR 到 `main` 分支
|
||||
|
||||
2. **Bug 修复分支:**
|
||||
|
||||
- 从 `main` 分支创建
|
||||
- 命名格式:`fix/issue-number-brief-description`
|
||||
- 完成后提交 PR 到 `main` 分支
|
||||
|
||||
3. **文档更新分支:**
|
||||
|
||||
- 从 `main` 分支创建
|
||||
- 命名格式:`docs/brief-description`
|
||||
- 完成后提交 PR 到 `main` 分支
|
||||
|
||||
4. **紧急修复分支:**
|
||||
|
||||
- 从 `main` 分支创建
|
||||
- 命名格式:`hotfix/issue-number-brief-description`
|
||||
- 完成后需要同时合并到 `main` 和相关的 `release` 分支
|
||||
|
||||
5. **发布分支:**
|
||||
- 从 `main` 分支创建
|
||||
- 命名格式:`release/version-number`
|
||||
- 用于版本发布前的最终准备工作
|
||||
- 只允许合并 bug 修复和文档更新
|
||||
- 完成测试和准备工作后,将代码合并回 `main` 分支并打上版本标签
|
||||
|
||||
## 工作流程
|
||||
|
||||

|
||||
|
||||
## 拉取请求(PR)指南
|
||||
|
||||
- 除非是修复生产环境的关键问题,否则所有 PR 都应该提交到 `main` 分支
|
||||
- 提交 PR 前确保你的分支已经同步了最新的 `main` 分支内容
|
||||
- 在 PR 描述中包含相关的 issue 编号
|
||||
- 确保所有测试通过,且代码符合我们的质量标准
|
||||
- 如果你添加了新功能或修改了 UI 组件,请附上更改前后的截图
|
||||
|
||||
## 版本标签管理
|
||||
|
||||
- 主要版本发布:v1.0.0、v2.0.0 等
|
||||
- 功能更新发布:v1.1.0、v1.2.0 等
|
||||
- 补丁修复发布:v1.0.1、v1.0.2 等
|
||||
- 紧急修复发布:v1.0.1-hotfix 等
|
||||
@@ -37,12 +37,6 @@ yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 消息的生命周期
|
||||
|
||||

|
||||
@@ -1,127 +0,0 @@
|
||||
# messageBlock.ts 使用指南
|
||||
|
||||
该文件定义了用于管理应用程序中所有 `MessageBlock` 实体的 Redux Slice。它使用 Redux Toolkit 的 `createSlice` 和 `createEntityAdapter` 来高效地处理规范化的状态,并提供了一系列 actions 和 selectors 用于与消息块数据交互。
|
||||
|
||||
## 核心目标
|
||||
|
||||
- **状态管理**: 集中管理所有 `MessageBlock` 的状态。`MessageBlock` 代表消息中的不同内容单元(如文本、代码、图片、引用等)。
|
||||
- **规范化**: 使用 `createEntityAdapter` 将 `MessageBlock` 数据存储在规范化的结构中(`{ ids: [], entities: {} }`),这有助于提高性能和简化更新逻辑。
|
||||
- **可预测性**: 提供明确的 actions 来修改状态,并通过 selectors 安全地访问状态。
|
||||
|
||||
## 关键概念
|
||||
|
||||
- **Slice (`createSlice`)**: Redux Toolkit 的核心 API,用于创建包含 reducer 逻辑、action creators 和初始状态的 Redux 模块。
|
||||
- **Entity Adapter (`createEntityAdapter`)**: Redux Toolkit 提供的工具,用于简化对规范化数据的 CRUD(创建、读取、更新、删除)操作。它会自动生成 reducer 函数和 selectors。
|
||||
- **Selectors**: 用于从 Redux store 中派生和计算数据的函数。Selectors 可以被记忆化(memoized),以提高性能。
|
||||
|
||||
## State 结构
|
||||
|
||||
`messageBlocks` slice 的状态结构由 `createEntityAdapter` 定义,大致如下:
|
||||
|
||||
```typescript
|
||||
{
|
||||
ids: string[]; // 存储所有 MessageBlock ID 的有序列表
|
||||
entities: { [id: string]: MessageBlock }; // 按 ID 存储 MessageBlock 对象的字典
|
||||
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed'; // (可选) 其他状态,如加载状态
|
||||
error: string | null; // (可选) 错误信息
|
||||
}
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
该 slice 导出以下 actions (由 `createSlice` 和 `createEntityAdapter` 自动生成或自定义):
|
||||
|
||||
- **`upsertOneBlock(payload: MessageBlock)`**:
|
||||
|
||||
- 添加一个新的 `MessageBlock` 或更新一个已存在的 `MessageBlock`。如果 payload 中的 `id` 已存在,则执行更新;否则执行插入。
|
||||
|
||||
- **`upsertManyBlocks(payload: MessageBlock[])`**:
|
||||
|
||||
- 添加或更新多个 `MessageBlock`。常用于批量加载数据(例如,加载一个 Topic 的所有消息块)。
|
||||
|
||||
- **`removeOneBlock(payload: string)`**:
|
||||
|
||||
- 根据提供的 `id` (payload) 移除单个 `MessageBlock`。
|
||||
|
||||
- **`removeManyBlocks(payload: string[])`**:
|
||||
|
||||
- 根据提供的 `id` 数组 (payload) 移除多个 `MessageBlock`。常用于删除消息或清空 Topic 时清理相关的块。
|
||||
|
||||
- **`removeAllBlocks()`**:
|
||||
|
||||
- 移除 state 中的所有 `MessageBlock` 实体。
|
||||
|
||||
- **`updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> })`**:
|
||||
|
||||
- 更新一个已存在的 `MessageBlock`。`payload` 需要包含块的 `id` 和一个包含要更改的字段的 `changes` 对象。
|
||||
|
||||
- **`setMessageBlocksLoading(payload: 'idle' | 'loading')`**:
|
||||
|
||||
- (自定义) 设置 `loadingState` 属性。
|
||||
|
||||
- **`setMessageBlocksError(payload: string)`**:
|
||||
- (自定义) 设置 `loadingState` 为 `'failed'` 并记录错误信息。
|
||||
|
||||
**使用示例 (在 Thunk 或其他 Dispatch 的地方):**
|
||||
|
||||
```typescript
|
||||
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
|
||||
import store from './store' // 假设这是你的 Redux store 实例
|
||||
|
||||
// 添加或更新一个块
|
||||
const newBlock: MessageBlock = {
|
||||
/* ... block data ... */
|
||||
}
|
||||
store.dispatch(upsertOneBlock(newBlock))
|
||||
|
||||
// 更新一个块的内容
|
||||
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
|
||||
|
||||
// 删除多个块
|
||||
const blockIdsToRemove = ['id1', 'id2']
|
||||
store.dispatch(removeManyBlocks(blockIdsToRemove))
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
该 slice 导出由 `createEntityAdapter` 生成的基础 selectors,并通过 `messageBlocksSelectors` 对象访问:
|
||||
|
||||
- **`messageBlocksSelectors.selectIds(state: RootState): string[]`**: 返回包含所有块 ID 的数组。
|
||||
- **`messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }`**: 返回块 ID 到块对象的映射字典。
|
||||
- **`messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]`**: 返回包含所有块对象的数组。
|
||||
- **`messageBlocksSelectors.selectTotal(state: RootState): number`**: 返回块的总数。
|
||||
- **`messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined`**: 根据 ID 返回单个块对象,如果找不到则返回 `undefined`。
|
||||
|
||||
**此外,还提供了一个自定义的、记忆化的 selector:**
|
||||
|
||||
- **`selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]`**:
|
||||
- 接收一个 `blockId`。
|
||||
- 如果该 ID 对应的块是 `CITATION` 类型,则提取并格式化其包含的引用信息(来自网页搜索、知识库等),进行去重和重新编号,最后返回一个 `Citation[]` 数组,用于在 UI 中显示。
|
||||
- 如果块不存在或类型不匹配,返回空数组 `[]`。
|
||||
- 这个 selector 封装了处理不同引用来源(Gemini, OpenAI, OpenRouter, Zhipu 等)的复杂逻辑。
|
||||
|
||||
**使用示例 (在 React 组件或 `useSelector` 中):**
|
||||
|
||||
```typescript
|
||||
import { useSelector } from 'react-redux'
|
||||
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
|
||||
import type { RootState } from './store'
|
||||
|
||||
// 获取所有块
|
||||
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
|
||||
|
||||
// 获取特定 ID 的块
|
||||
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
|
||||
|
||||
// 获取特定引用块格式化后的引用列表
|
||||
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
|
||||
|
||||
// 在组件中使用引用数据
|
||||
// {formattedCitations.map(citation => ...)}
|
||||
```
|
||||
|
||||
## 集成
|
||||
|
||||
`messageBlock.ts` slice 通常与 `messageThunk.ts` 中的 Thunks 紧密协作。Thunks 负责处理异步逻辑(如 API 调用、数据库操作),并在需要时 dispatch `messageBlock` slice 的 actions 来更新状态。例如,当 `messageThunk` 接收到流式响应时,它会 dispatch `upsertOneBlock` 或 `updateOneBlock` 来实时更新对应的 `MessageBlock`。同样,删除消息的 Thunk 会 dispatch `removeManyBlocks`。
|
||||
|
||||
理解 `messageBlock.ts` 的职责是管理**状态本身**,而 `messageThunk.ts` 负责**触发状态变更**的异步流程,这对于维护清晰的应用架构至关重要。
|
||||
@@ -1,105 +0,0 @@
|
||||
# messageThunk.ts 使用指南
|
||||
|
||||
该文件包含用于管理应用程序中消息流、处理助手交互以及同步 Redux 状态与 IndexedDB 数据库的核心 Thunk Action Creators。主要围绕 `Message` 和 `MessageBlock` 对象进行操作。
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **发送/接收消息**: 处理用户消息的发送,触发助手响应,并流式处理返回的数据,将其解析为不同的 `MessageBlock`。
|
||||
2. **状态管理**: 确保 Redux store 中的消息和消息块状态与 IndexedDB 中的持久化数据保持一致。
|
||||
3. **消息操作**: 提供删除、重发、重新生成、编辑后重发、追加响应、克隆等消息生命周期管理功能。
|
||||
4. **Block 处理**: 动态创建、更新和保存各种类型的 `MessageBlock`(文本、思考过程、工具调用、引用、图片、错误、翻译等)。
|
||||
|
||||
## 主要 Thunks
|
||||
|
||||
以下是一些关键的 Thunk 函数及其用途:
|
||||
|
||||
1. **`sendMessage(userMessage, userMessageBlocks, assistant, topicId)`**
|
||||
|
||||
- **用途**: 发送一条新的用户消息。
|
||||
- **流程**:
|
||||
- 保存用户消息 (`userMessage`) 及其块 (`userMessageBlocks`) 到 Redux 和 DB。
|
||||
- 检查 `@mentions` 以确定是单模型响应还是多模型响应。
|
||||
- 创建助手消息(们)的存根 (Stub)。
|
||||
- 将存根添加到 Redux 和 DB。
|
||||
- 将核心处理逻辑 `fetchAndProcessAssistantResponseImpl` 添加到该 `topicId` 的队列中以获取实际响应。
|
||||
- **Block 相关**: 主要处理用户消息的初始 `MessageBlock` 保存。
|
||||
|
||||
2. **`fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)`**
|
||||
|
||||
- **用途**: (内部函数) 获取并处理单个助手响应的核心逻辑,被 `sendMessage`, `resend...`, `regenerate...`, `append...` 等调用。
|
||||
- **流程**:
|
||||
- 设置 Topic 加载状态。
|
||||
- 准备上下文消息。
|
||||
- 调用 `fetchChatCompletion` API 服务。
|
||||
- 使用 `createStreamProcessor` 处理流式响应。
|
||||
- 通过各种回调 (`onTextChunk`, `onThinkingChunk`, `onToolCallComplete`, `onImageGenerated`, `onError`, `onComplete` 等) 处理不同类型的事件。
|
||||
- **Block 相关**:
|
||||
- 根据流事件创建初始 `UNKNOWN` 块。
|
||||
- 实时创建和更新 `MAIN_TEXT` 和 `THINKING` 块,使用 `throttledBlockUpdate` 和 `throttledBlockDbUpdate` 进行节流更新。
|
||||
- 创建 `TOOL`, `CITATION`, `IMAGE`, `ERROR` 等类型的块。
|
||||
- 在事件完成时(如 `onTextComplete`, `onToolCallComplete`)将块状态标记为 `SUCCESS` 或 `ERROR`,并使用 `saveUpdatedBlockToDB` 保存最终状态。
|
||||
- 使用 `handleBlockTransition` 管理非流式块(如 `TOOL`, `CITATION`)的添加和状态更新。
|
||||
|
||||
3. **`loadTopicMessagesThunk(topicId, forceReload)`**
|
||||
|
||||
- **用途**: 从数据库加载指定主题的所有消息及其关联的 `MessageBlock`。
|
||||
- **流程**:
|
||||
- 从 DB 获取 `Topic` 及其 `messages` 列表。
|
||||
- 根据消息 ID 列表从 DB 获取所有相关的 `MessageBlock`。
|
||||
- 使用 `upsertManyBlocks` 将块更新到 Redux。
|
||||
- 将消息更新到 Redux。
|
||||
- **Block 相关**: 负责将持久化的 `MessageBlock` 加载到 Redux 状态。
|
||||
|
||||
4. **删除 Thunks**
|
||||
|
||||
- `deleteSingleMessageThunk(topicId, messageId)`: 删除单个消息及其所有 `MessageBlock`。
|
||||
- `deleteMessageGroupThunk(topicId, askId)`: 删除一个用户消息及其所有相关的助手响应消息和它们的所有 `MessageBlock`。
|
||||
- `clearTopicMessagesThunk(topicId)`: 清空主题下的所有消息及其所有 `MessageBlock`。
|
||||
- **Block 相关**: 从 Redux 和 DB 中移除指定的 `MessageBlock`。
|
||||
|
||||
5. **重发/重新生成 Thunks**
|
||||
|
||||
- `resendMessageThunk(topicId, userMessageToResend, assistant)`: 重发用户消息。会重置(清空 Block 并标记为 PENDING)所有与该用户消息关联的助手响应,然后重新请求生成。
|
||||
- `resendUserMessageWithEditThunk(topicId, originalMessage, mainTextBlockId, editedContent, assistant)`: 用户编辑消息内容后重发。先更新用户消息的 `MAIN_TEXT` 块内容,然后调用 `resendMessageThunk`。
|
||||
- `regenerateAssistantResponseThunk(topicId, assistantMessageToRegenerate, assistant)`: 重新生成单个助手响应。重置该助手消息(清空 Block 并标记为 PENDING),然后重新请求生成。
|
||||
- **Block 相关**: 删除旧的 `MessageBlock`,并在重新生成过程中创建新的 `MessageBlock`。
|
||||
|
||||
6. **`appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)`**
|
||||
|
||||
- **用途**: 在已有的对话上下文中,针对同一个用户问题,使用新选择的模型追加一个新的助手响应。
|
||||
- **流程**:
|
||||
- 找到现有助手消息以获取原始 `askId`。
|
||||
- 创建使用 `newModel` 的新助手消息存根(使用相同的 `askId`)。
|
||||
- 添加新存根到 Redux 和 DB。
|
||||
- 将 `fetchAndProcessAssistantResponseImpl` 添加到队列以生成新响应。
|
||||
- **Block 相关**: 为新的助手响应创建全新的 `MessageBlock`。
|
||||
|
||||
7. **`cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)`**
|
||||
|
||||
- **用途**: 将源主题的部分消息(及其 Block)克隆到一个**已存在**的新主题中。
|
||||
- **流程**:
|
||||
- 复制指定索引前的消息。
|
||||
- 为所有克隆的消息和 Block 生成新的 UUID。
|
||||
- 正确映射克隆消息之间的 `askId` 关系。
|
||||
- 复制 `MessageBlock` 内容,更新其 `messageId` 指向新的消息 ID。
|
||||
- 更新文件引用计数(如果 Block 是文件或图片)。
|
||||
- 将克隆的消息和 Block 保存到新主题的 Redux 状态和 DB 中。
|
||||
- **Block 相关**: 创建 `MessageBlock` 的副本,并更新其 ID 和 `messageId`。
|
||||
|
||||
8. **`initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)`**
|
||||
- **用途**: 为指定消息启动翻译流程,创建一个初始的 `TRANSLATION` 类型的 `MessageBlock`。
|
||||
- **流程**:
|
||||
- 创建一个状态为 `STREAMING` 的 `TranslationMessageBlock`。
|
||||
- 将其添加到 Redux 和 DB。
|
||||
- 更新原消息的 `blocks` 列表以包含新的翻译块 ID。
|
||||
- **Block 相关**: 创建并保存一个占位的 `TranslationMessageBlock`。实际翻译内容的获取和填充需要后续步骤。
|
||||
|
||||
## 内部机制和注意事项
|
||||
|
||||
- **数据库交互**: 通过 `saveMessageAndBlocksToDB`, `updateExistingMessageAndBlocksInDB`, `saveUpdatesToDB`, `saveUpdatedBlockToDB`, `throttledBlockDbUpdate` 等辅助函数与 IndexedDB (`db`) 交互,确保数据持久化。
|
||||
- **状态同步**: Thunks 负责协调 Redux Store 和 IndexedDB 之间的数据一致性。
|
||||
- **队列 (`getTopicQueue`)**: 使用 `AsyncQueue` 确保对同一主题的操作(尤其是 API 请求)按顺序执行,避免竞态条件。
|
||||
- **节流 (`throttle`)**: 对流式响应中频繁的 Block 更新(文本、思考)使用 `lodash.throttle` 优化性能,减少 Redux dispatch 和 DB 写入次数。
|
||||
- **错误处理**: `fetchAndProcessAssistantResponseImpl` 内的回调函数(特别是 `onError`)处理流处理和 API 调用中可能出现的错误,并创建 `ERROR` 类型的 `MessageBlock`。
|
||||
|
||||
开发者在使用这些 Thunks 时,通常需要提供 `dispatch`, `getState` (由 Redux Thunk 中间件注入),以及如 `topicId`, `assistant` 配置对象, 相关的 `Message` 或 `MessageBlock` 对象/ID 等参数。理解每个 Thunk 的职责和它如何影响消息及块的状态至关重要。
|
||||
@@ -1,156 +0,0 @@
|
||||
# useMessageOperations.ts 使用指南
|
||||
|
||||
该文件定义了一个名为 `useMessageOperations` 的自定义 React Hook。这个 Hook 的主要目的是为 React 组件提供一个便捷的接口,用于执行与特定主题(Topic)相关的各种消息操作。它封装了调用 Redux Thunks (`messageThunk.ts`) 和 Actions (`newMessage.ts`, `messageBlock.ts`) 的逻辑,简化了组件与消息数据交互的代码。
|
||||
|
||||
## 核心目标
|
||||
|
||||
- **封装**: 将复杂的消息操作逻辑(如删除、重发、重新生成、编辑、翻译等)封装在易于使用的函数中。
|
||||
- **简化**: 让组件可以直接调用这些操作函数,而无需直接与 Redux `dispatch` 或 Thunks 交互。
|
||||
- **上下文关联**: 所有操作都与传入的 `topic` 对象相关联,确保操作作用于正确的主题。
|
||||
|
||||
## 如何使用
|
||||
|
||||
在你的 React 函数组件中,导入并调用 `useMessageOperations` Hook,并传入当前活动的 `Topic` 对象。
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations';
|
||||
import type { Topic, Message, Assistant, Model } from '@renderer/types';
|
||||
|
||||
interface MyComponentProps {
|
||||
currentTopic: Topic;
|
||||
currentAssistant: Assistant;
|
||||
}
|
||||
|
||||
function MyComponent({ currentTopic, currentAssistant }: MyComponentProps) {
|
||||
const {
|
||||
deleteMessage,
|
||||
resendMessage,
|
||||
regenerateAssistantMessage,
|
||||
appendAssistantResponse,
|
||||
getTranslationUpdater,
|
||||
createTopicBranch,
|
||||
// ... 其他操作函数
|
||||
} = useMessageOperations(currentTopic);
|
||||
|
||||
const handleDelete = (messageId: string) => {
|
||||
deleteMessage(messageId);
|
||||
};
|
||||
|
||||
const handleResend = (message: Message) => {
|
||||
resendMessage(message, currentAssistant);
|
||||
};
|
||||
|
||||
const handleAppend = (existingMsg: Message, newModel: Model) => {
|
||||
appendAssistantResponse(existingMsg, newModel, currentAssistant);
|
||||
}
|
||||
|
||||
// ... 在组件中使用其他操作函数
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Component UI */}
|
||||
<button onClick={() => handleDelete('some-message-id')}>Delete Message</button>
|
||||
{/* ... */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
`useMessageOperations(topic)` Hook 返回一个包含以下函数和值的对象:
|
||||
|
||||
- **`deleteMessage(id: string)`**:
|
||||
|
||||
- 删除指定 `id` 的单个消息。
|
||||
- 内部调用 `deleteSingleMessageThunk`。
|
||||
|
||||
- **`deleteGroupMessages(askId: string)`**:
|
||||
|
||||
- 删除与指定 `askId` 相关联的一组消息(通常是用户提问及其所有助手回答)。
|
||||
- 内部调用 `deleteMessageGroupThunk`。
|
||||
|
||||
- **`editMessage(messageId: string, updates: Partial<Message>)`**:
|
||||
|
||||
- 更新指定 `messageId` 的消息的部分属性。
|
||||
- **注意**: 目前主要用于更新 Redux 状态
|
||||
- 内部调用 `newMessagesActions.updateMessage`。
|
||||
|
||||
- **`resendMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 重新发送指定的用户消息 (`message`),这将触发其所有关联助手响应的重新生成。
|
||||
- 内部调用 `resendMessageThunk`。
|
||||
|
||||
- **`resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant)`**:
|
||||
|
||||
- 在用户消息的主要文本块被编辑后,重新发送该消息。
|
||||
- 会先查找消息的 `MAIN_TEXT` 块 ID,然后调用 `resendUserMessageWithEditThunk`。
|
||||
|
||||
- **`clearTopicMessages(_topicId?: string)`**:
|
||||
|
||||
- 清除当前主题(或可选的指定 `_topicId`)下的所有消息。
|
||||
- 内部调用 `clearTopicMessagesThunk`。
|
||||
|
||||
- **`createNewContext()`**:
|
||||
|
||||
- 发出一个全局事件 (`EVENT_NAMES.NEW_CONTEXT`),通常用于通知 UI 清空显示,准备新的上下文。不直接修改 Redux 状态。
|
||||
|
||||
- **`displayCount`**:
|
||||
|
||||
- (非操作函数) 从 Redux store 中获取当前的 `displayCount` 值。
|
||||
|
||||
- **`pauseMessages()`**:
|
||||
|
||||
- 尝试中止当前主题中正在进行的消息生成(状态为 `processing` 或 `pending`)。
|
||||
- 通过查找相关的 `askId` 并调用 `abortCompletion` 来实现。
|
||||
- 同时会 dispatch `setTopicLoading` action 将加载状态设为 `false`。
|
||||
|
||||
- **`resumeMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 恢复/重新发送一个用户消息。目前实现为直接调用 `resendMessage`。
|
||||
|
||||
- **`regenerateAssistantMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 重新生成指定的**助手**消息 (`message`) 的响应。
|
||||
- 内部调用 `regenerateAssistantResponseThunk`。
|
||||
|
||||
- **`appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant)`**:
|
||||
|
||||
- 针对 `existingAssistantMessage` 所回复的**同一用户提问**,使用 `newModel` 追加一个新的助手响应。
|
||||
- 内部调用 `appendAssistantResponseThunk`。
|
||||
|
||||
- **`getTranslationUpdater(messageId: string, targetLanguage: string, sourceBlockId?: string, sourceLanguage?: string)`**:
|
||||
|
||||
- **用途**: 获取一个用于逐步更新翻译块内容的函数。
|
||||
- **流程**:
|
||||
1. 内部调用 `initiateTranslationThunk` 来创建或获取一个 `TRANSLATION` 类型的 `MessageBlock`,并获取其 `blockId`。
|
||||
2. 返回一个**异步更新函数**。
|
||||
- **返回的更新函数 `(accumulatedText: string, isComplete?: boolean) => void`**:
|
||||
- 接收累积的翻译文本和完成状态。
|
||||
- 调用 `updateOneBlock` 更新 Redux 中的翻译块内容和状态 (`STREAMING` 或 `SUCCESS`)。
|
||||
- 调用 `throttledBlockDbUpdate` 将更新(节流地)保存到数据库。
|
||||
- 如果初始化失败(Thunk 返回 `undefined`),则此函数返回 `null`。
|
||||
|
||||
- **`createTopicBranch(sourceTopicId: string, branchPointIndex: number, newTopic: Topic)`**:
|
||||
- 创建一个主题分支,将 `sourceTopicId` 主题中 `branchPointIndex` 索引之前的消息克隆到 `newTopic` 中。
|
||||
- **注意**: `newTopic` 对象必须是调用此函数**之前**已经创建并添加到 Redux 和数据库中的。
|
||||
- 内部调用 `cloneMessagesToNewTopicThunk`。
|
||||
|
||||
## 依赖
|
||||
|
||||
- **`topic: Topic`**: 必须传入当前操作上下文的主题对象。Hook 返回的操作函数将始终作用于这个主题的 `topic.id`。
|
||||
- **Redux `dispatch`**: Hook 内部使用 `useAppDispatch` 获取 `dispatch` 函数来调用 actions 和 thunks。
|
||||
|
||||
## 相关 Hooks
|
||||
|
||||
在同一文件中还定义了两个辅助 Hook:
|
||||
|
||||
- **`useTopicMessages(topic: Topic)`**:
|
||||
|
||||
- 使用 `selectMessagesForTopic` selector 来获取并返回指定主题的消息列表。
|
||||
|
||||
- **`useTopicLoading(topic: Topic)`**:
|
||||
- 使用 `selectNewTopicLoading` selector 来获取并返回指定主题的加载状态。
|
||||
|
||||
这些 Hook 可以与 `useMessageOperations` 结合使用,方便地在组件中获取消息数据、加载状态,并执行相关操作。
|
||||
|
Before Width: | Height: | Size: 563 KiB |
@@ -1,14 +1,5 @@
|
||||
appId: com.kangfenmao.CherryStudio
|
||||
productName: Cherry Studio
|
||||
electronLanguages:
|
||||
- zh-CN
|
||||
- zh-TW
|
||||
- en-US
|
||||
- ja # macOS/linux/win
|
||||
- ru # macOS/linux/win
|
||||
- zh_CN # for macOS
|
||||
- zh_TW # for macOS
|
||||
- en # for macOS
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
@@ -38,33 +29,25 @@ files:
|
||||
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
- '**/*.{metal,exp,lib}'
|
||||
- '**/*.{node,dll,metal,exp,lib}'
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
artifactName: ${productName}-${version}-portable.${ext}
|
||||
target:
|
||||
- target: nsis
|
||||
- target: portable
|
||||
signtoolOptions:
|
||||
sign: scripts/win-sign.js
|
||||
verifyUpdateCodeSignature: false
|
||||
nsis:
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
allowToChangeInstallationDirectory: true
|
||||
oneClick: false
|
||||
include: build/nsis-installer.nsh
|
||||
buildUniversalInstaller: false
|
||||
portable:
|
||||
artifactName: ${productName}-${version}-${arch}-portable.${ext}
|
||||
buildUniversalInstaller: false
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
minimumSystemVersion: '20.1.0' # 最低支持 macOS 11.0
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -72,36 +55,35 @@ mac:
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
- target: zip
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
linux:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
desktop:
|
||||
entry:
|
||||
StartupWMClass: CherryStudio
|
||||
mimeTypes:
|
||||
- x-scheme-handler/cherrystudio
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://releases.cherry-ai.com
|
||||
# provider: generic
|
||||
# url: https://cherrystudio.ocool.online
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: CherryHQ
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
⚠️ 注意:升级前请备份数据,否则将无法降级
|
||||
增加 TokenFlux 服务商
|
||||
增加 Claude 4 模型支持
|
||||
Grok 模型增加联网能力
|
||||
小程序支持前进和后退
|
||||
修复 Windows 用户 MCP 无法启动问题
|
||||
修复无法搜索历史消息问题
|
||||
修复 MCP 代理问题
|
||||
修复精简备份恢复覆盖文件问题
|
||||
修复@模型回复插入位置错误问题
|
||||
修复搜索小程序崩溃问题
|
||||
小程序支持多开
|
||||
支持 GPT-4o 图像生成
|
||||
修复 MCP 服务器无法使用问题
|
||||
修复升级导致旧版本数据丢失问题
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
@@ -42,27 +42,24 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@shared': resolve('packages/shared')
|
||||
}
|
||||
}
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
react({
|
||||
plugins: [
|
||||
[
|
||||
'@swc/plugin-styled-components',
|
||||
{
|
||||
displayName: true, // 开发环境下启用组件名称
|
||||
fileName: false, // 不在类名中包含文件名
|
||||
pure: true, // 优化性能
|
||||
ssr: false // 不需要服务端渲染
|
||||
}
|
||||
babel: {
|
||||
plugins: [
|
||||
[
|
||||
'styled-components',
|
||||
{
|
||||
displayName: true, // 开发环境下启用组件名称
|
||||
fileName: false, // 不在类名中包含文件名
|
||||
pure: true, // 优化性能
|
||||
ssr: false // 不需要服务端渲染
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}),
|
||||
...visualizerPlugin('renderer')
|
||||
],
|
||||
@@ -73,18 +70,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['pyodide']
|
||||
},
|
||||
worker: {
|
||||
format: 'es'
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/renderer/index.html'),
|
||||
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html')
|
||||
}
|
||||
}
|
||||
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js', 'chunk-ALDIEZMG.js', 'chunk-4X6ZJEXY.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
122
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.3.11",
|
||||
"version": "1.1.17",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -23,13 +23,12 @@
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
|
||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
||||
"build:win": "dotenv npm run build && electron-builder --win",
|
||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --arm64 --x64",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac",
|
||||
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
|
||||
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --x64 --arm64",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux",
|
||||
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
|
||||
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
|
||||
"build:npm": "node scripts/build-npm.js",
|
||||
@@ -44,62 +43,48 @@
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"check:i18n": "node scripts/check-i18n.js",
|
||||
"test": "yarn test:renderer",
|
||||
"test:coverage": "yarn test:renderer:coverage",
|
||||
"test:node": "npx -y tsx --test src/**/*.test.ts",
|
||||
"test:renderer": "vitest run",
|
||||
"test:renderer:ui": "vitest --ui",
|
||||
"test:renderer:coverage": "vitest run --coverage",
|
||||
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||
"test": "npx -y tsx --test src/**/*.test.ts",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cherrystudio/embedjs": "^0.1.31",
|
||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-image": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-msoffice": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-pdf": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-web": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
|
||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||
"@cherrystudio/embedjs": "^0.1.28",
|
||||
"@cherrystudio/embedjs-libsql": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-csv": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-image": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-markdown": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-msoffice": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-pdf": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-sitemap": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-web": "^0.1.28",
|
||||
"@cherrystudio/embedjs-loader-xml": "^0.1.28",
|
||||
"@cherrystudio/embedjs-openai": "^0.1.28",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tanstack/react-query": "^5.27.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"color": "^5.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"adm-zip": "^0.5.16",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "6.6.4",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"fast-diff": "^1.3.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"fast-xml-parser": "^5.0.9",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got-scraping": "^4.1.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"tar": "^7.4.3",
|
||||
"turndown": "^7.2.0",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
"undici": "^7.4.0",
|
||||
"webdav": "^5.8.0",
|
||||
"ws": "^8.18.1",
|
||||
"zipread": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -107,7 +92,7 @@
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.41.0",
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
@@ -115,17 +100,15 @@
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@google/genai": "^0.4.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.4",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.4.2",
|
||||
"@swc/plugin-styled-components": "^7.1.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/diff": "^7",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
@@ -135,28 +118,22 @@
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-window": "^1",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/ws": "^8",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.12",
|
||||
"@uiw/codemirror-themes-all": "^4.23.12",
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"@vitest/web-worker": "^3.1.3",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.22.5",
|
||||
"applescript": "^1.0.0",
|
||||
"axios": "^1.7.3",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"electron": "35.4.0",
|
||||
"electron-builder": "26.0.15",
|
||||
"electron": "31.7.6",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^3.1.0",
|
||||
"electron-vite": "^2.3.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^9.22.0",
|
||||
@@ -168,16 +145,12 @@
|
||||
"i18next": "^23.11.5",
|
||||
"lint-staged": "^15.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.10.5",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"rc-virtual-list": "^3.18.6",
|
||||
"rc-virtual-list": "^3.18.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
@@ -188,7 +161,6 @@
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-window": "^1.8.11",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -198,30 +170,24 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.88.0",
|
||||
"shiki": "^3.4.2",
|
||||
"sass": "^1.77.2",
|
||||
"shiki": "^1.22.2",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tokenx": "^0.4.1",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "6.2.6",
|
||||
"vitest": "^3.1.1"
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"resolutions": {
|
||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"node-gyp": "^9.1.0",
|
||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch"
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
||||
"prettier --write",
|
||||
|
||||
1
packages/artifacts/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Cherry Studio Artifacts
|
||||
19
packages/artifacts/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"description": "Cherry Studio Artifacts",
|
||||
"main": "index.js",
|
||||
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"artifacts"
|
||||
],
|
||||
"author": "kangfenmao",
|
||||
"license": "ISC"
|
||||
}
|
||||
108
packages/artifacts/statics/word-explanation-card.css
Normal file
@@ -0,0 +1,108 @@
|
||||
:root {
|
||||
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
|
||||
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
|
||||
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
|
||||
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
|
||||
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
|
||||
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
|
||||
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
|
||||
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
|
||||
}
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
|
||||
}
|
||||
.card {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
|
||||
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.content {
|
||||
padding: 30px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.word {
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.word-main {
|
||||
font-family: 'Noto Serif SC', serif;
|
||||
font-size: 36px;
|
||||
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.word-main::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -5px;
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
|
||||
}
|
||||
.word-sub {
|
||||
font-size: 14px;
|
||||
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
|
||||
margin: 5px 0;
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
|
||||
margin: 20px 0;
|
||||
}
|
||||
.explanation {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.quote {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
|
||||
}
|
||||
.background-text {
|
||||
position: absolute;
|
||||
font-size: 150px;
|
||||
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
|
||||
z-index: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
3
packages/database/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
data/*
|
||||
!data/.gitkeep
|
||||
|
||||
BIN
packages/database/.yarn/install-state.gz
Normal file
3
packages/database/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Cherry Studio Database
|
||||
|
||||
Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取
|
||||
0
packages/database/data/.gitkeep
Normal file
13
packages/database/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@cherry-studio/database",
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"dependencies": {
|
||||
"csv-parser": "^3.0.0",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"scripts": {
|
||||
"agents": "node src/agents.js",
|
||||
"email": "yarn csv && node src/email.js",
|
||||
"csv": "node src/csv.js"
|
||||
}
|
||||
}
|
||||
47
packages/database/src/agents.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
const fs = require('fs')
|
||||
|
||||
// 连接到数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to the database:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Connected to the database.')
|
||||
})
|
||||
|
||||
// 查询数据并转换为JSON
|
||||
db.all('SELECT * FROM agents', [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error querying the database:', err.message)
|
||||
return
|
||||
}
|
||||
|
||||
// 将 ID 类型转换为字符串
|
||||
for (const row of rows) {
|
||||
row.id = row.id.toString()
|
||||
row.group = row.group.toString().split(',')
|
||||
row.group = row.group.map((item) => item.trim().replace('\r\n', ''))
|
||||
}
|
||||
|
||||
// 将查询结果转换为JSON字符串
|
||||
const jsonData = JSON.stringify(rows, null, 2)
|
||||
|
||||
// 将JSON数据写入文件
|
||||
fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => {
|
||||
if (err) {
|
||||
console.error('Error writing to file:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Data has been written to agents.json')
|
||||
})
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing the database:', err.message)
|
||||
return
|
||||
}
|
||||
console.log('Database connection closed.')
|
||||
})
|
||||
})
|
||||
77
packages/database/src/csv.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const fs = require('fs')
|
||||
const csv = require('csv-parser')
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
|
||||
// 连接到 SQLite 数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database', err)
|
||||
return
|
||||
}
|
||||
console.log('Connected to the SQLite database.')
|
||||
})
|
||||
|
||||
// 创建一个数组来存储 CSV 数据
|
||||
const results = []
|
||||
|
||||
// 读取 CSV 文件
|
||||
fs.createReadStream('./data/data.csv')
|
||||
.pipe(csv())
|
||||
.on('data', (data) => results.push(data))
|
||||
.on('end', () => {
|
||||
// 准备 SQL 插入语句,使用 INSERT OR IGNORE
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO emails (email, github, sent) VALUES (?, ?, ?)')
|
||||
|
||||
// 插入每一行数据
|
||||
let inserted = 0
|
||||
let skipped = 0
|
||||
let emptyEmail = 0
|
||||
|
||||
db.serialize(() => {
|
||||
// 开始一个事务以提高性能
|
||||
db.run('BEGIN TRANSACTION')
|
||||
|
||||
results.forEach((row) => {
|
||||
// 检查 email 是否为空
|
||||
if (!row.email || row.email.trim() === '') {
|
||||
emptyEmail++
|
||||
return // 跳过这一行
|
||||
}
|
||||
|
||||
stmt.run(row.email, row['user-href'], 0, function (err) {
|
||||
if (err) {
|
||||
console.error('Error inserting row', err)
|
||||
} else {
|
||||
if (this.changes === 1) {
|
||||
inserted++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 提交事务
|
||||
db.run('COMMIT', (err) => {
|
||||
if (err) {
|
||||
console.error('Error committing transaction', err)
|
||||
} else {
|
||||
console.log(
|
||||
`Insertion complete. Inserted: ${inserted}, Skipped (duplicate): ${skipped}, Skipped (empty email): ${emptyEmail}`
|
||||
)
|
||||
}
|
||||
|
||||
// 完成插入
|
||||
stmt.finalize()
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database', err)
|
||||
} else {
|
||||
console.log('Database connection closed.')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
36
packages/database/src/email.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const sqlite3 = require('sqlite3').verbose()
|
||||
|
||||
// 连接到数据库
|
||||
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// 查询数据并转换为JSON
|
||||
db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error querying the database:', err.message)
|
||||
return
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
console.log(row.email)
|
||||
// Update row set sent = 1
|
||||
db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => {
|
||||
if (err) {
|
||||
console.error('Error updating the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing the database:', err.message)
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
1643
packages/database/yarn.lock
Normal file
@@ -1,180 +0,0 @@
|
||||
export enum IpcChannel {
|
||||
App_GetCacheSize = 'app:get-cache-size',
|
||||
App_ClearCache = 'app:clear-cache',
|
||||
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
|
||||
App_SetLanguage = 'app:set-language',
|
||||
App_ShowUpdateDialog = 'app:show-update-dialog',
|
||||
App_CheckForUpdate = 'app:check-for-update',
|
||||
App_Reload = 'app:reload',
|
||||
App_Info = 'app:info',
|
||||
App_Proxy = 'app:proxy',
|
||||
App_SetLaunchToTray = 'app:set-launch-to-tray',
|
||||
App_SetTray = 'app:set-tray',
|
||||
App_SetTrayOnClose = 'app:set-tray-on-close',
|
||||
App_RestartTray = 'app:restart-tray',
|
||||
App_SetTheme = 'app:set-theme',
|
||||
App_SetAutoUpdate = 'app:set-auto-update',
|
||||
App_HandleZoomFactor = 'app:handle-zoom-factor',
|
||||
|
||||
App_IsBinaryExist = 'app:is-binary-exist',
|
||||
App_GetBinaryPath = 'app:get-binary-path',
|
||||
App_InstallUvBinary = 'app:install-uv-binary',
|
||||
App_InstallBunBinary = 'app:install-bun-binary',
|
||||
|
||||
Notification_Send = 'notification:send',
|
||||
Notification_OnClick = 'notification:on-click',
|
||||
|
||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||
|
||||
// Open
|
||||
Open_Path = 'open:path',
|
||||
Open_Website = 'open:website',
|
||||
|
||||
Minapp = 'minapp',
|
||||
|
||||
Config_Set = 'config:set',
|
||||
Config_Get = 'config:get',
|
||||
|
||||
MiniWindow_Show = 'miniwindow:show',
|
||||
MiniWindow_Hide = 'miniwindow:hide',
|
||||
MiniWindow_Close = 'miniwindow:close',
|
||||
MiniWindow_Toggle = 'miniwindow:toggle',
|
||||
MiniWindow_SetPin = 'miniwindow:set-pin',
|
||||
|
||||
// Mcp
|
||||
Mcp_AddServer = 'mcp:add-server',
|
||||
Mcp_RemoveServer = 'mcp:remove-server',
|
||||
Mcp_RestartServer = 'mcp:restart-server',
|
||||
Mcp_StopServer = 'mcp:stop-server',
|
||||
Mcp_ListTools = 'mcp:list-tools',
|
||||
Mcp_CallTool = 'mcp:call-tool',
|
||||
Mcp_ListPrompts = 'mcp:list-prompts',
|
||||
Mcp_GetPrompt = 'mcp:get-prompt',
|
||||
Mcp_ListResources = 'mcp:list-resources',
|
||||
Mcp_GetResource = 'mcp:get-resource',
|
||||
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
||||
Mcp_ServersChanged = 'mcp:servers-changed',
|
||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||
Mcp_CheckConnectivity = 'mcp:check-connectivity',
|
||||
|
||||
//copilot
|
||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||
Copilot_SaveCopilotToken = 'copilot:save-copilot-token',
|
||||
Copilot_GetToken = 'copilot:get-token',
|
||||
Copilot_Logout = 'copilot:logout',
|
||||
Copilot_GetUser = 'copilot:get-user',
|
||||
|
||||
// obsidian
|
||||
Obsidian_GetVaults = 'obsidian:get-vaults',
|
||||
Obsidian_GetFiles = 'obsidian:get-files',
|
||||
|
||||
// nutstore
|
||||
Nutstore_GetSsoUrl = 'nutstore:get-sso-url',
|
||||
Nutstore_DecryptToken = 'nutstore:decrypt-token',
|
||||
Nutstore_GetDirectoryContents = 'nutstore:get-directory-contents',
|
||||
|
||||
//aes
|
||||
Aes_Encrypt = 'aes:encrypt',
|
||||
Aes_Decrypt = 'aes:decrypt',
|
||||
|
||||
Gemini_UploadFile = 'gemini:upload-file',
|
||||
Gemini_Base64File = 'gemini:base64-file',
|
||||
Gemini_RetrieveFile = 'gemini:retrieve-file',
|
||||
Gemini_ListFiles = 'gemini:list-files',
|
||||
Gemini_DeleteFile = 'gemini:delete-file',
|
||||
|
||||
Windows_ResetMinimumSize = 'window:reset-minimum-size',
|
||||
Windows_SetMinimumSize = 'window:set-minimum-size',
|
||||
|
||||
KnowledgeBase_Create = 'knowledge-base:create',
|
||||
KnowledgeBase_Reset = 'knowledge-base:reset',
|
||||
KnowledgeBase_Delete = 'knowledge-base:delete',
|
||||
KnowledgeBase_Add = 'knowledge-base:add',
|
||||
KnowledgeBase_Remove = 'knowledge-base:remove',
|
||||
KnowledgeBase_Search = 'knowledge-base:search',
|
||||
KnowledgeBase_Rerank = 'knowledge-base:rerank',
|
||||
|
||||
//file
|
||||
File_Open = 'file:open',
|
||||
File_OpenPath = 'file:openPath',
|
||||
File_Save = 'file:save',
|
||||
File_Select = 'file:select',
|
||||
File_Upload = 'file:upload',
|
||||
File_Clear = 'file:clear',
|
||||
File_Read = 'file:read',
|
||||
File_Delete = 'file:delete',
|
||||
File_Get = 'file:get',
|
||||
File_SelectFolder = 'file:selectFolder',
|
||||
File_Create = 'file:create',
|
||||
File_Write = 'file:write',
|
||||
File_WriteWithId = 'file:writeWithId',
|
||||
File_SaveImage = 'file:saveImage',
|
||||
File_Base64Image = 'file:base64Image',
|
||||
File_Download = 'file:download',
|
||||
File_Copy = 'file:copy',
|
||||
File_BinaryImage = 'file:binaryImage',
|
||||
File_Base64File = 'file:base64File',
|
||||
Fs_Read = 'fs:read',
|
||||
|
||||
Export_Word = 'export:word',
|
||||
|
||||
Shortcuts_Update = 'shortcuts:update',
|
||||
|
||||
// backup
|
||||
Backup_Backup = 'backup:backup',
|
||||
Backup_Restore = 'backup:restore',
|
||||
Backup_BackupToWebdav = 'backup:backupToWebdav',
|
||||
Backup_RestoreFromWebdav = 'backup:restoreFromWebdav',
|
||||
Backup_ListWebdavFiles = 'backup:listWebdavFiles',
|
||||
Backup_CheckConnection = 'backup:checkConnection',
|
||||
Backup_CreateDirectory = 'backup:createDirectory',
|
||||
Backup_DeleteWebdavFile = 'backup:deleteWebdavFile',
|
||||
|
||||
// zip
|
||||
Zip_Compress = 'zip:compress',
|
||||
Zip_Decompress = 'zip:decompress',
|
||||
|
||||
// system
|
||||
System_GetDeviceType = 'system:getDeviceType',
|
||||
System_GetHostname = 'system:getHostname',
|
||||
|
||||
// DevTools
|
||||
System_ToggleDevTools = 'system:toggleDevTools',
|
||||
|
||||
// events
|
||||
BackupProgress = 'backup-progress',
|
||||
ThemeChange = 'theme:change',
|
||||
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
|
||||
RestoreProgress = 'restore-progress',
|
||||
UpdateError = 'update-error',
|
||||
UpdateAvailable = 'update-available',
|
||||
UpdateNotAvailable = 'update-not-available',
|
||||
DownloadProgress = 'download-progress',
|
||||
UpdateDownloaded = 'update-downloaded',
|
||||
DownloadUpdate = 'download-update',
|
||||
|
||||
DirectoryProcessingPercent = 'directory-processing-percent',
|
||||
|
||||
FullscreenStatusChanged = 'fullscreen-status-changed',
|
||||
|
||||
HideMiniWindow = 'hide-mini-window',
|
||||
ShowMiniWindow = 'show-mini-window',
|
||||
|
||||
ReduxStateChange = 'redux-state-change',
|
||||
ReduxStoreReady = 'redux-store-ready',
|
||||
|
||||
// Search Window
|
||||
SearchWindow_Open = 'search-window:open',
|
||||
SearchWindow_Close = 'search-window:close',
|
||||
SearchWindow_OpenUrl = 'search-window:open-url',
|
||||
|
||||
//Store Sync
|
||||
StoreSync_Subscribe = 'store-sync:subscribe',
|
||||
StoreSync_Unsubscribe = 'store-sync:unsubscribe',
|
||||
StoreSync_OnUpdate = 'store-sync:on-update',
|
||||
StoreSync_BroadcastSync = 'store-sync:broadcast-sync',
|
||||
|
||||
// Provider
|
||||
Provider_AddKey = 'provider:add-key'
|
||||
}
|
||||
@@ -134,14 +134,6 @@ export const textExts = [
|
||||
'.tf' // Technology File
|
||||
]
|
||||
|
||||
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
|
||||
|
||||
// 从 ZOOM_LEVELS 生成 Ant Design Select 所需的 options 结构
|
||||
export const ZOOM_OPTIONS = ZOOM_LEVELS.map((level) => ({
|
||||
value: level,
|
||||
label: `${Math.round(level * 100)}%`
|
||||
}))
|
||||
|
||||
export const ZOOM_SHORTCUTS = [
|
||||
{
|
||||
key: 'zoom_in',
|
||||
@@ -165,8 +157,3 @@ export const ZOOM_SHORTCUTS = [
|
||||
system: true
|
||||
}
|
||||
]
|
||||
|
||||
export const KB = 1024
|
||||
export const MB = 1024 * KB
|
||||
export const GB = 1024 * MB
|
||||
export const defaultLanguage = 'en-US'
|
||||
|
||||
@@ -1,197 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>许可协议 | License Agreement</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CherryStudio 许可协议-ZH/EN</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<!-- 中文版本 -->
|
||||
<div class="mb-12">
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">核心原则</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用 <strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。</li>
|
||||
<li><strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取 <strong>商业许可证 (Commercial License)</strong>。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">定义:"10人及以下"</h2>
|
||||
<p class="text-gray-700">
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
||||
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
||||
</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a> 获取。
|
||||
</li>
|
||||
<li><strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
||||
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。</li>
|
||||
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3
|
||||
义务的用户</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>强制要求:</strong>
|
||||
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
||||
Cherry Studio。</li>
|
||||
<li><strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong>无法满足 AGPLv3
|
||||
的条款要求</strong>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3 <strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
||||
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。</li>
|
||||
<li><strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>您的组织规模超过10人。</li>
|
||||
<li>(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3 公开您修改部分的源代码。</li>
|
||||
<li>(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据 AGPLv3 向服务使用者提供修改后的源代码。</li>
|
||||
<li>(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>获取商业许可:</strong> 请通过邮箱 <a href="mailto:bd@cherry-ai.com"
|
||||
class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry Studio 开发团队洽谈商业授权事宜。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. 贡献 (Contributions)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 <strong>AGPLv3</strong> 许可证下提供。</li>
|
||||
<li>通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。</li>
|
||||
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. 其他条款 (Other Terms)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
||||
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
|
||||
</ul>
|
||||
</section>
|
||||
<body class="bg-gray-100 p-8">
|
||||
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
|
||||
<p class="mb-4">
|
||||
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
|
||||
Studio 时还应遵守以下附加条款:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
|
||||
<li>
|
||||
<strong>商业授权</strong>:如果您满足以下任意条件之一,需取得商业授权:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。</li>
|
||||
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。</li>
|
||||
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
||||
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
|
||||
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
|
||||
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
||||
</ol>
|
||||
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
||||
<p>
|
||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr class="my-12 border-gray-300">
|
||||
|
||||
<!-- English Version -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">Licensing</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Core Principle</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
||||
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.</li>
|
||||
<li><strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
||||
<strong>Commercial License</strong>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
||||
<p class="text-gray-700">
|
||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
||||
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit
|
||||
from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited
|
||||
to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. Open Source License: AGPLv3 - For Individuals and
|
||||
Organizations of 10 or Fewer</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
||||
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
||||
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at <a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a>.
|
||||
</li>
|
||||
<li><strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
||||
make it available over a network, or distribute the modified version, you must provide the <strong>complete
|
||||
corresponding source code</strong> under the AGPLv3 license to the recipients. Even if you qualify under
|
||||
the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will
|
||||
need to obtain a Commercial License (see below).</li>
|
||||
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. Commercial License - For Organizations with More Than 10
|
||||
Individuals, or Users Needing to Avoid AGPLv3 Obligations</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
||||
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
||||
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
||||
Studio.</li>
|
||||
<li><strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
||||
condition, if your intended use case <strong>cannot comply with the terms of the AGPLv3</strong>
|
||||
(particularly the obligations regarding <strong>source code disclosure</strong>), or if you require specific
|
||||
commercial terms <strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom
|
||||
from copyleft restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial
|
||||
License.</li>
|
||||
<li><strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>Your organization has more than 10 individuals who can access, use, or benefit from the software.</li>
|
||||
<li>(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
||||
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
||||
</li>
|
||||
<li>(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
||||
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
||||
of the service under AGPLv3.</li>
|
||||
<li>(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
||||
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
||||
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
||||
discuss commercial licensing options.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. Contributions</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
||||
considered to be offered under the <strong>AGPLv3</strong> license.</li>
|
||||
<li>By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
||||
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
||||
operate under AGPLv3 or a Commercial License).</li>
|
||||
<li>You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
||||
offered under our commercial license.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. Other Terms</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
||||
agreement signed by both parties.</li>
|
||||
<li>The project maintainers reserve the right to update this licensing policy (including the definition and
|
||||
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
||||
code repository, official website).</li>
|
||||
</ul>
|
||||
</section>
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">License Agreement</h2>
|
||||
<p class="mb-4">
|
||||
This software is licensed under the <strong>Apache License 2.0</strong>. In addition to the terms of the
|
||||
Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without
|
||||
modifying
|
||||
the code.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial License Required</strong>: A commercial license is required if any of the
|
||||
following
|
||||
conditions are met:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>
|
||||
You modify, develop, or alter the software, including but not limited to changes to the
|
||||
application
|
||||
name, logo, code, or functionality.
|
||||
</li>
|
||||
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
|
||||
<li>
|
||||
You pre-install or integrate the software into hardware devices or products and bundle it
|
||||
for sale.
|
||||
</li>
|
||||
<li>
|
||||
You are engaging in large-scale procurement for government or educational institutions,
|
||||
especially
|
||||
involving security, data privacy, or other sensitive requirements.
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source
|
||||
license as
|
||||
needed, making it stricter or more lenient.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes,
|
||||
including but
|
||||
not limited to cloud business operations.
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">III. Other Terms</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>The interpretation of these terms is subject to the discretion of Cherry Studio developers.</li>
|
||||
<li>These terms may be updated, and users will be notified through the software when changes occur.</li>
|
||||
</ol>
|
||||
<p class="mb-4">
|
||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
||||
</p>
|
||||
<p>
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
|
||||
License 2.0. Detailed information about the Apache License 2.0 can be found at
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Github Releases Timeline</title>
|
||||
@@ -8,201 +9,194 @@
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<body id="app">
|
||||
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
|
||||
<!-- Loading状态 -->
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div
|
||||
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error 状态 -->
|
||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||
|
||||
<!-- Release 列表 -->
|
||||
<div v-else class="space-y-8">
|
||||
<div
|
||||
v-for="release in releases"
|
||||
:key="release.id"
|
||||
class="relative pl-8"
|
||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||
<div
|
||||
class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ release.name || release.tag_name }}
|
||||
</h2>
|
||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
{{ formatDate(release.published_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="prose"
|
||||
:class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||
v-html="renderMarkdown(release.body)"></div>
|
||||
<!-- Loading状态 -->
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error 状态 -->
|
||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||
|
||||
<!-- Release 列表 -->
|
||||
<div v-else class="space-y-8">
|
||||
<div v-for="release in releases" :key="release.id" class="relative pl-8"
|
||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||
<div class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ release.name || release.tag_name }}
|
||||
</h2>
|
||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
{{ formatDate(release.published_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
|
||||
v-html="renderMarkdown(release.body)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const md = window.markdownit({
|
||||
breaks: true,
|
||||
linkify: true
|
||||
})
|
||||
const md = window.markdownit({
|
||||
breaks: true,
|
||||
linkify: true
|
||||
})
|
||||
|
||||
const { createApp } = Vue
|
||||
const { createApp } = Vue
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
isDark: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchReleases() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch releases')
|
||||
}
|
||||
this.releases = await response.json()
|
||||
} catch (err) {
|
||||
this.error = 'Error loading releases: ' + err.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
isDark: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchReleases() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch releases')
|
||||
}
|
||||
this.releases = await response.json()
|
||||
} catch (err) {
|
||||
this.error = 'Error loading releases: ' + err.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return ''
|
||||
return md.render(content)
|
||||
},
|
||||
initTheme() {
|
||||
// 从 URL 参数获取主题设置
|
||||
const url = new URL(window.location.href)
|
||||
const theme = url.searchParams.get('theme')
|
||||
this.isDark = theme === 'dark'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTheme()
|
||||
this.fetchReleases()
|
||||
}
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
},
|
||||
renderMarkdown(content) {
|
||||
if (!content) return ''
|
||||
return md.render(content)
|
||||
},
|
||||
initTheme() {
|
||||
// 从 URL 参数获取主题设置
|
||||
const url = new URL(window.location.href)
|
||||
const theme = url.searchParams.get('theme')
|
||||
this.isDark = theme === 'dark'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTheme()
|
||||
this.fetchReleases()
|
||||
}
|
||||
}).mount('#app')
|
||||
}).mount('#app')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 基础的 Markdown 样式 */
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* 基础的 Markdown 样式 */
|
||||
.prose {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.prose h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
.prose h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.1em;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
.prose h3 {
|
||||
font-size: 1.1em;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.prose ol {
|
||||
list-style-type: decimal;
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.prose code {
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.dark .prose code {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
.dark .prose code {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
.prose code {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.prose pre code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.prose a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dark .prose a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.dark .prose a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dark .prose blockquote {
|
||||
border-left-color: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.dark .prose blockquote {
|
||||
border-left-color: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .prose {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.dark .prose {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark-bg {
|
||||
background-color: #151515;
|
||||
}
|
||||
.dark-bg {
|
||||
background-color: #151515;
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
.bg {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
68
resources/graphrag.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="3d-graph"></div>
|
||||
<script src="./js/bridge.js"></script>
|
||||
<script type="module">
|
||||
import { getQueryParam } from './js/utils.js'
|
||||
|
||||
const apiUrl = getQueryParam('apiUrl')
|
||||
const modelId = getQueryParam('modelId')
|
||||
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
|
||||
|
||||
const infoCard = document.createElement('div')
|
||||
infoCard.style.position = 'fixed'
|
||||
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
|
||||
infoCard.style.padding = '8px'
|
||||
infoCard.style.borderRadius = '4px'
|
||||
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
|
||||
infoCard.style.fontSize = '12px'
|
||||
infoCard.style.maxWidth = '200px'
|
||||
infoCard.style.display = 'none'
|
||||
infoCard.style.zIndex = '1000'
|
||||
document.body.appendChild(infoCard)
|
||||
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
infoCard.style.left = `${event.clientX + 10}px`
|
||||
infoCard.style.top = `${event.clientY + 10}px`
|
||||
})
|
||||
|
||||
const elem = document.getElementById('3d-graph')
|
||||
const Graph = ForceGraph3D()(elem)
|
||||
.jsonUrl(jsonUrl)
|
||||
.nodeAutoColorBy((node) => node.properties.type || 'default')
|
||||
.nodeVal((node) => node.properties.degree)
|
||||
.linkWidth((link) => link.properties.weight)
|
||||
.onNodeHover((node) => {
|
||||
if (node) {
|
||||
infoCard.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
|
||||
${node.properties.title}
|
||||
</div>
|
||||
<div style="color: #666;">
|
||||
${node.properties.description}
|
||||
</div>`
|
||||
infoCard.style.display = 'block'
|
||||
} else {
|
||||
infoCard.style.display = 'none'
|
||||
}
|
||||
})
|
||||
.onNodeClick((node) => {
|
||||
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
|
||||
window.api.minApp({
|
||||
url,
|
||||
windowOptions: {
|
||||
title: node.properties.title,
|
||||
width: 500,
|
||||
height: 800
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
@@ -6,8 +6,8 @@ const AdmZip = require('adm-zip')
|
||||
const { downloadWithRedirects } = require('./download')
|
||||
|
||||
// Base URL for downloading bun binaries
|
||||
const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download'
|
||||
const DEFAULT_BUN_VERSION = '1.2.9' // Default fallback version
|
||||
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
|
||||
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
|
||||
|
||||
// Mapping of platform+arch to binary package name
|
||||
const BUN_PACKAGES = {
|
||||
@@ -15,8 +15,6 @@ const BUN_PACKAGES = {
|
||||
'darwin-x64': 'bun-darwin-x64.zip',
|
||||
'win32-x64': 'bun-windows-x64.zip',
|
||||
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
|
||||
'win32-arm64': 'bun-windows-x64.zip',
|
||||
'win32-arm64-baseline': 'bun-windows-x64-baseline.zip',
|
||||
'linux-x64': 'bun-linux-x64.zip',
|
||||
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
|
||||
'linux-arm64': 'bun-linux-aarch64.zip',
|
||||
|
||||
@@ -7,8 +7,8 @@ const AdmZip = require('adm-zip')
|
||||
const { downloadWithRedirects } = require('./download')
|
||||
|
||||
// Base URL for downloading uv binaries
|
||||
const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download'
|
||||
const DEFAULT_UV_VERSION = '0.6.14'
|
||||
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
|
||||
const DEFAULT_UV_VERSION = '0.6.6'
|
||||
|
||||
// Mapping of platform+arch to binary package name
|
||||
const UV_PACKAGES = {
|
||||
|
||||
117
resources/textMonitor.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TextSelectionObserver: NSObject {
|
||||
let workspace = NSWorkspace.shared
|
||||
var lastSelectedText: String?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// 注册通知观察者
|
||||
let observer = NSWorkspace.shared.notificationCenter
|
||||
observer.addObserver(
|
||||
self,
|
||||
selector: #selector(handleSelectionChange),
|
||||
name: NSWorkspace.didActivateApplicationNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// 监听选择变化通知
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
|
||||
let selfPointer = userData!.load(as: TextSelectionObserver.self)
|
||||
selfPointer.checkSelectedText()
|
||||
}, &axObserver)
|
||||
|
||||
if error == .success, let axObserver = axObserver {
|
||||
CFRunLoopAddSource(
|
||||
RunLoop.main.getCFRunLoop(),
|
||||
AXObserverGetRunLoopSource(axObserver),
|
||||
.defaultMode
|
||||
)
|
||||
|
||||
// 当前活动应用添加监听
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleSelectionChange(_ notification: Notification) {
|
||||
// 应用切换时更新监听
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
|
||||
if error == .success, let axObserver = axObserver {
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func updateActiveAppObserver(_ axObserver: AXObserver) {
|
||||
guard let app = workspace.frontmostApplication else { return }
|
||||
let pid = app.processIdentifier
|
||||
let element = AXUIElementCreateApplication(pid)
|
||||
|
||||
// 添加选择变化通知监听
|
||||
AXObserverAddNotification(
|
||||
axObserver,
|
||||
element,
|
||||
kAXSelectedTextChangedNotification as CFString,
|
||||
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
)
|
||||
}
|
||||
|
||||
func checkSelectedText() {
|
||||
if let text = getSelectedText() {
|
||||
if text.count > 0 && text != lastSelectedText {
|
||||
print(text)
|
||||
fflush(stdout)
|
||||
lastSelectedText = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSelectedText() -> String? {
|
||||
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
|
||||
let pid = app.processIdentifier
|
||||
|
||||
let axApp = AXUIElementCreateApplication(pid)
|
||||
var focusedElement: AnyObject?
|
||||
|
||||
// Get focused element
|
||||
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
|
||||
guard result == .success else { return nil }
|
||||
|
||||
// Try different approaches to get selected text
|
||||
var selectedText: AnyObject?
|
||||
|
||||
// First try: Direct selected text
|
||||
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
|
||||
// Second try: Selected text in text area
|
||||
if textResult != .success {
|
||||
var selectedTextRange: AnyObject?
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
|
||||
if textResult == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
// Third try: Get selected text from parent element
|
||||
if textResult != .success {
|
||||
var parent: AnyObject?
|
||||
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
guard textResult == .success, let text = selectedText as? String else { return nil }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
let observer = TextSelectionObserver()
|
||||
|
||||
signal(SIGINT) { _ in
|
||||
exit(0)
|
||||
}
|
||||
|
||||
RunLoop.main.run()
|
||||
@@ -1,8 +1,10 @@
|
||||
const { Arch } = require('electron-builder')
|
||||
const { default: removeLocales } = require('./remove-locales')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
await removeLocales(context)
|
||||
const platform = context.packager.platform.name
|
||||
const arch = context.arch
|
||||
|
||||
@@ -16,48 +18,28 @@ exports.default = async function (context) {
|
||||
'node_modules'
|
||||
)
|
||||
|
||||
keepPackageNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
|
||||
keepPackageNodeFiles(node_modules_path, '@libsql', _arch)
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
|
||||
}
|
||||
|
||||
if (platform === 'windows') {
|
||||
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
|
||||
if (arch === Arch.arm64) {
|
||||
keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-arm64-msvc'])
|
||||
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-arm64-msvc'])
|
||||
}
|
||||
if (arch === Arch.x64) {
|
||||
keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-x64-msvc'])
|
||||
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||
}
|
||||
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定架构的 node_modules 文件
|
||||
* @param {*} nodeModulesPath
|
||||
* @param {*} packageName
|
||||
* @param {*} arch
|
||||
* @returns
|
||||
*/
|
||||
function keepPackageNodeFiles(nodeModulesPath, packageName, arch) {
|
||||
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
|
||||
const modulePath = path.join(nodeModulesPath, packageName)
|
||||
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
console.log(`[After Pack] Directory does not exist: ${modulePath}`)
|
||||
return
|
||||
}
|
||||
|
||||
const dirs = fs.readdirSync(modulePath)
|
||||
dirs
|
||||
.filter((dir) => !arch.includes(dir))
|
||||
.forEach((dir) => {
|
||||
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
|
||||
console.log(`[After Pack] Removed dir: ${dir}`, arch)
|
||||
console.log(`Removed dir: ${dir}`, arch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
const fs = require('fs')
|
||||
|
||||
exports.default = function (buildResult) {
|
||||
try {
|
||||
console.log('[artifact build completed] rename artifact file...')
|
||||
if (!buildResult.file.includes(' ')) {
|
||||
return
|
||||
}
|
||||
|
||||
let oldFilePath = buildResult.file
|
||||
const newfilePath = oldFilePath.replace(/ /g, '-')
|
||||
fs.renameSync(oldFilePath, newfilePath)
|
||||
buildResult.file = newfilePath
|
||||
console.log(`[artifact build completed] rename file ${oldFilePath} to ${newfilePath} `)
|
||||
} catch (error) {
|
||||
console.error('Error renaming file:', error)
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,6 @@ async function downloadNpm(platform) {
|
||||
'@libsql/win32-x64-msvc',
|
||||
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
|
||||
)
|
||||
downloadNpmPackage(
|
||||
'@strongtz/win32-arm64-msvc',
|
||||
'https://registry.npmjs.org/@strongtz/win32-arm64-msvc/-/win32-arm64-msvc-0.4.7.tgz'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true })
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
var baseLocale = 'en-us'
|
||||
var baseLocale = 'zh-CN'
|
||||
var baseFileName = ''.concat(baseLocale, '.json')
|
||||
var baseFilePath = path.join(translationsDir, baseFileName)
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ exports.default = async function notarizing(context) {
|
||||
}
|
||||
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
|
||||
console.log('Skipping notarization')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
58
scripts/remove-locales.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
const platform = context.packager.platform.name
|
||||
|
||||
// 根据平台确定 locales 目录位置
|
||||
let resourceDirs = []
|
||||
if (platform === 'mac') {
|
||||
// macOS 的语言文件位置
|
||||
resourceDirs = [
|
||||
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
|
||||
path.join(
|
||||
context.appOutDir,
|
||||
'Cherry Studio.app',
|
||||
'Contents',
|
||||
'Frameworks',
|
||||
'Electron Framework.framework',
|
||||
'Resources'
|
||||
)
|
||||
]
|
||||
} else {
|
||||
// Windows 和 Linux 的语言文件位置
|
||||
resourceDirs = [path.join(context.appOutDir, 'locales')]
|
||||
}
|
||||
|
||||
// 处理每个资源目录
|
||||
for (const resourceDir of resourceDirs) {
|
||||
if (!fs.existsSync(resourceDir)) {
|
||||
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取所有文件和目录
|
||||
const items = fs.readdirSync(resourceDir)
|
||||
|
||||
// 遍历并删除不需要的语言文件
|
||||
for (const item of items) {
|
||||
if (platform === 'mac') {
|
||||
// 在 macOS 上检查 .lproj 目录
|
||||
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
|
||||
const dirPath = path.join(resourceDir, item)
|
||||
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
|
||||
}
|
||||
} else {
|
||||
// 其他平台处理 .pak 文件
|
||||
if (!item.match(/^(en|zh|ru)/)) {
|
||||
const filePath = path.join(resourceDir, item)
|
||||
fs.unlinkSync(filePath)
|
||||
console.log(`Removed locale file: ${item} from ${resourceDir}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Locale cleanup completed!')
|
||||
}
|
||||
58
scripts/replace-spaces.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// replaceSpaces.js
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const directory = 'dist'
|
||||
|
||||
// 处理文件名中的空格
|
||||
function replaceFileNames() {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) throw err
|
||||
|
||||
files.forEach((file) => {
|
||||
const oldPath = path.join(directory, file)
|
||||
const newPath = path.join(directory, file.replace(/ /g, '-'))
|
||||
|
||||
fs.stat(oldPath, (err, stats) => {
|
||||
if (err) throw err
|
||||
|
||||
if (stats.isFile() && oldPath !== newPath) {
|
||||
fs.rename(oldPath, newPath, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`Renamed: ${oldPath} -> ${newPath}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function replaceYmlContent() {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) throw err
|
||||
|
||||
files.forEach((file) => {
|
||||
if (path.extname(file).toLowerCase() === '.yml') {
|
||||
const filePath = path.join(directory, file)
|
||||
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) throw err
|
||||
|
||||
// 替换内容
|
||||
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
|
||||
|
||||
// 写回文件
|
||||
fs.writeFile(filePath, newContent, 'utf8', (err) => {
|
||||
if (err) throw err
|
||||
console.log(`Updated content in: ${filePath}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 执行两个操作
|
||||
replaceFileNames()
|
||||
replaceYmlContent()
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Paratera_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
|
||||
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
|
||||
*/
|
||||
|
||||
// OCOOL API KEY
|
||||
const Paratera_API_KEY = process.env.Paratera_API_KEY
|
||||
const OCOOL_API_KEY = process.env.OCOOL_API_KEY
|
||||
|
||||
const INDEX = [
|
||||
// 语言的名称 代码 用来翻译的模型
|
||||
{ name: 'France', code: 'fr-fr', model: 'Qwen3-235B-A22B' },
|
||||
{ name: 'Spanish', code: 'es-es', model: 'Qwen3-235B-A22B' },
|
||||
{ name: 'Portuguese', code: 'pt-pt', model: 'Qwen3-235B-A22B' },
|
||||
{ name: 'Greek', code: 'el-gr', model: 'Qwen3-235B-A22B' }
|
||||
{ name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
|
||||
{ name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
|
||||
{ name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
|
||||
{ name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
|
||||
]
|
||||
|
||||
const fs = require('fs')
|
||||
@@ -19,8 +19,8 @@ import OpenAI from 'openai'
|
||||
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: Paratera_API_KEY,
|
||||
baseURL: 'https://llmapi.paratera.com/v1'
|
||||
apiKey: OCOOL_API_KEY,
|
||||
baseURL: 'https://one.ocoolai.com/v1'
|
||||
})
|
||||
|
||||
// 递归遍历翻译
|
||||
|
||||
@@ -22,8 +22,7 @@ function downloadNpmPackage(packageName, url) {
|
||||
console.log(`Extracting ${filename}...`)
|
||||
execSync(`tar -xvf ${filename}`)
|
||||
execSync(`rm -rf ${filename}`)
|
||||
execSync(`mkdir -p ${targetDir}`)
|
||||
execSync(`mv package/* ${targetDir}/`)
|
||||
execSync(`mv package ${targetDir}`)
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${packageName}: ${error.message}`)
|
||||
if (fs.existsSync(filename)) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
exports.default = async function (configuration) {
|
||||
if (process.env.WIN_SIGN) {
|
||||
const { path } = configuration
|
||||
if (configuration.path) {
|
||||
try {
|
||||
console.log('Start code signing...')
|
||||
console.log('Signing file:', path)
|
||||
const signCommand = `signtool sign /tr http://timestamp.comodoca.com /td sha256 /fd sha256 /a /v "${path}"`
|
||||
execSync(signCommand, { stdio: 'inherit' })
|
||||
console.log('Code signing completed')
|
||||
} catch (error) {
|
||||
console.error('Code signing failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ export const DATA_PATH = getDataPath()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 40,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#fff'
|
||||
color: 'rgba(0,0,0,0)',
|
||||
symbolColor: '#ffffff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 40,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#000'
|
||||
symbolColor: '#000000'
|
||||
}
|
||||
|
||||
@@ -2,4 +2,3 @@ export const isMac = process.platform === 'darwin'
|
||||
export const isWin = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
||||
|
||||
@@ -23,14 +23,14 @@ export default class EmbeddingsFactory {
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
// dimensions,
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
// dimensions,
|
||||
dimensions,
|
||||
batchSize,
|
||||
configuration: { baseURL }
|
||||
})
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
import '@main/config'
|
||||
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||
import { app } from 'electron'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { isDev } from './constant'
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
registerProtocolClient,
|
||||
setupAppImageDeepLink
|
||||
} from './services/ProtocolClient'
|
||||
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { setUserDataDir } from './utils/file'
|
||||
|
||||
Logger.initialize()
|
||||
|
||||
// in production mode, handle uncaught exception and unhandled rejection globally
|
||||
if (!isDev) {
|
||||
// handle uncaught exception
|
||||
process.on('uncaughtException', (error) => {
|
||||
Logger.error('Uncaught Exception:', error)
|
||||
})
|
||||
|
||||
// handle unhandled rejection
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
Logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
}
|
||||
|
||||
// Check for single instance lock
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit()
|
||||
process.exit(0)
|
||||
} else {
|
||||
// Portable dir must be setup before app ready
|
||||
setUserDataDir()
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
@@ -76,14 +47,22 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
replaceDevtoolsFont(mainWindow)
|
||||
|
||||
// Setup deep link for AppImage on Linux
|
||||
await setupAppImageDeepLink()
|
||||
|
||||
if (isDev) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
ipcMain.handle('system:getDeviceType', () => {
|
||||
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
|
||||
})
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
|
||||
// macOS specific: handle protocol when app is already running
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
@@ -112,15 +91,6 @@ if (!app.requestSingleInstanceLock()) {
|
||||
app.isQuitting = true
|
||||
})
|
||||
|
||||
app.on('will-quit', async () => {
|
||||
// event.preventDefault()
|
||||
try {
|
||||
await mcpService.cleanup()
|
||||
} catch (error) {
|
||||
Logger.error('Error cleaning up MCP service:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
interface CreateOAuthUrlArgs {
|
||||
app: string;
|
||||
}
|
||||
declare function createOAuthUrl({ app }: CreateOAuthUrlArgs): Promise<string>;
|
||||
declare function _dont_use_in_prod_createOAuthUrl({ app, }: CreateOAuthUrlArgs): Promise<string>;
|
||||
|
||||
interface DecryptSecretArgs {
|
||||
app: string;
|
||||
s: string;
|
||||
}
|
||||
declare function decryptSecret({ app, s }: DecryptSecretArgs): Promise<string>;
|
||||
declare function _dont_use_in_prod_decryptSecret({ app, s, }: DecryptSecretArgs): Promise<string>;
|
||||
|
||||
export { type CreateOAuthUrlArgs, type DecryptSecretArgs, _dont_use_in_prod_createOAuthUrl, _dont_use_in_prod_decryptSecret, createOAuthUrl, decryptSecret };
|
||||
8
src/main/integration/nutstore/sso/lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare function decrypt(app: string, s: string): string;
|
||||
|
||||
interface Secret {
|
||||
app: string;
|
||||
}
|
||||
declare function createOAuthUrl(secret: Secret): string;
|
||||
|
||||
export { type Secret, createOAuthUrl, decrypt };
|
||||
9
src/main/integration/nutstore/sso/lib/index.js
Normal file
322
src/main/ipc.ts
@@ -1,14 +1,10 @@
|
||||
import fs from 'node:fs'
|
||||
import { arch } from 'node:os'
|
||||
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
@@ -21,19 +17,15 @@ import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import mcpService from './services/MCPService'
|
||||
import NotificationService from './services/NotificationService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { searchService } from './services/SearchService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { setOpenLinkExternal } from './services/WebviewService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { calculateDirectorySize, getResourcePath } from './utils'
|
||||
import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
|
||||
import { getFilesDir } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
@@ -43,22 +35,18 @@ const obsidianVaultService = new ObsidianVaultService()
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater(mainWindow)
|
||||
const notificationService = new NotificationService(mainWindow)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Info, () => ({
|
||||
ipcMain.handle('app:info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged,
|
||||
appPath: app.getAppPath(),
|
||||
filesPath: getFilesDir(),
|
||||
configPath: getConfigDir(),
|
||||
appDataPath: app.getPath('userData'),
|
||||
resourcesPath: getResourcePath(),
|
||||
logsPath: log.transports.file.getFile().path,
|
||||
arch: arch(),
|
||||
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
||||
logsPath: log.transports.file.getFile().path
|
||||
}))
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
|
||||
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
||||
let proxyConfig: ProxyConfig
|
||||
|
||||
if (proxy === 'system') {
|
||||
@@ -72,19 +60,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
await proxyManager.configureProxy(proxyConfig)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Reload, () => mainWindow.reload())
|
||||
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||
|
||||
// Update
|
||||
ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
|
||||
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
|
||||
|
||||
// language
|
||||
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
||||
ipcMain.handle('app:set-language', (_, language) => {
|
||||
configManager.setLanguage(language)
|
||||
})
|
||||
|
||||
// launch on boot
|
||||
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => {
|
||||
ipcMain.handle('app:set-launch-on-boot', (_, openAtLogin: boolean) => {
|
||||
// Set login item settings for windows and mac
|
||||
// linux is not supported because it requires more file operations
|
||||
if (isWin || isMac) {
|
||||
@@ -93,76 +81,52 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// launch to tray
|
||||
ipcMain.handle(IpcChannel.App_SetLaunchToTray, (_, isActive: boolean) => {
|
||||
ipcMain.handle('app:set-launch-to-tray', (_, isActive: boolean) => {
|
||||
configManager.setLaunchToTray(isActive)
|
||||
})
|
||||
|
||||
// tray
|
||||
ipcMain.handle(IpcChannel.App_SetTray, (_, isActive: boolean) => {
|
||||
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
// to tray on close
|
||||
ipcMain.handle(IpcChannel.App_SetTrayOnClose, (_, isActive: boolean) => {
|
||||
ipcMain.handle('app:set-tray-on-close', (_, isActive: boolean) => {
|
||||
configManager.setTrayOnClose(isActive)
|
||||
})
|
||||
|
||||
// auto update
|
||||
ipcMain.handle(IpcChannel.App_SetAutoUpdate, (_, isActive: boolean) => {
|
||||
appUpdater.setAutoUpdate(isActive)
|
||||
configManager.setAutoUpdate(isActive)
|
||||
})
|
||||
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
|
||||
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||
configManager.set(key, value)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
|
||||
ipcMain.handle('config:get', (_, key: string) => {
|
||||
return configManager.get(key)
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
|
||||
const updateTitleBarOverlay = () => {
|
||||
if (!mainWindow?.setTitleBarOverlay) return
|
||||
const isDark = nativeTheme.shouldUseDarkColors
|
||||
mainWindow.setTitleBarOverlay(isDark ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
}
|
||||
ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => {
|
||||
if (theme === configManager.getTheme()) return
|
||||
|
||||
const broadcastThemeChange = () => {
|
||||
const isDark = nativeTheme.shouldUseDarkColors
|
||||
const effectiveTheme = isDark ? ThemeMode.dark : ThemeMode.light
|
||||
BrowserWindow.getAllWindows().forEach((win) => win.webContents.send(IpcChannel.ThemeChange, effectiveTheme))
|
||||
}
|
||||
|
||||
const notifyThemeChange = () => {
|
||||
updateTitleBarOverlay()
|
||||
broadcastThemeChange()
|
||||
}
|
||||
|
||||
if (theme === ThemeMode.auto) {
|
||||
nativeTheme.themeSource = 'system'
|
||||
nativeTheme.on('updated', notifyThemeChange)
|
||||
} else {
|
||||
nativeTheme.themeSource = theme
|
||||
nativeTheme.off('updated', notifyThemeChange)
|
||||
}
|
||||
|
||||
updateTitleBarOverlay()
|
||||
configManager.setTheme(theme)
|
||||
notifyThemeChange()
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.App_HandleZoomFactor, (_, delta: number, reset: boolean = false) => {
|
||||
// should sync theme change to all windows
|
||||
const senderWindowId = event.sender.id
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
handleZoomFactor(windows, delta, reset)
|
||||
return configManager.getZoomFactor()
|
||||
// 向其他窗口广播主题变化
|
||||
windows.forEach((win) => {
|
||||
if (win.webContents.id !== senderWindowId) {
|
||||
win.webContents.send('theme:change', theme)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
// clear cache
|
||||
ipcMain.handle(IpcChannel.App_ClearCache, async () => {
|
||||
ipcMain.handle('app:clear-cache', async () => {
|
||||
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||
|
||||
try {
|
||||
@@ -183,90 +147,72 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
}
|
||||
})
|
||||
|
||||
// get cache size
|
||||
ipcMain.handle(IpcChannel.App_GetCacheSize, async () => {
|
||||
const cachePath = getCacheDir()
|
||||
log.info(`Calculating cache size for path: ${cachePath}`)
|
||||
|
||||
try {
|
||||
const sizeInBytes = await calculateDirectorySize(cachePath)
|
||||
const sizeInMB = (sizeInBytes / (1024 * 1024)).toFixed(2)
|
||||
return `${sizeInMB}`
|
||||
} catch (error: any) {
|
||||
log.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
|
||||
return '0'
|
||||
// check for update
|
||||
ipcMain.handle('app:check-for-update', async () => {
|
||||
const update = await appUpdater.autoUpdater.checkForUpdates()
|
||||
return {
|
||||
currentVersion: appUpdater.autoUpdater.currentVersion,
|
||||
updateInfo: update?.updateInfo
|
||||
}
|
||||
})
|
||||
|
||||
// check for update
|
||||
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
|
||||
await appUpdater.checkForUpdates()
|
||||
})
|
||||
|
||||
// notification
|
||||
ipcMain.handle(IpcChannel.Notification_Send, async (_, notification: Notification) => {
|
||||
await notificationService.sendNotification(notification)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.Notification_OnClick, (_, notification: Notification) => {
|
||||
mainWindow.webContents.send('notification-click', notification)
|
||||
})
|
||||
|
||||
// zip
|
||||
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
|
||||
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
|
||||
|
||||
// system
|
||||
ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux'))
|
||||
ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname())
|
||||
ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
win && win.webContents.toggleDevTools()
|
||||
})
|
||||
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||
|
||||
// backup
|
||||
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
|
||||
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav)
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav)
|
||||
ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles)
|
||||
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
|
||||
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile)
|
||||
ipcMain.handle('backup:backup', backupManager.backup)
|
||||
ipcMain.handle('backup:restore', backupManager.restore)
|
||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
|
||||
ipcMain.handle('backup:checkConnection', backupManager.checkConnection)
|
||||
ipcMain.handle('backup:createDirectory', backupManager.createDirectory)
|
||||
|
||||
// file
|
||||
ipcMain.handle(IpcChannel.File_Open, fileManager.open)
|
||||
ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath)
|
||||
ipcMain.handle(IpcChannel.File_Save, fileManager.save)
|
||||
ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile)
|
||||
ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile)
|
||||
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear)
|
||||
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile)
|
||||
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile)
|
||||
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile)
|
||||
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
|
||||
ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile)
|
||||
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
|
||||
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
|
||||
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
|
||||
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
|
||||
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
|
||||
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
|
||||
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
|
||||
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage)
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
ipcMain.handle('file:openPath', fileManager.openPath)
|
||||
ipcMain.handle('file:save', fileManager.save)
|
||||
ipcMain.handle('file:select', fileManager.selectFile)
|
||||
ipcMain.handle('file:upload', fileManager.uploadFile)
|
||||
ipcMain.handle('file:clear', fileManager.clear)
|
||||
ipcMain.handle('file:read', fileManager.readFile)
|
||||
ipcMain.handle('file:delete', fileManager.deleteFile)
|
||||
ipcMain.handle('file:get', fileManager.getFile)
|
||||
ipcMain.handle('file:selectFolder', fileManager.selectFolder)
|
||||
ipcMain.handle('file:create', fileManager.createTempFile)
|
||||
ipcMain.handle('file:write', fileManager.writeFile)
|
||||
ipcMain.handle('file:saveImage', fileManager.saveImage)
|
||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||
ipcMain.handle('file:download', fileManager.downloadFile)
|
||||
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
|
||||
|
||||
// fs
|
||||
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
|
||||
ipcMain.handle('fs:read', FileService.readFile)
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
windowService.createMinappWindow({
|
||||
url: args.url,
|
||||
parent: mainWindow,
|
||||
windowOptions: {
|
||||
...mainWindow.getBounds(),
|
||||
...args.windowOptions
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// export
|
||||
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord)
|
||||
ipcMain.handle('export:word', exportService.exportToWord)
|
||||
|
||||
// open path
|
||||
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
|
||||
ipcMain.handle('open:path', async (_, path: string) => {
|
||||
await shell.openPath(path)
|
||||
})
|
||||
|
||||
// shortcuts
|
||||
ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
|
||||
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
|
||||
configManager.setShortcuts(shortcuts)
|
||||
// Refresh shortcuts registration
|
||||
if (mainWindow) {
|
||||
@@ -276,20 +222,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// knowledge base
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank)
|
||||
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
|
||||
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
|
||||
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
|
||||
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
|
||||
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||
ipcMain.handle('knowledge-base:rerank', KnowledgeService.rerank)
|
||||
|
||||
// window
|
||||
ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {
|
||||
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
|
||||
mainWindow?.setMinimumSize(width, height)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Windows_ResetMinimumSize, () => {
|
||||
ipcMain.handle('window:reset-minimum-size', () => {
|
||||
mainWindow?.setMinimumSize(1080, 600)
|
||||
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
|
||||
if (width < 1080) {
|
||||
@@ -298,85 +244,59 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// gemini
|
||||
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
|
||||
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
|
||||
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
|
||||
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
|
||||
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
|
||||
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
|
||||
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
|
||||
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
|
||||
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
|
||||
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
|
||||
|
||||
// mini window
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Close, () => windowService.closeMiniWindow())
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Toggle, () => windowService.toggleMiniWindow())
|
||||
ipcMain.handle(IpcChannel.MiniWindow_SetPin, (_, isPinned) => windowService.setPinMiniWindow(isPinned))
|
||||
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
|
||||
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||
ipcMain.handle('miniwindow:set-pin', (_, isPinned) => windowService.setPinMiniWindow(isPinned))
|
||||
|
||||
// aes
|
||||
ipcMain.handle(IpcChannel.Aes_Encrypt, (_, text: string, secretKey: string, iv: string) =>
|
||||
encrypt(text, secretKey, iv)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.Aes_Decrypt, (_, encryptedData: string, iv: string, secretKey: string) =>
|
||||
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
||||
ipcMain.handle('aes:decrypt', (_, encryptedData: string, iv: string, secretKey: string) =>
|
||||
decrypt(encryptedData, iv, secretKey)
|
||||
)
|
||||
|
||||
// Register MCP handlers
|
||||
ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer)
|
||||
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_CallTool, mcpService.callTool)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
||||
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
|
||||
ipcMain.handle('mcp:remove-server', mcpService.removeServer)
|
||||
ipcMain.handle('mcp:restart-server', mcpService.restartServer)
|
||||
ipcMain.handle('mcp:stop-server', mcpService.stopServer)
|
||||
ipcMain.handle('mcp:list-tools', mcpService.listTools)
|
||||
ipcMain.handle('mcp:call-tool', mcpService.callTool)
|
||||
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
|
||||
ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js'))
|
||||
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
|
||||
|
||||
//copilot
|
||||
ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser)
|
||||
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
|
||||
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
|
||||
ipcMain.handle('copilot:save-copilot-token', CopilotService.saveCopilotToken)
|
||||
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||
|
||||
// Obsidian service
|
||||
ipcMain.handle(IpcChannel.Obsidian_GetVaults, () => {
|
||||
ipcMain.handle('obsidian:get-vaults', () => {
|
||||
return obsidianVaultService.getVaults()
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Obsidian_GetFiles, (_event, vaultName) => {
|
||||
ipcMain.handle('obsidian:get-files', (_event, vaultName) => {
|
||||
return obsidianVaultService.getFilesByVaultName(vaultName)
|
||||
})
|
||||
|
||||
// nutstore
|
||||
ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl)
|
||||
ipcMain.handle(IpcChannel.Nutstore_DecryptToken, (_, token: string) => NutstoreService.decryptToken(token))
|
||||
ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) =>
|
||||
ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl)
|
||||
ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token))
|
||||
ipcMain.handle('nutstore:get-directory-contents', (_, token: string, path: string) =>
|
||||
NutstoreService.getDirectoryContents(token, path)
|
||||
)
|
||||
|
||||
// search window
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
|
||||
await searchService.openSearchWindow(uid)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
|
||||
await searchService.closeSearchWindow(uid)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
|
||||
return await searchService.openUrlInSearchWindow(uid, url)
|
||||
})
|
||||
|
||||
// webview
|
||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||
setOpenLinkExternal(webviewId, isExternal)
|
||||
)
|
||||
|
||||
// store sync
|
||||
storeSyncService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
// Brave Search MCP Server
|
||||
// port https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search/index.ts
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
|
||||
const WEB_SEARCH_TOOL: Tool = {
|
||||
name: 'brave_web_search',
|
||||
description:
|
||||
'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. ' +
|
||||
'Use this for broad information gathering, recent events, or when you need diverse web sources. ' +
|
||||
'Supports pagination, content filtering, and freshness controls. ' +
|
||||
'Maximum 20 results per request, with offset for pagination. ',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (max 400 chars, 50 words)'
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of results (1-20, default 10)',
|
||||
default: 10
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Pagination offset (max 9, default 0)',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
|
||||
const LOCAL_SEARCH_TOOL: Tool = {
|
||||
name: 'brave_local_search',
|
||||
description:
|
||||
"Searches for local businesses and places using Brave's Local Search API. " +
|
||||
'Best for queries related to physical locations, businesses, restaurants, services, etc. ' +
|
||||
'Returns detailed information including:\n' +
|
||||
'- Business names and addresses\n' +
|
||||
'- Ratings and review counts\n' +
|
||||
'- Phone numbers and opening hours\n' +
|
||||
"Use this when the query implies 'near me' or mentions specific locations. " +
|
||||
'Automatically falls back to web search if no local results are found.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: "Local search query (e.g. 'pizza near Central Park')"
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of results (1-20, default 5)',
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
|
||||
const RATE_LIMIT = {
|
||||
perSecond: 1,
|
||||
perMonth: 15000
|
||||
}
|
||||
|
||||
const requestCount = {
|
||||
second: 0,
|
||||
month: 0,
|
||||
lastReset: Date.now()
|
||||
}
|
||||
|
||||
function checkRateLimit() {
|
||||
const now = Date.now()
|
||||
if (now - requestCount.lastReset > 1000) {
|
||||
requestCount.second = 0
|
||||
requestCount.lastReset = now
|
||||
}
|
||||
if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) {
|
||||
throw new Error('Rate limit exceeded')
|
||||
}
|
||||
requestCount.second++
|
||||
requestCount.month++
|
||||
}
|
||||
|
||||
interface BraveWeb {
|
||||
web?: {
|
||||
results?: Array<{
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
language?: string
|
||||
published?: string
|
||||
rank?: number
|
||||
}>
|
||||
}
|
||||
locations?: {
|
||||
results?: Array<{
|
||||
id: string // Required by API
|
||||
title?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
interface BraveLocation {
|
||||
id: string
|
||||
name: string
|
||||
address: {
|
||||
streetAddress?: string
|
||||
addressLocality?: string
|
||||
addressRegion?: string
|
||||
postalCode?: string
|
||||
}
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
phone?: string
|
||||
rating?: {
|
||||
ratingValue?: number
|
||||
ratingCount?: number
|
||||
}
|
||||
openingHours?: string[]
|
||||
priceRange?: string
|
||||
}
|
||||
|
||||
interface BravePoiResponse {
|
||||
results: BraveLocation[]
|
||||
}
|
||||
|
||||
interface BraveDescription {
|
||||
descriptions: { [id: string]: string }
|
||||
}
|
||||
|
||||
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
'query' in args &&
|
||||
typeof (args as { query: string }).query === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
'query' in args &&
|
||||
typeof (args as { query: string }).query === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
async function performWebSearch(apiKey: string, query: string, count: number = 10, offset: number = 0) {
|
||||
checkRateLimit()
|
||||
const url = new URL('https://api.search.brave.com/res/v1/web/search')
|
||||
url.searchParams.set('q', query)
|
||||
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
|
||||
url.searchParams.set('offset', offset.toString())
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': apiKey
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BraveWeb
|
||||
|
||||
// Extract just web results
|
||||
const results = (data.web?.results || []).map((result) => ({
|
||||
title: result.title || '',
|
||||
description: result.description || '',
|
||||
url: result.url || ''
|
||||
}))
|
||||
|
||||
return results.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n')
|
||||
}
|
||||
|
||||
async function performLocalSearch(apiKey: string, query: string, count: number = 5) {
|
||||
checkRateLimit()
|
||||
// Initial search to get location IDs
|
||||
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search')
|
||||
webUrl.searchParams.set('q', query)
|
||||
webUrl.searchParams.set('search_lang', 'en')
|
||||
webUrl.searchParams.set('result_filter', 'locations')
|
||||
webUrl.searchParams.set('count', Math.min(count, 20).toString())
|
||||
|
||||
const webResponse = await fetch(webUrl, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': apiKey
|
||||
}
|
||||
})
|
||||
|
||||
if (!webResponse.ok) {
|
||||
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
|
||||
}
|
||||
|
||||
const webData = (await webResponse.json()) as BraveWeb
|
||||
const locationIds =
|
||||
webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map((r) => r.id) || []
|
||||
|
||||
if (locationIds.length === 0) {
|
||||
return performWebSearch(apiKey, query, count) // Fallback to web search
|
||||
}
|
||||
|
||||
// Get POI details and descriptions in parallel
|
||||
const [poisData, descriptionsData] = await Promise.all([
|
||||
getPoisData(apiKey, locationIds),
|
||||
getDescriptionsData(apiKey, locationIds)
|
||||
])
|
||||
|
||||
return formatLocalResults(poisData, descriptionsData)
|
||||
}
|
||||
|
||||
async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiResponse> {
|
||||
checkRateLimit()
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
|
||||
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': apiKey
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||
}
|
||||
|
||||
return (await response.json()) as BravePoiResponse
|
||||
}
|
||||
|
||||
async function getDescriptionsData(apiKey: string, ids: string[]): Promise<BraveDescription> {
|
||||
checkRateLimit()
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
|
||||
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': apiKey
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
|
||||
}
|
||||
|
||||
return (await response.json()) as BraveDescription
|
||||
}
|
||||
|
||||
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
|
||||
return (
|
||||
(poisData.results || [])
|
||||
.map((poi) => {
|
||||
const address =
|
||||
[
|
||||
poi.address?.streetAddress ?? '',
|
||||
poi.address?.addressLocality ?? '',
|
||||
poi.address?.addressRegion ?? '',
|
||||
poi.address?.postalCode ?? ''
|
||||
]
|
||||
.filter((part) => part !== '')
|
||||
.join(', ') || 'N/A'
|
||||
|
||||
return `Name: ${poi.name}
|
||||
Address: ${address}
|
||||
Phone: ${poi.phone || 'N/A'}
|
||||
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
|
||||
Price Range: ${poi.priceRange || 'N/A'}
|
||||
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
|
||||
Description: ${descData.descriptions[poi.id] || 'No description available'}
|
||||
`
|
||||
})
|
||||
.join('\n---\n') || 'No local results found'
|
||||
)
|
||||
}
|
||||
|
||||
class BraveSearchServer {
|
||||
public server: Server
|
||||
private apiKey: string
|
||||
|
||||
constructor(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error('BRAVE_API_KEY is required for Brave Search MCP server')
|
||||
}
|
||||
this.apiKey = apiKey
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'brave-search-server',
|
||||
version: '0.1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Tool handlers
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL]
|
||||
}))
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
if (!args) {
|
||||
throw new Error('No arguments provided')
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case 'brave_web_search': {
|
||||
if (!isBraveWebSearchArgs(args)) {
|
||||
throw new Error('Invalid arguments for brave_web_search')
|
||||
}
|
||||
const { query, count = 10 } = args
|
||||
const results = await performWebSearch(this.apiKey, query, count)
|
||||
return {
|
||||
content: [{ type: 'text', text: results }],
|
||||
isError: false
|
||||
}
|
||||
}
|
||||
|
||||
case 'brave_local_search': {
|
||||
if (!isBraveLocalSearchArgs(args)) {
|
||||
throw new Error('Invalid arguments for brave_local_search')
|
||||
}
|
||||
const { query, count = 5 } = args
|
||||
const results = await performLocalSearch(this.apiKey, query, count)
|
||||
return {
|
||||
content: [{ type: 'text', text: results }],
|
||||
isError: false
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default BraveSearchServer
|
||||
@@ -1,260 +0,0 @@
|
||||
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { z } from 'zod'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
interface DifyKnowledgeServerConfig {
|
||||
difyKey: string
|
||||
apiHost: string
|
||||
}
|
||||
|
||||
interface DifyListKnowledgeResponse {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface DifySearchKnowledgeResponse {
|
||||
query: {
|
||||
content: string
|
||||
}
|
||||
records: Array<{
|
||||
segment: {
|
||||
id: string
|
||||
position: number
|
||||
document_id: string
|
||||
content: string
|
||||
keywords: string[]
|
||||
document?: {
|
||||
id: string
|
||||
data_source_type: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
|
||||
const SearchKnowledgeArgsSchema = z.object({
|
||||
id: z.string().describe('Knowledge ID'),
|
||||
query: z.string().describe('Query string'),
|
||||
topK: z.number().optional().describe('Number of top results to return')
|
||||
})
|
||||
|
||||
type McpResponse = {
|
||||
content: Array<{ type: 'text'; text: string }>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
class DifyKnowledgeServer {
|
||||
public server: Server
|
||||
private config: DifyKnowledgeServerConfig
|
||||
|
||||
constructor(difyKey: string, args: string[]) {
|
||||
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}`)
|
||||
}
|
||||
return await this.performSearchKnowledge(
|
||||
parsed.data.id,
|
||||
parsed.data.query,
|
||||
parsed.data.topK || 6,
|
||||
this.config.difyKey,
|
||||
this.config.apiHost
|
||||
)
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const apiResponse = await response.json()
|
||||
|
||||
const knowledges: DifyListKnowledgeResponse[] =
|
||||
apiResponse?.data?.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || ''
|
||||
})) || []
|
||||
|
||||
const listText =
|
||||
knowledges.length > 0
|
||||
? knowledges.map((k) => `- **${k.name}** (ID: ${k.id})\n ${k.description || 'No Description'}`).join('\n')
|
||||
: '- No knowledges found.'
|
||||
|
||||
const formattedText = `### 可用知识库:\n\n${listText}`
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库列表时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
// 返回包含错误信息的 MCP 响应
|
||||
return {
|
||||
content: [{ type: 'text', text: `Accessing Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async performSearchKnowledge(
|
||||
id: string,
|
||||
query: string,
|
||||
topK: number,
|
||||
difyKey: string,
|
||||
apiHost: string
|
||||
): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
retrieval_model: {
|
||||
top_k: topK,
|
||||
// will be error if not set
|
||||
reranking_enable: null,
|
||||
score_threshold_enabled: null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const searchResponse: DifySearchKnowledgeResponse = await response.json()
|
||||
|
||||
if (!searchResponse || !Array.isArray(searchResponse.records)) {
|
||||
throw new Error(`从 Dify API 收到的响应格式无效: ${JSON.stringify(searchResponse)}`)
|
||||
}
|
||||
|
||||
const header = `### Query: ${query}\n\n`
|
||||
let body: string
|
||||
|
||||
if (searchResponse.records.length === 0) {
|
||||
body = 'No results found.'
|
||||
} else {
|
||||
const resultsText = searchResponse.records
|
||||
.map((record, index) => {
|
||||
const docName = record.segment.document?.name || 'Unknown Document'
|
||||
const content = record.segment.content.trim()
|
||||
const score = record.score
|
||||
const keywords = record.segment.keywords || []
|
||||
|
||||
let resultEntry = `#### ${index + 1}. ${docName} (Relevant Score: ${(score * 100).toFixed(1)}%)`
|
||||
resultEntry += `\n${content}`
|
||||
if (keywords.length > 0) {
|
||||
resultEntry += `\n*Keywords: ${keywords.join(', ')}*`
|
||||
}
|
||||
return resultEntry
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
body = `Found ${searchResponse.records.length} results:\n\n${resultsText}`
|
||||
}
|
||||
|
||||
const formattedText = header + body
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索知识库时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DifyKnowledgeServer
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import BraveSearchServer from './brave-search'
|
||||
import DifyKnowledgeServer from './dify-knowledge'
|
||||
import FetchServer from './fetch'
|
||||
import FileSystemServer from './filesystem'
|
||||
import MemoryServer from './memory'
|
||||
import ThinkingServer from './sequentialthinking'
|
||||
|
||||
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
|
||||
Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
|
||||
switch (name) {
|
||||
case '@cherry/memory': {
|
||||
const envPath = envs.MEMORY_FILE_PATH
|
||||
return new MemoryServer(envPath).server
|
||||
}
|
||||
case '@cherry/sequentialthinking': {
|
||||
return new ThinkingServer().server
|
||||
}
|
||||
case '@cherry/brave-search': {
|
||||
return new BraveSearchServer(envs.BRAVE_API_KEY).server
|
||||
}
|
||||
case '@cherry/fetch': {
|
||||
return new FetchServer().server
|
||||
}
|
||||
case '@cherry/filesystem': {
|
||||
return new FileSystemServer(args).server
|
||||
}
|
||||
case '@cherry/dify-knowledge': {
|
||||
const difyKey = envs.DIFY_KEY
|
||||
return new DifyKnowledgeServer(difyKey, args).server
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// port https://github.com/zcaceres/fetch-mcp/blob/main/src/index.ts
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import TurndownService from 'turndown'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const RequestPayloadSchema = z.object({
|
||||
url: z.string().url(),
|
||||
headers: z.record(z.string()).optional()
|
||||
})
|
||||
|
||||
export type RequestPayload = z.infer<typeof RequestPayloadSchema>
|
||||
|
||||
export class Fetcher {
|
||||
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
...headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error: ${response.status}`)
|
||||
}
|
||||
return response
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error(`Failed to fetch ${url}: ${e.message}`)
|
||||
} else {
|
||||
throw new Error(`Failed to fetch ${url}: Unknown error`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async html(requestPayload: RequestPayload) {
|
||||
try {
|
||||
const response = await this._fetch(requestPayload)
|
||||
const html = await response.text()
|
||||
return { content: [{ type: 'text', text: html }], isError: false }
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: (error as Error).message }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async json(requestPayload: RequestPayload) {
|
||||
try {
|
||||
const response = await this._fetch(requestPayload)
|
||||
const json = await response.json()
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(json) }],
|
||||
isError: false
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: (error as Error).message }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async txt(requestPayload: RequestPayload) {
|
||||
try {
|
||||
const response = await this._fetch(requestPayload)
|
||||
const html = await response.text()
|
||||
|
||||
const dom = new JSDOM(html)
|
||||
const document = dom.window.document
|
||||
|
||||
const scripts = document.getElementsByTagName('script')
|
||||
const styles = document.getElementsByTagName('style')
|
||||
Array.from(scripts).forEach((script: any) => script.remove())
|
||||
Array.from(styles).forEach((style: any) => style.remove())
|
||||
|
||||
const text = document.body.textContent || ''
|
||||
|
||||
const normalizedText = text.replace(/\s+/g, ' ').trim()
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: normalizedText }],
|
||||
isError: false
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: (error as Error).message }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async markdown(requestPayload: RequestPayload) {
|
||||
try {
|
||||
const response = await this._fetch(requestPayload)
|
||||
const html = await response.text()
|
||||
const turndownService = new TurndownService()
|
||||
const markdown = turndownService.turndown(html)
|
||||
return { content: [{ type: 'text', text: markdown }], isError: false }
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: (error as Error).message }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'zcaceres/fetch',
|
||||
version: '0.1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'fetch_html',
|
||||
description: 'Fetch a website and return the content as HTML',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the website to fetch'
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Optional headers to include in the request'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fetch_markdown',
|
||||
description: 'Fetch a website and return the content as Markdown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the website to fetch'
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Optional headers to include in the request'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fetch_txt',
|
||||
description: 'Fetch a website, return the content as plain text (no HTML)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the website to fetch'
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Optional headers to include in the request'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fetch_json',
|
||||
description: 'Fetch a JSON file from a URL',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the JSON to fetch'
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Optional headers to include in the request'
|
||||
}
|
||||
},
|
||||
required: ['url']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { arguments: args } = request.params
|
||||
|
||||
const validatedArgs = RequestPayloadSchema.parse(args)
|
||||
|
||||
if (request.params.name === 'fetch_html') {
|
||||
return await Fetcher.html(validatedArgs)
|
||||
}
|
||||
if (request.params.name === 'fetch_json') {
|
||||
return await Fetcher.json(validatedArgs)
|
||||
}
|
||||
if (request.params.name === 'fetch_txt') {
|
||||
return await Fetcher.txt(validatedArgs)
|
||||
}
|
||||
if (request.params.name === 'fetch_markdown') {
|
||||
return await Fetcher.markdown(validatedArgs)
|
||||
}
|
||||
throw new Error('Tool not found')
|
||||
})
|
||||
|
||||
class FetchServer {
|
||||
public server: Server
|
||||
constructor() {
|
||||
this.server = server
|
||||
}
|
||||
}
|
||||
export default FetchServer
|
||||
@@ -1,654 +0,0 @@
|
||||
// port https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { createTwoFilesPatch } from 'diff'
|
||||
import fs from 'fs/promises'
|
||||
import { minimatch } from 'minimatch'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { z } from 'zod'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
// Normalize all paths consistently
|
||||
function normalizePath(p: string): string {
|
||||
return path.normalize(p)
|
||||
}
|
||||
|
||||
function expandHome(filepath: string): string {
|
||||
if (filepath.startsWith('~/') || filepath === '~') {
|
||||
return path.join(os.homedir(), filepath.slice(1))
|
||||
}
|
||||
return filepath
|
||||
}
|
||||
|
||||
// Security utilities
|
||||
async function validatePath(allowedDirectories: string[], requestedPath: string): Promise<string> {
|
||||
const expandedPath = expandHome(requestedPath)
|
||||
const absolute = path.isAbsolute(expandedPath)
|
||||
? path.resolve(expandedPath)
|
||||
: path.resolve(process.cwd(), expandedPath)
|
||||
|
||||
const normalizedRequested = normalizePath(absolute)
|
||||
|
||||
// Check if path is within allowed directories
|
||||
const isAllowed = allowedDirectories.some((dir) => normalizedRequested.startsWith(dir))
|
||||
if (!isAllowed) {
|
||||
throw new Error(
|
||||
`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
// Handle symlinks by checking their real path
|
||||
try {
|
||||
const realPath = await fs.realpath(absolute)
|
||||
const normalizedReal = normalizePath(realPath)
|
||||
const isRealPathAllowed = allowedDirectories.some((dir) => normalizedReal.startsWith(dir))
|
||||
if (!isRealPathAllowed) {
|
||||
throw new Error('Access denied - symlink target outside allowed directories')
|
||||
}
|
||||
return realPath
|
||||
} catch (error) {
|
||||
// For new files that don't exist yet, verify parent directory
|
||||
const parentDir = path.dirname(absolute)
|
||||
try {
|
||||
const realParentPath = await fs.realpath(parentDir)
|
||||
const normalizedParent = normalizePath(realParentPath)
|
||||
const isParentAllowed = allowedDirectories.some((dir) => normalizedParent.startsWith(dir))
|
||||
if (!isParentAllowed) {
|
||||
throw new Error('Access denied - parent directory outside allowed directories')
|
||||
}
|
||||
return absolute
|
||||
} catch {
|
||||
throw new Error(`Parent directory does not exist: ${parentDir}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schema definitions
|
||||
const ReadFileArgsSchema = z.object({
|
||||
path: z.string()
|
||||
})
|
||||
|
||||
const ReadMultipleFilesArgsSchema = z.object({
|
||||
paths: z.array(z.string())
|
||||
})
|
||||
|
||||
const WriteFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string()
|
||||
})
|
||||
|
||||
const EditOperation = z.object({
|
||||
oldText: z.string().describe('Text to search for - must match exactly'),
|
||||
newText: z.string().describe('Text to replace with')
|
||||
})
|
||||
|
||||
const EditFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
edits: z.array(EditOperation),
|
||||
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
|
||||
})
|
||||
|
||||
const CreateDirectoryArgsSchema = z.object({
|
||||
path: z.string()
|
||||
})
|
||||
|
||||
const ListDirectoryArgsSchema = z.object({
|
||||
path: z.string()
|
||||
})
|
||||
|
||||
const DirectoryTreeArgsSchema = z.object({
|
||||
path: z.string()
|
||||
})
|
||||
|
||||
const MoveFileArgsSchema = z.object({
|
||||
source: z.string(),
|
||||
destination: z.string()
|
||||
})
|
||||
|
||||
const SearchFilesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
pattern: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
})
|
||||
|
||||
const GetFileInfoArgsSchema = z.object({
|
||||
path: z.string()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
|
||||
interface FileInfo {
|
||||
size: number
|
||||
created: Date
|
||||
modified: Date
|
||||
accessed: Date
|
||||
isDirectory: boolean
|
||||
isFile: boolean
|
||||
permissions: string
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
async function getFileStats(filePath: string): Promise<FileInfo> {
|
||||
const stats = await fs.stat(filePath)
|
||||
return {
|
||||
size: stats.size,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime,
|
||||
accessed: stats.atime,
|
||||
isDirectory: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
permissions: stats.mode.toString(8).slice(-3)
|
||||
}
|
||||
}
|
||||
|
||||
async function searchFiles(
|
||||
allowedDirectories: string[],
|
||||
rootPath: string,
|
||||
pattern: string,
|
||||
excludePatterns: string[] = []
|
||||
): Promise<string[]> {
|
||||
const results: string[] = []
|
||||
|
||||
async function search(currentPath: string) {
|
||||
const entries = await fs.readdir(currentPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name)
|
||||
|
||||
try {
|
||||
// Validate each path before processing
|
||||
await validatePath(allowedDirectories, fullPath)
|
||||
|
||||
// Check if path matches any exclude pattern
|
||||
const relativePath = path.relative(rootPath, fullPath)
|
||||
const shouldExclude = excludePatterns.some((pattern) => {
|
||||
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`
|
||||
return minimatch(relativePath, globPattern, { dot: true })
|
||||
})
|
||||
|
||||
if (shouldExclude) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
|
||||
results.push(fullPath)
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await search(fullPath)
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip invalid paths during search
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await search(rootPath)
|
||||
return results
|
||||
}
|
||||
|
||||
// file editing and diffing utilities
|
||||
function normalizeLineEndings(text: string): string {
|
||||
return text.replace(/\r\n/g, '\n')
|
||||
}
|
||||
|
||||
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
|
||||
// Ensure consistent line endings for diff
|
||||
const normalizedOriginal = normalizeLineEndings(originalContent)
|
||||
const normalizedNew = normalizeLineEndings(newContent)
|
||||
|
||||
return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified')
|
||||
}
|
||||
|
||||
async function applyFileEdits(
|
||||
filePath: string,
|
||||
edits: Array<{ oldText: string; newText: string }>,
|
||||
dryRun = false
|
||||
): Promise<string> {
|
||||
// Read file content and normalize line endings
|
||||
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'))
|
||||
|
||||
// Apply edits sequentially
|
||||
let modifiedContent = content
|
||||
for (const edit of edits) {
|
||||
const normalizedOld = normalizeLineEndings(edit.oldText)
|
||||
const normalizedNew = normalizeLineEndings(edit.newText)
|
||||
|
||||
// If exact match exists, use it
|
||||
if (modifiedContent.includes(normalizedOld)) {
|
||||
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew)
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||
const oldLines = normalizedOld.split('\n')
|
||||
const contentLines = modifiedContent.split('\n')
|
||||
let matchFound = false
|
||||
|
||||
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||
const potentialMatch = contentLines.slice(i, i + oldLines.length)
|
||||
|
||||
// Compare lines with normalized whitespace
|
||||
const isMatch = oldLines.every((oldLine, j) => {
|
||||
const contentLine = potentialMatch[j]
|
||||
return oldLine.trim() === contentLine.trim()
|
||||
})
|
||||
|
||||
if (isMatch) {
|
||||
// Preserve original indentation of first line
|
||||
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''
|
||||
const newLines = normalizedNew.split('\n').map((line, j) => {
|
||||
if (j === 0) return originalIndent + line.trimStart()
|
||||
// For subsequent lines, try to preserve relative indentation
|
||||
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''
|
||||
const newIndent = line.match(/^\s*/)?.[0] || ''
|
||||
if (oldIndent && newIndent) {
|
||||
const relativeIndent = newIndent.length - oldIndent.length
|
||||
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart()
|
||||
}
|
||||
return line
|
||||
})
|
||||
|
||||
contentLines.splice(i, oldLines.length, ...newLines)
|
||||
modifiedContent = contentLines.join('\n')
|
||||
matchFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchFound) {
|
||||
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Create unified diff
|
||||
const diff = createUnifiedDiff(content, modifiedContent, filePath)
|
||||
|
||||
// Format diff with appropriate number of backticks
|
||||
let numBackticks = 3
|
||||
while (diff.includes('`'.repeat(numBackticks))) {
|
||||
numBackticks++
|
||||
}
|
||||
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`
|
||||
|
||||
if (!dryRun) {
|
||||
await fs.writeFile(filePath, modifiedContent, 'utf-8')
|
||||
}
|
||||
|
||||
return formattedDiff
|
||||
}
|
||||
|
||||
class FileSystemServer {
|
||||
public server: Server
|
||||
private allowedDirectories: string[]
|
||||
constructor(allowedDirs: string[]) {
|
||||
if (!Array.isArray(allowedDirs) || allowedDirs.length === 0) {
|
||||
throw new Error('No allowed directories provided, please specify at least one directory in args')
|
||||
}
|
||||
|
||||
this.allowedDirectories = allowedDirs.map((dir) => normalizePath(path.resolve(expandHome(dir))))
|
||||
|
||||
// Validate that all directories exist and are accessible
|
||||
this.validateDirs().catch((error) => {
|
||||
console.error('Error validating allowed directories:', error)
|
||||
throw new Error(`Error validating allowed directories: ${error}`)
|
||||
})
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'secure-filesystem-server',
|
||||
version: '0.2.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
async validateDirs() {
|
||||
// Validate that all directories exist and are accessible
|
||||
await Promise.all(
|
||||
this.allowedDirectories.map(async (dir) => {
|
||||
try {
|
||||
const stats = await fs.stat(expandHome(dir))
|
||||
if (!stats.isDirectory()) {
|
||||
console.error(`Error: ${dir} is not a directory`)
|
||||
throw new Error(`Error: ${dir} is not a directory`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Error accessing directory ${dir}:`, error)
|
||||
throw new Error(`Error accessing directory ${dir}:`, error)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Tool handlers
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'read_file',
|
||||
description:
|
||||
'Read the complete contents of a file from the file system. ' +
|
||||
'Handles various text encodings and provides detailed error messages ' +
|
||||
'if the file cannot be read. Use this tool when you need to examine ' +
|
||||
'the contents of a single file. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'read_multiple_files',
|
||||
description:
|
||||
'Read the contents of multiple files simultaneously. This is more ' +
|
||||
'efficient than reading files one by one when you need to analyze ' +
|
||||
"or compare multiple files. Each file's content is returned with its " +
|
||||
"path as a reference. Failed reads for individual files won't stop " +
|
||||
'the entire operation. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'write_file',
|
||||
description:
|
||||
'Create a new file or completely overwrite an existing file with new content. ' +
|
||||
'Use with caution as it will overwrite existing files without warning. ' +
|
||||
'Handles text content with proper encoding. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'edit_file',
|
||||
description:
|
||||
'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
|
||||
'with new content. Returns a git-style diff showing the changes made. ' +
|
||||
'Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'create_directory',
|
||||
description:
|
||||
'Create a new directory or ensure a directory exists. Can create multiple ' +
|
||||
'nested directories in one operation. If the directory already exists, ' +
|
||||
'this operation will succeed silently. Perfect for setting up directory ' +
|
||||
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'list_directory',
|
||||
description:
|
||||
'Get a detailed listing of all files and directories in a specified path. ' +
|
||||
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
|
||||
'prefixes. This tool is essential for understanding directory structure and ' +
|
||||
'finding specific files within a directory. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'directory_tree',
|
||||
description:
|
||||
'Get a recursive tree view of files and directories as a JSON structure. ' +
|
||||
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||
'Files have no children array, while directories always have a children array (which may be empty). ' +
|
||||
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'move_file',
|
||||
description:
|
||||
'Move or rename files and directories. Can move files between directories ' +
|
||||
'and rename them in a single operation. If the destination exists, the ' +
|
||||
'operation will fail. Works across different directories and can be used ' +
|
||||
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'search_files',
|
||||
description:
|
||||
'Recursively search for files and directories matching a pattern. ' +
|
||||
'Searches through all subdirectories from the starting path. The search ' +
|
||||
'is case-insensitive and matches partial names. Returns full paths to all ' +
|
||||
"matching items. Great for finding files when you don't know their exact location. " +
|
||||
'Only searches within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'get_file_info',
|
||||
description:
|
||||
'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
|
||||
'information including size, creation time, last modified time, permissions, ' +
|
||||
'and type. This tool is perfect for understanding file characteristics ' +
|
||||
'without reading the actual content. Only works within allowed directories.',
|
||||
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: 'list_allowed_directories',
|
||||
description:
|
||||
'Returns the list of directories that this server is allowed to access. ' +
|
||||
'Use this to understand which directories are available before trying to access files.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
switch (name) {
|
||||
case 'read_file': {
|
||||
const parsed = ReadFileArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
const content = await fs.readFile(validPath, 'utf-8')
|
||||
return {
|
||||
content: [{ type: 'text', text: content }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'read_multiple_files': {
|
||||
const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`)
|
||||
}
|
||||
const results = await Promise.all(
|
||||
parsed.data.paths.map(async (filePath: string) => {
|
||||
try {
|
||||
const validPath = await validatePath(this.allowedDirectories, filePath)
|
||||
const content = await fs.readFile(validPath, 'utf-8')
|
||||
return `${filePath}:\n${content}\n`
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return `${filePath}: Error - ${errorMessage}`
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
content: [{ type: 'text', text: results.join('\n---\n') }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'write_file': {
|
||||
const parsed = WriteFileArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
|
||||
return {
|
||||
content: [{ type: 'text', text: `Successfully wrote to ${parsed.data.path}` }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'edit_file': {
|
||||
const parsed = EditFileArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun)
|
||||
return {
|
||||
content: [{ type: 'text', text: result }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'create_directory': {
|
||||
const parsed = CreateDirectoryArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
await fs.mkdir(validPath, { recursive: true })
|
||||
return {
|
||||
content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_directory': {
|
||||
const parsed = ListDirectoryArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true })
|
||||
const formatted = entries
|
||||
.map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
|
||||
.join('\n')
|
||||
return {
|
||||
content: [{ type: 'text', text: formatted }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'directory_tree': {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`)
|
||||
}
|
||||
|
||||
interface TreeEntry {
|
||||
name: string
|
||||
type: 'file' | 'directory'
|
||||
children?: TreeEntry[]
|
||||
}
|
||||
|
||||
async function buildTree(allowedDirectories: string[], currentPath: string): Promise<TreeEntry[]> {
|
||||
const validPath = await validatePath(allowedDirectories, currentPath)
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true })
|
||||
const result: TreeEntry[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryData: TreeEntry = {
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file'
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subPath = path.join(currentPath, entry.name)
|
||||
entryData.children = await buildTree(allowedDirectories, subPath)
|
||||
}
|
||||
|
||||
result.push(entryData)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const treeData = await buildTree(this.allowedDirectories, parsed.data.path)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(treeData, null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
case 'move_file': {
|
||||
const parsed = MoveFileArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
|
||||
}
|
||||
const validSourcePath = await validatePath(this.allowedDirectories, parsed.data.source)
|
||||
const validDestPath = await validatePath(this.allowedDirectories, parsed.data.destination)
|
||||
await fs.rename(validSourcePath, validDestPath)
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
case 'search_files': {
|
||||
const parsed = SearchFilesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for search_files: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
const results = await searchFiles(
|
||||
this.allowedDirectories,
|
||||
validPath,
|
||||
parsed.data.pattern,
|
||||
parsed.data.excludePatterns
|
||||
)
|
||||
return {
|
||||
content: [{ type: 'text', text: results.length > 0 ? results.join('\n') : 'No matches found' }]
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_file_info': {
|
||||
const parsed = GetFileInfoArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`)
|
||||
}
|
||||
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
|
||||
const info = await getFileStats(validPath)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: Object.entries(info)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_allowed_directories': {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Allowed directories:\n${this.allowedDirectories.join('\n')}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default FileSystemServer
|
||||
@@ -1,701 +0,0 @@
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { Mutex } from 'async-mutex' // 引入 Mutex
|
||||
import Logger from 'electron-log'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// Define memory file path
|
||||
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
|
||||
|
||||
// Interfaces remain the same
|
||||
interface Entity {
|
||||
name: string
|
||||
entityType: string
|
||||
observations: string[]
|
||||
}
|
||||
|
||||
interface Relation {
|
||||
from: string
|
||||
to: string
|
||||
relationType: string
|
||||
}
|
||||
|
||||
// Structure for storing the graph in memory and in the file
|
||||
interface KnowledgeGraph {
|
||||
entities: Entity[]
|
||||
relations: Relation[]
|
||||
}
|
||||
|
||||
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
||||
class KnowledgeGraphManager {
|
||||
private memoryPath: string
|
||||
private entities: Map<string, Entity> // Use Map for efficient entity lookup
|
||||
private relations: Set<string> // Store stringified relations for easy Set operations
|
||||
private fileMutex: Mutex // Mutex for file writing
|
||||
|
||||
private constructor(memoryPath: string) {
|
||||
this.memoryPath = memoryPath
|
||||
this.entities = new Map<string, Entity>()
|
||||
this.relations = new Set<string>()
|
||||
this.fileMutex = new Mutex()
|
||||
}
|
||||
|
||||
// Static async factory method for initialization
|
||||
public static async create(memoryPath: string): Promise<KnowledgeGraphManager> {
|
||||
const manager = new KnowledgeGraphManager(memoryPath)
|
||||
await manager._ensureMemoryPathExists()
|
||||
await manager._loadGraphFromDisk()
|
||||
return manager
|
||||
}
|
||||
|
||||
private async _ensureMemoryPathExists(): Promise<void> {
|
||||
try {
|
||||
const directory = path.dirname(this.memoryPath)
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
try {
|
||||
await fs.access(this.memoryPath)
|
||||
} catch (error) {
|
||||
// File doesn't exist, create an empty file with initial structure
|
||||
await fs.writeFile(this.memoryPath, JSON.stringify({ entities: [], relations: [] }, null, 2))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to ensure memory path exists:', error)
|
||||
// Propagate the error or handle it more gracefully depending on requirements
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Load graph from disk into memory (called once during initialization)
|
||||
private async _loadGraphFromDisk(): Promise<void> {
|
||||
try {
|
||||
const data = await fs.readFile(this.memoryPath, 'utf-8')
|
||||
// Handle empty file case
|
||||
if (data.trim() === '') {
|
||||
this.entities = new Map()
|
||||
this.relations = new Set()
|
||||
// Optionally write the initial empty structure back
|
||||
await this._persistGraph()
|
||||
return
|
||||
}
|
||||
const graph: KnowledgeGraph = JSON.parse(data)
|
||||
this.entities.clear()
|
||||
this.relations.clear()
|
||||
graph.entities.forEach((entity) => this.entities.set(entity.name, entity))
|
||||
graph.relations.forEach((relation) => this.relations.add(this._serializeRelation(relation)))
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||
// File doesn't exist (should have been created by _ensureMemoryPathExists, but handle defensively)
|
||||
this.entities = new Map()
|
||||
this.relations = new Set()
|
||||
await this._persistGraph() // Create the file with empty structure
|
||||
} else if (error instanceof SyntaxError) {
|
||||
console.error('Failed to parse memory.json, initializing with empty graph:', error)
|
||||
// If JSON is invalid, start fresh and overwrite the corrupted file
|
||||
this.entities = new Map()
|
||||
this.relations = new Set()
|
||||
await this._persistGraph()
|
||||
} else {
|
||||
console.error('Failed to load knowledge graph from disk:', error)
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to load graph: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the current in-memory graph to disk using a mutex
|
||||
private async _persistGraph(): Promise<void> {
|
||||
const release = await this.fileMutex.acquire()
|
||||
try {
|
||||
const graphData: KnowledgeGraph = {
|
||||
entities: Array.from(this.entities.values()),
|
||||
relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr))
|
||||
}
|
||||
await fs.writeFile(this.memoryPath, JSON.stringify(graphData, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Failed to save knowledge graph:', error)
|
||||
// Decide how to handle write errors - potentially retry or notify
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to save graph: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to consistently serialize relations for Set storage
|
||||
private _serializeRelation(relation: Relation): string {
|
||||
// Simple serialization, ensure order doesn't matter if properties are consistent
|
||||
return JSON.stringify({ from: relation.from, to: relation.to, relationType: relation.relationType })
|
||||
}
|
||||
|
||||
// Helper to deserialize relations from Set storage
|
||||
private _deserializeRelation(relationStr: string): Relation {
|
||||
return JSON.parse(relationStr) as Relation
|
||||
}
|
||||
|
||||
async createEntities(entities: Entity[]): Promise<Entity[]> {
|
||||
const newEntities: Entity[] = []
|
||||
entities.forEach((entity) => {
|
||||
if (!this.entities.has(entity.name)) {
|
||||
// Ensure observations is always an array
|
||||
const newEntity = { ...entity, observations: Array.isArray(entity.observations) ? entity.observations : [] }
|
||||
this.entities.set(entity.name, newEntity)
|
||||
newEntities.push(newEntity)
|
||||
}
|
||||
})
|
||||
if (newEntities.length > 0) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
return newEntities
|
||||
}
|
||||
|
||||
async createRelations(relations: Relation[]): Promise<Relation[]> {
|
||||
const newRelations: Relation[] = []
|
||||
relations.forEach((relation) => {
|
||||
// Ensure related entities exist before creating a relation
|
||||
if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) {
|
||||
console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`)
|
||||
return // Skip this relation
|
||||
}
|
||||
const relationStr = this._serializeRelation(relation)
|
||||
if (!this.relations.has(relationStr)) {
|
||||
this.relations.add(relationStr)
|
||||
newRelations.push(relation)
|
||||
}
|
||||
})
|
||||
if (newRelations.length > 0) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
return newRelations
|
||||
}
|
||||
|
||||
async addObservations(
|
||||
observations: { entityName: string; contents: string[] }[]
|
||||
): Promise<{ entityName: string; addedObservations: string[] }[]> {
|
||||
const results: { entityName: string; addedObservations: string[] }[] = []
|
||||
let changed = false
|
||||
observations.forEach((o) => {
|
||||
const entity = this.entities.get(o.entityName)
|
||||
if (!entity) {
|
||||
// Option 1: Throw error
|
||||
throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`)
|
||||
// Option 2: Skip and warn
|
||||
// console.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`);
|
||||
// return;
|
||||
}
|
||||
// Ensure observations array exists
|
||||
if (!Array.isArray(entity.observations)) {
|
||||
entity.observations = []
|
||||
}
|
||||
const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
|
||||
if (newObservations.length > 0) {
|
||||
entity.observations.push(...newObservations)
|
||||
results.push({ entityName: o.entityName, addedObservations: newObservations })
|
||||
changed = true
|
||||
} else {
|
||||
// Still include in results even if nothing was added, to confirm processing
|
||||
results.push({ entityName: o.entityName, addedObservations: [] })
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async deleteEntities(entityNames: string[]): Promise<void> {
|
||||
let changed = false
|
||||
const namesToDelete = new Set(entityNames)
|
||||
|
||||
// Delete entities
|
||||
namesToDelete.forEach((name) => {
|
||||
if (this.entities.delete(name)) {
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
// Delete relations involving deleted entities
|
||||
const relationsToDelete = new Set<string>()
|
||||
this.relations.forEach((relStr) => {
|
||||
const rel = this._deserializeRelation(relStr)
|
||||
if (namesToDelete.has(rel.from) || namesToDelete.has(rel.to)) {
|
||||
relationsToDelete.add(relStr)
|
||||
}
|
||||
})
|
||||
|
||||
relationsToDelete.forEach((relStr) => {
|
||||
if (this.relations.delete(relStr)) {
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
if (changed) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
|
||||
let changed = false
|
||||
deletions.forEach((d) => {
|
||||
const entity = this.entities.get(d.entityName)
|
||||
if (entity && Array.isArray(entity.observations)) {
|
||||
const initialLength = entity.observations.length
|
||||
const observationsToDelete = new Set(d.observations)
|
||||
entity.observations = entity.observations.filter((o) => !observationsToDelete.has(o))
|
||||
if (entity.observations.length !== initialLength) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRelations(relations: Relation[]): Promise<void> {
|
||||
let changed = false
|
||||
relations.forEach((rel) => {
|
||||
const relStr = this._serializeRelation(rel)
|
||||
if (this.relations.delete(relStr)) {
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
await this._persistGraph()
|
||||
}
|
||||
}
|
||||
|
||||
// Read the current state from memory
|
||||
async readGraph(): Promise<KnowledgeGraph> {
|
||||
// Return a deep copy to prevent external modification of the internal state
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
entities: Array.from(this.entities.values()),
|
||||
relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Search operates on the in-memory graph
|
||||
async searchNodes(query: string): Promise<KnowledgeGraph> {
|
||||
const lowerCaseQuery = query.toLowerCase()
|
||||
const filteredEntities = Array.from(this.entities.values()).filter(
|
||||
(e) =>
|
||||
e.name.toLowerCase().includes(lowerCaseQuery) ||
|
||||
e.entityType.toLowerCase().includes(lowerCaseQuery) ||
|
||||
(Array.isArray(e.observations) && e.observations.some((o) => o.toLowerCase().includes(lowerCaseQuery)))
|
||||
)
|
||||
|
||||
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
||||
|
||||
const filteredRelations = Array.from(this.relations)
|
||||
.map((rStr) => this._deserializeRelation(rStr))
|
||||
.filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to))
|
||||
|
||||
return {
|
||||
entities: filteredEntities,
|
||||
relations: filteredRelations
|
||||
}
|
||||
}
|
||||
|
||||
// Open operates on the in-memory graph
|
||||
async openNodes(names: string[]): Promise<KnowledgeGraph> {
|
||||
const nameSet = new Set(names)
|
||||
const filteredEntities = Array.from(this.entities.values()).filter((e) => nameSet.has(e.name))
|
||||
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
||||
|
||||
const filteredRelations = Array.from(this.relations)
|
||||
.map((rStr) => this._deserializeRelation(rStr))
|
||||
.filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to))
|
||||
|
||||
return {
|
||||
entities: filteredEntities,
|
||||
relations: filteredRelations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryServer {
|
||||
public server: Server
|
||||
// Hold the manager instance, initialized asynchronously
|
||||
private knowledgeGraphManager: KnowledgeGraphManager | null = null
|
||||
private initializationPromise: Promise<void> // To track initialization
|
||||
|
||||
constructor(envPath: string = '') {
|
||||
const memoryPath = envPath
|
||||
? path.isAbsolute(envPath)
|
||||
? envPath
|
||||
: path.resolve(envPath) // Use path.resolve for relative paths based on CWD
|
||||
: defaultMemoryPath
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'memory-server',
|
||||
version: '1.1.0' // Incremented version for changes
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
// Start initialization, but don't block constructor
|
||||
this.initializationPromise = this._initializeManager(memoryPath)
|
||||
this.setupRequestHandlers() // Setup handlers immediately
|
||||
}
|
||||
|
||||
// Private async method to handle manager initialization
|
||||
private async _initializeManager(memoryPath: string): Promise<void> {
|
||||
try {
|
||||
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath)
|
||||
Logger.log('KnowledgeGraphManager initialized successfully.')
|
||||
} catch (error) {
|
||||
Logger.error('Failed to initialize KnowledgeGraphManager:', error)
|
||||
// Server might be unusable, consider how to handle this state
|
||||
// Maybe set a flag and return errors for all tool calls?
|
||||
this.knowledgeGraphManager = null // Ensure it's null if init fails
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures the manager is initialized before handling tool calls
|
||||
private async _getManager(): Promise<KnowledgeGraphManager> {
|
||||
await this.initializationPromise // Wait for initialization to complete
|
||||
if (!this.knowledgeGraphManager) {
|
||||
throw new McpError(ErrorCode.InternalError, 'Memory server failed to initialize. Cannot process requests.')
|
||||
}
|
||||
return this.knowledgeGraphManager
|
||||
}
|
||||
|
||||
// Setup handlers (can be called from constructor)
|
||||
setupRequestHandlers() {
|
||||
// ListTools remains largely the same, descriptions might be updated if needed
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
// Ensure manager is ready before listing tools that depend on it
|
||||
// Although ListTools itself doesn't *call* the manager, it implies the
|
||||
// manager is ready to handle calls for those tools.
|
||||
try {
|
||||
await this._getManager() // Wait for initialization before confirming tools are available
|
||||
} catch (error) {
|
||||
// If manager failed to init, maybe return an empty tool list or throw?
|
||||
console.error('Cannot list tools, manager initialization failed:', error)
|
||||
return { tools: [] } // Return empty list if server is not ready
|
||||
}
|
||||
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'create_entities',
|
||||
description: 'Create multiple new entities in the knowledge graph. Skips existing entities.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entities: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'The name of the entity' },
|
||||
entityType: { type: 'string', description: 'The type of the entity' },
|
||||
observations: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'An array of observation contents associated with the entity',
|
||||
default: [] // Add default empty array
|
||||
}
|
||||
},
|
||||
required: ['name', 'entityType'] // Observations are optional now on creation
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['entities']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'create_relations',
|
||||
description:
|
||||
'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
relations: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
||||
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
||||
relationType: { type: 'string', description: 'The type of the relation' }
|
||||
},
|
||||
required: ['from', 'to', 'relationType']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['relations']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add_observations',
|
||||
description: 'Add new observations to existing entities. Skips duplicate observations.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
observations: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
|
||||
contents: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'An array of observation contents to add'
|
||||
}
|
||||
},
|
||||
required: ['entityName', 'contents']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['observations']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_entities',
|
||||
description: 'Delete multiple entities and their associated relations.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entityNames: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'An array of entity names to delete'
|
||||
}
|
||||
},
|
||||
required: ['entityNames']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_observations',
|
||||
description: 'Delete specific observations from entities.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
deletions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entityName: { type: 'string', description: 'The name of the entity containing the observations' },
|
||||
observations: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'An array of observations to delete'
|
||||
}
|
||||
},
|
||||
required: ['entityName', 'observations']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['deletions']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'delete_relations',
|
||||
description: 'Delete multiple specific relations.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
relations: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
||||
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
||||
relationType: { type: 'string', description: 'The type of the relation' }
|
||||
},
|
||||
required: ['from', 'to', 'relationType']
|
||||
},
|
||||
description: 'An array of relations to delete'
|
||||
}
|
||||
},
|
||||
required: ['relations']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'read_graph',
|
||||
description: 'Read the entire knowledge graph from memory.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_nodes',
|
||||
description: 'Search nodes (entities and relations) in memory based on a query.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query to match against entity names, types, and observation content'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'open_nodes',
|
||||
description: 'Retrieve specific entities and their connecting relations from memory by name.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
names: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'An array of entity names to retrieve'
|
||||
}
|
||||
},
|
||||
required: ['names']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// CallTool handler needs to await the manager and the async methods
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const manager = await this._getManager() // Ensure manager is ready
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
if (!args) {
|
||||
// Use McpError for standard errors
|
||||
throw new McpError(ErrorCode.InvalidParams, `No arguments provided for tool: ${name}`)
|
||||
}
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case 'create_entities':
|
||||
// Validate args structure if necessary, though SDK might do basic validation
|
||||
if (!args.entities || !Array.isArray(args.entities)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'entities' array is required.`
|
||||
)
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) }
|
||||
]
|
||||
}
|
||||
case 'create_relations':
|
||||
if (!args.relations || !Array.isArray(args.relations)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'relations' array is required.`
|
||||
)
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2)
|
||||
}
|
||||
]
|
||||
}
|
||||
case 'add_observations':
|
||||
if (!args.observations || !Array.isArray(args.observations)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'observations' array is required.`
|
||||
)
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]),
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
case 'delete_entities':
|
||||
if (!args.entityNames || !Array.isArray(args.entityNames)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'entityNames' array is required.`
|
||||
)
|
||||
}
|
||||
await manager.deleteEntities(args.entityNames as string[])
|
||||
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
|
||||
case 'delete_observations':
|
||||
if (!args.deletions || !Array.isArray(args.deletions)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'deletions' array is required.`
|
||||
)
|
||||
}
|
||||
await manager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[])
|
||||
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
|
||||
case 'delete_relations':
|
||||
if (!args.relations || !Array.isArray(args.relations)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Invalid arguments for ${name}: 'relations' array is required.`
|
||||
)
|
||||
}
|
||||
await manager.deleteRelations(args.relations as Relation[])
|
||||
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
|
||||
case 'read_graph':
|
||||
// No arguments expected or needed for read_graph based on original schema
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(await manager.readGraph(), null, 2) }]
|
||||
}
|
||||
case 'search_nodes':
|
||||
if (typeof args.query !== 'string') {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`)
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) }
|
||||
]
|
||||
}
|
||||
case 'open_nodes':
|
||||
if (!args.names || !Array.isArray(args.names)) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`)
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) }
|
||||
]
|
||||
}
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// Catch errors from manager methods (like entity not found) or other issues
|
||||
if (error instanceof McpError) {
|
||||
throw error // Re-throw McpErrors directly
|
||||
}
|
||||
console.error(`Error executing tool ${name}:`, error)
|
||||
// Throw a generic internal error for unexpected issues
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default MemoryServer
|
||||
@@ -1,289 +0,0 @@
|
||||
// Sequential Thinking MCP Server
|
||||
// port https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
// Fixed chalk import for ESM
|
||||
import chalk from 'chalk'
|
||||
|
||||
interface ThoughtData {
|
||||
thought: string
|
||||
thoughtNumber: number
|
||||
totalThoughts: number
|
||||
isRevision?: boolean
|
||||
revisesThought?: number
|
||||
branchFromThought?: number
|
||||
branchId?: string
|
||||
needsMoreThoughts?: boolean
|
||||
nextThoughtNeeded: boolean
|
||||
}
|
||||
|
||||
class SequentialThinkingServer {
|
||||
private thoughtHistory: ThoughtData[] = []
|
||||
private branches: Record<string, ThoughtData[]> = {}
|
||||
|
||||
private validateThoughtData(input: unknown): ThoughtData {
|
||||
const data = input as Record<string, unknown>
|
||||
|
||||
if (!data.thought || typeof data.thought !== 'string') {
|
||||
throw new Error('Invalid thought: must be a string')
|
||||
}
|
||||
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') {
|
||||
throw new Error('Invalid thoughtNumber: must be a number')
|
||||
}
|
||||
if (!data.totalThoughts || typeof data.totalThoughts !== 'number') {
|
||||
throw new Error('Invalid totalThoughts: must be a number')
|
||||
}
|
||||
if (typeof data.nextThoughtNeeded !== 'boolean') {
|
||||
throw new Error('Invalid nextThoughtNeeded: must be a boolean')
|
||||
}
|
||||
|
||||
return {
|
||||
thought: data.thought,
|
||||
thoughtNumber: data.thoughtNumber,
|
||||
totalThoughts: data.totalThoughts,
|
||||
nextThoughtNeeded: data.nextThoughtNeeded,
|
||||
isRevision: data.isRevision as boolean | undefined,
|
||||
revisesThought: data.revisesThought as number | undefined,
|
||||
branchFromThought: data.branchFromThought as number | undefined,
|
||||
branchId: data.branchId as string | undefined,
|
||||
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined
|
||||
}
|
||||
}
|
||||
|
||||
private formatThought(thoughtData: ThoughtData): string {
|
||||
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } =
|
||||
thoughtData
|
||||
|
||||
let prefix: string
|
||||
let context: string
|
||||
|
||||
if (isRevision) {
|
||||
prefix = chalk.yellow('🔄 Revision')
|
||||
context = ` (revising thought ${revisesThought})`
|
||||
} else if (branchFromThought) {
|
||||
prefix = chalk.green('🌿 Branch')
|
||||
context = ` (from thought ${branchFromThought}, ID: ${branchId})`
|
||||
} else {
|
||||
prefix = chalk.blue('💭 Thought')
|
||||
context = ''
|
||||
}
|
||||
|
||||
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`
|
||||
const border = '─'.repeat(Math.max(header.length, thought.length) + 4)
|
||||
|
||||
return `
|
||||
┌${border}┐
|
||||
│ ${header} │
|
||||
├${border}┤
|
||||
│ ${thought.padEnd(border.length - 2)} │
|
||||
└${border}┘`
|
||||
}
|
||||
|
||||
public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } {
|
||||
try {
|
||||
const validatedInput = this.validateThoughtData(input)
|
||||
|
||||
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
|
||||
validatedInput.totalThoughts = validatedInput.thoughtNumber
|
||||
}
|
||||
|
||||
this.thoughtHistory.push(validatedInput)
|
||||
|
||||
if (validatedInput.branchFromThought && validatedInput.branchId) {
|
||||
if (!this.branches[validatedInput.branchId]) {
|
||||
this.branches[validatedInput.branchId] = []
|
||||
}
|
||||
this.branches[validatedInput.branchId].push(validatedInput)
|
||||
}
|
||||
|
||||
const formattedThought = this.formatThought(validatedInput)
|
||||
console.error(formattedThought)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
thoughtNumber: validatedInput.thoughtNumber,
|
||||
totalThoughts: validatedInput.totalThoughts,
|
||||
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
|
||||
branches: Object.keys(this.branches),
|
||||
thoughtHistoryLength: this.thoughtHistory.length
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
status: 'failed'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SEQUENTIAL_THINKING_TOOL: Tool = {
|
||||
name: 'sequentialthinking',
|
||||
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
|
||||
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
|
||||
Each thought can build on, question, or revise previous insights as understanding deepens.
|
||||
|
||||
When to use this tool:
|
||||
- Breaking down complex problems into steps
|
||||
- Planning and design with room for revision
|
||||
- Analysis that might need course correction
|
||||
- Problems where the full scope might not be clear initially
|
||||
- Problems that require a multi-step solution
|
||||
- Tasks that need to maintain context over multiple steps
|
||||
- Situations where irrelevant information needs to be filtered out
|
||||
|
||||
Key features:
|
||||
- You can adjust total_thoughts up or down as you progress
|
||||
- You can question or revise previous thoughts
|
||||
- You can add more thoughts even after reaching what seemed like the end
|
||||
- You can express uncertainty and explore alternative approaches
|
||||
- Not every thought needs to build linearly - you can branch or backtrack
|
||||
- Generates a solution hypothesis
|
||||
- Verifies the hypothesis based on the Chain of Thought steps
|
||||
- Repeats the process until satisfied
|
||||
- Provides a correct answer
|
||||
|
||||
Parameters explained:
|
||||
- thought: Your current thinking step, which can include:
|
||||
* Regular analytical steps
|
||||
* Revisions of previous thoughts
|
||||
* Questions about previous decisions
|
||||
* Realizations about needing more analysis
|
||||
* Changes in approach
|
||||
* Hypothesis generation
|
||||
* Hypothesis verification
|
||||
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
|
||||
- thought_number: Current number in sequence (can go beyond initial total if needed)
|
||||
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
|
||||
- is_revision: A boolean indicating if this thought revises previous thinking
|
||||
- revises_thought: If is_revision is true, which thought number is being reconsidered
|
||||
- branch_from_thought: If branching, which thought number is the branching point
|
||||
- branch_id: Identifier for the current branch (if any)
|
||||
- needs_more_thoughts: If reaching end but realizing more thoughts needed
|
||||
|
||||
You should:
|
||||
1. Start with an initial estimate of needed thoughts, but be ready to adjust
|
||||
2. Feel free to question or revise previous thoughts
|
||||
3. Don't hesitate to add more thoughts if needed, even at the "end"
|
||||
4. Express uncertainty when present
|
||||
5. Mark thoughts that revise previous thinking or branch into new paths
|
||||
6. Ignore information that is irrelevant to the current step
|
||||
7. Generate a solution hypothesis when appropriate
|
||||
8. Verify the hypothesis based on the Chain of Thought steps
|
||||
9. Repeat the process until satisfied with the solution
|
||||
10. Provide a single, ideally correct answer as the final output
|
||||
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
thought: {
|
||||
type: 'string',
|
||||
description: 'Your current thinking step'
|
||||
},
|
||||
nextThoughtNeeded: {
|
||||
type: 'boolean',
|
||||
description: 'Whether another thought step is needed'
|
||||
},
|
||||
thoughtNumber: {
|
||||
type: 'integer',
|
||||
description: 'Current thought number',
|
||||
minimum: 1
|
||||
},
|
||||
totalThoughts: {
|
||||
type: 'integer',
|
||||
description: 'Estimated total thoughts needed',
|
||||
minimum: 1
|
||||
},
|
||||
isRevision: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this revises previous thinking'
|
||||
},
|
||||
revisesThought: {
|
||||
type: 'integer',
|
||||
description: 'Which thought is being reconsidered',
|
||||
minimum: 1
|
||||
},
|
||||
branchFromThought: {
|
||||
type: 'integer',
|
||||
description: 'Branching point thought number',
|
||||
minimum: 1
|
||||
},
|
||||
branchId: {
|
||||
type: 'string',
|
||||
description: 'Branch identifier'
|
||||
},
|
||||
needsMoreThoughts: {
|
||||
type: 'boolean',
|
||||
description: 'If more thoughts are needed'
|
||||
}
|
||||
},
|
||||
required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts']
|
||||
}
|
||||
}
|
||||
|
||||
class ThinkingServer {
|
||||
public server: Server
|
||||
private thinkingServer: SequentialThinkingServer
|
||||
|
||||
constructor() {
|
||||
this.thinkingServer = new SequentialThinkingServer()
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'sequential-thinking-server',
|
||||
version: '0.2.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [SEQUENTIAL_THINKING_TOOL]
|
||||
}))
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'sequentialthinking') {
|
||||
return this.thinkingServer.processThought(request.params.arguments)
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unknown tool: ${request.params.name}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ThinkingServer
|
||||
@@ -3,114 +3,14 @@ import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
export default abstract class BaseReranker {
|
||||
protected base: KnowledgeBaseParams
|
||||
|
||||
constructor(base: KnowledgeBaseParams) {
|
||||
if (!base.rerankModel) {
|
||||
throw new Error('Rerank model is required')
|
||||
}
|
||||
this.base = base
|
||||
}
|
||||
|
||||
abstract rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]>
|
||||
|
||||
/**
|
||||
* Get Rerank Request Url
|
||||
*/
|
||||
protected getRerankUrl() {
|
||||
if (this.base.rerankModelProvider === 'dashscope') {
|
||||
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
|
||||
}
|
||||
|
||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
? this.base.rerankBaseURL.slice(0, -1)
|
||||
: this.base.rerankBaseURL
|
||||
// 必须携带/v1,否则会404
|
||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||
baseURL = `${baseURL}/v1`
|
||||
}
|
||||
|
||||
return `${baseURL}/rerank`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Rerank Request Body
|
||||
*/
|
||||
protected getRerankRequestBody(query: string, searchResults: ExtractChunkData[]) {
|
||||
const provider = this.base.rerankModelProvider
|
||||
const documents = searchResults.map((doc) => doc.pageContent)
|
||||
const topN = this.base.documentCount
|
||||
|
||||
if (provider === 'voyageai') {
|
||||
return {
|
||||
model: this.base.rerankModel,
|
||||
query,
|
||||
documents,
|
||||
top_k: topN
|
||||
}
|
||||
} else if (provider === 'dashscope') {
|
||||
return {
|
||||
model: this.base.rerankModel,
|
||||
input: {
|
||||
query,
|
||||
documents
|
||||
},
|
||||
parameters: {
|
||||
top_n: topN
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
model: this.base.rerankModel,
|
||||
query,
|
||||
documents,
|
||||
top_n: topN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Rerank Result
|
||||
*/
|
||||
protected extractRerankResult(data: any) {
|
||||
const provider = this.base.rerankModelProvider
|
||||
if (provider === 'dashscope') {
|
||||
return data.output.results
|
||||
} else if (provider === 'voyageai') {
|
||||
return data.data
|
||||
} else {
|
||||
return data.results
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Rerank Result
|
||||
* @param searchResults
|
||||
* @param rerankResults
|
||||
* @protected
|
||||
*/
|
||||
protected getRerankResult(
|
||||
searchResults: ExtractChunkData[],
|
||||
rerankResults: Array<{
|
||||
index: number
|
||||
relevance_score: number
|
||||
}>
|
||||
) {
|
||||
const resultMap = new Map(rerankResults.map((result) => [result.index, result.relevance_score || 0]))
|
||||
|
||||
return searchResults
|
||||
.map((doc: ExtractChunkData, index: number) => {
|
||||
const score = resultMap.get(index)
|
||||
if (score === undefined) return undefined
|
||||
|
||||
return {
|
||||
...doc,
|
||||
score
|
||||
}
|
||||
})
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
}
|
||||
|
||||
public defaultHeaders() {
|
||||
return {
|
||||
Authorization: `Bearer ${this.base.rerankApiKey}`,
|
||||
@@ -118,7 +18,7 @@ export default abstract class BaseReranker {
|
||||
}
|
||||
}
|
||||
|
||||
protected formatErrorMessage(url: string, error: any, requestBody: any) {
|
||||
public formatErrorMessage(url: string, error: any, requestBody: any) {
|
||||
const errorDetails = {
|
||||
url: url,
|
||||
message: error.message,
|
||||
|
||||