├── packages ├── database │ ├── data │ │ └── .gitkeep │ ├── .gitignore │ ├── README.md │ ├── .yarn │ │ └── install-state.gz │ ├── package.json │ └── src │ │ ├── email.js │ │ └── agents.js └── artifacts │ ├── README.md │ └── package.json ├── .vscode ├── extensions.json ├── snippet.code-snippets ├── settings.json └── launch.json ├── .eslintignore ├── api ├── screenshot.png ├── screenshot-chat.png ├── src │ ├── middleware │ │ └── auth.ts │ ├── config │ │ └── index.ts │ ├── routes │ │ ├── knowledge.ts │ │ └── provider.ts │ └── index.ts ├── tsconfig.json └── package.json ├── .yarnrc.yml ├── src ├── main │ ├── resources │ │ └── icon.ico │ ├── electron.d.ts │ ├── constant.ts │ ├── env.d.ts │ ├── utils │ │ ├── locales.ts │ │ ├── windowUtil.ts │ │ ├── file.ts │ │ ├── index.ts │ │ ├── aes.ts │ │ └── zip.ts │ ├── config.ts │ └── services │ │ └── CacheService.ts └── renderer │ ├── src │ ├── assets │ │ ├── images │ │ │ ├── avatar.png │ │ │ ├── logo.png │ │ │ ├── apps │ │ │ │ ├── nm.webp │ │ │ │ ├── devv.png │ │ │ │ ├── doubao.png │ │ │ │ ├── felo.png │ │ │ │ ├── gemini.png │ │ │ │ ├── hika.webp │ │ │ │ ├── kimi.jpg │ │ │ │ ├── poe.webp │ │ │ │ ├── wanzhi.jpg │ │ │ │ ├── yuewen.png │ │ │ │ ├── zhihu.png │ │ │ │ ├── baidu-ai.png │ │ │ │ ├── genspark.jpg │ │ │ │ ├── metaso.webp │ │ │ │ ├── qingyan.png │ │ │ │ ├── tiangong.png │ │ │ │ ├── yuanbao.png │ │ │ │ ├── duckduckgo.webp │ │ │ │ ├── perplexity.webp │ │ │ │ ├── sensetime.png │ │ │ │ ├── sparkdesk.png │ │ │ │ ├── thinkany.webp │ │ │ │ ├── baixiaoying.webp │ │ │ │ ├── github-copilot.webp │ │ │ │ ├── huggingchat.svg │ │ │ │ └── bolt.svg │ │ │ ├── models │ │ │ │ ├── 360.png │ │ │ │ ├── ai21.png │ │ │ │ ├── dbrx.png │ │ │ │ ├── flux.png │ │ │ │ ├── grok.png │ │ │ │ ├── ibm.png │ │ │ │ ├── jina.png │ │ │ │ ├── luma.png │ │ │ │ ├── palm.png │ │ │ │ ├── qwen.png │ │ │ │ ├── step.png │ │ │ │ ├── suno.png │ │ │ │ ├── tele.png │ │ │ │ ├── vidu.png │ │ │ │ ├── yi.png │ │ │ │ ├── adept.png │ │ │ │ ├── aimass.png │ │ │ │ ├── claude.png │ │ │ │ ├── cohere.png │ │ │ │ ├── dalle.png │ │ │ │ ├── doubao.png │ │ │ │ ├── gemini.png │ │ │ │ ├── gemma.png │ │ │ │ ├── google.png │ │ │ │ ├── gpt_4.png │ │ │ │ ├── gpt_o1.png │ │ │ │ ├── gryphe.png │ │ │ │ ├── hailuo.png │ │ │ │ ├── keling.png │ │ │ │ ├── llama.png │ │ │ │ ├── llava.png │ │ │ │ ├── magic.png │ │ │ │ ├── nvidia.png │ │ │ │ ├── wenxin.png │ │ │ │ ├── zhipu.png │ │ │ │ ├── 360_dark.png │ │ │ │ ├── ai21_dark.png │ │ │ │ ├── baichuan.png │ │ │ │ ├── bigcode.png │ │ │ │ ├── bigcode.webp │ │ │ │ ├── chatglm.png │ │ │ │ ├── chatgpt.jpeg │ │ │ │ ├── codegeex.png │ │ │ │ ├── cohere.webp │ │ │ │ ├── copilot.png │ │ │ │ ├── dbrx_dark.png │ │ │ │ ├── deepseek.png │ │ │ │ ├── dianxin.png │ │ │ │ ├── embedding.png │ │ │ │ ├── flux_dark.png │ │ │ │ ├── gpt_3.5.png │ │ │ │ ├── gpt_dark.png │ │ │ │ ├── grok_dark.png │ │ │ │ ├── hunyuan.png │ │ │ │ ├── ibm_dark.png │ │ │ │ ├── internlm.png │ │ │ │ ├── jina_dark.png │ │ │ │ ├── luma_dark.png │ │ │ │ ├── mediatek.png │ │ │ │ ├── microsoft.png │ │ │ │ ├── minicpm.webp │ │ │ │ ├── minimax.png │ │ │ │ ├── mixtral.png │ │ │ │ ├── moonshot.png │ │ │ │ ├── palm_dark.png │ │ │ │ ├── pixtral.png │ │ │ │ ├── qwen_dark.png │ │ │ │ ├── rakutenai.png │ │ │ │ ├── sparkdesk.png │ │ │ │ ├── stability.png │ │ │ │ ├── step_dark.png │ │ │ │ ├── suno_dark.png │ │ │ │ ├── tele_dark.png │ │ │ │ ├── upstage.png │ │ │ │ ├── vidu_dark.png │ │ │ │ ├── yi_dark.png │ │ │ │ ├── adept_dark.png │ │ │ │ ├── aimass_dark.png │ │ │ │ ├── aisingapore.png │ │ │ │ ├── claude_dark.png │ │ │ │ ├── cohere_dark.png │ │ │ │ ├── dalle_dark.png │ │ │ │ ├── doubao_dark.png │ │ │ │ ├── flashaudio.png │ │ │ │ ├── gemini_dark.png │ │ │ │ ├── gemma_dark.png │ │ │ │ ├── gryphe_dark.png │ │ │ │ ├── hailuo_dark.png │ │ │ │ ├── huggingface.png │ │ │ │ ├── keling_dark.png │ │ │ │ ├── llama_dark.png │ │ │ │ ├── llava_dark.png │ │ │ │ ├── magic_dark.png │ │ │ │ ├── midjourney.png │ │ │ │ ├── nvidia_dark.png │ │ │ │ ├── wenxin_dark.png │ │ │ │ ├── zhipu_dark.png │ │ │ │ ├── baichuan_dark.png │ │ │ │ ├── bigcode_dark.png │ │ │ │ ├── bigcode_dark.webp │ │ │ │ ├── chatglm_dark.png │ │ │ │ ├── codegeex_dark.png │ │ │ │ ├── copilot_dark.png │ │ │ │ ├── deepseek_dark.png │ │ │ │ ├── dianxin_dark.png │ │ │ │ ├── hunyuan_dark.png │ │ │ │ ├── internlm_dark.png │ │ │ │ ├── mediatek_dark.png │ │ │ │ ├── microsoft_dark.png │ │ │ │ ├── minimax_dark.png │ │ │ │ ├── mixtral_dark.png │ │ │ │ ├── moonshot_dark.png │ │ │ │ ├── nousresearch.png │ │ │ │ ├── pixtral_dark.png │ │ │ │ ├── rakutenai_dark.png │ │ │ │ ├── sparkdesk_dark.png │ │ │ │ ├── stability_dark.png │ │ │ │ ├── upstage_dark.png │ │ │ │ ├── aisingapore_dark.png │ │ │ │ ├── flashaudio_dark.png │ │ │ │ ├── huggingface_dark.png │ │ │ │ └── midjourney_dark.png │ │ │ ├── providers │ │ │ │ ├── doubao.png │ │ │ │ ├── gemini.png │ │ │ │ ├── github.png │ │ │ │ ├── google.png │ │ │ │ ├── grok.png │ │ │ │ ├── groq.png │ │ │ │ ├── jina.png │ │ │ │ ├── lepton.png │ │ │ │ ├── nvidia.png │ │ │ │ ├── ollama.png │ │ │ │ ├── openai.png │ │ │ │ ├── step.png │ │ │ │ ├── zhipu.png │ │ │ │ ├── aihubmix.jpg │ │ │ │ ├── baichuan.png │ │ │ │ ├── bailian.png │ │ │ │ ├── deepseek.png │ │ │ │ ├── minimax.png │ │ │ │ ├── mistral.png │ │ │ │ ├── moonshot.png │ │ │ │ ├── ocoolai.png │ │ │ │ ├── openai.jpeg │ │ │ │ ├── silicon.png │ │ │ │ ├── together.png │ │ │ │ ├── zero-one.png │ │ │ │ ├── anthropic.png │ │ │ │ ├── bytedance.png │ │ │ │ ├── dashscope.png │ │ │ │ ├── fireworks.png │ │ │ │ ├── graph-rag.png │ │ │ │ ├── hyperbolic.png │ │ │ │ ├── infini-ai.png │ │ │ │ ├── mixedbread.png │ │ │ │ ├── openrouter.png │ │ │ │ ├── perplexity.png │ │ │ │ └── volcengine.png │ │ │ └── paintings │ │ │ │ ├── image-size-1-2.svg │ │ │ │ ├── image-size-3-2.svg │ │ │ │ ├── image-size-3-4.svg │ │ │ │ ├── image-size-16-9.svg │ │ │ │ ├── image-size-9-16.svg │ │ │ │ └── image-size-1-1.svg │ │ ├── fonts │ │ │ ├── ubuntu │ │ │ │ ├── Ubuntu-Bold.ttf │ │ │ │ ├── Ubuntu-Light.ttf │ │ │ │ ├── Ubuntu-Italic.ttf │ │ │ │ ├── Ubuntu-Medium.ttf │ │ │ │ ├── Ubuntu-Regular.ttf │ │ │ │ ├── Ubuntu-BoldItalic.ttf │ │ │ │ ├── Ubuntu-LightItalic.ttf │ │ │ │ ├── Ubuntu-MediumItalic.ttf │ │ │ │ └── ubuntu.css │ │ │ └── icon-fonts │ │ │ │ ├── iconfont.woff2 │ │ │ │ └── iconfont.css │ │ └── styles │ │ │ ├── scrollbar.scss │ │ │ └── ant.scss │ ├── hooks │ │ ├── useAvatar.ts │ │ ├── useModel.ts │ │ ├── useRuntime.ts │ │ ├── useOllama.ts │ │ ├── useScrollPosition.ts │ │ ├── useStore.ts │ │ ├── useAgents.ts │ │ ├── useBridge.ts │ │ ├── useSettings.ts │ │ └── usePaintings.ts │ ├── main.tsx │ ├── config │ │ ├── env.ts │ │ ├── constant.ts │ │ ├── tools.ts │ │ └── prompts.ts │ ├── components │ │ ├── Icons │ │ │ ├── CopyIcon.tsx │ │ │ ├── VisionIcon.tsx │ │ │ └── WebSearchIcon.tsx │ │ ├── Avatar │ │ │ └── ModelAvatar.tsx │ │ ├── EmojiPicker │ │ │ └── index.tsx │ │ ├── IndicatorLight.tsx │ │ ├── ModelTags.tsx │ │ ├── Popups │ │ │ ├── TemplatePopup.tsx │ │ │ ├── SearchPopup.tsx │ │ │ ├── AppStorePopover.tsx │ │ │ └── PromptPopup.tsx │ │ ├── Scrollbar │ │ │ └── index.tsx │ │ └── ListItem │ │ │ └── index.tsx │ ├── providers │ │ ├── index.d.ts │ │ └── ProviderFactory.ts │ ├── pages │ │ ├── home │ │ │ ├── Markdown │ │ │ │ ├── Link.tsx │ │ │ │ ├── SvgPreview.tsx │ │ │ │ ├── Mermaid.tsx │ │ │ │ ├── Artifacts.tsx │ │ │ │ └── ImagePreview.tsx │ │ │ ├── Messages │ │ │ │ ├── MessageError.tsx │ │ │ │ ├── NarrowLayout.tsx │ │ │ │ ├── MessageErrorBoundary.tsx │ │ │ │ ├── Prompt.tsx │ │ │ │ ├── MessageAttachments.tsx │ │ │ │ └── MessageContent.tsx │ │ │ ├── Inputbar │ │ │ │ ├── SendMessageButton.tsx │ │ │ │ ├── AttachmentPreview.tsx │ │ │ │ └── AttachmentButton.tsx │ │ │ ├── Chat.tsx │ │ │ └── components │ │ │ │ └── SelectModelButton.tsx │ │ ├── agents │ │ │ └── components │ │ │ │ └── AddAgentCard.tsx │ │ ├── settings │ │ │ └── ProviderSettings │ │ │ │ ├── OllamaSettings.tsx │ │ │ │ └── GraphRAGSettings.tsx │ │ └── apps │ │ │ └── App.tsx │ ├── services │ │ ├── ProviderService.ts │ │ ├── NavigationService.ts │ │ ├── ModelService.ts │ │ ├── ImageStorage.ts │ │ ├── EventService.ts │ │ └── TranslateService.ts │ ├── init.ts │ ├── utils │ │ ├── oauth.ts │ │ ├── export.ts │ │ ├── style.ts │ │ └── formula.ts │ ├── env.d.ts │ ├── i18n │ │ └── index.ts │ ├── databases │ │ └── index.ts │ ├── store │ │ ├── paintings.ts │ │ ├── index.ts │ │ └── agents.ts │ └── context │ │ ├── ThemeProvider.tsx │ │ └── AntdProvider.tsx │ └── index.html ├── .prettierignore ├── .prettierrc ├── docs └── sponsor.md ├── tsconfig.json ├── resources └── js │ ├── utils.js │ └── bridge.js ├── .editorconfig ├── dev-app-update.yml ├── tsconfig.web.json ├── tsconfig.node.json ├── .gitignore ├── .yarn └── patches │ ├── @llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch │ ├── openai-npm-4.76.2-8ff1374617.patch │ ├── @llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch │ ├── @llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch │ ├── pdf-parse-npm-1.1.1-04a6109b2a.patch │ └── @llm-tools-embedjs-utils-npm-0.1.25-fd8fe8a193.patch ├── .eslintrc.cjs ├── scripts ├── notarize.js ├── utils.js ├── version.js ├── build-npm.js ├── replace-spaces.js ├── after-pack.js └── remove-locales.js ├── .github └── ISSUE_TEMPLATE │ ├── #1_feature_request.yml │ ├── #2_question.yml │ ├── 1_feature_request.yml │ ├── 2_question.yml │ ├── #0_bug_report.yml │ └── 0_bug_report.yml ├── CONTRIBUTING.md ├── electron.vite.config.ts └── CODE_OF_CONDUCT.md /packages/database/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/artifacts/README.md: -------------------------------------------------------------------------------- 1 | # Cherry Studio Artifacts 2 | -------------------------------------------------------------------------------- /packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | !data/.gitkeep 3 | 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | scripts/cloudflare-worker.js 6 | -------------------------------------------------------------------------------- /api/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/api/screenshot.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableImmutableInstalls: false 2 | 3 | httpTimeout: 300000 4 | 5 | nodeLinker: node-modules 6 | -------------------------------------------------------------------------------- /api/screenshot-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/api/screenshot-chat.png -------------------------------------------------------------------------------- /packages/database/README.md: -------------------------------------------------------------------------------- 1 | # Cherry Studio Database 2 | 3 | Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取 4 | -------------------------------------------------------------------------------- /src/main/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/main/resources/icon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | CHANGELOG*.md 8 | agents.json 9 | -------------------------------------------------------------------------------- /packages/database/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/packages/database/.yarn/install-state.gz -------------------------------------------------------------------------------- /src/renderer/src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/nm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/nm.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/devv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/devv.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/doubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/doubao.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/felo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/felo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/gemini.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/hika.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/hika.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/kimi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/kimi.jpg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/poe.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/poe.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/wanzhi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/wanzhi.jpg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/yuewen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/yuewen.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/zhihu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/zhihu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/360.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/ai21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/ai21.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dbrx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dbrx.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/flux.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/grok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/grok.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/ibm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/ibm.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/jina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/jina.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/luma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/luma.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/palm.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/qwen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/qwen.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/step.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/suno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/suno.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/tele.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/tele.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/vidu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/vidu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/yi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/yi.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/baidu-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/baidu-ai.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/genspark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/genspark.jpg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/metaso.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/metaso.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/qingyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/qingyan.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/tiangong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/tiangong.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/yuanbao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/yuanbao.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/adept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/adept.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/aimass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/aimass.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/claude.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/cohere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/cohere.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dalle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dalle.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/doubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/doubao.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gemini.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gemma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gemma.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/google.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gpt_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gpt_4.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gpt_o1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gpt_o1.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gryphe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gryphe.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/hailuo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/hailuo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/keling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/keling.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/llama.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/llava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/llava.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/magic.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/nvidia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/nvidia.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/wenxin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/wenxin.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/zhipu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/zhipu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/duckduckgo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/duckduckgo.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/perplexity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/perplexity.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/sensetime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/sensetime.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/sparkdesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/sparkdesk.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/thinkany.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/thinkany.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/360_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/360_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/ai21_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/ai21_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/baichuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/baichuan.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/bigcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/bigcode.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/bigcode.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/bigcode.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/chatglm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/chatglm.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/chatgpt.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/chatgpt.jpeg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/codegeex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/codegeex.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/cohere.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/cohere.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/copilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/copilot.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dbrx_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dbrx_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/deepseek.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dianxin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dianxin.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/embedding.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/flux_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/flux_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gpt_3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gpt_3.5.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gpt_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gpt_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/grok_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/grok_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/hunyuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/hunyuan.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/ibm_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/ibm_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/internlm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/internlm.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/jina_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/jina_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/luma_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/luma_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/mediatek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/mediatek.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/microsoft.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/minicpm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/minicpm.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/minimax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/minimax.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/mixtral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/mixtral.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/moonshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/moonshot.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/palm_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/palm_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/pixtral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/pixtral.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/qwen_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/qwen_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/rakutenai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/rakutenai.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/sparkdesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/sparkdesk.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/stability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/stability.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/step_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/step_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/suno_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/suno_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/tele_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/tele_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/upstage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/upstage.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/vidu_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/vidu_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/yi_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/yi_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/doubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/doubao.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/gemini.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/github.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/google.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/grok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/grok.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/groq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/groq.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/jina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/jina.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/lepton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/lepton.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/nvidia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/nvidia.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/ollama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/ollama.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/openai.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/step.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/zhipu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/zhipu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-Bold.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-Light.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/baixiaoying.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/baixiaoying.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/adept_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/adept_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/aimass_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/aimass_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/aisingapore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/aisingapore.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/claude_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/claude_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/cohere_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/cohere_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dalle_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dalle_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/doubao_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/doubao_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/flashaudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/flashaudio.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gemini_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gemini_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gemma_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gemma_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/gryphe_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/gryphe_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/hailuo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/hailuo_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/huggingface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/huggingface.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/keling_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/keling_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/llama_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/llama_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/llava_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/llava_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/magic_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/magic_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/midjourney.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/midjourney.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/nvidia_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/nvidia_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/wenxin_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/wenxin_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/zhipu_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/zhipu_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/aihubmix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/aihubmix.jpg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/baichuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/baichuan.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/bailian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/bailian.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/deepseek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/deepseek.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/minimax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/minimax.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/mistral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/mistral.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/moonshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/moonshot.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/ocoolai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/ocoolai.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/openai.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/openai.jpeg -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/silicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/silicon.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/together.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/together.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/zero-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/zero-one.png -------------------------------------------------------------------------------- /src/main/electron.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace Electron { 3 | interface App { 4 | isQuitting: boolean 5 | } 6 | } 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2 -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-Italic.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-Medium.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-Regular.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/github-copilot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/apps/github-copilot.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/baichuan_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/baichuan_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/bigcode_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/bigcode_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/bigcode_dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/bigcode_dark.webp -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/chatglm_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/chatglm_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/codegeex_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/codegeex_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/copilot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/copilot_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/deepseek_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/deepseek_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/dianxin_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/dianxin_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/hunyuan_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/hunyuan_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/internlm_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/internlm_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/mediatek_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/mediatek_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/microsoft_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/microsoft_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/minimax_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/minimax_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/mixtral_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/mixtral_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/moonshot_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/moonshot_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/nousresearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/nousresearch.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/pixtral_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/pixtral_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/rakutenai_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/rakutenai_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/sparkdesk_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/sparkdesk_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/stability_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/stability_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/upstage_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/upstage_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/anthropic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/anthropic.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/bytedance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/bytedance.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/dashscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/dashscope.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/fireworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/fireworks.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/graph-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/graph-rag.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/hyperbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/hyperbolic.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/infini-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/infini-ai.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/mixedbread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/mixedbread.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/openrouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/openrouter.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/perplexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/perplexity.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/providers/volcengine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/providers/volcengine.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 120, 5 | "trailingComma": "none", 6 | "endOfLine": "lf", 7 | "bracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-BoldItalic.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/aisingapore_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/aisingapore_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/flashaudio_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/flashaudio_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/huggingface_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/huggingface_dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/images/models/midjourney_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/images/models/midjourney_dark.png -------------------------------------------------------------------------------- /src/main/constant.ts: -------------------------------------------------------------------------------- 1 | export const isMac = process.platform === 'darwin' 2 | export const isWin = process.platform === 'win32' 3 | export const isLinux = process.platform === 'linux' 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-LightItalic.ttf -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/Ubuntu-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufeiping/api-for-cherrystudio/HEAD/src/renderer/src/assets/fonts/ubuntu/Ubuntu-MediumItalic.ttf -------------------------------------------------------------------------------- /docs/sponsor.md: -------------------------------------------------------------------------------- 1 | # Sponsor 2 | 3 |
4 | Buy Me a Coffee 5 |
6 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useAvatar.ts: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '@renderer/store' 2 | 3 | export default function useAvatar() { 4 | return useAppSelector((state) => state.runtime.avatar) 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.web.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /resources/js/utils.js: -------------------------------------------------------------------------------- 1 | export function getQueryParam(paramName) { 2 | const url = new URL(window.location.href) 3 | const params = new URLSearchParams(url.search) 4 | return params.get(paramName) 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/main/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | VITE_MAIN_BUNDLE_ID: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } 10 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | # provider: generic 2 | # url: http://127.0.0.1:8080 3 | # updaterCacheDirName: cherry-studio-updater 4 | # provider: github 5 | # repo: cherry-studio 6 | # owner: kangfenmao 7 | provider: generic 8 | url: https://cherrystudio.ocool.online 9 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './assets/styles/index.scss' 2 | import './init' 3 | 4 | import ReactDOM from 'react-dom/client' 5 | 6 | import App from './App' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render() 9 | -------------------------------------------------------------------------------- /src/renderer/src/config/env.ts: -------------------------------------------------------------------------------- 1 | export { default as UserAvatar } from '@renderer/assets/images/avatar.png' 2 | export { default as AppLogo } from '@renderer/assets/images/logo.png' 3 | 4 | export const APP_NAME = 'Cherry Studio' 5 | export const isLocalAi = false 6 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useModel.ts: -------------------------------------------------------------------------------- 1 | import { useProviders } from './useProvider' 2 | 3 | export function useModel(id?: string) { 4 | const { providers } = useProviders() 5 | const allModels = providers.map((p) => p.models).flat() 6 | return allModels.find((m) => m.id === id) 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/src/components/Icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | const CopyIcon: FC, HTMLElement>> = (props) => { 4 | return 5 | } 6 | 7 | export default CopyIcon 8 | -------------------------------------------------------------------------------- /api/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | 3 | export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { 4 | const apiKey = req.headers['x-api-key'] 5 | 6 | if (!apiKey || apiKey !== process.env.API_KEY) { 7 | return res.status(401).json({ error: 'Unauthorized' }) 8 | } 9 | 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cherry-studio/database", 3 | "packageManager": "yarn@4.3.1", 4 | "dependencies": { 5 | "@cherry-studio/database": "file:", 6 | "csv-parser": "^3.0.0", 7 | "sqlite3": "^5.1.7" 8 | }, 9 | "scripts": { 10 | "agents": "node src/agents.js", 11 | "email": "yarn csv && node src/email.js", 12 | "csv": "node src/csv.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/providers/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Assistant, Metrics } from '@renderer/types' 2 | 3 | interface ChunkCallbackData { 4 | text?: string 5 | usage?: OpenAI.Completions.CompletionUsage 6 | metrics?: Metrics 7 | } 8 | 9 | interface CompletionsParams { 10 | messages: Message[] 11 | assistant: Assistant 12 | onChunk: ({ text, usage }: ChunkCallbackData) => void 13 | onFilterMessages: (messages: Message[]) => void 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Markdown/Link.tsx: -------------------------------------------------------------------------------- 1 | import { omit } from 'lodash' 2 | import React from 'react' 3 | 4 | const Link: React.FC = (props: React.AnchorHTMLAttributes) => { 5 | if (props.href?.startsWith('#')) { 6 | return {props.children} 7 | } 8 | 9 | return e.stopPropagation()} /> 10 | } 11 | 12 | export default Link 13 | -------------------------------------------------------------------------------- /src/renderer/src/services/ProviderService.ts: -------------------------------------------------------------------------------- 1 | import i18n from '@renderer/i18n' 2 | import store from '@renderer/store' 3 | 4 | export function getProviderName(id: string) { 5 | const provider = store.getState().llm.providers.find((p) => p.id === id) 6 | if (!provider) { 7 | return '' 8 | } 9 | 10 | if (provider.isSystem) { 11 | return i18n.t(`provider.${provider.id}`, { defaultValue: provider.name }) 12 | } 13 | 14 | return provider?.name 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/init.ts: -------------------------------------------------------------------------------- 1 | import KeyvStorage from '@kangfenmao/keyv-storage' 2 | 3 | import { startAutoSync } from './services/BackupService' 4 | import store from './store' 5 | 6 | function initKeyv() { 7 | window.keyv = new KeyvStorage() 8 | window.keyv.init() 9 | } 10 | 11 | function initAutoSync() { 12 | const { webdavAutoSync } = store.getState().settings 13 | if (webdavAutoSync) { 14 | startAutoSync() 15 | } 16 | } 17 | 18 | initKeyv() 19 | initAutoSync() 20 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/**/*", 5 | "src/preload/*.d.ts", 6 | "local/src/renderer/**/*", 7 | "packages/shared/**/*" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "jsx": "react-jsx", 12 | "baseUrl": ".", 13 | "paths": { 14 | "@renderer/*": ["src/renderer/src/*"], 15 | "@shared/*": ["packages/shared/*"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Markdown/SvgPreview.tsx: -------------------------------------------------------------------------------- 1 | const SvgPreview = ({ children }: { children: string }) => { 2 | return ( 3 |
13 | ) 14 | } 15 | 16 | export default SvgPreview 17 | -------------------------------------------------------------------------------- /.vscode/snippet.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Function component": { 3 | "prefix": "rnfc", 4 | "body": [ 5 | "import { FC } from 'react'", 6 | "import styled from 'styled-components'", 7 | "", 8 | "const $1: FC = () => {", 9 | " return ", 10 | "}", 11 | "", 12 | "const Container = styled.div``", 13 | "", 14 | "export default $1", 15 | "" 16 | ], 17 | "description": "Function component" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/services/NavigationService.ts: -------------------------------------------------------------------------------- 1 | import { NavigateFunction } from 'react-router-dom' 2 | 3 | interface INavigationService { 4 | navigate: NavigateFunction | null 5 | setNavigate: (navigateFunc: NavigateFunction) => void 6 | } 7 | 8 | const NavigationService: INavigationService = { 9 | navigate: null, 10 | 11 | setNavigate: (navigateFunc: NavigateFunction): void => { 12 | NavigationService.navigate = navigateFunc 13 | } 14 | } 15 | 16 | export default NavigationService 17 | -------------------------------------------------------------------------------- /src/renderer/src/components/Icons/VisionIcon.tsx: -------------------------------------------------------------------------------- 1 | import { EyeOutlined } from '@ant-design/icons' 2 | import React, { FC } from 'react' 3 | import styled from 'styled-components' 4 | 5 | const VisionIcon: FC, HTMLElement>> = (props) => { 6 | return 7 | } 8 | 9 | const Icon = styled(EyeOutlined)` 10 | color: var(--color-primary); 11 | font-size: 14px; 12 | margin-left: 4px; 13 | ` 14 | 15 | export default VisionIcon 16 | -------------------------------------------------------------------------------- /src/main/utils/locales.ts: -------------------------------------------------------------------------------- 1 | import EnUs from '../../renderer/src/i18n/locales/en-us.json' 2 | import JaJP from '../../renderer/src/i18n/locales/ja-jp.json' 3 | import RuRu from '../../renderer/src/i18n/locales/ru-ru.json' 4 | import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json' 5 | import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json' 6 | 7 | const locales = { 8 | 'en-US': EnUs, 9 | 'zh-CN': ZhCn, 10 | 'zh-TW': ZhTw, 11 | 'ja-JP': JaJP, 12 | 'ru-RU': RuRu 13 | } 14 | 15 | export { locales } 16 | -------------------------------------------------------------------------------- /src/renderer/src/components/Icons/WebSearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalOutlined } from '@ant-design/icons' 2 | import React, { FC } from 'react' 3 | import styled from 'styled-components' 4 | 5 | const WebSearchIcon: FC, HTMLElement>> = (props) => { 6 | return 7 | } 8 | 9 | const Icon = styled(GlobalOutlined)` 10 | color: var(--color-link); 11 | font-size: 12px; 12 | margin-left: 4px; 13 | ` 14 | 15 | export default WebSearchIcon 16 | -------------------------------------------------------------------------------- /src/main/utils/windowUtil.ts: -------------------------------------------------------------------------------- 1 | function isTilingWindowManager() { 2 | if (process.platform === 'darwin') { 3 | return false 4 | } 5 | 6 | if (process.platform !== 'linux') { 7 | return true 8 | } 9 | 10 | const desktopEnv = process.env.XDG_CURRENT_DESKTOP?.toLowerCase() 11 | const tilingSystems = ['hyprland', 'i3', 'sway', 'bspwm', 'dwm', 'awesome', 'qtile', 'herbstluftwm', 'xmonad'] 12 | 13 | return tilingSystems.some((system) => desktopEnv?.includes(system)) 14 | } 15 | 16 | export { isTilingWindowManager } 17 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useRuntime.ts: -------------------------------------------------------------------------------- 1 | import i18n from '@renderer/i18n' 2 | import store, { useAppSelector } from '@renderer/store' 3 | 4 | export function useRuntime() { 5 | return useAppSelector((state) => state.runtime) 6 | } 7 | 8 | export function modelGenerating() { 9 | const generating = store.getState().runtime.generating 10 | 11 | if (generating) { 12 | window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' }) 13 | return Promise.reject() 14 | } 15 | 16 | return Promise.resolve() 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/src/services/ModelService.ts: -------------------------------------------------------------------------------- 1 | import store from '@renderer/store' 2 | import { Model } from '@renderer/types' 3 | import { pick } from 'lodash' 4 | 5 | export const getModelUniqId = (m?: Model) => { 6 | return m?.id ? JSON.stringify(pick(m, ['id', 'provider'])) : '' 7 | } 8 | 9 | export const hasModel = (m?: Model) => { 10 | const allModels = store 11 | .getState() 12 | .llm.providers.filter((p) => p.enabled) 13 | .map((p) => p.models) 14 | .flat() 15 | 16 | return allModels.find((model) => model.id === m?.id) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/config.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | import { getDataPath } from './utils' 4 | 5 | const isDev = process.env.NODE_ENV === 'development' 6 | 7 | if (isDev) { 8 | app.setPath('userData', app.getPath('userData') + 'Dev') 9 | } 10 | 11 | export const DATA_PATH = getDataPath() 12 | 13 | export const titleBarOverlayDark = { 14 | height: 40, 15 | color: '#00000000', 16 | symbolColor: '#ffffff' 17 | } 18 | 19 | export const titleBarOverlayLight = { 20 | height: 40, 21 | color: '#00000000', 22 | symbolColor: '#000000' 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/config/constant.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TEMPERATURE = 0.7 2 | export const DEFAULT_CONTEXTCOUNT = 5 3 | export const DEFAULT_MAX_TOKENS = 4096 4 | export const FONT_FAMILY = 5 | "Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif" 6 | 7 | export const platform = window.electron?.process?.platform 8 | export const isMac = platform === 'darwin' 9 | export const isWindows = platform === 'win32' || platform === 'win64' 10 | export const isLinux = platform === 'linux' 11 | -------------------------------------------------------------------------------- /src/main/utils/file.ts: -------------------------------------------------------------------------------- 1 | import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant' 2 | import { FileTypes } from '@types' 3 | 4 | export function getFileType(ext: string): FileTypes { 5 | ext = ext.toLowerCase() 6 | if (imageExts.includes(ext)) return FileTypes.IMAGE 7 | if (videoExts.includes(ext)) return FileTypes.VIDEO 8 | if (audioExts.includes(ext)) return FileTypes.AUDIO 9 | if (textExts.includes(ext)) return FileTypes.TEXT 10 | if (documentExts.includes(ext)) return FileTypes.DOCUMENT 11 | return FileTypes.OTHER 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/src/utils/oauth.ts: -------------------------------------------------------------------------------- 1 | export const oauthWithSiliconFlow = async (setKey) => { 2 | const clientId = 'SFrugiu0ezVmREv8BAU6GV' 3 | const ACCOUNT_ENDPOINT = 'https://account.siliconflow.cn' 4 | const authUrl = `${ACCOUNT_ENDPOINT}/oauth?client_id=${clientId}` 5 | const popup = window.open(authUrl, 'oauthPopup', 'width=600,height=600') 6 | window.addEventListener('message', (event) => { 7 | if (event.data.length > 0 && event.data[0]['secretKey'] !== undefined) { 8 | setKey(event.data[0]['secretKey']) 9 | popup?.close() 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Markdown/Mermaid.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | 3 | import MermaidPopup from './MermaidPopup' 4 | 5 | interface Props { 6 | chart: string 7 | } 8 | 9 | const Mermaid: React.FC = ({ chart }) => { 10 | useEffect(() => { 11 | window?.mermaid?.contentLoaded() 12 | }, []) 13 | 14 | const onPreview = () => { 15 | MermaidPopup.show({ chart }) 16 | } 17 | 18 | return ( 19 |
20 | {chart} 21 |
22 | ) 23 | } 24 | 25 | export default Mermaid 26 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type KeyvStorage from '@kangfenmao/keyv-storage' 4 | import { MessageInstance } from 'antd/es/message/interface' 5 | import { HookAPI } from 'antd/es/modal/useModal' 6 | 7 | interface ImportMetaEnv { 8 | VITE_RENDERER_INTEGRATED_MODEL: string 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv 13 | } 14 | 15 | declare global { 16 | interface Window { 17 | root: HTMLElement 18 | message: MessageInstance 19 | modal: HookAPI 20 | keyv: KeyvStorage 21 | mermaid: any 22 | store: any 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/MessageError.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from '@renderer/types' 2 | import { Alert } from 'antd' 3 | import { t } from 'i18next' 4 | import { FC } from 'react' 5 | 6 | import Markdown from '../Markdown/Markdown' 7 | 8 | const MessageError: FC<{ message: Message }> = ({ message }) => { 9 | return ( 10 | <> 11 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default MessageError 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "electron.vite.config.*", 5 | "src/main/**/*", 6 | "src/preload/**/*", 7 | "src/main/env.d.ts", 8 | "src/renderer/src/types/index.ts", 9 | "packages/shared/**/*" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "types": [ 14 | "electron-vite/node" 15 | ], 16 | "baseUrl": ".", 17 | "paths": { 18 | "@main/*": ["src/main/*"], 19 | "@types": ["src/renderer/src/types/index.ts"], 20 | "@shared/*": ["packages/shared/*"] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | import { app } from 'electron' 5 | 6 | export function getResourcePath() { 7 | return path.join(app.getAppPath(), 'resources') 8 | } 9 | 10 | export function getDataPath() { 11 | const dataPath = path.join(app.getPath('userData'), 'Data') 12 | if (!fs.existsSync(dataPath)) { 13 | fs.mkdirSync(dataPath, { recursive: true }) 14 | } 15 | return dataPath 16 | } 17 | 18 | export function getInstanceName(baseURL: string) { 19 | try { 20 | return new URL(baseURL).host.split('.')[0] 21 | } catch (error) { 22 | return '' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/artifacts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cherry-studio/artifacts", 3 | "version": "0.1.0", 4 | "description": "Cherry Studio Artifacts", 5 | "main": "index.js", 6 | "homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts", 7 | "publishConfig": { 8 | "access": "public", 9 | "registry": "https://registry.npmjs.org/" 10 | }, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "artifacts" 16 | ], 17 | "author": "kangfenmao", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@cherry-studio/artifacts": "file:" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log* 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | .yarn-cache 10 | 11 | # Editor directories and files 12 | .vscode/* 13 | !.vscode/extensions.json 14 | .idea 15 | .DS_Store 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | 22 | # Yarn 23 | .pnp.* 24 | .yarn/* 25 | !.yarn/patches 26 | !.yarn/plugins 27 | !.yarn/releases 28 | !.yarn/sdks 29 | !.yarn/versions 30 | 31 | # Windows 32 | Thumbs.db 33 | 34 | # Project 35 | node_modules 36 | dist 37 | out 38 | build/icons 39 | stats.html 40 | 41 | # ENV 42 | .env 43 | .env.* 44 | 45 | # Local 46 | local 47 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | interface Props { 4 | disabled: boolean 5 | sendMessage: () => void 6 | } 7 | 8 | const SendMessageButton: FC = ({ disabled, sendMessage }) => { 9 | return ( 10 | 21 | ) 22 | } 23 | 24 | export default SendMessageButton 25 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/NarrowLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useSettings } from '@renderer/hooks/useSettings' 2 | import { FC, HTMLAttributes } from 'react' 3 | import styled from 'styled-components' 4 | 5 | interface Props extends HTMLAttributes { 6 | children: React.ReactNode 7 | } 8 | 9 | const NarrowLayout: FC = ({ children, ...props }) => { 10 | const { narrowMode } = useSettings() 11 | 12 | if (narrowMode) { 13 | return {children} 14 | } 15 | 16 | return children 17 | } 18 | 19 | const Container = styled.div` 20 | max-width: 800px; 21 | width: 100%; 22 | margin: 0 auto; 23 | ` 24 | 25 | export default NarrowLayout 26 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useOllama.ts: -------------------------------------------------------------------------------- 1 | import store, { useAppSelector } from '@renderer/store' 2 | import { setOllamaKeepAliveTime } from '@renderer/store/llm' 3 | import { useDispatch } from 'react-redux' 4 | 5 | export function useOllamaSettings() { 6 | const settings = useAppSelector((state) => state.llm.settings.ollama) 7 | const dispatch = useDispatch() 8 | 9 | return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) } 10 | } 11 | 12 | export function getOllamaSettings() { 13 | return store.getState().llm.settings.ollama 14 | } 15 | 16 | export function getOllamaKeepAliveTime() { 17 | return store.getState().llm.settings.ollama.keepAliveTime + 'm' 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useScrollPosition.ts: -------------------------------------------------------------------------------- 1 | import { throttle } from 'lodash' 2 | import { useEffect, useRef } from 'react' 3 | 4 | export default function useScrollPosition(key: string) { 5 | const containerRef = useRef(null) 6 | const scrollKey = `scroll:${key}` 7 | 8 | const handleScroll = throttle(() => { 9 | const position = containerRef.current?.scrollTop ?? 0 10 | window.keyv.set(scrollKey, position) 11 | }, 100) 12 | 13 | useEffect(() => { 14 | const scroll = () => containerRef.current?.scrollTo({ top: window.keyv.get(scrollKey) || 0 }) 15 | scroll() 16 | setTimeout(scroll, 50) 17 | }, [scrollKey]) 18 | 19 | return { containerRef, handleScroll } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-1-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-3-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-3-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-16-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-9-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/paintings/image-size-1-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/renderer/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | 4 | import enUS from './locales/en-us.json' 5 | import jaJP from './locales/ja-jp.json' 6 | import ruRU from './locales/ru-ru.json' 7 | import zhCN from './locales/zh-cn.json' 8 | import zhTW from './locales/zh-tw.json' 9 | 10 | const resources = { 11 | 'en-US': enUS, 12 | 'zh-CN': zhCN, 13 | 'zh-TW': zhTW, 14 | 'ja-JP': jaJP, 15 | 'ru-RU': ruRU 16 | } 17 | 18 | i18n.use(initReactI18next).init({ 19 | resources, 20 | lng: localStorage.getItem('language') || navigator.language || 'en-US', 21 | fallbackLng: 'en-US', 22 | interpolation: { 23 | escapeValue: false 24 | } 25 | }) 26 | 27 | export default i18n 28 | -------------------------------------------------------------------------------- /src/renderer/src/components/Avatar/ModelAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { getModelLogo } from '@renderer/config/models' 2 | import { Model } from '@renderer/types' 3 | import { Avatar, AvatarProps } from 'antd' 4 | import { first } from 'lodash' 5 | import { FC } from 'react' 6 | 7 | interface Props { 8 | model: Model 9 | size: number 10 | props?: AvatarProps 11 | } 12 | 13 | const ModelAvatar: FC = ({ model, size, props }) => { 14 | return ( 15 | 19 | {first(model?.name)} 20 | 21 | ) 22 | } 23 | 24 | export default ModelAvatar 25 | -------------------------------------------------------------------------------- /.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/core/rag-embedding.js b/src/core/rag-embedding.js 2 | index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d2560e6afc1 100644 3 | --- a/src/core/rag-embedding.js 4 | +++ b/src/core/rag-embedding.js 5 | @@ -1,10 +1,8 @@ 6 | export class RAGEmbedding { 7 | static singleton; 8 | static async init(embeddingModel) { 9 | - if (!this.singleton) { 10 | - await embeddingModel.init(); 11 | - this.singleton = new RAGEmbedding(embeddingModel); 12 | - } 13 | + await embeddingModel.init(); 14 | + this.singleton = new RAGEmbedding(embeddingModel); 15 | } 16 | static getInstance() { 17 | return RAGEmbedding.singleton; 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['unused-imports', 'simple-import-sort'], 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:react/jsx-runtime', 7 | 'plugin:react-hooks/recommended', 8 | '@electron-toolkit/eslint-config-ts/recommended', 9 | '@electron-toolkit/eslint-config-prettier' 10 | ], 11 | rules: { 12 | '@typescript-eslint/explicit-function-return-type': 'off', 13 | '@typescript-eslint/no-explicit-any': 'off', 14 | 'unused-imports/no-unused-imports': 'error', 15 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 16 | 'react/prop-types': 'off', 17 | 'simple-import-sort/imports': 'error', 18 | 'simple-import-sort/exports': 'error', 19 | 'react/no-is-mounted': 'off' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/providers/ProviderFactory.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@renderer/types' 2 | 3 | import AnthropicProvider from './AnthropicProvider' 4 | import BaseProvider from './BaseProvider' 5 | import GeminiProvider from './GeminiProvider' 6 | import OpenAIProvider from './OpenAIProvider' 7 | 8 | export default class ProviderFactory { 9 | static create(provider: Provider): BaseProvider { 10 | switch (provider.type) { 11 | case 'anthropic': 12 | return new AnthropicProvider(provider) 13 | case 'gemini': 14 | return new GeminiProvider(provider) 15 | default: 16 | return new OpenAIProvider(provider) 17 | } 18 | } 19 | } 20 | 21 | export function isOpenAIProvider(provider: Provider) { 22 | return !['anthropic', 'gemini'].includes(provider.type) 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/config/tools.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '@renderer/types' 2 | import { ChatCompletionTool } from 'openai/resources' 3 | 4 | import { isWebSearchModel } from './models' 5 | 6 | export function getWebSearchTools(model: Model): ChatCompletionTool[] { 7 | if (model && model.provider === 'zhipu') { 8 | if (isWebSearchModel(model)) { 9 | if (model.id === 'glm-4-alltools') { 10 | return [ 11 | { 12 | type: 'web_browser' 13 | } as unknown as ChatCompletionTool 14 | ] 15 | } 16 | return [ 17 | { 18 | type: 'web_search', 19 | web_search: { 20 | enable: true, 21 | search_result: true 22 | } 23 | } as unknown as ChatCompletionTool 24 | ] 25 | } 26 | } 27 | 28 | return [] 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/components/EmojiPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@renderer/context/ThemeProvider' 2 | import { FC, useEffect, useRef } from 'react' 3 | 4 | interface Props { 5 | onEmojiClick: (emoji: string) => void 6 | } 7 | 8 | const EmojiPicker: FC = ({ onEmojiClick }) => { 9 | const { theme } = useTheme() 10 | const ref = useRef(null) 11 | 12 | useEffect(() => { 13 | if (ref.current) { 14 | ref.current.addEventListener('emoji-click', (event: any) => { 15 | event.stopPropagation() 16 | onEmojiClick(event.detail.emoji.unicode) 17 | }) 18 | } 19 | }, [onEmojiClick]) 20 | 21 | // @ts-ignore next-line 22 | return 23 | } 24 | 25 | export default EmojiPicker 26 | -------------------------------------------------------------------------------- /scripts/notarize.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { notarize } = require('@electron/notarize') 3 | 4 | exports.default = async function notarizing(context) { 5 | if (context.electronPlatformName !== 'darwin') { 6 | return 7 | } 8 | 9 | if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) { 10 | console.log('Skipping notarization') 11 | return 12 | } 13 | 14 | const appName = context.packager.appInfo.productFilename 15 | const appPath = `${context.appOutDir}/${appName}.app` 16 | 17 | await notarize({ 18 | appPath, 19 | appBundleId: 'com.kangfenmao.CherryStudio', 20 | appleId: process.env.APPLE_ID, 21 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, 22 | teamId: process.env.APPLE_TEAM_ID 23 | }) 24 | 25 | console.log(' • Notarized app:', appPath) 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/huggingchat.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/src/services/ImageStorage.ts: -------------------------------------------------------------------------------- 1 | import db from '@renderer/databases' 2 | import { convertToBase64 } from '@renderer/utils' 3 | 4 | const IMAGE_PREFIX = 'image://' 5 | 6 | export default class ImageStorage { 7 | static async set(key: string, file: File) { 8 | const id = IMAGE_PREFIX + key 9 | try { 10 | const base64Image = await convertToBase64(file) 11 | if (typeof base64Image === 'string') { 12 | if (await db.settings.get(id)) { 13 | db.settings.update(id, { value: base64Image }) 14 | return 15 | } 16 | await db.settings.add({ id, value: base64Image }) 17 | } 18 | } catch (error) { 19 | console.error('Error storing the image', error) 20 | } 21 | } 22 | 23 | static async get(key: string): Promise { 24 | const id = IMAGE_PREFIX + key 25 | return (await db.settings.get(id))?.value 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/components/IndicatorLight.tsx: -------------------------------------------------------------------------------- 1 | // src/renderer/src/components/IndicatorLight.tsx 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | 5 | interface IndicatorLightProps { 6 | color: string 7 | } 8 | 9 | const Light = styled.div<{ color: string }>` 10 | width: 8px; 11 | height: 8px; 12 | border-radius: 50%; 13 | background-color: ${({ color }) => color}; 14 | box-shadow: 0 0 6px ${({ color }) => color}; 15 | animation: pulse 2s infinite; 16 | 17 | @keyframes pulse { 18 | 0% { 19 | opacity: 1; 20 | } 21 | 50% { 22 | opacity: 0.6; 23 | } 24 | 100% { 25 | opacity: 1; 26 | } 27 | } 28 | ` 29 | 30 | const IndicatorLight: React.FC = ({ color }) => { 31 | const actualColor = color === 'green' ? '#22c55e' : color 32 | return 33 | } 34 | 35 | export default IndicatorLight 36 | -------------------------------------------------------------------------------- /src/main/utils/aes.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | 3 | // 定义密钥和初始化向量(IV) 4 | const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr' 5 | const iv = Buffer.from('Cherry Studio', 'hex') 6 | 7 | // 加密函数 8 | export function encrypt(text: string): { iv: string; encryptedData: string } { 9 | const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv) 10 | let encrypted = cipher.update(text, 'utf8', 'hex') 11 | encrypted += cipher.final('hex') 12 | return { 13 | iv: iv.toString('hex'), 14 | encryptedData: encrypted 15 | } 16 | } 17 | 18 | // 解密函数 19 | export function decrypt(encryptedData: string, iv: string): string { 20 | const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex')) 21 | let decrypted = decipher.update(encryptedData, 'hex', 'utf8') 22 | decrypted += decipher.final('utf8') 23 | return decrypted 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#1_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 功能建议 2 | description: 为项目提出新的想法 3 | title: '[功能]: ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢您花时间提出新的功能建议! 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: 您的功能建议是否与某个问题相关? 15 | description: 请简明扼要地描述您遇到的问题 16 | placeholder: 我总是感到沮丧,因为... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: 请描述您希望实现的解决方案 24 | description: 请简明扼要地描述您希望发生的情况 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: alternatives 30 | attributes: 31 | label: 请描述您考虑过的其他方案 32 | description: 请简明扼要地描述您考虑过的任何其他解决方案或功能 33 | 34 | - type: textarea 35 | id: additional 36 | attributes: 37 | label: 其他补充信息 38 | description: 在此添加任何其他与功能建议相关的上下文或截图 39 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useStore.ts: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from '@renderer/store' 2 | import { setShowAssistants, setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings' 3 | 4 | export function useShowAssistants() { 5 | const showAssistants = useAppSelector((state) => state.settings.showAssistants) 6 | const dispatch = useAppDispatch() 7 | 8 | return { 9 | showAssistants, 10 | setShowAssistants: (show: boolean) => dispatch(setShowAssistants(show)), 11 | toggleShowAssistants: () => dispatch(toggleShowAssistants()) 12 | } 13 | } 14 | 15 | export function useShowTopics() { 16 | const showTopics = useAppSelector((state) => state.settings.showTopics) 17 | const dispatch = useAppDispatch() 18 | 19 | return { 20 | showTopics, 21 | setShowTopics: (show: boolean) => dispatch(setShowTopics(show)), 22 | toggleShowTopics: () => dispatch(toggleShowTopics()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/services/EventService.ts: -------------------------------------------------------------------------------- 1 | import Emittery from 'emittery' 2 | 3 | export const EventEmitter = new Emittery() 4 | 5 | export const EVENT_NAMES = { 6 | SEND_MESSAGE: 'SEND_MESSAGE', 7 | RECEIVE_MESSAGE: 'RECEIVE_MESSAGE', 8 | AI_AUTO_RENAME: 'AI_AUTO_RENAME', 9 | CLEAR_MESSAGES: 'CLEAR_MESSAGES', 10 | ADD_ASSISTANT: 'ADD_ASSISTANT', 11 | EDIT_MESSAGE: 'EDIT_MESSAGE', 12 | REGENERATE_MESSAGE: 'REGENERATE_MESSAGE', 13 | CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED', 14 | ESTIMATED_TOKEN_COUNT: 'ESTIMATED_TOKEN_COUNT', 15 | SHOW_ASSISTANTS: 'SHOW_ASSISTANTS', 16 | SHOW_CHAT_SETTINGS: 'SHOW_CHAT_SETTINGS', 17 | SHOW_TOPIC_SIDEBAR: 'SHOW_TOPIC_SIDEBAR', 18 | SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR', 19 | NEW_CONTEXT: 'NEW_CONTEXT', 20 | NEW_BRANCH: 'NEW_BRANCH', 21 | EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE', 22 | LOCATE_MESSAGE: 'LOCATE_MESSAGE', 23 | ADD_NEW_TOPIC: 'ADD_NEW_TOPIC', 24 | RESEND_MESSAGE: 'RESEND_MESSAGE' 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "search.exclude": { 7 | "**/dist/**": true 8 | }, 9 | "[javascript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[typescript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[typescriptreact]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[json]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[jsonc]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[css]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[scss]": { 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | }, 30 | "[markdown]": { 31 | "files.trimTrailingWhitespace": false 32 | }, 33 | "i18n-ally.localesPaths": ["src/renderer/src/i18n"] 34 | } 35 | -------------------------------------------------------------------------------- /packages/database/src/email.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose() 2 | 3 | // 连接到数据库 4 | const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => { 5 | if (err) { 6 | console.error('Error connecting to the database:', err.message) 7 | return 8 | } 9 | }) 10 | 11 | // 查询数据并转换为JSON 12 | db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => { 13 | if (err) { 14 | console.error('Error querying the database:', err.message) 15 | return 16 | } 17 | 18 | for (const row of rows) { 19 | console.log(row.email) 20 | // Update row set sent = 1 21 | db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => { 22 | if (err) { 23 | console.error('Error updating the database:', err.message) 24 | return 25 | } 26 | }) 27 | } 28 | 29 | // 关闭数据库连接 30 | db.close((err) => { 31 | if (err) { 32 | console.error('Error closing the database:', err.message) 33 | return 34 | } 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /resources/js/bridge.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | let messageId = 0 3 | const pendingCalls = new Map() 4 | 5 | function api(method, ...args) { 6 | const id = messageId++ 7 | return new Promise((resolve, reject) => { 8 | pendingCalls.set(id, { resolve, reject }) 9 | window.parent.postMessage({ id, type: 'api-call', method, args }, '*') 10 | }) 11 | } 12 | 13 | window.addEventListener('message', (event) => { 14 | if (event.data.type === 'api-response') { 15 | const { id, result, error } = event.data 16 | const pendingCall = pendingCalls.get(id) 17 | if (pendingCall) { 18 | if (error) { 19 | pendingCall.reject(new Error(error)) 20 | } else { 21 | pendingCall.resolve(result) 22 | } 23 | pendingCalls.delete(id) 24 | } 25 | } 26 | }) 27 | 28 | window.api = new Proxy( 29 | {}, 30 | { 31 | get: (target, prop) => { 32 | return (...args) => api(prop, ...args) 33 | } 34 | } 35 | ) 36 | })() 37 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cherry-studio-api", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "API service for Cherry Studio knowledge base", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "dev": "tsx watch src/index.ts", 9 | "build": "tsc", 10 | "start": "node dist/index.js" 11 | }, 12 | "dependencies": { 13 | "@llm-tools/embedjs": "^0.1.25", 14 | "@llm-tools/embedjs-libsql": "^0.1.25", 15 | "@llm-tools/embedjs-openai": "^0.1.25", 16 | "cherry-studio-api": "file:", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.0.3", 19 | "express": "^4.18.2", 20 | "express-rate-limit": "^6.7.0", 21 | "fs-extra": "^11.2.0", 22 | "helmet": "^6.0.1", 23 | "level": "^9.0.0", 24 | "openai": "^4.28.0", 25 | "sqlite3": "^5.1.7" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.13", 29 | "@types/express": "^4.17.17", 30 | "@types/fs-extra": "^11.0.4", 31 | "@types/node": "^18.15.11", 32 | "tsx": "^4.7.1", 33 | "typescript": "^5.0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#2_question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ 提问 2 | description: 提出一个问题或寻求帮助 3 | title: '[问题]: ' 4 | labels: ['question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。 10 | 11 | - type: textarea 12 | id: question 13 | attributes: 14 | label: 您的问题 15 | description: 请详细描述您的问题 16 | placeholder: 请尽可能清楚地说明您的问题... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: context 22 | attributes: 23 | label: 相关背景 24 | description: 请提供一些背景信息,帮助我们更好地理解您的问题 25 | placeholder: 例如:使用场景、已尝试的解决方案等 26 | 27 | - type: textarea 28 | id: additional 29 | attributes: 30 | label: 补充信息 31 | description: 任何其他相关的信息、截图或代码示例 32 | render: shell 33 | 34 | - type: dropdown 35 | id: priority 36 | attributes: 37 | label: 优先级 38 | description: 这个问题对您来说有多紧急? 39 | options: 40 | - 低 (有空再看) 41 | - 中 (希望尽快得到答复) 42 | - 高 (阻碍工作进行) 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /.yarn/patches/openai-npm-4.76.2-8ff1374617.patch: -------------------------------------------------------------------------------- 1 | diff --git a/core.js b/core.js 2 | index 30c91e66bf595a66c09eb3dbcbda7d58154865f5..b511ff24ea1891904c60174c6ed26ecdd4d5ac51 100644 3 | --- a/core.js 4 | +++ b/core.js 5 | @@ -156,7 +156,7 @@ class APIClient { 6 | Accept: 'application/json', 7 | 'Content-Type': 'application/json', 8 | 'User-Agent': this.getUserAgent(), 9 | - ...getPlatformHeaders(), 10 | + // ...getPlatformHeaders(), 11 | ...this.authHeaders(opts), 12 | }; 13 | } 14 | diff --git a/core.mjs b/core.mjs 15 | index ac267bcfcff44b1f7c9bea5513bba94726a31795..dd5bd9f29609d3f0eea4bd5b225f302893df14ad 100644 16 | --- a/core.mjs 17 | +++ b/core.mjs 18 | @@ -149,7 +149,7 @@ export class APIClient { 19 | Accept: 'application/json', 20 | 'Content-Type': 'application/json', 21 | 'User-Agent': this.getUserAgent(), 22 | - ...getPlatformHeaders(), 23 | + // ...getPlatformHeaders(), 24 | ...this.authHeaders(opts), 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/databases/index.ts: -------------------------------------------------------------------------------- 1 | import { FileType, KnowledgeItem, Topic } from '@renderer/types' 2 | import { Dexie, type EntityTable } from 'dexie' 3 | 4 | // Database declaration (move this to its own module also) 5 | export const db = new Dexie('CherryStudio') as Dexie & { 6 | files: EntityTable 7 | topics: EntityTable, 'id'> 8 | settings: EntityTable<{ id: string; value: any }, 'id'> 9 | knowledge_notes: EntityTable 10 | } 11 | 12 | db.version(1).stores({ 13 | files: 'id, name, origin_name, path, size, ext, type, created_at, count' 14 | }) 15 | 16 | db.version(2).stores({ 17 | files: 'id, name, origin_name, path, size, ext, type, created_at, count', 18 | topics: '&id, messages', 19 | settings: '&id, value' 20 | }) 21 | 22 | db.version(3).stores({ 23 | files: 'id, name, origin_name, path, size, ext, type, created_at, count', 24 | topics: '&id, messages', 25 | settings: '&id, value', 26 | knowledge_notes: '&id, baseId, type, content, created_at, updated_at' 27 | }) 28 | 29 | export default db 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /api/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os' 2 | import path from 'node:path' 3 | 4 | import dotenv from 'dotenv' 5 | 6 | dotenv.config() 7 | 8 | // 获取用户数据目录 9 | const getUserDataPath = () => { 10 | const platform = process.platform 11 | 12 | switch (platform) { 13 | case 'win32': 14 | // Windows: C:\Users\\AppData\Roaming\CherryStudio 15 | return path.join(process.env.APPDATA || '', 'CherryStudio') 16 | case 'darwin': 17 | // macOS: ~/Library/Application Support/CherryStudio 18 | return path.join(os.homedir(), 'Library/Application Support/CherryStudio') 19 | default: 20 | // Linux: ~/.config/CherryStudio 21 | return path.join(os.homedir(), '.config/CherryStudio') 22 | } 23 | } 24 | 25 | const config = { 26 | port: process.env.PORT || 3000, 27 | userDataPath: process.env.DATA_PATH || getUserDataPath(), 28 | openaiApiKey: process.env.OPENAI_API_KEY, 29 | openaiBaseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', 30 | rateLimit: { 31 | windowMs: 15 * 60 * 1000, 32 | max: 100 33 | } 34 | } 35 | 36 | export default config 37 | -------------------------------------------------------------------------------- /src/renderer/src/utils/export.ts: -------------------------------------------------------------------------------- 1 | import db from '@renderer/databases' 2 | import { Message, Topic } from '@renderer/types' 3 | 4 | export const messageToMarkdown = (message: Message) => { 5 | const roleText = message.role === 'user' ? '🧑‍💻 User' : '🤖 Assistant' 6 | const titleSection = `### ${roleText}` 7 | const contentSection = message.content 8 | 9 | return [titleSection, '', contentSection].join('\n') 10 | } 11 | 12 | export const messagesToMarkdown = (messages: Message[]) => { 13 | return messages.map((message) => messageToMarkdown(message)).join('\n\n---\n\n') 14 | } 15 | 16 | export const topicToMarkdown = async (topic: Topic) => { 17 | const topicName = `# ${topic.name}` 18 | const topicMessages = await db.topics.get(topic.id) 19 | 20 | if (topicMessages) { 21 | return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages) 22 | } 23 | 24 | return '' 25 | } 26 | 27 | export const exportTopicAsMarkdown = async (topic: Topic) => { 28 | const fileName = topic.name + '.md' 29 | const markdown = await topicToMarkdown(topic) 30 | window.api.file.save(fileName, markdown) 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/scrollbar.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-scrollbar-thumb: rgba(255, 255, 255, 0.15); 3 | --color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2); 4 | --color-scrollbar-thumb-right: rgba(255, 255, 255, 0.18); 5 | --color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.25); 6 | } 7 | 8 | body[theme-mode='light'] { 9 | --color-scrollbar-thumb: rgba(0, 0, 0, 0.15); 10 | --color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.2); 11 | --color-scrollbar-thumb-right: rgba(0, 0, 0, 0.18); 12 | --color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.25); 13 | } 14 | 15 | /* 全局初始化滚动条样式 */ 16 | ::-webkit-scrollbar { 17 | width: 6px; 18 | height: 6px; 19 | } 20 | 21 | ::-webkit-scrollbar-track { 22 | background: transparent; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb { 26 | border-radius: 10px; 27 | background: var(--color-scrollbar-thumb); 28 | &:hover { 29 | background: var(--color-scrollbar-thumb-hover); 30 | } 31 | } 32 | 33 | pre::-webkit-scrollbar-thumb { 34 | border-radius: 0; 35 | background: rgba(0, 0, 0, 0.08); 36 | &:hover { 37 | background: rgba(0, 0, 0, 0.15); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/MessageErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'antd' 2 | import React from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | 5 | interface Props { 6 | fallback?: React.ReactNode 7 | children: React.ReactNode 8 | } 9 | 10 | interface State { 11 | hasError: boolean 12 | } 13 | 14 | const ErrorFallback = ({ fallback }: { fallback?: React.ReactNode }) => { 15 | const { t } = useTranslation() 16 | return ( 17 | fallback || ( 18 | 19 | ) 20 | ) 21 | } 22 | 23 | class MessageErrorBoundary extends React.Component { 24 | constructor(props: Props) { 25 | super(props) 26 | this.state = { hasError: false } 27 | } 28 | 29 | static getDerivedStateFromError() { 30 | return { hasError: true } 31 | } 32 | 33 | render() { 34 | if (this.state.hasError) { 35 | return 36 | } 37 | return this.props.children 38 | } 39 | } 40 | 41 | export default MessageErrorBoundary 42 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useAgents.ts: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from '@renderer/store' 2 | import { addAgent, removeAgent, updateAgent, updateAgents, updateAgentSettings } from '@renderer/store/agents' 3 | import { Agent, AssistantSettings } from '@renderer/types' 4 | 5 | export function useAgents() { 6 | const agents = useAppSelector((state) => state.agents.agents) 7 | const dispatch = useAppDispatch() 8 | 9 | return { 10 | agents, 11 | updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents)), 12 | addAgent: (agent: Agent) => dispatch(addAgent(agent)), 13 | removeAgent: (id: string) => dispatch(removeAgent({ id })) 14 | } 15 | } 16 | 17 | export function useAgent(id: string) { 18 | const agent = useAppSelector((state) => state.agents.agents.find((a) => a.id === id) as Agent) 19 | const dispatch = useAppDispatch() 20 | 21 | return { 22 | agent, 23 | updateAgent: (agent: Agent) => dispatch(updateAgent(agent)), 24 | updateAgentSettings: (settings: Partial) => { 25 | dispatch(updateAgentSettings({ assistantId: agent.id, settings })) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/utils/zip.ts: -------------------------------------------------------------------------------- 1 | import util from 'node:util' 2 | import zlib from 'node:zlib' 3 | 4 | import logger from 'electron-log' 5 | 6 | // 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本 7 | const gzipPromise = util.promisify(zlib.gzip) 8 | const gunzipPromise = util.promisify(zlib.gunzip) 9 | 10 | /** 11 | * 压缩字符串 12 | * @param {string} string - 要压缩的 JSON 字符串 13 | * @returns {Promise} 压缩后的 Buffer 14 | */ 15 | export async function compress(str) { 16 | try { 17 | const buffer = Buffer.from(str, 'utf-8') 18 | const compressedBuffer = await gzipPromise(buffer) 19 | return compressedBuffer 20 | } catch (error) { 21 | logger.error('Compression failed:', error) 22 | throw error 23 | } 24 | } 25 | 26 | /** 27 | * 解压缩 Buffer 到 JSON 字符串 28 | * @param {Buffer} compressedBuffer - 压缩的 Buffer 29 | * @returns {Promise} 解压缩后的 JSON 字符串 30 | */ 31 | export async function decompress(compressedBuffer) { 32 | try { 33 | const buffer = await gunzipPromise(compressedBuffer) 34 | return buffer.toString('utf-8') 35 | } catch (error) { 36 | logger.error('Decompression failed:', error) 37 | throw error 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/ant.scss: -------------------------------------------------------------------------------- 1 | #inputbar { 2 | resize: none; 3 | } 4 | 5 | .ant-image-preview-switch-left { 6 | -webkit-app-region: no-drag; 7 | } 8 | 9 | .ant-btn:not(:disabled):focus-visible { 10 | outline: none; 11 | } 12 | 13 | .ant-segmented-group { 14 | gap: 4px; 15 | } 16 | 17 | .minapp-drawer { 18 | max-width: calc(100vw - var(--sidebar-width)); 19 | .ant-drawer-content-wrapper { 20 | box-shadow: none; 21 | } 22 | .ant-drawer-header { 23 | position: absolute; 24 | -webkit-app-region: drag; 25 | min-height: calc(var(--navbar-height) + 0.5px); 26 | width: calc(100vw - var(--sidebar-width)); 27 | margin-top: -0.5px; 28 | border-bottom: none; 29 | } 30 | .ant-drawer-body { 31 | padding: 0; 32 | margin-top: var(--navbar-height); 33 | overflow: hidden; 34 | @extend #content-container; 35 | } 36 | .minapp-mask { 37 | background-color: transparent !important; 38 | } 39 | } 40 | 41 | .ant-drawer-header { 42 | -webkit-app-region: no-drag; 43 | } 44 | 45 | .message-attachments { 46 | .ant-upload-list-item:hover { 47 | background-color: initial !important; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/markdown-loader.js b/src/markdown-loader.js 2 | index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644 3 | --- a/src/markdown-loader.js 4 | +++ b/src/markdown-loader.js 5 | @@ -1,5 +1,4 @@ 6 | import { micromark } from 'micromark'; 7 | -import { mdxJsx } from 'micromark-extension-mdx-jsx'; 8 | import { gfmHtml, gfm } from 'micromark-extension-gfm'; 9 | import createDebugMessages from 'debug'; 10 | import fs from 'node:fs'; 11 | @@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader { 12 | ? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body 13 | : await stream2buffer(fs.createReadStream(this.filePathOrUrl)); 14 | this.debug('MarkdownLoader stream created'); 15 | - const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] }); 16 | + const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] }); 17 | this.debug('Markdown parsed...'); 18 | const webLoader = new WebLoader({ 19 | urlOrContent: result, 20 | -------------------------------------------------------------------------------- /src/renderer/src/components/ModelTags.tsx: -------------------------------------------------------------------------------- 1 | import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' 2 | import { Model } from '@renderer/types' 3 | import { isFreeModel } from '@renderer/utils' 4 | import { Tag } from 'antd' 5 | import { FC } from 'react' 6 | import { useTranslation } from 'react-i18next' 7 | 8 | import VisionIcon from './Icons/VisionIcon' 9 | import WebSearchIcon from './Icons/WebSearchIcon' 10 | 11 | interface ModelTagsProps { 12 | model: Model 13 | showFree?: boolean 14 | } 15 | 16 | const ModelTags: FC = ({ model, showFree = true }) => { 17 | const { t } = useTranslation() 18 | return ( 19 | <> 20 | {isVisionModel(model) && } 21 | {isWebSearchModel(model) && } 22 | {showFree && isFreeModel(model) && ( 23 | 24 | {t('models.free')} 25 | 26 | )} 27 | {isEmbeddingModel(model) && ( 28 | 29 | {t('models.embedding')} 30 | 31 | )} 32 | 33 | ) 34 | } 35 | 36 | export default ModelTags 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Cherry Studio 贡献者指南 2 | 3 | 欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。 4 | 5 | ## 如何贡献 6 | 7 | 以下是您可以参与的几种方式: 8 | 9 | 1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。 10 | 11 | 2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。 12 | 13 | 3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。 14 | 15 | 4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。 16 | 17 | 5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。 18 | 19 | 6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。 20 | 21 | 7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。 22 | 23 | ## 开始贡献 24 | 25 | 1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。 26 | 27 | 2. **创建分支**:为您要进行的更改创建一个新的分支。 28 | 29 | 3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。 30 | 31 | 4. **发起 Pull Request**:将您的更改推送到 GitHub,并发起 Pull Request。请描述您的更改内容和原因。 32 | 33 | ### 其他建议 34 | 35 | - **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。 36 | - **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。 37 | 38 | ## 联系我们 39 | 40 | 如果您有任何问题或建议,欢迎通过以下方式联系我们: 41 | 42 | - 微信:kangfenmao 43 | - [GitHub Issues](https://github.com/kangfenmao/cherry-studio/issues) 44 | 45 | 感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。 46 | -------------------------------------------------------------------------------- /src/renderer/src/pages/agents/components/AddAgentCard.tsx: -------------------------------------------------------------------------------- 1 | import { PlusOutlined } from '@ant-design/icons' 2 | import { useTranslation } from 'react-i18next' 3 | import styled from 'styled-components' 4 | 5 | interface AddAgentCardProps { 6 | onClick: () => void 7 | className?: string 8 | } 9 | 10 | const AddAgentCard = ({ onClick, className }: AddAgentCardProps) => { 11 | const { t } = useTranslation() 12 | 13 | return ( 14 | 15 | 16 | {t('agents.add.title')} 17 | 18 | ) 19 | } 20 | 21 | const StyledCard = styled.div` 22 | width: 100%; 23 | height: 180px; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | background-color: var(--color-background); 29 | border-radius: 15px; 30 | border: 1px dashed var(--color-border); 31 | cursor: pointer; 32 | transition: all 0.3s ease; 33 | color: var(--color-text-soft); 34 | 35 | &:hover { 36 | border-color: var(--color-primary); 37 | color: var(--color-primary); 38 | } 39 | ` 40 | 41 | export default AddAgentCard 42 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const os = require('os') 4 | 5 | function downloadNpmPackage(packageName, url) { 6 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-')) 7 | 8 | const targetDir = path.join('./node_modules/', packageName) 9 | const filename = packageName.replace('/', '-') + '.tgz' 10 | 11 | // Skip if directory already exists 12 | if (fs.existsSync(targetDir)) { 13 | console.log(`${targetDir} already exists, skipping download...`) 14 | return 15 | } 16 | 17 | try { 18 | console.log(`Downloading ${packageName}...`, url) 19 | const { execSync } = require('child_process') 20 | execSync(`curl --fail -o ${filename} ${url}`) 21 | 22 | console.log(`Extracting ${filename}...`) 23 | execSync(`tar -xvf ${filename}`) 24 | execSync(`rm -rf ${filename}`) 25 | execSync(`mv package ${targetDir}`) 26 | } catch (error) { 27 | console.error(`Error processing ${packageName}: ${error.message}`) 28 | if (fs.existsSync(filename)) { 29 | fs.unlinkSync(filename) 30 | } 31 | throw error 32 | } 33 | 34 | fs.rmSync(tempDir, { recursive: true, force: true }) 35 | } 36 | 37 | module.exports = { 38 | downloadNpmPackage 39 | } 40 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const fs = require('fs') 3 | 4 | // 执行命令并返回输出 5 | function exec(command) { 6 | return execSync(command, { encoding: 'utf8' }).trim() 7 | } 8 | 9 | // 获取命令行参数 10 | const args = process.argv.slice(2) 11 | const versionType = args[0] || 'patch' 12 | const shouldPush = args.includes('push') 13 | 14 | // 验证版本类型 15 | if (!['patch', 'minor', 'major'].includes(versionType)) { 16 | console.error('Invalid version type. Use patch, minor, or major.') 17 | process.exit(1) 18 | } 19 | 20 | // 更新版本 21 | exec(`yarn version ${versionType} --immediate`) 22 | 23 | // 读取更新后的 package.json 获取新版本号 24 | const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) 25 | const newVersion = updatedPackageJson.version 26 | 27 | // Git 操作 28 | exec('git add .') 29 | exec(`git commit -m "chore(version): ${newVersion}"`) 30 | exec(`git tag -a v${newVersion} -m "Version ${newVersion}"`) 31 | 32 | console.log(`Version bumped to ${newVersion}`) 33 | 34 | if (shouldPush) { 35 | console.log('Pushing to remote...') 36 | exec('git push && git push --tags') 37 | console.log('Pushed to remote.') 38 | } else { 39 | console.log('Changes are committed locally. Use "git push && git push --tags" to push to remote.') 40 | } 41 | -------------------------------------------------------------------------------- /.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/libsql-db.js b/src/libsql-db.js 2 | index 58c42e4910bd0e53bc497ff9b9702b1f7a961266..250bc97c50a9b790e8798441d904d040f2d2af43 100644 3 | --- a/src/libsql-db.js 4 | +++ b/src/libsql-db.js 5 | @@ -41,9 +41,9 @@ export class LibSqlDb { 6 | } 7 | async similaritySearch(query, k) { 8 | const statement = `SELECT id, pageContent, uniqueLoaderId, source, metadata, 9 | - vector_distance_cos(vector, vector32('[${query.join(',')}]')) 10 | + vector_distance_cos(vector, vector32('[${query.join(',')}]')) as distance 11 | FROM ${this.tableName} 12 | - ORDER BY vector_distance_cos(vector, vector32('[${query.join(',')}]')) ASC 13 | + ORDER BY distance ASC 14 | LIMIT ${k};`; 15 | this.debug(`Executing statement - ${truncateCenterString(statement, 700)}`); 16 | const results = await this.client.execute(statement); 17 | @@ -52,7 +52,7 @@ export class LibSqlDb { 18 | return { 19 | metadata, 20 | pageContent: result.pageContent.toString(), 21 | - score: 1, 22 | + score: 1 - result.distance, 23 | }; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/src/services/TranslateService.ts: -------------------------------------------------------------------------------- 1 | import i18n from '@renderer/i18n' 2 | import store from '@renderer/store' 3 | 4 | import { fetchTranslate } from './ApiService' 5 | import { getDefaultTopic } from './AssistantService' 6 | import { getDefaultTranslateAssistant } from './AssistantService' 7 | import { getUserMessage } from './MessagesService' 8 | 9 | export const translateText = async (text: string, targetLanguage: string) => { 10 | const translateModel = store.getState().llm.translateModel 11 | 12 | if (!translateModel) { 13 | window.message.error({ 14 | content: i18n.t('translate.error.not_configured'), 15 | key: 'translate-message' 16 | }) 17 | return Promise.reject(new Error(i18n.t('translate.error.not_configured'))) 18 | } 19 | 20 | const assistant = getDefaultTranslateAssistant(targetLanguage, text) 21 | const message = getUserMessage({ 22 | assistant, 23 | topic: getDefaultTopic('default'), 24 | type: 'text', 25 | content: text 26 | }) 27 | 28 | const translatedText = await fetchTranslate({ message, assistant }) 29 | 30 | const trimmedText = translatedText.trim() 31 | 32 | if (!trimmedText) { 33 | return Promise.reject(new Error(i18n.t('translate.error.failed'))) 34 | } 35 | 36 | return trimmedText 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Suggest an idea for this project 3 | title: '[Feature]: ' 4 | labels: ['enhancement'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to suggest a new feature! 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: Is your feature request related to a problem? 15 | description: A clear and concise description of what the problem is 16 | placeholder: I'm always frustrated when... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: A clear and concise description of what you want to happen 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: alternatives 30 | attributes: 31 | label: Describe alternatives you've considered 32 | description: A clear and concise description of any alternative solutions or features you've considered 33 | 34 | - type: textarea 35 | id: additional 36 | attributes: 37 | label: Additional Context 38 | description: Add any other context or screenshots about the feature request here 39 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/Prompt.tsx: -------------------------------------------------------------------------------- 1 | import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' 2 | import { Assistant } from '@renderer/types' 3 | import { FC } from 'react' 4 | import { useTranslation } from 'react-i18next' 5 | import styled from 'styled-components' 6 | 7 | interface Props { 8 | assistant: Assistant 9 | } 10 | 11 | const Prompt: FC = ({ assistant }) => { 12 | const { t } = useTranslation() 13 | 14 | const prompt = assistant.prompt || t('chat.default.description') 15 | 16 | if (!prompt) { 17 | return null 18 | } 19 | 20 | return ( 21 | AssistantSettingsPopup.show({ assistant })}> 22 | {prompt} 23 | 24 | ) 25 | } 26 | 27 | const Container = styled.div` 28 | padding: 10px 20px; 29 | background-color: var(--color-background-soft); 30 | margin-bottom: 20px; 31 | margin: 4px 20px 0 20px; 32 | border-radius: 6px; 33 | cursor: pointer; 34 | border: 0.5px solid var(--color-border); 35 | ` 36 | 37 | const Text = styled.div` 38 | color: var(--color-text-2); 39 | font-size: 12px; 40 | display: -webkit-box; 41 | -webkit-line-clamp: 2; 42 | -webkit-box-orient: vertical; 43 | overflow: hidden; 44 | ` 45 | 46 | export default Prompt 47 | -------------------------------------------------------------------------------- /src/renderer/src/pages/settings/ProviderSettings/OllamaSettings.tsx: -------------------------------------------------------------------------------- 1 | import { useOllamaSettings } from '@renderer/hooks/useOllama' 2 | import { InputNumber } from 'antd' 3 | import { FC, useState } from 'react' 4 | import { useTranslation } from 'react-i18next' 5 | import styled from 'styled-components' 6 | 7 | import { SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..' 8 | 9 | const OllamSettings: FC = () => { 10 | const { keepAliveTime, setKeepAliveTime } = useOllamaSettings() 11 | const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime) 12 | const { t } = useTranslation() 13 | 14 | return ( 15 | 16 | {t('ollama.keep_alive_time.title')} 17 | setKeepAliveMinutes(Number(e))} 21 | onBlur={() => setKeepAliveTime(keepAliveMinutes)} 22 | suffix={t('ollama.keep_alive_time.placeholder')} 23 | step={5} 24 | /> 25 | 26 | {t('ollama.keep_alive_time.description')} 27 | 28 | 29 | ) 30 | } 31 | 32 | const Container = styled.div`` 33 | 34 | export default OllamSettings 35 | -------------------------------------------------------------------------------- /.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/pdf-parse.js b/lib/pdf-parse.js 2 | index 96bfbc705dcb4fb73cb077a75f02c115371b3477..6d02d2bb426063c3a31cb740c3d86841de162a22 100644 3 | --- a/lib/pdf-parse.js 4 | +++ b/lib/pdf-parse.js 5 | @@ -21,12 +21,12 @@ function render_page(pageData) { 6 | for (let item of textContent.items) { 7 | if (lastY == item.transform[5] || !lastY){ 8 | text += item.str; 9 | - } 10 | + } 11 | else{ 12 | text += '\n' + item.str; 13 | - } 14 | + } 15 | lastY = item.transform[5]; 16 | - } 17 | + } 18 | //let strings = textContent.items.map(item => item.str); 19 | //let text = strings.join("\n"); 20 | //text = text.replace(/[ ]+/ig," "); 21 | @@ -60,7 +60,7 @@ async function PDF(dataBuffer, options) { 22 | if (typeof options.version != 'string') options.version = DEFAULT_OPTIONS.version; 23 | if (options.version == 'default') options.version = DEFAULT_OPTIONS.version; 24 | 25 | - PDFJS = PDFJS ? PDFJS : require(`./pdf.js/${options.version}/build/pdf.js`); 26 | + PDFJS = PDFJS ? PDFJS : require(`./pdf.js/v1.10.100/build/pdf.js`); 27 | 28 | ret.version = PDFJS.version; 29 | 30 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/ubuntu/ubuntu.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Ubuntu'; 3 | src: url('Ubuntu-Regular.ttf') format('truetype'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Ubuntu'; 10 | src: url('Ubuntu-Italic.ttf') format('truetype'); 11 | font-weight: normal; 12 | font-style: italic; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Ubuntu'; 17 | src: url('Ubuntu-Bold.ttf') format('truetype'); 18 | font-weight: bold; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: 'Ubuntu'; 24 | src: url('Ubuntu-BoldItalic.ttf') format('truetype'); 25 | font-weight: bold; 26 | font-style: italic; 27 | } 28 | 29 | @font-face { 30 | font-family: 'Ubuntu'; 31 | src: url('Ubuntu-Light.ttf') format('truetype'); 32 | font-weight: 300; 33 | font-style: normal; 34 | } 35 | 36 | @font-face { 37 | font-family: 'Ubuntu'; 38 | src: url('Ubuntu-LightItalic.ttf') format('truetype'); 39 | font-weight: 300; 40 | font-style: italic; 41 | } 42 | 43 | @font-face { 44 | font-family: 'Ubuntu'; 45 | src: url('Ubuntu-Medium.ttf') format('truetype'); 46 | font-weight: 500; 47 | font-style: normal; 48 | } 49 | 50 | @font-face { 51 | font-family: 'Ubuntu'; 52 | src: url('Ubuntu-MediumItalic.ttf') format('truetype'); 53 | font-weight: 500; 54 | font-style: italic; 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useBridge.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export function useBridge() { 4 | useEffect(() => { 5 | const handleMessage = async (event: MessageEvent) => { 6 | const targetOrigin = { targetOrigin: '*' } 7 | 8 | try { 9 | if (event.origin !== 'file://') { 10 | return 11 | } 12 | 13 | const { type, method, args, id } = event.data 14 | 15 | if (type !== 'api-call' || !window.api) { 16 | return 17 | } 18 | 19 | const apiMethod = window.api[method] 20 | 21 | if (typeof apiMethod !== 'function') { 22 | return 23 | } 24 | 25 | event.source?.postMessage( 26 | { 27 | id, 28 | type: 'api-response', 29 | result: await apiMethod(...args) 30 | }, 31 | targetOrigin 32 | ) 33 | } catch (error) { 34 | event.source?.postMessage( 35 | { 36 | id: event.data?.id, 37 | type: 'api-response', 38 | error: error instanceof Error ? error.message : String(error) 39 | }, 40 | targetOrigin 41 | ) 42 | } 43 | } 44 | 45 | window.addEventListener('message', handleMessage) 46 | 47 | return () => { 48 | window.removeEventListener('message', handleMessage) 49 | } 50 | }, []) 51 | } 52 | -------------------------------------------------------------------------------- /packages/database/src/agents.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose() 2 | const fs = require('fs') 3 | 4 | // 连接到数据库 5 | const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => { 6 | if (err) { 7 | console.error('Error connecting to the database:', err.message) 8 | return 9 | } 10 | console.log('Connected to the database.') 11 | }) 12 | 13 | // 查询数据并转换为JSON 14 | db.all('SELECT * FROM agents', [], (err, rows) => { 15 | if (err) { 16 | console.error('Error querying the database:', err.message) 17 | return 18 | } 19 | 20 | // 将 ID 类型转换为字符串 21 | for (const row of rows) { 22 | row.id = row.id.toString() 23 | row.group = row.group.toString().split(',') 24 | row.group = row.group.map((item) => item.trim().replace('\r\n', '')) 25 | } 26 | 27 | // 将查询结果转换为JSON字符串 28 | const jsonData = JSON.stringify(rows, null, 2) 29 | 30 | // 将JSON数据写入文件 31 | fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => { 32 | if (err) { 33 | console.error('Error writing to file:', err.message) 34 | return 35 | } 36 | console.log('Data has been written to agents.json') 37 | }) 38 | 39 | // 关闭数据库连接 40 | db.close((err) => { 41 | if (err) { 42 | console.error('Error closing the database:', err.message) 43 | return 44 | } 45 | console.log('Database connection closed.') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/renderer/src/utils/style.ts: -------------------------------------------------------------------------------- 1 | type ClassValue = string | number | boolean | undefined | null | ClassDictionary | ClassArray 2 | 3 | interface ClassDictionary { 4 | [id: string]: any 5 | } 6 | 7 | interface ClassArray extends Array {} 8 | 9 | // Example: 10 | // classNames('foo', 'bar'); // => 'foo bar' 11 | // classNames('foo', { bar: true }); // => 'foo bar' 12 | // classNames({ foo: true, bar: false }); // => 'foo' 13 | // classNames(['foo', 'bar']); // => 'foo bar' 14 | // classNames('foo', null, 'bar'); // => 'foo bar' 15 | // classNames({ message: true, 'message-assistant': true }); // => 'message message-assistant' 16 | 17 | /** 18 | * 生成 class 字符串 19 | * @param args 20 | * @returns 21 | */ 22 | export function classNames(...args: ClassValue[]): string { 23 | const classes: string[] = [] 24 | 25 | args.forEach((arg) => { 26 | if (!arg) return 27 | 28 | if (typeof arg === 'string' || typeof arg === 'number') { 29 | classes.push(arg.toString()) 30 | } else if (Array.isArray(arg)) { 31 | const inner = classNames(...arg) 32 | if (inner) { 33 | classes.push(inner) 34 | } 35 | } else if (typeof arg === 'object') { 36 | Object.entries(arg).forEach(([key, value]) => { 37 | if (value) { 38 | classes.push(key) 39 | } 40 | }) 41 | } 42 | }) 43 | 44 | return classes.filter(Boolean).join(' ') 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/src/pages/apps/App.tsx: -------------------------------------------------------------------------------- 1 | import MinApp from '@renderer/components/MinApp' 2 | import { MinAppType } from '@renderer/types' 3 | import { FC } from 'react' 4 | import styled from 'styled-components' 5 | 6 | interface Props { 7 | app: MinAppType 8 | onClick?: () => void 9 | size?: number 10 | } 11 | 12 | const App: FC = ({ app, onClick, size = 60 }) => { 13 | const handleClick = () => { 14 | MinApp.start(app) 15 | onClick?.() 16 | } 17 | 18 | return ( 19 | 20 | 28 | {app.name} 29 | 30 | ) 31 | } 32 | 33 | const Container = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: center; 37 | align-items: center; 38 | cursor: pointer; 39 | overflow: hidden; 40 | ` 41 | 42 | const AppIcon = styled.img` 43 | border-radius: 16px; 44 | user-select: none; 45 | -webkit-user-drag: none; 46 | ` 47 | 48 | const AppTitle = styled.div` 49 | font-size: 12px; 50 | margin-top: 5px; 51 | color: var(--color-text-soft); 52 | text-align: center; 53 | user-select: none; 54 | white-space: nowrap; 55 | ` 56 | 57 | export default App 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_question.yml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Ask a question or seek help 3 | title: '[Question]: ' 4 | labels: ['question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for asking a question! Please provide as much detail as possible so we can better assist you. 10 | 11 | - type: textarea 12 | id: question 13 | attributes: 14 | label: Your Question 15 | description: Please describe your question in detail 16 | placeholder: Please explain your question as clearly as possible... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: context 22 | attributes: 23 | label: Context 24 | description: Please provide some background information to help us better understand your question 25 | placeholder: "For example: use case, solutions you've tried, etc." 26 | 27 | - type: textarea 28 | id: additional 29 | attributes: 30 | label: Additional Information 31 | description: Any other relevant information, screenshots, or code examples 32 | render: shell 33 | 34 | - type: dropdown 35 | id: priority 36 | attributes: 37 | label: Priority 38 | description: How urgent is this question for you? 39 | options: 40 | - Low (Can wait) 41 | - Medium (Would like a response soon) 42 | - High (Blocking progress) 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import store, { useAppDispatch, useAppSelector } from '@renderer/store' 2 | import { 3 | SendMessageShortcut, 4 | setSendMessageShortcut as _setSendMessageShortcut, 5 | setTheme, 6 | SettingsState, 7 | setTopicPosition, 8 | setTray, 9 | setWindowStyle 10 | } from '@renderer/store/settings' 11 | import { ThemeMode } from '@renderer/types' 12 | 13 | export function useSettings() { 14 | const settings = useAppSelector((state) => state.settings) 15 | const dispatch = useAppDispatch() 16 | 17 | return { 18 | ...settings, 19 | setSendMessageShortcut(shortcut: SendMessageShortcut) { 20 | dispatch(_setSendMessageShortcut(shortcut)) 21 | }, 22 | setTray(isActive: boolean) { 23 | dispatch(setTray(isActive)) 24 | }, 25 | setTheme(theme: ThemeMode) { 26 | dispatch(setTheme(theme)) 27 | }, 28 | setWindowStyle(windowStyle: 'transparent' | 'opaque') { 29 | dispatch(setWindowStyle(windowStyle)) 30 | }, 31 | setTopicPosition(topicPosition: 'left' | 'right') { 32 | dispatch(setTopicPosition(topicPosition)) 33 | } 34 | } 35 | } 36 | 37 | export function useMessageStyle() { 38 | const { messageStyle } = useSettings() 39 | const isBubbleStyle = messageStyle === 'bubble' 40 | 41 | return { 42 | isBubbleStyle 43 | } 44 | } 45 | 46 | export const getStoreSetting = (key: keyof SettingsState) => { 47 | return store.getState().settings[key] 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/src/pages/settings/ProviderSettings/GraphRAGSettings.tsx: -------------------------------------------------------------------------------- 1 | import MinApp from '@renderer/components/MinApp' 2 | import { MinAppType, Provider } from '@renderer/types' 3 | import { Button } from 'antd' 4 | import { FC } from 'react' 5 | import { useTranslation } from 'react-i18next' 6 | import styled from 'styled-components' 7 | 8 | import { SettingSubtitle } from '..' 9 | 10 | interface Props { 11 | provider: Provider 12 | } 13 | 14 | const GraphRAGSettings: FC = ({ provider }) => { 15 | const apiUrl = provider.apiHost 16 | const modalId = provider.models.filter((model) => model.id.includes('global'))[0]?.id 17 | const { t } = useTranslation() 18 | 19 | const onShowGraphRAG = async () => { 20 | const { appPath } = await window.api.getAppInfo() 21 | const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}` 22 | 23 | const app: MinAppType = { 24 | name: t('words.knowledgeGraph'), 25 | logo: '', 26 | url 27 | } 28 | 29 | MinApp.start(app) 30 | } 31 | 32 | if (!modalId) { 33 | return null 34 | } 35 | 36 | return ( 37 | 38 | {t('words.knowledgeGraph')} 39 | 42 | 43 | ) 44 | } 45 | 46 | const Container = styled.div`` 47 | 48 | export default GraphRAGSettings 49 | -------------------------------------------------------------------------------- /scripts/build-npm.js: -------------------------------------------------------------------------------- 1 | const { downloadNpmPackage } = require('./utils') 2 | 3 | async function downloadNpm(platform) { 4 | if (!platform || platform === 'mac') { 5 | downloadNpmPackage( 6 | '@libsql/darwin-arm64', 7 | 'https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz' 8 | ) 9 | downloadNpmPackage('@libsql/darwin-x64', 'https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz') 10 | } 11 | 12 | if (!platform || platform === 'linux') { 13 | downloadNpmPackage( 14 | '@libsql/linux-arm64-gnu', 15 | 'https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz' 16 | ) 17 | downloadNpmPackage( 18 | '@libsql/linux-arm64-musl', 19 | 'https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz' 20 | ) 21 | downloadNpmPackage( 22 | '@libsql/linux-x64-gnu', 23 | 'https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz' 24 | ) 25 | downloadNpmPackage( 26 | '@libsql/linux-x64-musl', 27 | 'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz' 28 | ) 29 | } 30 | 31 | if (!platform || platform === 'windows') { 32 | downloadNpmPackage( 33 | '@libsql/win32-x64-msvc', 34 | 'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz' 35 | ) 36 | } 37 | } 38 | 39 | const platformArg = process.argv[2] 40 | downloadNpm(platformArg) 41 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/MessageAttachments.tsx: -------------------------------------------------------------------------------- 1 | import FileManager from '@renderer/services/FileManager' 2 | import { FileTypes, Message } from '@renderer/types' 3 | import { Image as AntdImage, Upload } from 'antd' 4 | import { FC } from 'react' 5 | import styled from 'styled-components' 6 | 7 | interface Props { 8 | message: Message 9 | } 10 | 11 | const MessageAttachments: FC = ({ message }) => { 12 | if (!message.files) { 13 | return null 14 | } 15 | 16 | if (message?.files && message.files[0]?.type === FileTypes.IMAGE) { 17 | return ( 18 | 19 | {message.files?.map((image) => )} 20 | 21 | ) 22 | } 23 | 24 | return ( 25 | 26 | ({ 30 | uid: file.id, 31 | url: 'file://' + FileManager.getSafePath(file), 32 | status: 'done', 33 | name: file.origin_name 34 | }))} 35 | /> 36 | 37 | ) 38 | } 39 | 40 | const Container = styled.div` 41 | display: flex; 42 | flex-direction: row; 43 | gap: 10px; 44 | margin-top: 8px; 45 | ` 46 | 47 | const Image = styled(AntdImage)` 48 | border-radius: 10px; 49 | ` 50 | 51 | export default MessageAttachments 52 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Inputbar/AttachmentPreview.tsx: -------------------------------------------------------------------------------- 1 | import FileManager from '@renderer/services/FileManager' 2 | import { FileType } from '@renderer/types' 3 | import { Upload } from 'antd' 4 | import { isEmpty } from 'lodash' 5 | import { FC } from 'react' 6 | import styled from 'styled-components' 7 | 8 | interface Props { 9 | files: FileType[] 10 | setFiles: (files: FileType[]) => void 11 | } 12 | 13 | const AttachmentPreview: FC = ({ files, setFiles }) => { 14 | if (isEmpty(files)) { 15 | return null 16 | } 17 | 18 | return ( 19 | 20 | 21 | 20 ? 'text' : 'picture-card'} 23 | fileList={files.map((file) => ({ 24 | uid: file.id, 25 | url: 'file://' + FileManager.getSafePath(file), 26 | status: 'done', 27 | name: file.name 28 | }))} 29 | onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))} 30 | /> 31 | 32 | 33 | ) 34 | } 35 | 36 | const Container = styled.div` 37 | display: flex; 38 | flex-direction: row; 39 | gap: 10px; 40 | padding: 10px 0; 41 | background: var(--color-background); 42 | border-top: 1px solid var(--color-border-mute); 43 | ` 44 | 45 | const ContentContainer = styled.div` 46 | max-height: 40vh; 47 | width: 100%; 48 | overflow-y: auto; 49 | padding: 0 20px; 50 | ` 51 | 52 | export default AttachmentPreview 53 | -------------------------------------------------------------------------------- /src/renderer/src/assets/images/apps/bolt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/renderer/src/components/Popups/TemplatePopup.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'antd' 2 | import { useState } from 'react' 3 | 4 | import { Box } from '../Layout' 5 | import { TopView } from '../TopView' 6 | 7 | interface ShowParams { 8 | title: string 9 | } 10 | 11 | interface Props extends ShowParams { 12 | resolve: (data: any) => void 13 | } 14 | 15 | const PopupContainer: React.FC = ({ title, resolve }) => { 16 | const [open, setOpen] = useState(true) 17 | 18 | const onOk = () => { 19 | setOpen(false) 20 | } 21 | 22 | const onCancel = () => { 23 | setOpen(false) 24 | } 25 | 26 | const onClose = () => { 27 | resolve({}) 28 | } 29 | 30 | TemplatePopup.hide = onCancel 31 | 32 | return ( 33 | 41 | Name 42 | 43 | ) 44 | } 45 | 46 | const TopViewKey = 'TemplatePopup' 47 | 48 | export default class TemplatePopup { 49 | static topviewId = 0 50 | static hide() { 51 | TopView.hide(TopViewKey) 52 | } 53 | static show(props: ShowParams) { 54 | return new Promise((resolve) => { 55 | TopView.show( 56 | { 59 | resolve(v) 60 | TopView.hide(TopViewKey) 61 | }} 62 | />, 63 | TopViewKey 64 | ) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/utils/formula.ts: -------------------------------------------------------------------------------- 1 | export function escapeDollarNumber(text: string) { 2 | let escapedText = '' 3 | 4 | for (let i = 0; i < text.length; i += 1) { 5 | let char = text[i] 6 | const nextChar = text[i + 1] || ' ' 7 | 8 | if (char === '$' && nextChar >= '0' && nextChar <= '9') { 9 | char = '\\$' 10 | } 11 | 12 | escapedText += char 13 | } 14 | 15 | return escapedText 16 | } 17 | 18 | export function escapeBrackets(text: string) { 19 | const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g 20 | return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => { 21 | if (codeBlock) { 22 | return codeBlock 23 | } else if (squareBracket) { 24 | return ` 25 | $$ 26 | ${squareBracket} 27 | $$ 28 | ` 29 | } else if (roundBracket) { 30 | return `$${roundBracket}$` 31 | } 32 | return match 33 | }) 34 | } 35 | 36 | export function extractTitle(html: string): string | null { 37 | const titleRegex = /(.*?)<\/title>/i 38 | const match = html.match(titleRegex) 39 | 40 | if (match && match[1]) { 41 | return match[1].trim() 42 | } 43 | 44 | return null 45 | } 46 | 47 | export function removeSvgEmptyLines(text: string): string { 48 | // 用正则表达式匹配 <svg> 标签内的内容 49 | const svgPattern = /(<svg[\s\S]*?<\/svg>)/g 50 | 51 | return text.replace(svgPattern, (svgMatch) => { 52 | // 将 SVG 内容按行分割,过滤掉空行,然后重新组合 53 | return svgMatch 54 | .split('\n') 55 | .filter((line) => line.trim() !== '') 56 | .join('\n') 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /scripts/replace-spaces.js: -------------------------------------------------------------------------------- 1 | // replaceSpaces.js 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const directory = 'dist' 7 | 8 | // 处理文件名中的空格 9 | function replaceFileNames() { 10 | fs.readdir(directory, (err, files) => { 11 | if (err) throw err 12 | 13 | files.forEach((file) => { 14 | const oldPath = path.join(directory, file) 15 | const newPath = path.join(directory, file.replace(/ /g, '-')) 16 | 17 | fs.stat(oldPath, (err, stats) => { 18 | if (err) throw err 19 | 20 | if (stats.isFile() && oldPath !== newPath) { 21 | fs.rename(oldPath, newPath, (err) => { 22 | if (err) throw err 23 | console.log(`Renamed: ${oldPath} -> ${newPath}`) 24 | }) 25 | } 26 | }) 27 | }) 28 | }) 29 | } 30 | 31 | function replaceYmlContent() { 32 | fs.readdir(directory, (err, files) => { 33 | if (err) throw err 34 | 35 | files.forEach((file) => { 36 | if (path.extname(file).toLowerCase() === '.yml') { 37 | const filePath = path.join(directory, file) 38 | 39 | fs.readFile(filePath, 'utf8', (err, data) => { 40 | if (err) throw err 41 | 42 | // 替换内容 43 | const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-') 44 | 45 | // 写回文件 46 | fs.writeFile(filePath, newContent, 'utf8', (err) => { 47 | if (err) throw err 48 | console.log(`Updated content in: ${filePath}`) 49 | }) 50 | }) 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | // 执行两个操作 57 | replaceFileNames() 58 | replaceYmlContent() 59 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Markdown/Artifacts.tsx: -------------------------------------------------------------------------------- 1 | import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons' 2 | import MinApp from '@renderer/components/MinApp' 3 | import { AppLogo } from '@renderer/config/env' 4 | import { extractTitle } from '@renderer/utils/formula' 5 | import { Button } from 'antd' 6 | import { FC } from 'react' 7 | import { useTranslation } from 'react-i18next' 8 | import styled from 'styled-components' 9 | 10 | interface Props { 11 | html: string 12 | } 13 | 14 | const Artifacts: FC<Props> = ({ html }) => { 15 | const { t } = useTranslation() 16 | const title = extractTitle(html) || 'Artifacts' + ' ' + t('chat.artifacts.button.preview') 17 | 18 | const onPreview = async () => { 19 | const path = await window.api.file.create('artifacts-preview.html') 20 | await window.api.file.write(path, html) 21 | 22 | MinApp.start({ 23 | name: title, 24 | logo: AppLogo, 25 | url: `file://${path}` 26 | }) 27 | } 28 | 29 | const onDownload = () => { 30 | window.api.file.save(`${title}.html`, html) 31 | } 32 | 33 | return ( 34 | <Container> 35 | <Button type="primary" icon={<ExpandOutlined />} onClick={onPreview} size="small"> 36 | {t('chat.artifacts.button.preview')} 37 | </Button> 38 | <Button icon={<DownloadOutlined />} onClick={onDownload} size="small"> 39 | {t('chat.artifacts.button.download')} 40 | </Button> 41 | </Container> 42 | ) 43 | } 44 | 45 | const Container = styled.div` 46 | margin: 10px; 47 | display: flex; 48 | flex-direction: row; 49 | gap: 8px; 50 | ` 51 | 52 | export default Artifacts 53 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/usePaintings.ts: -------------------------------------------------------------------------------- 1 | import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models' 2 | import FileManager from '@renderer/services/FileManager' 3 | import { useAppDispatch, useAppSelector } from '@renderer/store' 4 | import { addPainting, removePainting, updatePainting, updatePaintings } from '@renderer/store/paintings' 5 | import { Painting } from '@renderer/types' 6 | import { uuid } from '@renderer/utils' 7 | 8 | export function usePaintings() { 9 | const paintings = useAppSelector((state) => state.paintings.paintings) 10 | const dispatch = useAppDispatch() 11 | const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString() 12 | 13 | return { 14 | paintings, 15 | addPainting: () => { 16 | const newPainting: Painting = { 17 | model: TEXT_TO_IMAGES_MODELS[0].id, 18 | id: uuid(), 19 | urls: [], 20 | files: [], 21 | prompt: '', 22 | negativePrompt: '', 23 | imageSize: '1024x1024', 24 | numImages: 1, 25 | seed: generateRandomSeed(), 26 | steps: 25, 27 | guidanceScale: 4.5, 28 | promptEnhancement: true 29 | } 30 | dispatch(addPainting(newPainting)) 31 | return newPainting 32 | }, 33 | removePainting: async (painting: Painting) => { 34 | FileManager.deleteFiles(painting.files) 35 | dispatch(removePainting(painting)) 36 | }, 37 | updatePainting: (painting: Painting) => { 38 | dispatch(updatePainting(painting)) 39 | }, 40 | updatePaintings: (paintings: Painting[]) => { 41 | dispatch(updatePaintings(paintings)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/src/components/Popups/SearchPopup.tsx: -------------------------------------------------------------------------------- 1 | import HistoryPage from '@renderer/pages/history/HistoryPage' 2 | import { Modal } from 'antd' 3 | import { useState } from 'react' 4 | 5 | import { TopView } from '../TopView' 6 | 7 | interface Props { 8 | resolve: (data: any) => void 9 | } 10 | 11 | const PopupContainer: React.FC<Props> = ({ resolve }) => { 12 | const [open, setOpen] = useState(true) 13 | 14 | const onOk = () => { 15 | setOpen(false) 16 | } 17 | 18 | const onCancel = () => { 19 | setOpen(false) 20 | } 21 | 22 | const onClose = () => { 23 | resolve({}) 24 | } 25 | 26 | SearchPopup.hide = onCancel 27 | 28 | return ( 29 | <Modal 30 | open={open} 31 | onOk={onOk} 32 | onCancel={onCancel} 33 | afterClose={onClose} 34 | title={null} 35 | width="920px" 36 | transitionName="ant-move-down" 37 | styles={{ 38 | content: { 39 | padding: 0, 40 | border: `1px solid var(--color-frame-border)` 41 | }, 42 | body: { height: '85vh' } 43 | }} 44 | centered 45 | footer={null}> 46 | <HistoryPage /> 47 | </Modal> 48 | ) 49 | } 50 | 51 | export default class SearchPopup { 52 | static topviewId = 0 53 | static hide() { 54 | TopView.hide('SearchPopup') 55 | } 56 | static show() { 57 | return new Promise<any>((resolve) => { 58 | TopView.show( 59 | <PopupContainer 60 | resolve={(v) => { 61 | resolve(v) 62 | TopView.hide('SearchPopup') 63 | }} 64 | />, 65 | 'SearchPopup' 66 | ) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx: -------------------------------------------------------------------------------- 1 | import { PaperClipOutlined } from '@ant-design/icons' 2 | import { isVisionModel } from '@renderer/config/models' 3 | import { FileType, Model } from '@renderer/types' 4 | import { documentExts, imageExts, textExts } from '@shared/config/constant' 5 | import { Tooltip } from 'antd' 6 | import { FC } from 'react' 7 | import { useTranslation } from 'react-i18next' 8 | 9 | interface Props { 10 | model: Model 11 | files: FileType[] 12 | setFiles: (files: FileType[]) => void 13 | ToolbarButton: any 14 | disabled?: boolean 15 | } 16 | 17 | const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton, disabled }) => { 18 | const { t } = useTranslation() 19 | const extensions = isVisionModel(model) 20 | ? [...imageExts, ...documentExts, ...textExts] 21 | : [...documentExts, ...textExts] 22 | 23 | const onSelectFile = async () => { 24 | const _files = await window.api.file.select({ 25 | properties: ['openFile', 'multiSelections'], 26 | filters: [ 27 | { 28 | name: 'Files', 29 | extensions: extensions.map((i) => i.replace('.', '')) 30 | } 31 | ] 32 | }) 33 | 34 | if (_files) { 35 | setFiles([...files, ..._files]) 36 | } 37 | } 38 | 39 | return ( 40 | <Tooltip placement="top" title={t('chat.input.upload')} arrow> 41 | <ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}> 42 | <PaperClipOutlined style={{ rotate: '135deg' }} /> 43 | </ToolbarButton> 44 | </Tooltip> 45 | ) 46 | } 47 | 48 | export default AttachmentButton 49 | -------------------------------------------------------------------------------- /src/renderer/src/config/prompts.ts: -------------------------------------------------------------------------------- 1 | export const AGENT_PROMPT = ` 2 | 你是一个 Prompt 生成器。你会将用户输入的信息整合成一个 Markdown 语法的结构化的 Prompt。请务必不要使用代码块输出,而是直接显示! 3 | 4 | ## Role : 5 | [请填写你想定义的角色名称] 6 | 7 | ## Background : 8 | [请描述角色的背景信息,例如其历史、来源或特定的知识背景] 9 | 10 | ## Preferences : 11 | [请描述角色的偏好或特定风格,例如对某种设计或文化的偏好] 12 | 13 | ## Profile : 14 | - version: 0.2 15 | - language: 中文 16 | - description: [请简短描述该角色的主要功能,50 字以内] 17 | 18 | ## Goals : 19 | [请列出该角色的主要目标 1] 20 | [请列出该角色的主要目标 2] 21 | ... 22 | 23 | ## Constrains : 24 | [请列出该角色在互动中必须遵循的限制条件 1] 25 | [请列出该角色在互动中必须遵循的限制条件 2] 26 | ... 27 | 28 | ## Skills : 29 | [为了在限制条件下实现目标,该角色需要拥有的技能 1] 30 | [为了在限制条件下实现目标,该角色需要拥有的技能 2] 31 | ... 32 | 33 | ## Examples : 34 | [提供一个输出示例 1,展示角色的可能回答或行为] 35 | [提供一个输出示例 2] 36 | ... 37 | 38 | ## OutputFormat : 39 | [请描述该角色的工作流程的第一步] 40 | [请描述该角色的工作流程的第二步] 41 | ... 42 | 43 | ## Initialization : 44 | 作为 [角色名称], 拥有 [列举技能], 严格遵守 [列举限制条件], 使用默认 [选择语言] 与用户对话,友好的欢迎用户。然后介绍自己,并提示用户输入. 45 | ` 46 | 47 | export const SUMMARIZE_PROMPT = 48 | '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号' 49 | 50 | export const TRANSLATE_PROMPT = 51 | 'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.' 52 | 53 | export const REFERENCE_PROMPT = `请根据参考资料回答问题,并使用脚注格式引用数据来源。请忽略无关的参考资料。 54 | 55 | ## 脚注格式: 56 | 57 | 1. **脚注标记**:在正文中使用 [^数字] 的形式标记脚注,例如 [^1]。 58 | 2. **脚注内容**:在文档末尾使用 [^数字]: 脚注内容 的形式定义脚注的具体内容 59 | 3. **脚注内容**:应该尽量简洁 60 | 61 | ## 我的问题是: 62 | 63 | {question} 64 | 65 | ## 参考资料: 66 | 67 | {references} 68 | ` 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#0_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 错误报告 2 | description: 创建一个报告以帮助我们改进 3 | title: '[错误]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢您花时间填写此错误报告! 10 | 11 | - type: dropdown 12 | id: platform 13 | attributes: 14 | label: 平台 15 | description: 您正在使用哪个平台? 16 | options: 17 | - Windows 18 | - macOS 19 | - Linux 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: version 25 | attributes: 26 | label: 版本 27 | description: 您正在运行的 Cherry Studio 版本是什么? 28 | placeholder: 例如 v1.0.0 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: description 34 | attributes: 35 | label: 错误描述 36 | description: 清晰简洁地描述错误是什么 37 | placeholder: 告诉我们发生了什么... 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: reproduction 43 | attributes: 44 | label: 重现步骤 45 | description: 重现行为的步骤 46 | placeholder: | 47 | 1. 转到 '...' 48 | 2. 点击 '....' 49 | 3. 向下滚动到 '....' 50 | 4. 看到错误 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: expected 56 | attributes: 57 | label: 预期行为 58 | description: 清晰简洁地描述您期望发生的事情 59 | validations: 60 | required: true 61 | 62 | - type: textarea 63 | id: logs 64 | attributes: 65 | label: 相关日志输出 66 | description: 请复制并粘贴任何相关的日志输出 67 | render: shell 68 | 69 | - type: textarea 70 | id: additional 71 | attributes: 72 | label: 附加信息 73 | description: 在此添加有关问题的任何其他上下文 74 | -------------------------------------------------------------------------------- /.yarn/patches/@llm-tools-embedjs-utils-npm-0.1.25-fd8fe8a193.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/util/strings.cjs b/src/util/strings.cjs 2 | index 9933cc6e3866c476b47342a29ddb206eb90fa4a5..2965c4f2808bf94af9ef3e2ec889e5552e30e6ae 100644 3 | --- a/src/util/strings.cjs 4 | +++ b/src/util/strings.cjs 5 | @@ -38,13 +38,16 @@ function toTitleCase(str) { 6 | }); 7 | } 8 | function isValidURL(url) { 9 | - try { 10 | - new URL(url); 11 | - return true; 12 | - } 13 | - catch { 14 | - return false; 15 | + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) { 16 | + try { 17 | + new URL(url); 18 | + return true; 19 | + } 20 | + catch { 21 | + return false; 22 | + } 23 | } 24 | + return false; 25 | } 26 | function isValidJson(str) { 27 | try { 28 | diff --git a/src/util/strings.js b/src/util/strings.js 29 | index f5c1655512099b880fc5022e95d5e0c4d1d073f2..1a64bd662a22efd2effd9d2846ffcf0b93391963 100644 30 | --- a/src/util/strings.js 31 | +++ b/src/util/strings.js 32 | @@ -29,13 +29,16 @@ export function toTitleCase(str) { 33 | }); 34 | } 35 | export function isValidURL(url) { 36 | - try { 37 | - new URL(url); 38 | - return true; 39 | - } 40 | - catch { 41 | - return false; 42 | + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) { 43 | + try { 44 | + new URL(url); 45 | + return true; 46 | + } 47 | + catch { 48 | + return false; 49 | + } 50 | } 51 | + return false; 52 | } 53 | export function isValidJson(str) { 54 | try { 55 | -------------------------------------------------------------------------------- /src/renderer/src/store/paintings.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models' 3 | import { Painting } from '@renderer/types' 4 | import { uuid } from '@renderer/utils' 5 | 6 | export interface PaintingsState { 7 | paintings: Painting[] 8 | } 9 | 10 | export const DEFAULT_PAINTING: Painting = { 11 | id: uuid(), 12 | urls: [], 13 | files: [], 14 | prompt: '', 15 | negativePrompt: '', 16 | imageSize: '1024x1024', 17 | numImages: 1, 18 | seed: '', 19 | steps: 25, 20 | guidanceScale: 4.5, 21 | model: TEXT_TO_IMAGES_MODELS[0].id 22 | } 23 | 24 | const initialState: PaintingsState = { 25 | paintings: [DEFAULT_PAINTING] 26 | } 27 | 28 | const paintingsSlice = createSlice({ 29 | name: 'paintings', 30 | initialState, 31 | reducers: { 32 | updatePaintings: (state, action: PayloadAction<Painting[]>) => { 33 | state.paintings = action.payload 34 | }, 35 | addPainting: (state, action: PayloadAction<Painting>) => { 36 | state.paintings.push(action.payload) 37 | }, 38 | removePainting: (state, action: PayloadAction<Painting>) => { 39 | if (state.paintings.length === 1) { 40 | state.paintings = [DEFAULT_PAINTING] 41 | } else { 42 | state.paintings = state.paintings.filter((c) => c.id !== action.payload.id) 43 | } 44 | }, 45 | updatePainting: (state, action: PayloadAction<Painting>) => { 46 | state.paintings = state.paintings.map((c) => (c.id === action.payload.id ? action.payload : c)) 47 | } 48 | } 49 | }) 50 | 51 | export const { updatePaintings, addPainting, removePainting, updatePainting } = paintingsSlice.actions 52 | 53 | export default paintingsSlice.reducer 54 | -------------------------------------------------------------------------------- /src/main/services/CacheService.ts: -------------------------------------------------------------------------------- 1 | interface CacheItem<T> { 2 | data: T 3 | timestamp: number 4 | duration: number 5 | } 6 | 7 | export class CacheService { 8 | private static cache: Map<string, CacheItem<any>> = new Map() 9 | 10 | /** 11 | * Set cache 12 | * @param key Cache key 13 | * @param data Cache data 14 | * @param duration Cache duration (in milliseconds) 15 | */ 16 | static set<T>(key: string, data: T, duration: number): void { 17 | this.cache.set(key, { 18 | data, 19 | timestamp: Date.now(), 20 | duration 21 | }) 22 | } 23 | 24 | /** 25 | * Get cache 26 | * @param key Cache key 27 | * @returns Returns data if cache exists and not expired, otherwise returns null 28 | */ 29 | static get<T>(key: string): T | null { 30 | const item = this.cache.get(key) 31 | if (!item) return null 32 | 33 | const now = Date.now() 34 | if (now - item.timestamp > item.duration) { 35 | this.remove(key) 36 | return null 37 | } 38 | 39 | return item.data 40 | } 41 | 42 | /** 43 | * Remove specific cache 44 | * @param key Cache key 45 | */ 46 | static remove(key: string): void { 47 | this.cache.delete(key) 48 | } 49 | 50 | /** 51 | * Clear all cache 52 | */ 53 | static clear(): void { 54 | this.cache.clear() 55 | } 56 | 57 | /** 58 | * Check if cache exists and is valid 59 | * @param key Cache key 60 | * @returns boolean 61 | */ 62 | static has(key: string): boolean { 63 | const item = this.cache.get(key) 64 | if (!item) return false 65 | 66 | const now = Date.now() 67 | if (now - item.timestamp > item.duration) { 68 | this.remove(key) 69 | return false 70 | } 71 | 72 | return true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/src/components/Popups/AppStorePopover.tsx: -------------------------------------------------------------------------------- 1 | import { Center } from '@renderer/components/Layout' 2 | import { getAllMinApps } from '@renderer/config/minapps' 3 | import App from '@renderer/pages/apps/App' 4 | import { Popover } from 'antd' 5 | import { Empty } from 'antd' 6 | import { isEmpty } from 'lodash' 7 | import { FC, useState } from 'react' 8 | import { useHotkeys } from 'react-hotkeys-hook' 9 | import styled from 'styled-components' 10 | 11 | import Scrollbar from '../Scrollbar' 12 | 13 | interface Props { 14 | children: React.ReactNode 15 | } 16 | 17 | const AppStorePopover: FC<Props> = ({ children }) => { 18 | const [open, setOpen] = useState(false) 19 | const apps = getAllMinApps() 20 | 21 | useHotkeys('esc', () => { 22 | setOpen(false) 23 | }) 24 | 25 | const handleClose = () => { 26 | setOpen(false) 27 | } 28 | 29 | const content = ( 30 | <PopoverContent> 31 | <AppsContainer> 32 | {apps.map((app) => ( 33 | <App key={app.id} app={app} onClick={handleClose} size={50} /> 34 | ))} 35 | {isEmpty(apps) && ( 36 | <Center> 37 | <Empty /> 38 | </Center> 39 | )} 40 | </AppsContainer> 41 | </PopoverContent> 42 | ) 43 | 44 | return ( 45 | <Popover 46 | open={open} 47 | onOpenChange={setOpen} 48 | content={content} 49 | trigger="click" 50 | placement="bottomRight" 51 | styles={{ body: { padding: 25 } }}> 52 | {children} 53 | </Popover> 54 | ) 55 | } 56 | 57 | const PopoverContent = styled(Scrollbar)`` 58 | 59 | const AppsContainer = styled.div` 60 | display: grid; 61 | grid-template-columns: repeat(6, minmax(90px, 1fr)); 62 | gap: 18px; 63 | ` 64 | 65 | export default AppStorePopover 66 | -------------------------------------------------------------------------------- /scripts/after-pack.js: -------------------------------------------------------------------------------- 1 | const { Arch } = require('electron-builder') 2 | const { default: removeLocales } = require('./remove-locales') 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | exports.default = async function (context) { 7 | await removeLocales(context) 8 | const platform = context.packager.platform.name 9 | const arch = context.arch 10 | 11 | if (platform === 'mac') { 12 | const node_modules_path = path.join( 13 | context.appOutDir, 14 | 'Cherry Studio.app', 15 | 'Contents', 16 | 'Resources', 17 | 'app.asar.unpacked', 18 | 'node_modules' 19 | ) 20 | 21 | removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64']) 22 | } 23 | 24 | if (platform === 'linux') { 25 | const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules') 26 | const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl'] 27 | removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch) 28 | } 29 | 30 | if (platform === 'windows') { 31 | const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules') 32 | removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc']) 33 | } 34 | } 35 | 36 | function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) { 37 | const modulePath = path.join(nodeModulesPath, packageName) 38 | const dirs = fs.readdirSync(modulePath) 39 | dirs 40 | .filter((dir) => !arch.includes(dir)) 41 | .forEach((dir) => { 42 | fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true }) 43 | console.log(`Removed dir: ${dir}`, arch) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/src/assets/fonts/icon-fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 4753420 */ 3 | src: url('iconfont.woff2?t=1736309723926') format('woff2'), 4 | url('iconfont.woff?t=1736309723926') format('woff'), 5 | url('iconfont.ttf?t=1736309723926') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-at:before { 17 | content: "\e623"; 18 | } 19 | 20 | .icon-icon-adaptive-width:before { 21 | content: "\e87a"; 22 | } 23 | 24 | .icon-a-darkmode:before { 25 | content: "\e6cd"; 26 | } 27 | 28 | .icon-ai-model:before { 29 | content: "\e827"; 30 | } 31 | 32 | .icon-ai-model1:before { 33 | content: "\ec09"; 34 | } 35 | 36 | .icon-gridlines:before { 37 | content: "\e942"; 38 | } 39 | 40 | .icon-inbox:before { 41 | content: "\e869"; 42 | } 43 | 44 | .icon-business-smart-assistant:before { 45 | content: "\e601"; 46 | } 47 | 48 | .icon-copy:before { 49 | content: "\e6ae"; 50 | } 51 | 52 | .icon-ic_send:before { 53 | content: "\e795"; 54 | } 55 | 56 | .icon-dark1:before { 57 | content: "\e72f"; 58 | } 59 | 60 | .icon-theme-light:before { 61 | content: "\e6b7"; 62 | } 63 | 64 | .icon-translate_line:before { 65 | content: "\e7de"; 66 | } 67 | 68 | .icon-history:before { 69 | content: "\e758"; 70 | } 71 | 72 | .icon-hide-sidebar:before { 73 | content: "\e8eb"; 74 | } 75 | 76 | .icon-show-sidebar:before { 77 | content: "\e944"; 78 | } 79 | 80 | .icon-appstore:before { 81 | content: "\e792"; 82 | } 83 | 84 | .icon-chat:before { 85 | content: "\e615"; 86 | } 87 | 88 | .icon-setting:before { 89 | content: "\e78e"; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/renderer/src/components/Scrollbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { throttle } from 'lodash' 2 | import { FC, forwardRef, useCallback, useEffect, useRef, useState } from 'react' 3 | import styled from 'styled-components' 4 | 5 | interface Props extends React.HTMLAttributes<HTMLDivElement> { 6 | right?: boolean 7 | ref?: any 8 | } 9 | 10 | const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => { 11 | const [isScrolling, setIsScrolling] = useState(false) 12 | const timeoutRef = useRef<NodeJS.Timeout | null>(null) 13 | 14 | const handleScroll = useCallback( 15 | throttle(() => { 16 | setIsScrolling(true) 17 | 18 | if (timeoutRef.current) { 19 | clearTimeout(timeoutRef.current) 20 | } 21 | 22 | timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) // 增加到 2 秒 23 | }, 200), 24 | [] 25 | ) 26 | 27 | useEffect(() => { 28 | return () => { 29 | if (timeoutRef.current) { 30 | clearTimeout(timeoutRef.current) 31 | } 32 | } 33 | }, []) 34 | 35 | return ( 36 | <Container {...props} isScrolling={isScrolling} onScroll={handleScroll} ref={ref}> 37 | {props.children} 38 | </Container> 39 | ) 40 | }) 41 | 42 | const Container = styled.div<{ isScrolling: boolean; right?: boolean }>` 43 | overflow-y: auto; 44 | &::-webkit-scrollbar-thumb { 45 | transition: background 2s ease; 46 | background: ${(props) => 47 | props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''})` : 'transparent'}; 48 | &:hover { 49 | background: ${(props) => 50 | props.isScrolling ? `var(--color-scrollbar-thumb${props.right ? '-right' : ''}-hover)` : 'transparent'}; 51 | } 52 | } 53 | ` 54 | 55 | Scrollbar.displayName = 'Scrollbar' 56 | 57 | export default Scrollbar 58 | -------------------------------------------------------------------------------- /api/src/routes/knowledge.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | import knowledgeService from '../services/knowledge' 4 | 5 | const router = Router() 6 | 7 | // 添加路由日志 8 | router.use((req, res, next) => { 9 | console.log('Knowledge route accessed:', req.method, req.url) 10 | next() 11 | }) 12 | 13 | // 获取所有知识库 14 | router.get('/bases', async (req, res) => { 15 | try { 16 | console.log('Getting knowledge bases...') 17 | const bases = await knowledgeService.getKnowledgeBases() 18 | console.log('Retrieved bases:', bases) 19 | res.json(bases) 20 | } catch (error) { 21 | console.error('Failed to get knowledge bases:', error) 22 | res.status(500).json({ error: 'Failed to get knowledge bases' }) 23 | } 24 | }) 25 | 26 | // 搜索知识库 27 | router.get('/search', async (req, res) => { 28 | try { 29 | const { baseId, query, limit } = req.query 30 | const results = await knowledgeService.search(baseId as string, query as string, Number(limit) || 5) 31 | res.json(results) 32 | } catch (error) { 33 | res.status(500).json({ error: 'Search failed' }) 34 | } 35 | }) 36 | 37 | // 添加RAG对话接口 38 | router.post('/chat', async (req, res) => { 39 | try { 40 | const { baseId, modelId, query } = req.body 41 | 42 | if (!baseId || !modelId || !query) { 43 | return res.status(400).json({ 44 | error: 'Missing required parameters: baseId, modelId and query are required' 45 | }) 46 | } 47 | 48 | console.log('RAG Chat request:', { baseId, modelId, query }) 49 | 50 | const result = await knowledgeService.chat(baseId, modelId, query) 51 | 52 | res.json(result) 53 | } catch (error) { 54 | console.error('RAG Chat error:', error) 55 | res.status(500).json({ 56 | error: 'Chat failed', 57 | message: error.message 58 | }) 59 | } 60 | }) 61 | 62 | export default router 63 | -------------------------------------------------------------------------------- /src/renderer/src/context/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { isMac } from '@renderer/config/constant' 2 | import { useSettings } from '@renderer/hooks/useSettings' 3 | import { ThemeMode } from '@renderer/types' 4 | import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' 5 | 6 | interface ThemeContextType { 7 | theme: ThemeMode 8 | toggleTheme: () => void 9 | } 10 | 11 | const ThemeContext = createContext<ThemeContextType>({ 12 | theme: ThemeMode.light, 13 | toggleTheme: () => {} 14 | }) 15 | 16 | export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => { 17 | const { theme, setTheme } = useSettings() 18 | const [_theme, _setTheme] = useState(theme) 19 | 20 | const toggleTheme = () => { 21 | setTheme(theme === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark) 22 | } 23 | 24 | useEffect((): any => { 25 | if (theme === ThemeMode.auto) { 26 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 27 | _setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light) 28 | const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light) 29 | mediaQuery.addEventListener('change', handleChange) 30 | return () => mediaQuery.removeEventListener('change', handleChange) 31 | } else { 32 | _setTheme(theme) 33 | } 34 | }, [theme]) 35 | 36 | useEffect(() => { 37 | document.body.setAttribute('theme-mode', _theme) 38 | window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light') 39 | }, [_theme]) 40 | 41 | useEffect(() => { 42 | document.body.setAttribute('os', isMac ? 'mac' : 'windows') 43 | }, []) 44 | 45 | return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider> 46 | } 47 | 48 | export const useTheme = () => useContext(ThemeContext) 49 | -------------------------------------------------------------------------------- /api/src/routes/provider.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import providerService from '../services/provider' 3 | 4 | const router = Router() 5 | 6 | // 添加路由日志 7 | router.use((req, res, next) => { 8 | console.log('Provider route accessed:', req.method, req.url) 9 | next() 10 | }) 11 | 12 | // 获取所有提供商 13 | router.get('/providers', async (req, res) => { 14 | try { 15 | const providers = await providerService.getProviders() 16 | res.json(providers) 17 | } catch (error) { 18 | console.error('Failed to get providers:', error) 19 | res.status(500).json({ error: 'Failed to get providers' }) 20 | } 21 | }) 22 | 23 | // 获取特定提供商 24 | router.get('/providers/:providerId', async (req, res) => { 25 | try { 26 | const provider = await providerService.getProvider(req.params.providerId) 27 | if (!provider) { 28 | return res.status(404).json({ error: 'Provider not found' }) 29 | } 30 | res.json(provider) 31 | } catch (error) { 32 | res.status(500).json({ error: 'Failed to get provider' }) 33 | } 34 | }) 35 | 36 | // 获取所有模型 37 | router.get('/providers/:providerId/models', async (req, res) => { 38 | try { 39 | const models = await providerService.getModels(req.params.providerId) 40 | res.json(models) 41 | } catch (error) { 42 | res.status(500).json({ error: 'Failed to get models' }) 43 | } 44 | }) 45 | 46 | // 获取特定类型的模型 47 | router.get('/models/:type', async (req, res) => { 48 | try { 49 | const type = req.params.type as 'chat' | 'embedding' 50 | if (type !== 'chat' && type !== 'embedding') { 51 | return res.status(400).json({ error: 'Invalid model type' }) 52 | } 53 | const models = await providerService.getModelsByType(type) 54 | res.json(models) 55 | } catch (error) { 56 | res.status(500).json({ error: 'Failed to get models' }) 57 | } 58 | }) 59 | 60 | export default router 61 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import { resolve } from 'path' 4 | import { visualizer } from 'rollup-plugin-visualizer' 5 | 6 | const visualizerPlugin = (type: 'renderer' | 'main') => { 7 | return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : [] 8 | } 9 | 10 | export default defineConfig({ 11 | main: { 12 | plugins: [ 13 | externalizeDepsPlugin({ 14 | exclude: [ 15 | '@llm-tools/embedjs', 16 | '@llm-tools/embedjs-openai', 17 | '@llm-tools/embedjs-loader-web', 18 | '@llm-tools/embedjs-loader-markdown', 19 | '@llm-tools/embedjs-loader-msoffice', 20 | '@llm-tools/embedjs-loader-xml', 21 | '@llm-tools/embedjs-loader-pdf', 22 | '@llm-tools/embedjs-loader-sitemap', 23 | '@llm-tools/embedjs-libsql', 24 | '@llm-tools/embedjs-loader-image' 25 | ] 26 | }), 27 | ...visualizerPlugin('main') 28 | ], 29 | resolve: { 30 | alias: { 31 | '@main': resolve('src/main'), 32 | '@types': resolve('src/renderer/src/types'), 33 | '@shared': resolve('packages/shared') 34 | } 35 | }, 36 | build: { 37 | rollupOptions: { 38 | external: ['@libsql/client', '@llm-tools/embedjs-loader-image'] 39 | } 40 | } 41 | }, 42 | preload: { 43 | plugins: [externalizeDepsPlugin()] 44 | }, 45 | renderer: { 46 | plugins: [react(), ...visualizerPlugin('renderer')], 47 | resolve: { 48 | alias: { 49 | '@renderer': resolve('src/renderer/src'), 50 | '@shared': resolve('packages/shared') 51 | } 52 | }, 53 | optimizeDeps: { 54 | exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js'] 55 | } 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /scripts/remove-locales.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | exports.default = async function (context) { 5 | const platform = context.packager.platform.name 6 | 7 | // 根据平台确定 locales 目录位置 8 | let resourceDirs = [] 9 | if (platform === 'mac') { 10 | // macOS 的语言文件位置 11 | resourceDirs = [ 12 | path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'), 13 | path.join( 14 | context.appOutDir, 15 | 'Cherry Studio.app', 16 | 'Contents', 17 | 'Frameworks', 18 | 'Electron Framework.framework', 19 | 'Resources' 20 | ) 21 | ] 22 | } else { 23 | // Windows 和 Linux 的语言文件位置 24 | resourceDirs = [path.join(context.appOutDir, 'locales')] 25 | } 26 | 27 | // 处理每个资源目录 28 | for (const resourceDir of resourceDirs) { 29 | if (!fs.existsSync(resourceDir)) { 30 | console.log(`Resource directory not found: ${resourceDir}, skipping...`) 31 | continue 32 | } 33 | 34 | // 读取所有文件和目录 35 | const items = fs.readdirSync(resourceDir) 36 | 37 | // 遍历并删除不需要的语言文件 38 | for (const item of items) { 39 | if (platform === 'mac') { 40 | // 在 macOS 上检查 .lproj 目录 41 | if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) { 42 | const dirPath = path.join(resourceDir, item) 43 | fs.rmSync(dirPath, { recursive: true, force: true }) 44 | console.log(`Removed locale directory: ${item} from ${resourceDir}`) 45 | } 46 | } else { 47 | // 其他平台处理 .pak 文件 48 | if (!item.match(/^(en|zh|ru)/)) { 49 | const filePath = path.join(resourceDir, item) 50 | fs.unlinkSync(filePath) 51 | console.log(`Removed locale file: ${item} from ${resourceDir}`) 52 | } 53 | } 54 | } 55 | } 56 | 57 | console.log('Locale cleanup completed!') 58 | } 59 | -------------------------------------------------------------------------------- /src/renderer/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit' 2 | import { useDispatch, useSelector, useStore } from 'react-redux' 3 | import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist' 4 | import storage from 'redux-persist/lib/storage' 5 | 6 | import agents from './agents' 7 | import assistants from './assistants' 8 | import knowledge from './knowledge' 9 | import llm from './llm' 10 | import migrate from './migrate' 11 | import paintings from './paintings' 12 | import runtime from './runtime' 13 | import settings from './settings' 14 | import shortcuts from './shortcuts' 15 | 16 | const rootReducer = combineReducers({ 17 | assistants, 18 | agents, 19 | paintings, 20 | llm, 21 | settings, 22 | runtime, 23 | shortcuts, 24 | knowledge 25 | }) 26 | 27 | const persistedReducer = persistReducer( 28 | { 29 | key: 'cherry-studio', 30 | storage, 31 | version: 54, 32 | blacklist: ['runtime'], 33 | migrate 34 | }, 35 | rootReducer 36 | ) 37 | 38 | const store = configureStore({ 39 | // @ts-ignore store type is unknown 40 | reducer: persistedReducer as typeof rootReducer, 41 | middleware: (getDefaultMiddleware) => { 42 | return getDefaultMiddleware({ 43 | serializableCheck: { 44 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] 45 | } 46 | }) 47 | }, 48 | devTools: true 49 | }) 50 | 51 | export type RootState = ReturnType<typeof rootReducer> 52 | export type AppDispatch = typeof store.dispatch 53 | 54 | export const persistor = persistStore(store) 55 | export const useAppDispatch = useDispatch.withTypes<AppDispatch>() 56 | export const useAppSelector = useSelector.withTypes<RootState>() 57 | export const useAppStore = useStore.withTypes<typeof store>() 58 | 59 | window.store = store 60 | 61 | export default store 62 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | import dotenv from 'dotenv' 3 | import express from 'express' 4 | 5 | import knowledgeRouter from './routes/knowledge' 6 | import providerRouter from './routes/provider' 7 | 8 | console.log('Starting server initialization...') 9 | 10 | process.on('uncaughtException', (error) => { 11 | console.error('Uncaught Exception:', error) 12 | }) 13 | 14 | process.on('unhandledRejection', (error) => { 15 | console.error('Unhandled Rejection:', error) 16 | }) 17 | 18 | try { 19 | // 加载环境变量 20 | dotenv.config() 21 | console.log('Environment variables loaded') 22 | 23 | // 检查但不强制要求 BACKUP_PATH 24 | if (process.env.BACKUP_PATH) { 25 | console.log('Using backup path:', process.env.BACKUP_PATH) 26 | } else { 27 | console.warn('BACKUP_PATH not set, will use default path') 28 | } 29 | 30 | const app = express() 31 | const port = process.env.PORT || 4000 32 | console.log('Port configured:', port) 33 | 34 | // 中间件 35 | app.use(cors()) 36 | app.use(express.json()) 37 | console.log('Middleware configured') 38 | 39 | // 配置路由 40 | app.use('/api/knowledge', knowledgeRouter) 41 | app.use('/api/provider', providerRouter) 42 | console.log('Routes configured') 43 | 44 | // 错误处理中间件 45 | app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { 46 | console.error('Server error:', err.stack) 47 | res.status(500).json({ error: 'Something broke!' }) 48 | }) 49 | 50 | // 404 处理 51 | app.use((req: express.Request, res: express.Response) => { 52 | console.log('404 Not Found:', req.method, req.url) // 添加请求日志 53 | res.status(404).json({ error: 'Not Found' }) 54 | }) 55 | 56 | // 启动服务器 57 | app 58 | .listen(port, () => { 59 | console.log(`Server is running on port ${port}`) 60 | }) 61 | .on('error', (error) => { 62 | console.error('Failed to start server:', error) 63 | }) 64 | } catch (error) { 65 | console.error('Initialization error:', error) 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { useAssistant } from '@renderer/hooks/useAssistant' 2 | import { useSettings } from '@renderer/hooks/useSettings' 3 | import { useShowTopics } from '@renderer/hooks/useStore' 4 | import { Assistant, Topic } from '@renderer/types' 5 | import { Flex } from 'antd' 6 | import { FC } from 'react' 7 | import styled from 'styled-components' 8 | 9 | import Inputbar from './Inputbar/Inputbar' 10 | import Messages from './Messages/Messages' 11 | import Tabs from './Tabs' 12 | 13 | interface Props { 14 | assistant: Assistant 15 | activeTopic: Topic 16 | setActiveTopic: (topic: Topic) => void 17 | setActiveAssistant: (assistant: Assistant) => void 18 | } 19 | 20 | const Chat: FC<Props> = (props) => { 21 | const { assistant } = useAssistant(props.assistant.id) 22 | const { topicPosition, messageStyle } = useSettings() 23 | const { showTopics } = useShowTopics() 24 | 25 | return ( 26 | <Container id="chat" className={messageStyle}> 27 | <Main id="chat-main" vertical flex={1} justify="space-between"> 28 | <Messages 29 | key={props.activeTopic.id} 30 | assistant={assistant} 31 | topic={props.activeTopic} 32 | setActiveTopic={props.setActiveTopic} 33 | /> 34 | <Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} /> 35 | </Main> 36 | {topicPosition === 'right' && showTopics && ( 37 | <Tabs 38 | activeAssistant={assistant} 39 | activeTopic={props.activeTopic} 40 | setActiveAssistant={props.setActiveAssistant} 41 | setActiveTopic={props.setActiveTopic} 42 | position="right" 43 | /> 44 | )} 45 | </Container> 46 | ) 47 | } 48 | 49 | const Container = styled.div` 50 | display: flex; 51 | flex-direction: row; 52 | height: 100%; 53 | flex: 1; 54 | justify-content: space-between; 55 | ` 56 | 57 | const Main = styled(Flex)` 58 | height: calc(100vh - var(--navbar-height)); 59 | ` 60 | 61 | export default Chat 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Create a report to help us improve 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | - type: dropdown 12 | id: platform 13 | attributes: 14 | label: Platform 15 | description: What platform are you using? 16 | options: 17 | - Windows 18 | - macOS 19 | - Linux 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: version 25 | attributes: 26 | label: Version 27 | description: What version of Cherry Studio are you running? 28 | placeholder: e.g. v1.0.0 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: description 34 | attributes: 35 | label: Bug Description 36 | description: A clear and concise description of what the bug is 37 | placeholder: Tell us what happened... 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: reproduction 43 | attributes: 44 | label: Steps To Reproduce 45 | description: Steps to reproduce the behavior 46 | placeholder: | 47 | 1. Go to '...' 48 | 2. Click on '....' 49 | 3. Scroll down to '....' 50 | 4. See error 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: expected 56 | attributes: 57 | label: Expected Behavior 58 | description: A clear and concise description of what you expected to happen 59 | validations: 60 | required: true 61 | 62 | - type: textarea 63 | id: logs 64 | attributes: 65 | label: Relevant Log Output 66 | description: Please copy and paste any relevant log output 67 | render: shell 68 | 69 | - type: textarea 70 | id: additional 71 | attributes: 72 | label: Additional Context 73 | description: Add any other context about the problem here 74 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Markdown/ImagePreview.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DownloadOutlined, 3 | RotateLeftOutlined, 4 | RotateRightOutlined, 5 | SwapOutlined, 6 | UndoOutlined, 7 | ZoomInOutlined, 8 | ZoomOutOutlined 9 | } from '@ant-design/icons' 10 | import { download } from '@renderer/utils/download' 11 | import { Image as AntImage, ImageProps as AntImageProps, Space } from 'antd' 12 | import React from 'react' 13 | import styled from 'styled-components' 14 | 15 | interface ImagePreviewProps extends AntImageProps { 16 | src: string 17 | } 18 | 19 | const ImagePreview: React.FC<ImagePreviewProps> = ({ src, ...props }) => { 20 | return ( 21 | <AntImage 22 | src={src} 23 | {...props} 24 | preview={{ 25 | mask: typeof props.preview === 'object' ? props.preview.mask : false, 26 | toolbarRender: ( 27 | _, 28 | { 29 | transform: { scale }, 30 | actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset } 31 | } 32 | ) => ( 33 | <ToobarWrapper size={12} className="toolbar-wrapper"> 34 | <SwapOutlined rotate={90} onClick={onFlipY} /> 35 | <SwapOutlined onClick={onFlipX} /> 36 | <RotateLeftOutlined onClick={onRotateLeft} /> 37 | <RotateRightOutlined onClick={onRotateRight} /> 38 | <ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} /> 39 | <ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} /> 40 | <UndoOutlined onClick={onReset} /> 41 | <DownloadOutlined onClick={() => download(src)} /> 42 | </ToobarWrapper> 43 | ) 44 | }} 45 | /> 46 | ) 47 | } 48 | 49 | const ToobarWrapper = styled(Space)` 50 | padding: 0px 24px; 51 | color: #fff; 52 | font-size: 20px; 53 | background-color: rgba(0, 0, 0, 0.1); 54 | border-radius: 100px; 55 | .anticon { 56 | padding: 12px; 57 | cursor: pointer; 58 | } 59 | .anticon:hover { 60 | opacity: 0.3; 61 | } 62 | ` 63 | 64 | export default ImagePreview 65 | -------------------------------------------------------------------------------- /src/renderer/src/store/agents.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 2 | import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant' 3 | import { Agent, AssistantSettings } from '@renderer/types' 4 | 5 | export interface AgentsState { 6 | agents: Agent[] 7 | } 8 | 9 | const initialState: AgentsState = { 10 | agents: [] 11 | } 12 | 13 | const assistantsSlice = createSlice({ 14 | name: 'agents', 15 | initialState, 16 | reducers: { 17 | updateAgents: (state, action: PayloadAction<Agent[]>) => { 18 | state.agents = action.payload 19 | }, 20 | addAgent: (state, action: PayloadAction<Agent>) => { 21 | state.agents.push(action.payload) 22 | }, 23 | removeAgent: (state, action: PayloadAction<{ id: string }>) => { 24 | state.agents = state.agents.filter((c) => c.id !== action.payload.id) 25 | }, 26 | updateAgent: (state, action: PayloadAction<Agent>) => { 27 | state.agents = state.agents.map((c) => (c.id === action.payload.id ? action.payload : c)) 28 | }, 29 | updateAgentSettings: ( 30 | state, 31 | action: PayloadAction<{ assistantId: string; settings: Partial<AssistantSettings> }> 32 | ) => { 33 | for (const agent of state.agents) { 34 | const settings = action.payload.settings 35 | if (agent.id === action.payload.assistantId) { 36 | for (const key in settings) { 37 | if (!agent.settings) { 38 | agent.settings = { 39 | temperature: DEFAULT_TEMPERATURE, 40 | contextCount: DEFAULT_CONTEXTCOUNT, 41 | enableMaxTokens: false, 42 | maxTokens: 0, 43 | streamOutput: true, 44 | hideMessages: false, 45 | autoResetModel: false 46 | } 47 | } 48 | agent.settings[key] = settings[key] 49 | } 50 | } 51 | } 52 | } 53 | } 54 | }) 55 | 56 | export const { updateAgents, addAgent, removeAgent, updateAgent, updateAgentSettings } = assistantsSlice.actions 57 | 58 | export default assistantsSlice.reducer 59 | -------------------------------------------------------------------------------- /src/renderer/src/components/Popups/PromptPopup.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Modal } from 'antd' 2 | import { TextAreaProps } from 'antd/es/input' 3 | import { useState } from 'react' 4 | 5 | import { Box } from '../Layout' 6 | import { TopView } from '../TopView' 7 | 8 | interface PromptPopupShowParams { 9 | title: string 10 | message: string 11 | defaultValue?: string 12 | inputPlaceholder?: string 13 | inputProps?: TextAreaProps 14 | } 15 | 16 | interface Props extends PromptPopupShowParams { 17 | resolve: (value: any) => void 18 | } 19 | 20 | const PromptPopupContainer: React.FC<Props> = ({ 21 | title, 22 | message, 23 | defaultValue = '', 24 | inputPlaceholder = '', 25 | inputProps = {}, 26 | resolve 27 | }) => { 28 | const [value, setValue] = useState(defaultValue) 29 | const [open, setOpen] = useState(true) 30 | 31 | const onOk = () => { 32 | setOpen(false) 33 | resolve(value) 34 | } 35 | 36 | const onCancel = () => { 37 | setOpen(false) 38 | } 39 | 40 | const onClose = () => { 41 | resolve(null) 42 | } 43 | 44 | PromptPopup.hide = onCancel 45 | 46 | return ( 47 | <Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered> 48 | <Box mb={8}>{message}</Box> 49 | <Input.TextArea 50 | placeholder={inputPlaceholder} 51 | value={value} 52 | onChange={(e) => setValue(e.target.value)} 53 | allowClear 54 | autoFocus 55 | onPressEnter={onOk} 56 | rows={1} 57 | {...inputProps} 58 | /> 59 | </Modal> 60 | ) 61 | } 62 | 63 | const TopViewKey = 'PromptPopup' 64 | 65 | export default class PromptPopup { 66 | static topviewId = 0 67 | static hide() { 68 | TopView.hide(TopViewKey) 69 | } 70 | static show(props: PromptPopupShowParams) { 71 | return new Promise<string>((resolve) => { 72 | TopView.show( 73 | <PromptPopupContainer 74 | {...props} 75 | resolve={(v) => { 76 | resolve(v) 77 | TopView.hide(TopViewKey) 78 | }} 79 | />, 80 | 'PromptPopup' 81 | ) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/Messages/MessageContent.tsx: -------------------------------------------------------------------------------- 1 | import { SyncOutlined, TranslationOutlined } from '@ant-design/icons' 2 | import { Message, Model } from '@renderer/types' 3 | import { getBriefInfo } from '@renderer/utils' 4 | import { Divider } from 'antd' 5 | import React from 'react' 6 | import { useTranslation } from 'react-i18next' 7 | import BeatLoader from 'react-spinners/BeatLoader' 8 | import styled from 'styled-components' 9 | 10 | import Markdown from '../Markdown/Markdown' 11 | import MessageAttachments from './MessageAttachments' 12 | import MessageError from './MessageError' 13 | 14 | const MessageContent: React.FC<{ 15 | message: Message 16 | model?: Model 17 | }> = ({ message, model }) => { 18 | const { t } = useTranslation() 19 | 20 | if (message.status === 'sending') { 21 | return ( 22 | <MessageContentLoading> 23 | <SyncOutlined spin size={24} /> 24 | </MessageContentLoading> 25 | ) 26 | } 27 | 28 | if (message.status === 'error') { 29 | return <MessageError message={message} /> 30 | } 31 | 32 | if (message.type === '@' && model) { 33 | const content = `[@${model.name}](#) ${getBriefInfo(message.content)}` 34 | return <Markdown message={{ ...message, content }} /> 35 | } 36 | 37 | return ( 38 | <> 39 | <Markdown message={message} /> 40 | {message.translatedContent && ( 41 | <> 42 | <Divider style={{ margin: 0, marginBottom: 10 }}> 43 | <TranslationOutlined /> 44 | </Divider> 45 | {message.translatedContent === t('translate.processing') ? ( 46 | <BeatLoader color="var(--color-text-2)" size="10" style={{ marginBottom: 15 }} /> 47 | ) : ( 48 | <Markdown message={{ ...message, content: message.translatedContent }} /> 49 | )} 50 | </> 51 | )} 52 | <MessageAttachments message={message} /> 53 | </> 54 | ) 55 | } 56 | 57 | const MessageContentLoading = styled.div` 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | height: 32px; 62 | margin-top: -5px; 63 | margin-bottom: 5px; 64 | ` 65 | 66 | export default React.memo(MessageContent) 67 | -------------------------------------------------------------------------------- /src/renderer/src/components/ListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface ListItemProps { 5 | active?: boolean 6 | icon?: ReactNode 7 | title: string 8 | subtitle?: string 9 | onClick?: () => void 10 | } 11 | 12 | const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => { 13 | return ( 14 | <ListItemContainer className={active ? 'active' : ''} onClick={onClick}> 15 | <ListItemContent> 16 | {icon && <IconWrapper>{icon}</IconWrapper>} 17 | <TextContainer> 18 | <TitleText>{title}</TitleText> 19 | {subtitle && <SubtitleText>{subtitle}</SubtitleText>} 20 | </TextContainer> 21 | </ListItemContent> 22 | </ListItemContainer> 23 | ) 24 | } 25 | 26 | const ListItemContainer = styled.div` 27 | padding: 7px 12px; 28 | border-radius: var(--list-item-border-radius); 29 | font-size: 13px; 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: space-between; 33 | position: relative; 34 | font-family: Ubuntu; 35 | cursor: pointer; 36 | border: 1px solid transparent; 37 | 38 | &:hover { 39 | background-color: var(--color-background-soft); 40 | } 41 | 42 | &.active { 43 | background-color: var(--color-background-soft); 44 | border: 1px solid var(--color-border-soft); 45 | } 46 | ` 47 | 48 | const ListItemContent = styled.div` 49 | display: flex; 50 | align-items: center; 51 | gap: 8px; 52 | overflow: hidden; 53 | font-size: 13px; 54 | ` 55 | 56 | const IconWrapper = styled.span` 57 | margin-right: 8px; 58 | ` 59 | 60 | const TextContainer = styled.div` 61 | display: flex; 62 | flex-direction: column; 63 | overflow: hidden; 64 | ` 65 | 66 | const TitleText = styled.div` 67 | overflow: hidden; 68 | text-overflow: ellipsis; 69 | white-space: nowrap; 70 | ` 71 | 72 | const SubtitleText = styled.div` 73 | font-size: 10px; 74 | color: var(--color-text-soft); 75 | margin-top: 2px; 76 | display: -webkit-box; 77 | -webkit-line-clamp: 1; 78 | -webkit-box-orient: vertical; 79 | overflow: hidden; 80 | color: var(--color-text-3); 81 | ` 82 | 83 | export default ListItem 84 | -------------------------------------------------------------------------------- /src/renderer/src/context/AntdProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useSettings } from '@renderer/hooks/useSettings' 2 | import { LanguageVarious } from '@renderer/types' 3 | import { ConfigProvider, theme } from 'antd' 4 | import enUS from 'antd/locale/en_US' 5 | import jaJP from 'antd/locale/ja_JP' 6 | import ruRU from 'antd/locale/ru_RU' 7 | import zhCN from 'antd/locale/zh_CN' 8 | import zhTW from 'antd/locale/zh_TW' 9 | import { FC, PropsWithChildren } from 'react' 10 | 11 | import { useTheme } from './ThemeProvider' 12 | 13 | const AntdProvider: FC<PropsWithChildren> = ({ children }) => { 14 | const { language } = useSettings() 15 | const { theme: _theme } = useTheme() 16 | const isDarkTheme = _theme === 'dark' 17 | 18 | return ( 19 | <ConfigProvider 20 | locale={getAntdLocale(language)} 21 | theme={{ 22 | algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm], 23 | components: { 24 | Segmented: { 25 | trackBg: 'transparent', 26 | itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', 27 | boxShadowTertiary: undefined, 28 | borderRadiusLG: 16, 29 | borderRadiusSM: 16, 30 | borderRadiusXS: 16 31 | }, 32 | Menu: { 33 | activeBarBorderWidth: 0, 34 | darkItemBg: 'transparent' 35 | }, 36 | Button: { 37 | boxShadow: 'none', 38 | boxShadowSecondary: 'none', 39 | defaultShadow: 'none', 40 | dangerShadow: 'none', 41 | primaryShadow: 'none' 42 | } 43 | }, 44 | token: { 45 | colorPrimary: '#00b96b' 46 | } 47 | }}> 48 | {children} 49 | </ConfigProvider> 50 | ) 51 | } 52 | 53 | function getAntdLocale(language: LanguageVarious) { 54 | switch (language) { 55 | case 'zh-CN': 56 | return zhCN 57 | case 'zh-TW': 58 | return zhTW 59 | case 'en-US': 60 | return enUS 61 | case 'ru-RU': 62 | return ruRU 63 | case 'ja-JP': 64 | return jaJP 65 | 66 | default: 67 | return zhCN 68 | } 69 | } 70 | 71 | export default AntdProvider 72 | -------------------------------------------------------------------------------- /src/renderer/src/pages/home/components/SelectModelButton.tsx: -------------------------------------------------------------------------------- 1 | import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' 2 | import ModelTags from '@renderer/components/ModelTags' 3 | import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' 4 | import { isLocalAi } from '@renderer/config/env' 5 | import { useAssistant } from '@renderer/hooks/useAssistant' 6 | import { getProviderName } from '@renderer/services/ProviderService' 7 | import { Assistant } from '@renderer/types' 8 | import { Button } from 'antd' 9 | import { FC } from 'react' 10 | import { useTranslation } from 'react-i18next' 11 | import styled from 'styled-components' 12 | 13 | interface Props { 14 | assistant: Assistant 15 | } 16 | 17 | const SelectModelButton: FC<Props> = ({ assistant }) => { 18 | const { model, setModel } = useAssistant(assistant.id) 19 | const { t } = useTranslation() 20 | 21 | if (isLocalAi) { 22 | return null 23 | } 24 | 25 | const onSelectModel = async (event: React.MouseEvent<HTMLElement>) => { 26 | event.currentTarget.blur() 27 | const selectedModel = await SelectModelPopup.show({ model }) 28 | if (selectedModel) { 29 | setModel(selectedModel) 30 | } 31 | } 32 | 33 | const providerName = getProviderName(model?.provider) 34 | 35 | return ( 36 | <DropdownButton size="small" type="default" onClick={onSelectModel}> 37 | <ButtonContent> 38 | <ModelAvatar model={model} size={20} /> 39 | <ModelName> 40 | {model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''} 41 | </ModelName> 42 | <ModelTags model={model} showFree={false} /> 43 | </ButtonContent> 44 | </DropdownButton> 45 | ) 46 | } 47 | 48 | const DropdownButton = styled(Button)` 49 | font-size: 11px; 50 | border-radius: 15px; 51 | padding: 12px 8px 12px 3px; 52 | -webkit-app-region: none; 53 | box-shadow: none; 54 | background-color: transparent; 55 | border: 1px solid transparent; 56 | ` 57 | 58 | const ButtonContent = styled.div` 59 | display: flex; 60 | align-items: center; 61 | gap: 5px; 62 | ` 63 | 64 | const ModelName = styled.span` 65 | font-weight: 500; 66 | ` 67 | 68 | export default SelectModelButton 69 | --------------------------------------------------------------------------------