Merge remote-tracking branch 'origin/main' into feat/cherry-store-render

This commit is contained in:
MyPrototypeWhat 2025-06-09 14:47:41 +08:00
commit b7d9949832
530 changed files with 48667 additions and 34194 deletions

86
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,86 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 7
target-branch: "main"
commit-message:
prefix: "chore"
include: "scope"
groups:
# 核心框架
core-framework:
patterns:
- "react"
- "react-dom"
- "electron"
- "typescript"
- "@types/react*"
- "@types/node"
update-types:
- "minor"
- "patch"
# Electron 生态和构建工具
electron-build:
patterns:
- "electron-*"
- "@electron*"
- "vite"
- "@vitejs/*"
- "dotenv-cli"
- "rollup-plugin-*"
- "@swc/*"
update-types:
- "minor"
- "patch"
# 测试工具
testing-tools:
patterns:
- "vitest"
- "@vitest/*"
- "playwright"
- "@playwright/*"
- "eslint*"
- "@eslint*"
- "prettier"
- "husky"
- "lint-staged"
update-types:
- "minor"
- "patch"
# CherryStudio 自定义包
cherrystudio-packages:
patterns:
- "@cherrystudio/*"
update-types:
- "minor"
- "patch"
# 兜底其他 dependencies
other-dependencies:
dependency-type: "production"
# 兜底其他 devDependencies
other-dev-dependencies:
dependency-type: "development"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 3
commit-message:
prefix: "ci"
include: "scope"
groups:
github-actions:
patterns:
- "*"
update-types:
- "minor"
- "patch"

View File

@ -54,5 +54,5 @@ jobs:
days-before-pr-close: -1 # Completely disable closing for PRs
# Temporary to reduce the huge issues number
operations-per-run: 100
operations-per-run: 1000
debug-only: false

View File

@ -52,6 +52,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
ref: main
- name: Install Node.js
uses: actions/setup-node@v4

View File

@ -5,6 +5,7 @@ on:
pull_request:
branches:
- main
- develop
jobs:
build:

View File

@ -26,6 +26,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
ref: main
- name: Get release tag
id: get-tag
@ -111,5 +113,40 @@ jobs:
allowUpdates: true
makeLatest: false
tag: ${{ steps.get-tag.outputs.tag }}
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
token: ${{ secrets.GITHUB_TOKEN }}
dispatch-docs-update:
needs: release
if: success() && github.repository == 'CherryHQ/cherry-studio' # 确保所有构建成功且在主仓库中运行
runs-on: ubuntu-latest
steps:
- name: Get release tag
id: get-tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Check if tag is pre-release
id: check-tag
shell: bash
run: |
TAG="${{ steps.get-tag.outputs.tag }}"
if [[ "$TAG" == *"rc"* || "$TAG" == *"pre-release"* ]]; then
echo "is_pre_release=true" >> $GITHUB_OUTPUT
else
echo "is_pre_release=false" >> $GITHUB_OUTPUT
fi
- name: Dispatch update-download-version workflow to cherry-studio-docs
if: steps.check-tag.outputs.is_pre_release == 'false'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs
event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

10
.gitignore vendored
View File

@ -45,9 +45,15 @@ stats.html
local
.aider*
.cursorrules
.cursor/rules
.cursor/*
# test
# vitest
coverage
.vitest-cache
vitest.config.*.timestamp-*
# playwright
playwright-report
test-results
YOUR_MEMORY_FILE_PATH

View File

@ -0,0 +1,71 @@
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
index 973b0d0e75aeaf8de579419af31b879b32975413..f23c7caa8b9dc8bd404132725346a4786f6b278b 100644
--- a/dist/utils/tiktoken.cjs
+++ b/dist/utils/tiktoken.cjs
@@ -1,25 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodingForModel = exports.getEncoding = void 0;
-const lite_1 = require("js-tiktoken/lite");
const async_caller_js_1 = require("./async_caller.cjs");
const cache = {};
const caller = /* #__PURE__ */ new async_caller_js_1.AsyncCaller({});
async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new lite_1.Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
exports.getEncoding = getEncoding;
async function encodingForModel(model) {
- return getEncoding((0, lite_1.getEncodingNameForModel)(model));
+ throw new Error("TikToken Not implemented");
}
exports.encodingForModel = encodingForModel;
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..aa5f314a6349ad0d1c5aea8631a56aad099176e0 100644
--- a/dist/utils/tiktoken.js
+++ b/dist/utils/tiktoken.js
@@ -1,20 +1,9 @@
-import { Tiktoken, getEncodingNameForModel, } from "js-tiktoken/lite";
import { AsyncCaller } from "./async_caller.js";
const cache = {};
const caller = /* #__PURE__ */ new AsyncCaller({});
export async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
export async function encodingForModel(model) {
- return getEncoding(getEncodingNameForModel(model));
+ throw new Error("TikToken Not implemented");
}
diff --git a/package.json b/package.json
index 36072aecf700fca1bc49832a19be832eca726103..90b8922fba1c3d1b26f78477c891b07816d6238a 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,6 @@
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
- "js-tiktoken": "^1.0.12",
"langsmith": ">=0.2.8 <0.4.0",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",

View File

@ -0,0 +1,159 @@
diff --git a/out/macPackager.js b/out/macPackager.js
index 852f6c4d16f86a7bb8a78bf1ed5a14647a279aa1..60e7f5f16a844541eb1909b215fcda1811e924b8 100644
--- a/out/macPackager.js
+++ b/out/macPackager.js
@@ -423,7 +423,7 @@ class MacPackager extends platformPackager_1.PlatformPackager {
}
appPlist.CFBundleName = appInfo.productName;
appPlist.CFBundleDisplayName = appInfo.productName;
- const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion;
+ const minimumSystemVersion = this.platformSpecificBuildOptions.LSMinimumSystemVersion;
if (minimumSystemVersion != null) {
appPlist.LSMinimumSystemVersion = minimumSystemVersion;
}
diff --git a/out/publish/updateInfoBuilder.js b/out/publish/updateInfoBuilder.js
index 7924c5b47d01f8dfccccb8f46658015fa66da1f7..1a1588923c3939ae1297b87931ba83f0ebc052d8 100644
--- a/out/publish/updateInfoBuilder.js
+++ b/out/publish/updateInfoBuilder.js
@@ -133,6 +133,7 @@ async function createUpdateInfo(version, event, releaseInfo) {
const customUpdateInfo = event.updateInfo;
const url = path.basename(event.file);
const sha512 = (customUpdateInfo == null ? null : customUpdateInfo.sha512) || (await (0, hash_1.hashFile)(event.file));
+ const minimumSystemVersion = customUpdateInfo == null ? null : customUpdateInfo.minimumSystemVersion;
const files = [{ url, sha512 }];
const result = {
// @ts-ignore
@@ -143,9 +144,13 @@ async function createUpdateInfo(version, event, releaseInfo) {
path: url /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
// @ts-ignore
sha512 /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
+ minimumSystemVersion,
...releaseInfo,
};
if (customUpdateInfo != null) {
+ if (customUpdateInfo.minimumSystemVersion) {
+ delete customUpdateInfo.minimumSystemVersion;
+ }
// file info or nsis web installer packages info
Object.assign("sha512" in customUpdateInfo ? files[0] : result, customUpdateInfo);
}
diff --git a/out/targets/ArchiveTarget.js b/out/targets/ArchiveTarget.js
index e1f52a5fa86fff6643b2e57eaf2af318d541f865..47cc347f154a24b365e70ae5e1f6d309f3582ed0 100644
--- a/out/targets/ArchiveTarget.js
+++ b/out/targets/ArchiveTarget.js
@@ -69,6 +69,9 @@ class ArchiveTarget extends core_1.Target {
}
}
}
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
+ }
await packager.info.emitArtifactBuildCompleted({
updateInfo,
file: artifactPath,
diff --git a/out/targets/nsis/NsisTarget.js b/out/targets/nsis/NsisTarget.js
index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01de4b89b1 100644
--- a/out/targets/nsis/NsisTarget.js
+++ b/out/targets/nsis/NsisTarget.js
@@ -305,6 +305,9 @@ class NsisTarget extends core_1.Target {
if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) {
updateInfo.isAdminRightsRequired = true;
}
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
+ }
await packager.info.emitArtifactBuildCompleted({
file: installerPath,
updateInfo,
diff --git a/scheme.json b/scheme.json
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43ebd0fa8b61 100644
--- a/scheme.json
+++ b/scheme.json
@@ -1975,6 +1975,13 @@
],
"description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing."
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"packageCategory": {
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
"type": [
@@ -2327,6 +2334,13 @@
"MacConfiguration": {
"additionalProperties": false,
"properties": {
+ "LSMinimumSystemVersion": {
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"additionalArguments": {
"anyOf": [
{
@@ -2737,7 +2751,7 @@
"type": "boolean"
},
"minimumSystemVersion": {
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "description": "The minimum os kernel version required to install the application.",
"type": [
"null",
"string"
@@ -2959,6 +2973,13 @@
"MasConfiguration": {
"additionalProperties": false,
"properties": {
+ "LSMinimumSystemVersion": {
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"additionalArguments": {
"anyOf": [
{
@@ -3369,7 +3390,7 @@
"type": "boolean"
},
"minimumSystemVersion": {
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "description": "The minimum os kernel version required to install the application.",
"type": [
"null",
"string"
@@ -6507,6 +6528,13 @@
"string"
]
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"protocols": {
"anyOf": [
{
@@ -7376,6 +7404,13 @@
],
"description": "MAS (Mac Application Store) development options (`mas-dev` target)."
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"msi": {
"anyOf": [
{

View File

@ -1,39 +0,0 @@
diff --git a/core.js b/core.js
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
--- a/core.js
+++ b/core.js
@@ -159,7 +159,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/core.mjs b/core.mjs
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
--- a/core.mjs
+++ b/core.mjs
@@ -152,7 +152,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/error.mjs b/error.mjs
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
--- a/error.mjs
+++ b/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}

View File

@ -0,0 +1,279 @@
diff --git a/client.js b/client.js
index 33b4ff6309d5f29187dab4e285d07dac20340bab..8f568637ee9e4677585931fb0284c8165a933f69 100644
--- a/client.js
+++ b/client.js
@@ -433,7 +433,7 @@ class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...(0, detect_platform_1.getPlatformHeaders)(),
+ // ...(0, detect_platform_1.getPlatformHeaders)(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/client.mjs b/client.mjs
index c34c18213073540ebb296ea540b1d1ad39527906..1ce1a98256d7e90e26ca963582f235b23e996e73 100644
--- a/client.mjs
+++ b/client.mjs
@@ -430,7 +430,7 @@ export class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/core/error.js b/core/error.js
index a12d9d9ccd242050161adeb0f82e1b98d9e78e20..fe3a5462480558bc426deea147f864f12b36f9bd 100644
--- a/core/error.js
+++ b/core/error.js
@@ -40,7 +40,7 @@ class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: (0, errors_1.castToError)(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/core/error.mjs b/core/error.mjs
index 83cefbaffeb8c657536347322d8de9516af479a2..63334b7972ec04882aa4a0800c1ead5982345045 100644
--- a/core/error.mjs
+++ b/core/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/resources/embeddings.js b/resources/embeddings.js
index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b90ae6790 100644
--- a/resources/embeddings.js
+++ b/resources/embeddings.js
@@ -5,52 +5,64 @@ exports.Embeddings = void 0;
const resource_1 = require("../core/resource.js");
const utils_1 = require("../internal/utils.js");
class Embeddings extends resource_1.APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- (0, utils_1.loggerFor)(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- (0, utils_1.loggerFor)(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
+ embeddingBase64Str
+ );
+ });
+ }
+ return response;
+ });
+ }
}
exports.Embeddings = Embeddings;
//# sourceMappingURL=embeddings.js.map
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a74bd9200 100644
--- a/resources/embeddings.mjs
+++ b/resources/embeddings.mjs
@@ -2,51 +2,61 @@
import { APIResource } from "../core/resource.mjs";
import { loggerFor, toFloat32Array } from "../internal/utils.mjs";
export class Embeddings extends APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- loggerFor(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- loggerFor(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ loggerFor(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ loggerFor(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
+ });
+ }
+ return response;
+ });
+ }
}
//# sourceMappingURL=embeddings.mjs.map

Binary file not shown.

BIN
.yarn/releases/yarn-4.9.1.cjs vendored Executable file

Binary file not shown.

View File

@ -4,4 +4,4 @@ httpTimeout: 300000
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.9.1.cjs

162
README.md
View File

@ -3,10 +3,42 @@
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
<!-- 题头徽章组合 -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
@ -17,15 +49,13 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
# 📖 Guide
https://docs.cherry-ai.com
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
![](https://github.com/user-attachments/assets/f549e8a0-2385-40b4-b52b-2039e39f2930)
![](https://github.com/user-attachments/assets/58e0237c-4d36-40de-b428-53051d982026)
# 🌟 Key Features
@ -65,35 +95,53 @@ https://docs.cherry-ai.com
- 📝 Complete Markdown Rendering
- 🤲 Easy Content Sharing
# 📝 TODO
# 📝 Roadmap
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
- [x] Comparison of multi-model answers
- [x] Support login using SSO provided by service providers
- [x] All models support networking
- [x] Launch of the first official version
- [x] Bug fixes and improvements (In progress...)
- [ ] Plugin functionality (JavaScript)
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
- [ ] iOS & Android client
- [ ] AI notes
- [ ] Voice input and output (AI call)
- [ ] Data backup supports custom backup content
We're actively working on the following features and improvements:
1. 🎯 **Core Features**
- Selection Assistant - Smart content selection enhancement
- Deep Research - Advanced research capabilities
- Memory System - Global context awareness
- Document Preprocessing - Improved document handling
- MCP Marketplace - Model Context Protocol ecosystem
2. 🗂 **Knowledge Management**
- Notes and Collections
- Dynamic Canvas visualization
- OCR capabilities
- TTS (Text-to-Speech) support
3. 📱 **Platform Support**
- HarmonyOS Edition (PC)
- Android App (Phase 1)
- iOS App (Phase 1)
- Multi-Window support
- Window Pinning functionality
4. 🔌 **Advanced Features**
- Plugin System
- ASR (Automatic Speech Recognition)
- Assistant and Topic Interaction Refactoring
Track our progress and contribute on our [project board](https://github.com/orgs/CherryHQ/projects/7).
Want to influence our roadmap? Join our [GitHub Discussions](https://github.com/CherryHQ/cherry-studio/discussions) to share your ideas and feedback!
# 🌈 Theme
- Theme Gallery: https://cherrycss.com
- Aero Theme: https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial Theme: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude dynamic-style: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- Maple Neon Theme: https://github.com/BoningtonChen/CherryStudio_themes
- Theme Gallery: <https://cherrycss.com>
- Aero Theme: <https://github.com/hakadao/CherryStudio-Aero>
- PaperMaterial Theme: <https://github.com/rainoffallingstar/CherryStudio-PaperMaterial>
- Claude dynamic-style: <https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic>
- Maple Neon Theme: <https://github.com/BoningtonChen/CherryStudio_themes>
Welcome PR for more themes
# 🖥️ Develop
Refer to the [development documentation](docs/dev.md)
# 🤝 Contributing
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
@ -106,6 +154,8 @@ We welcome contributions to Cherry Studio! Here are some ways you can contribute
6. **Community Engagement**: Join discussions and help users.
7. **Promote Usage**: Spread the word about Cherry Studio.
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
## Getting Started
1. **Fork the Repository**: Fork and clone it to your local machine.
@ -117,7 +167,7 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
Thank you for your support and contributions!
## Related Projects
# 🔗 Related Projects
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
@ -126,26 +176,38 @@ Thank you for your support and contributions!
# 🚀 Contributors
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
# 🌐 Community
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ Sponsor
[Buy Me a Coffee](docs/sponsor.md)
# 📃 License
[LICENSE](./LICENSE)
# ✉️ Contact
yinsenho@cherry-ai.com
# ⭐️ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- Links & Images -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- Links & Images -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=Commercial%20License%20Inquiry
[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@ -1,32 +1,63 @@
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 | <a href="https://cherry-ai.com">公式サイト</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/ja">ドキュメント</a> | <a href="./dev.md">開発</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">フィードバック</a><br>
</p>
<!-- バッジコレクション -->
<div align="center">
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- プロジェクト統計 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
# 📖 ガイド
https://docs.cherry-ai.com
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
![](https://github.com/user-attachments/assets/f549e8a0-2385-40b4-b52b-2039e39f2930)
![](https://github.com/user-attachments/assets/58e0237c-4d36-40de-b428-53051d982026)
# 🌟 主な機能
@ -56,7 +87,7 @@ https://docs.cherry-ai.com
- 🔤 AI による翻訳機能
- 🎯 ドラッグ&ドロップによる整理
- 🔌 ミニプログラム対応
- ⚙️ MCPモデルコンテキストプロトコル サービス
- ⚙️ MCPモデルコンテキストプロトコルサービス
5. **優れたユーザー体験**
@ -66,84 +97,119 @@ https://docs.cherry-ai.com
- 📝 完全な Markdown レンダリング
- 🤲 簡単な共有機能
# 📝 TODO
# 📝 開発計画
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
- [x] 複数モデルの回答の比較
- [x] サービスプロバイダーが提供する SSO を使用したログインをサポート
- [x] すべてのモデルがネットワークをサポート
- [x] 最初の公式バージョンのリリース
- [ ] 錯誤修復と改善 (開発中...)
- [ ] プラグイン機能JavaScript
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
- [ ] iOS & Android クライアント
- [ ] AIート
- [ ] 音声入出力AI コール)
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
以下の機能と改善に積極的に取り組んでいます:
1. 🎯 **コア機能**
- 選択アシスタント - スマートな内容選択の強化
- ディープリサーチ - 高度な研究能力
- メモリーシステム - グローバルコンテキスト認識
- ドキュメント前処理 - 文書処理の改善
- MCP マーケットプレイス - モデルコンテキストプロトコルエコシステム
2. 🗂 **ナレッジ管理**
- ノートとコレクション
- ダイナミックキャンバス可視化
- OCR 機能
- TTSテキスト読み上げサポート
3. 📱 **プラットフォーム対応**
- HarmonyOS エディション
- Android アプリフェーズ1
- iOS アプリフェーズ1
- マルチウィンドウ対応
- ウィンドウピン留め機能
4. 🔌 **高度な機能**
- プラグインシステム
- ASR音声認識
- アシスタントとトピックの対話機能リファクタリング
[プロジェクトボード](https://github.com/orgs/CherryHQ/projects/7)で進捗を確認し、貢献することができます。
開発計画に影響を与えたいですか?[GitHub ディスカッション](https://github.com/CherryHQ/cherry-studio/discussions)に参加して、アイデアやフィードバックを共有してください!
# 🌈 テーマ
- テーマギャラリー: https://cherrycss.com
- Aero テーマ: https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial テーマ: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude テーマ: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- メープルネオンテーマ: https://github.com/BoningtonChen/CherryStudio_themes
- テーマギャラリーhttps://cherrycss.com
- Aero テーマhttps://github.com/hakadao/CherryStudio-Aero
- PaperMaterial テーマhttps://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude テーマhttps://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- メープルネオンテーマhttps://github.com/BoningtonChen/CherryStudio_themes
より多くのテーマのPRを歓迎します
# 🖥️ 開発
参考[開発ドキュメント](dev.md)
より多くのテーマの PR を歓迎します
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します。
2. **バグの修正**:見つけたバグを修正します。
3. **問題の管理**GitHub の問題を管理するのを手伝います。
4. **製品デザイン**:デザインの議論に参加します。
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します。
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します。
7. **使用の促進**Cherry Studio を広めます。
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
2. **バグの修正**:見つけたバグを修正します
3. **問題の管理**GitHub の問題を管理するのを手伝います
4. **製品デザイン**:デザインの議論に参加します
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**Cherry Studio を広めます
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
2. **ブランチを作成**:変更のためのブランチを作成します
3. **変更を提出**:変更をコミットしてプッシュします
4. **プルリクエストを開く**:変更内容と理由を説明します
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
2. **ブランチを作成**:変更のためのブランチを作成します
3. **変更を提出**:変更をコミットしてプッシュします
4. **プルリクエストを開く**:変更内容と理由を説明します
詳細なガイドラインについては、[貢献ガイド](../CONTRIBUTING.md)をご覧ください。
ご支援と貢献に感謝します!
## 関連頁版
# 🔗 関連プロジェクト
- [one-api](https://github.com/songquanpeng/one-api)LLM API の管理・配信システム。OpenAI、Azure、Anthropic などの主要モデルに対応し、統一 API インターフェースを提供。API キー管理と再配布に利用可能。
- [ublacklist](https://github.com/iorate/ublacklist)Google 検索結果から特定のサイトを非表示にします
# 🚀 コントリビューター
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
# コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# スポンサー
[Buy Me a Coffee](sponsor.md)
# 📃 ライセンス
[LICENSE](../LICENSE)
# ✉️ お問い合わせ
yinsenho@cherry-ai.com
<br /><br />
# ⭐️ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- リンクと画像 -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- プロジェクト統計 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- ライセンスとスポンサー -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用ライセンス-お問い合わせ-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商業ライセンスについて
[sponsor-shield]: https://img.shields.io/badge/スポンサー-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@ -1,13 +1,46 @@
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a> | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
</p>
<!-- 题头徽章组合 -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
@ -18,15 +51,25 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
# GitCode✖Cherry Studio【新源力】贡献挑战赛
<p align="center">
<a href="https://gitcode.com/CherryHQ/cherry-studio/discussion/2">
<img src="https://raw.gitcode.com/user-images/assets/5007375/8d8d7559-1141-4691-b90f-d154558c6896/cherry-studio-gitcode.jpg" width="100%" alt="banner" />
</a>
</p>
# 📖 使用教程
https://docs.cherry-ai.com
# 🌠 界面
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
![](https://github.com/user-attachments/assets/f549e8a0-2385-40b4-b52b-2039e39f2930)
![](https://github.com/user-attachments/assets/58e0237c-4d36-40de-b428-53051d982026)
# 🌟 主要特性
@ -66,85 +109,119 @@ https://docs.cherry-ai.com
- 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能
# 📝 待辦事項
# 📝 开发计划
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
- [x] 多模型回答对比
- [x] 支持使用服务供应商提供的 SSO 进行登入
- [x] 全部模型支持连网(开发中...
- [x] 推出第一个正式版
- [x] 错误修复和改进(开发中...
- [ ] 插件功能JavaScript
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
- [ ] iOS & Android 客户端
- [ ] AI 笔记
- [ ] 语音输入输出AI 通话)
- [ ] 数据备份支持自定义备份内容
我们正在积极开发以下功能和改进:
1. 🎯 **核心功能**
- 选择助手 - 智能内容选择增强
- 深度研究 - 高级研究能力
- 全局记忆 - 全局上下文感知
- 文档预处理 - 改进文档处理能力
- MCP 市场 - 模型上下文协议生态系统
2. 🗂 **知识管理**
- 笔记与收藏功能
- 动态画布可视化
- OCR 光学字符识别
- TTS 文本转语音支持
3. 📱 **平台支持**
- 鸿蒙版本 (PC)
- Android 应用(第一期)
- iOS 应用(第一期)
- 多窗口支持
- 窗口置顶功能
4. 🔌 **高级特性**
- 插件系统
- ASR 语音识别
- 助手与话题交互重构
在我们的[项目面板](https://github.com/orgs/CherryHQ/projects/7)上跟踪进展并参与贡献。
想要影响开发计划?欢迎加入我们的 [GitHub 讨论区](https://github.com/CherryHQ/cherry-studio/discussions) 分享您的想法和反馈!
# 🌈 主题
- 主题库https://cherrycss.com
- Aero 主题https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial 主题: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- 仿Claude 主题: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- 霓虹枫叶字体主题: https://github.com/BoningtonChen/CherryStudio_themes
- PaperMaterial 主题https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- 仿 Claude 主题:https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- 霓虹枫叶主题:https://github.com/BoningtonChen/CherryStudio_themes
欢迎 PR 更多主题
# 🖥️ 开发
参考[开发文档](dev.md)
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
1. **贡献代码**:开发新功能或优化现有代码。
2. **修复错误**:提交您发现的错误修复。
3. **维护问题**:帮助管理 GitHub 问题。
4. **产品设计**:参与设计讨论。
5. **撰写文档**:改进用户手册和指南。
6. **社区参与**:加入讨论并帮助用户。
7. **推广使用**:宣传 Cherry Studio。
1. **贡献代码**:开发新功能或优化现有代码
2. **修复错误**:提交您发现的错误修复
3. **维护问题**:帮助管理 GitHub 问题
4. **产品设计**:参与设计讨论
5. **撰写文档**:改进用户手册和指南
6. **社区参与**:加入讨论并帮助用户
7. **推广使用**:宣传 Cherry Studio
参考[分支策略](branching-strategy-zh.md)了解贡献指南
## 入门
1. **Fork 仓库**Fork 并克隆到您的本地机器
2. **创建分支**:为您的更改创建分支
3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因
1. **Fork 仓库**Fork 并克隆到您的本地机器
2. **创建分支**:为您的更改创建分支
3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
感谢您的支持和贡献!
## 相关项目
# 🔗 相关项目
- [one-api](https://github.com/songquanpeng/one-api)LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
- [ublacklist](https://github.com/iorate/ublacklist):屏蔽特定网站在 Google 搜索结果中显示
# 🚀 贡献者
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
# 🌐 社区
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ 赞助
[微信赞赏码](sponsor.md)
# 📃 许可证
[LICENSE](../LICENSE)
# ✉️ 联系我们
yinsenho@cherry-ai.com
# ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- 项目统计徽章 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- 许可和赞助徽章 -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商业授权咨询
[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@ -0,0 +1,71 @@
# 🌿 Branching Strategy
Cherry Studio implements a structured branching strategy to maintain code quality and streamline the development process.
## Main Branches
- `main`: Main development branch
- Contains the latest development code
- Direct commits are not allowed - changes must come through pull requests
- Code may contain features in development and might not be fully stable
- `release/*`: Release branches
- Created from `main` branch
- Contains stable code ready for release
- Only accepts documentation updates and bug fixes
- Thoroughly tested before production deployment
## Contributing Branches
When contributing to Cherry Studio, please follow these guidelines:
1. **Feature Branches:**
- Create from `main` branch
- Naming format: `feature/issue-number-brief-description`
- Submit PR back to `main`
2. **Bug Fix Branches:**
- Create from `main` branch
- Naming format: `fix/issue-number-brief-description`
- Submit PR back to `main`
3. **Documentation Branches:**
- Create from `main` branch
- Naming format: `docs/brief-description`
- Submit PR back to `main`
4. **Hotfix Branches:**
- Create from `main` branch
- Naming format: `hotfix/issue-number-brief-description`
- Submit PR to both `main` and relevant `release` branches
5. **Release Branches:**
- Create from `main` branch
- Naming format: `release/version-number`
- Used for final preparation work before version release
- Only accepts bug fixes and documentation updates
- After testing and preparation, merge back to `main` and tag with version
## Workflow Diagram
![](https://github.com/user-attachments/assets/61db64a2-fab1-4a16-8253-0c64c9df1a63)
## Pull Request Guidelines
- All PRs should be submitted to the `main` branch unless fixing a critical production issue
- Ensure your branch is up to date with the latest `main` changes before submitting
- Include relevant issue numbers in your PR description
- Make sure all tests pass and code meets our quality standards
- Add before/after screenshots if you add a new feature or modify a UI component
## Version Tag Management
- Major releases: v1.0.0, v2.0.0, etc.
- Feature releases: v1.1.0, v1.2.0, etc.
- Patch releases: v1.0.1, v1.0.2, etc.
- Hotfix releases: v1.0.1-hotfix, etc.

View File

@ -0,0 +1,71 @@
# 🌿 分支策略
Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发流程。
## 主要分支
- `main`:主开发分支
- 包含最新的开发代码
- 禁止直接提交 - 所有更改必须通过拉取请求Pull Request
- 此分支上的代码可能包含正在开发的功能,不一定完全稳定
- `release/*`:发布分支
- 从 `main` 分支创建
- 包含准备发布的稳定代码
- 只接受文档更新和 bug 修复
- 经过完整测试后可以发布到生产环境
## 贡献分支
在为 Cherry Studio 贡献代码时,请遵循以下准则:
1. **功能开发分支:**
- 从 `main` 分支创建
- 命名格式:`feature/issue-number-brief-description`
- 完成后提交 PR 到 `main` 分支
2. **Bug 修复分支:**
- 从 `main` 分支创建
- 命名格式:`fix/issue-number-brief-description`
- 完成后提交 PR 到 `main` 分支
3. **文档更新分支:**
- 从 `main` 分支创建
- 命名格式:`docs/brief-description`
- 完成后提交 PR 到 `main` 分支
4. **紧急修复分支:**
- 从 `main` 分支创建
- 命名格式:`hotfix/issue-number-brief-description`
- 完成后需要同时合并到 `main` 和相关的 `release` 分支
5. **发布分支:**
- 从 `main` 分支创建
- 命名格式:`release/version-number`
- 用于版本发布前的最终准备工作
- 只允许合并 bug 修复和文档更新
- 完成测试和准备工作后,将代码合并回 `main` 分支并打上版本标签
## 工作流程
![](https://github.com/user-attachments/assets/61db64a2-fab1-4a16-8253-0c64c9df1a63)
## 拉取请求PR指南
- 除非是修复生产环境的关键问题,否则所有 PR 都应该提交到 `main` 分支
- 提交 PR 前确保你的分支已经同步了最新的 `main` 分支内容
- 在 PR 描述中包含相关的 issue 编号
- 确保所有测试通过,且代码符合我们的质量标准
- 如果你添加了新功能或修改了 UI 组件,请附上更改前后的截图
## 版本标签管理
- 主要版本发布v1.0.0、v2.0.0 等
- 功能更新发布v1.1.0、v1.2.0 等
- 补丁修复发布v1.0.1、v1.0.2 等
- 紧急修复发布v1.0.1-hotfix 等

View File

@ -37,6 +37,14 @@ yarn install
yarn dev
```
### Debug
```bash
yarn debug
```
Then input chrome://inspect in browser
### Test
```bash

View File

@ -12,30 +12,43 @@ electronLanguages:
directories:
buildResources: build
files:
- '!{.vscode,.yarn,.github}'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '**/*'
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
- '!electron.vite.config.{js,ts,mjs,cjs}}'
- '!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}'
- '!**/{.editorconfig,.jekyll-metadata}'
- '!src'
- '!scripts'
- '!local'
- '!docs'
- '!packages'
- '!.swc'
- '!.bin'
- '!._*'
- '!*.log'
- '!stats.html'
- '!*.md'
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
- '!**/{test,tests,__tests__,coverage}/**'
- '!**/{test,tests,__tests__,powered-test,coverage}/**'
- '!**/{example,examples}/**'
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
- '!**/*.min.*.map'
- '!**/*.d.ts'
- '!**/{.DS_Store,Thumbs.db}'
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
- '!**/dist/es6/**'
- '!**/dist/demo/**'
- '!**/amd/**'
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken'
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters}' # filter .node build files
asarUnpack:
- resources/**
- '**/*.{metal,exp,lib}'
@ -45,6 +58,9 @@ win:
target:
- target: nsis
- target: portable
signtoolOptions:
sign: scripts/win-sign.js
verifyUpdateCodeSignature: false
nsis:
artifactName: ${productName}-${version}-${arch}-setup.${ext}
shortcutName: ${productName}
@ -61,6 +77,7 @@ mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
artifactName: ${productName}-${version}-${arch}.${ext}
minimumSystemVersion: '20.1.0' # 最低支持 macOS 11.0
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@ -90,9 +107,8 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
新增对 grok-2-image 和 gpt-4o-image 图像支持
支持 Windows 便携版使用 data 目录存储数据
MCP 界面改版,新增描述信息显示
Mermaid 渲染逻辑优化
支持关闭公示渲染
修复 OpenAI 类型渲染错误
新增划词助手
助手支持分组
支持主题颜色切换
划词助手支持应用过滤
翻译模块功能改进

View File

@ -1,80 +1,82 @@
import tailwindcssPlugin from '@tailwindcss/vite'
import react from '@vitejs/plugin-react-swc'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
export default defineConfig(async () => {
const tailwindcssPlugin = (await import('@tailwindcss/vite')).default // 动态导入
return {
main: {
plugins: [
externalizeDepsPlugin({
exclude: [
'@cherrystudio/embedjs',
'@cherrystudio/embedjs-openai',
'@cherrystudio/embedjs-loader-web',
'@cherrystudio/embedjs-loader-markdown',
'@cherrystudio/embedjs-loader-msoffice',
'@cherrystudio/embedjs-loader-xml',
'@cherrystudio/embedjs-loader-pdf',
'@cherrystudio/embedjs-loader-sitemap',
'@cherrystudio/embedjs-libsql',
'@cherrystudio/embedjs-loader-image',
'p-queue',
'webdav'
]
}),
...visualizerPlugin('main')
],
resolve: {
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@shared': resolve('packages/shared')
}
},
build: {
rollupOptions: {
external: ['@libsql/client']
}
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')],
resolve: {
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@shared': resolve('packages/shared')
}
},
preload: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@shared': resolve('packages/shared')
}
build: {
rollupOptions: {
external: ['@libsql/client', 'bufferutil', 'utf-8-validate']
},
sourcemap: process.env.NODE_ENV === 'development'
},
optimizeDeps: {
noDiscovery: process.env.NODE_ENV === 'development'
}
},
preload: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@shared': resolve('packages/shared')
}
},
renderer: {
plugins: [
react({
plugins: [
[
'@swc/plugin-styled-components',
{
displayName: true, // 开发环境下启用组件名称
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
build: {
sourcemap: process.env.NODE_ENV === 'development'
}
},
renderer: {
plugins: [
react({
plugins: [
[
'@swc/plugin-styled-components',
{
displayName: true, // 开发环境下启用组件名称
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
}),
tailwindcssPlugin(),
...visualizerPlugin('renderer')
],
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared')
]
}),
tailwindcssPlugin(),
...visualizerPlugin('renderer')
],
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared')
}
},
optimizeDeps: {
exclude: ['pyodide']
},
worker: {
format: 'es'
},
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html'),
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html')
}
},
optimizeDeps: {
exclude: []
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.2.10",
"version": "1.4.1",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@ -20,8 +20,9 @@
"scripts": {
"start": "electron-vite preview",
"dev": "electron-vite dev",
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
"build": "npm run typecheck && electron-vite build",
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
"build:check": "yarn typecheck && yarn check:i18n && yarn test",
"build:unpack": "dotenv npm run build && electron-builder --dir",
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
@ -37,47 +38,44 @@
"publish": "yarn build:check && yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "node scripts/check-i18n.js",
"test": "yarn test:renderer",
"test:coverage": "yarn test:renderer:coverage",
"test:node": "npx -y tsx --test src/**/*.test.ts",
"test:renderer": "vitest run",
"test:renderer:ui": "vitest --ui",
"test:renderer:coverage": "vitest run --coverage",
"test": "vitest run --silent",
"test:main": "vitest run --project main",
"test:renderer": "vitest run --project renderer",
"test:update": "yarn test:renderer --update",
"test:coverage": "vitest run --coverage --silent",
"test:ui": "vitest --ui",
"test:watch": "vitest",
"test:e2e": "yarn playwright test",
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"postinstall": "electron-builder install-app-deps",
"prepare": "husky"
},
"dependencies": {
"@cherrystudio/embedjs": "^0.1.28",
"@cherrystudio/embedjs-libsql": "^0.1.28",
"@cherrystudio/embedjs-loader-csv": "^0.1.28",
"@cherrystudio/embedjs-loader-image": "^0.1.28",
"@cherrystudio/embedjs-loader-markdown": "^0.1.28",
"@cherrystudio/embedjs-loader-msoffice": "^0.1.28",
"@cherrystudio/embedjs-loader-pdf": "^0.1.28",
"@cherrystudio/embedjs-loader-sitemap": "^0.1.28",
"@cherrystudio/embedjs-loader-web": "^0.1.28",
"@cherrystudio/embedjs-loader-xml": "^0.1.28",
"@cherrystudio/embedjs-openai": "^0.1.28",
"@cherrystudio/embedjs": "^0.1.31",
"@cherrystudio/embedjs-libsql": "^0.1.31",
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
"@cherrystudio/embedjs-loader-image": "^0.1.31",
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
"@cherrystudio/embedjs-loader-msoffice": "^0.1.31",
"@cherrystudio/embedjs-loader-pdf": "^0.1.31",
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
"@cherrystudio/embedjs-loader-web": "^0.1.31",
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@langchain/community": "^0.3.36",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0",
"adm-zip": "^0.5.16",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"bufferutil": "^4.0.9",
"color": "^5.0.0",
"diff": "^7.0.0",
"docx": "^9.0.2",
"electron-log": "^5.1.5",
@ -85,24 +83,19 @@
"electron-updater": "6.6.4",
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"extract-zip": "^2.0.1",
"fast-xml-parser": "^5.2.0",
"fetch-socks": "^1.3.2",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"opendal": "^0.47.11",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"selection-hook": "^0.9.22",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.4.0",
"webdav": "^5.8.0",
"ws": "^8.18.1",
"zipread": "^1.3.3"
},
"devDependencies": {
@ -110,21 +103,22 @@
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.38.0",
"@anthropic-ai/sdk": "^0.41.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^2.5.0",
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "^0.10.0",
"@google/genai": "^1.0.1",
"@hello-pangea/dnd": "^16.6.0",
"@iconify-json/svg-spinners": "^1.2.2",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@modelcontextprotocol/sdk": "^1.11.4",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
@ -133,12 +127,14 @@
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.6",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.2.2",
"@swc/plugin-styled-components": "^7.1.3",
"@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@tailwindcss/vite": "^4.1.5",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
@ -149,50 +145,57 @@
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-window": "^1",
"@types/tinycolor2": "^1",
"@types/ws": "^8",
"@uiw/codemirror-extensions-langs": "^4.23.12",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@vitest/browser": "^3.1.4",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4",
"@vitest/web-worker": "^3.1.4",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
"babel-plugin-styled-components": "^2.1.4",
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"color": "^5.0.0",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"dotenv-cli": "^7.4.2",
"electron": "31.7.6",
"electron": "35.4.0",
"electron-builder": "26.0.15",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.3.0",
"electron-vite": "^3.1.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"i18next": "^23.11.5",
"jest-styled-components": "^7.2.0",
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.511.0",
"mermaid": "^11.6.0",
"mime": "^4.0.4",
"motion": "^12.12.1",
"next-themes": "^0.4.6",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"rc-virtual-list": "^3.18.5",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1",
@ -203,6 +206,7 @@
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-window": "^1.8.11",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1",
@ -212,36 +216,35 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^3.2.2",
"sass": "^1.88.0",
"shiki": "^3.4.2",
"sonner": "^2.0.3",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"tiny-pinyin": "^1.3.2",
"tinycolor2": "^1.6.0",
"tokenx": "^0.4.1",
"tw-animate-css": "^1.2.9",
"typescript": "^5.6.2",
"usehooks-ts": "^3.1.1",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.1"
"vitest": "^3.1.4"
},
"resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"node-gyp": "^9.1.0",
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
"shiki": "3.2.2",
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch"
"openai@npm:^4.87.3": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch"
},
"packageManager": "yarn@4.6.0",
"packageManager": "yarn@4.9.1",
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
"prettier --write",

View File

@ -1 +0,0 @@
# Cherry Studio Artifacts

View File

@ -1,19 +0,0 @@
{
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"description": "Cherry Studio Artifacts",
"main": "index.js",
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"artifacts"
],
"author": "kangfenmao",
"license": "ISC"
}

View File

@ -1,108 +0,0 @@
:root {
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
font-family: 'Noto Sans SC', sans-serif;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
}
.card {
width: 300px;
height: 500px;
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.header {
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
padding: 20px;
text-align: left;
position: relative;
z-index: 1;
}
h1 {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
margin: 0;
font-weight: 700;
}
.content {
padding: 30px 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.word {
text-align: left;
margin-bottom: 20px;
}
.word-main {
font-family: 'Noto Serif SC', serif;
font-size: 36px;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
margin-bottom: 10px;
position: relative;
}
.word-main::after {
content: '';
position: absolute;
left: 0;
bottom: -5px;
width: 50px;
height: 3px;
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
}
.word-sub {
font-size: 14px;
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
margin: 5px 0;
}
.divider {
width: 100%;
height: 1px;
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
margin: 20px 0;
}
.explanation {
font-size: 18px;
line-height: 1.6;
text-align: left;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote {
position: relative;
padding-left: 20px;
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
}
.background-text {
position: absolute;
font-size: 150px;
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
z-index: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}

View File

@ -1,3 +0,0 @@
data/*
!data/.gitkeep

View File

@ -1,3 +0,0 @@
# Cherry Studio Database
Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取

View File

@ -1,13 +0,0 @@
{
"name": "@cherry-studio/database",
"packageManager": "yarn@4.6.0",
"dependencies": {
"csv-parser": "^3.0.0",
"sqlite3": "^5.1.7"
},
"scripts": {
"agents": "node src/agents.js",
"email": "yarn csv && node src/email.js",
"csv": "node src/csv.js"
}
}

View File

@ -1,47 +0,0 @@
const sqlite3 = require('sqlite3').verbose()
const fs = require('fs')
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
console.log('Connected to the database.')
})
// 查询数据并转换为JSON
db.all('SELECT * FROM agents', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
// 将 ID 类型转换为字符串
for (const row of rows) {
row.id = row.id.toString()
row.group = row.group.toString().split(',')
row.group = row.group.map((item) => item.trim().replace('\r\n', ''))
}
// 将查询结果转换为JSON字符串
const jsonData = JSON.stringify(rows, null, 2)
// 将JSON数据写入文件
fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => {
if (err) {
console.error('Error writing to file:', err.message)
return
}
console.log('Data has been written to agents.json')
})
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
console.log('Database connection closed.')
})
})

View File

@ -1,77 +0,0 @@
const fs = require('fs')
const csv = require('csv-parser')
const sqlite3 = require('sqlite3').verbose()
// 连接到 SQLite 数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error opening database', err)
return
}
console.log('Connected to the SQLite database.')
})
// 创建一个数组来存储 CSV 数据
const results = []
// 读取 CSV 文件
fs.createReadStream('./data/data.csv')
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', () => {
// 准备 SQL 插入语句,使用 INSERT OR IGNORE
const stmt = db.prepare('INSERT OR IGNORE INTO emails (email, github, sent) VALUES (?, ?, ?)')
// 插入每一行数据
let inserted = 0
let skipped = 0
let emptyEmail = 0
db.serialize(() => {
// 开始一个事务以提高性能
db.run('BEGIN TRANSACTION')
results.forEach((row) => {
// 检查 email 是否为空
if (!row.email || row.email.trim() === '') {
emptyEmail++
return // 跳过这一行
}
stmt.run(row.email, row['user-href'], 0, function (err) {
if (err) {
console.error('Error inserting row', err)
} else {
if (this.changes === 1) {
inserted++
} else {
skipped++
}
}
})
})
// 提交事务
db.run('COMMIT', (err) => {
if (err) {
console.error('Error committing transaction', err)
} else {
console.log(
`Insertion complete. Inserted: ${inserted}, Skipped (duplicate): ${skipped}, Skipped (empty email): ${emptyEmail}`
)
}
// 完成插入
stmt.finalize()
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing database', err)
} else {
console.log('Database connection closed.')
}
})
})
})
})

View File

@ -1,36 +0,0 @@
const sqlite3 = require('sqlite3').verbose()
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
})
// 查询数据并转换为JSON
db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
for (const row of rows) {
console.log(row.email)
// Update row set sent = 1
db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => {
if (err) {
console.error('Error updating the database:', err.message)
return
}
})
}
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
export enum IpcChannel {
App_GetCacheSize = 'app:get-cache-size',
App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
App_SetLanguage = 'app:set-language',
@ -10,17 +11,21 @@ export enum IpcChannel {
App_SetLaunchToTray = 'app:set-launch-to-tray',
App_SetTray = 'app:set-tray',
App_SetTrayOnClose = 'app:set-tray-on-close',
App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update',
App_SetZoomFactor = 'app:set-zoom-factor',
ZoomFactorUpdated = 'app:zoom-factor-updated',
App_SetFeedUrl = 'app:set-feed-url',
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
App_QuoteToMain = 'app:quote-to-main',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
@ -52,6 +57,7 @@ export enum IpcChannel {
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
Mcp_CheckConnectivity = 'mcp:check-connectivity',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
@ -104,8 +110,10 @@ export enum IpcChannel {
File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create',
File_Write = 'file:write',
File_WriteWithId = 'file:writeWithId',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_SaveBase64Image = 'file:saveBase64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryImage = 'file:binaryImage',
@ -134,9 +142,12 @@ export enum IpcChannel {
System_GetDeviceType = 'system:getDeviceType',
System_GetHostname = 'system:getHostname',
// DevTools
System_ToggleDevTools = 'system:toggleDevTools',
// events
BackupProgress = 'backup-progress',
ThemeChange = 'theme:change',
ThemeUpdated = 'theme:updated',
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
@ -165,5 +176,26 @@ export enum IpcChannel {
StoreSync_Subscribe = 'store-sync:subscribe',
StoreSync_Unsubscribe = 'store-sync:unsubscribe',
StoreSync_OnUpdate = 'store-sync:on-update',
StoreSync_BroadcastSync = 'store-sync:broadcast-sync'
StoreSync_BroadcastSync = 'store-sync:broadcast-sync',
// Provider
Provider_AddKey = 'provider:add-key',
//Selection Assistant
Selection_TextSelected = 'selection:text-selected',
Selection_ToolbarHide = 'selection:toolbar-hide',
Selection_ToolbarVisibilityChange = 'selection:toolbar-visibility-change',
Selection_ToolbarDetermineSize = 'selection:toolbar-determine-size',
Selection_WriteToClipboard = 'selection:write-to-clipboard',
Selection_SetEnabled = 'selection:set-enabled',
Selection_SetTriggerMode = 'selection:set-trigger-mode',
Selection_SetFilterMode = 'selection:set-filter-mode',
Selection_SetFilterList = 'selection:set-filter-list',
Selection_SetFollowToolbar = 'selection:set-follow-toolbar',
Selection_SetRemeberWinSize = 'selection:set-remeber-win-size',
Selection_ActionWindowClose = 'selection:action-window-close',
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',
Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data'
}

View File

@ -4,135 +4,368 @@ export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.org', // org-mode 文件
'.wiki', // VimWiki 文件
'.tex', // LaTeX 文件
'.bib', // BibTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.ipynb', // Jupyter 笔记本格式
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 或 MATLAB 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java', // Java 代码文件
'.cs', // C# 代码文件
'.cpp', // C++ 代码文件
'.c', // C++ 代码文件
'.h', // C++ 头文件
'.hpp', // C++ 头文件
'.cc', // C++ 源文件
'.cxx', // C++ 源文件
'.cppm', // C++20 模块接口文件
'.ipp', // 模板实现文件
'.ixx', // C++20 模块实现文件
'.f90', // Fortran 90 源文件
'.f', // Fortran 固定格式源代码文件
'.f03', // Fortran 2003+ 源代码文件
'.ahk', // AutoHotKey 语言文件
'.tcl', // Tcl 脚本
'.do', // Questa 或 Modelsim Tcl 脚本
'.v', // Verilog 源文件
'.sv', // SystemVerilog 源文件
'.svh', // SystemVerilog 头文件
'.vhd', // VHDL 源文件
'.vhdl', // VHDL 源文件
'.lef', // Library Exchange Format
'.def', // Design Exchange Format
'.edif', // Electronic Design Interchange Format
'.sdf', // Standard Delay Format
'.sdc', // Synopsys Design Constraints
'.xdc', // Xilinx Design Constraints
'.rpt', // 报告文件
'.lisp', // Lisp 脚本
'.il', // Cadence SKILL 脚本
'.ils', // Cadence SKILL++ 脚本
'.sp', // SPICE netlist 文件
'.spi', // SPICE netlist 文件
'.cir', // SPICE netlist 文件
'.net', // SPICE netlist 文件
'.scs', // Spectre netlist 文件
'.asc', // LTspice netlist schematic 文件
'.tf' // Technology File
]
const textExtsByCategory = new Map([
[
'language',
[
'.js',
'.mjs',
'.cjs',
'.ts',
'.jsx',
'.tsx', // JavaScript/TypeScript
'.py', // Python
'.java', // Java
'.cs', // C#
'.cpp',
'.c',
'.h',
'.hpp',
'.cc',
'.cxx',
'.cppm',
'.ipp',
'.ixx', // C/C++
'.php', // PHP
'.rb', // Ruby
'.pl', // Perl
'.go', // Go
'.rs', // Rust
'.swift', // Swift
'.kt',
'.kts', // Kotlin
'.scala', // Scala
'.lua', // Lua
'.groovy', // Groovy
'.dart', // Dart
'.hs', // Haskell
'.clj',
'.cljs', // Clojure
'.elm', // Elm
'.erl', // Erlang
'.ex',
'.exs', // Elixir
'.ml',
'.mli', // OCaml
'.fs', // F#
'.r',
'.R', // R
'.sol', // Solidity
'.awk', // AWK
'.cob', // COBOL
'.asm',
'.s', // Assembly
'.lisp',
'.lsp', // Lisp
'.coffee', // CoffeeScript
'.ino', // Arduino
'.jl', // Julia
'.nim', // Nim
'.zig', // Zig
'.d', // D语言
'.pas', // Pascal
'.vb', // Visual Basic
'.rkt', // Racket
'.scm', // Scheme
'.hx', // Haxe
'.as', // ActionScript
'.pde', // Processing
'.f90',
'.f',
'.f03',
'.for',
'.f95', // Fortran
'.adb',
'.ads', // Ada
'.pro', // Prolog
'.m',
'.mm', // Objective-C/MATLAB
'.rpy', // Ren'Py
'.ets', // OpenHarmony,
'.uniswap', // DeFi
'.vy', // Vyper
'.shader',
'.glsl',
'.frag',
'.vert',
'.gd' // Godot
]
],
[
'script',
[
'.sh', // Shell
'.bat',
'.cmd', // Windows批处理
'.ps1', // PowerShell
'.tcl',
'.do', // Tcl
'.ahk', // AutoHotkey
'.zsh', // Zsh
'.fish', // Fish shell
'.csh', // C shell
'.vbs', // VBScript
'.applescript', // AppleScript
'.au3', // AutoIt
'.bash',
'.nu'
]
],
[
'style',
[
'.css', // CSS
'.less', // Less
'.scss',
'.sass', // Sass
'.styl', // Stylus
'.pcss', // PostCSS
'.postcss' // PostCSS
]
],
[
'template',
[
'.vue', // Vue.js
'.pug',
'.jade', // Pug/Jade
'.haml', // Haml
'.slim', // Slim
'.tpl', // 通用模板
'.ejs', // EJS
'.hbs', // Handlebars
'.mustache', // Mustache
'.twig', // Twig
'.blade', // Blade (Laravel)
'.liquid', // Liquid
'.jinja',
'.jinja2',
'.j2', // Jinja
'.erb', // ERB
'.vm', // Velocity
'.ftl', // FreeMarker
'.svelte', // Svelte
'.astro' // Astro
]
],
[
'config',
[
'.ini', // INI配置
'.conf',
'.config', // 通用配置
'.env', // 环境变量
'.toml', // TOML
'.cfg', // 通用配置
'.properties', // Java属性
'.desktop', // Linux桌面文件
'.service', // systemd服务
'.rc',
'.bashrc',
'.zshrc', // Shell配置
'.fishrc', // Fish shell配置
'.vimrc', // Vim配置
'.htaccess', // Apache配置
'.robots', // robots.txt
'.editorconfig', // EditorConfig
'.eslintrc', // ESLint
'.prettierrc', // Prettier
'.babelrc', // Babel
'.npmrc', // npm
'.dockerignore', // Docker ignore
'.npmignore',
'.yarnrc',
'.prettierignore',
'.eslintignore',
'.browserslistrc',
'.json5',
'.tfvars'
]
],
[
'document',
[
'.txt',
'.text', // 纯文本
'.md',
'.mdx', // Markdown
'.html',
'.htm',
'.xhtml', // HTML
'.xml', // XML
'.org', // Org-mode
'.wiki', // Wiki
'.tex',
'.bib', // LaTeX
'.rst', // reStructuredText
'.rtf', // 富文本
'.nfo', // 信息文件
'.adoc',
'.asciidoc', // AsciiDoc
'.pod', // Perl文档
'.1',
'.2',
'.3',
'.4',
'.5',
'.6',
'.7',
'.8',
'.9', // man页面
'.man', // man页面
'.texi',
'.texinfo', // Texinfo
'.readme',
'.me', // README
'.changelog', // 变更日志
'.license', // 许可证
'.authors', // 作者文件
'.po',
'.pot'
]
],
[
'data',
[
'.json', // JSON
'.jsonc', // JSON with comments
'.yaml',
'.yml', // YAML
'.csv',
'.tsv', // 分隔值文件
'.edn', // Clojure数据
'.jsonl',
'.ndjson', // 换行分隔JSON
'.geojson', // GeoJSON
'.gpx', // GPS Exchange
'.kml', // Keyhole Markup
'.rss',
'.atom', // Feed格式
'.vcf', // vCard
'.ics', // iCalendar
'.ldif', // LDAP数据交换
'.pbtxt',
'.map'
]
],
[
'build',
[
'.gradle', // Gradle
'.make',
'.mk', // Make
'.cmake', // CMake
'.sbt', // SBT
'.rake', // Rake
'.spec', // RPM spec
'.pom',
'.build', // Meson
'.bazel' // Bazel
]
],
[
'database',
[
'.sql', // SQL
'.ddl',
'.dml', // DDL/DML
'.plsql', // PL/SQL
'.psql', // PostgreSQL
'.cypher', // Cypher
'.sparql' // SPARQL
]
],
[
'web',
[
'.graphql',
'.gql', // GraphQL
'.proto', // Protocol Buffers
'.thrift', // Thrift
'.wsdl', // WSDL
'.raml', // RAML
'.swagger',
'.openapi' // API文档
]
],
[
'version',
[
'.gitignore', // Git ignore
'.gitattributes', // Git attributes
'.gitconfig', // Git config
'.hgignore', // Mercurial ignore
'.bzrignore', // Bazaar ignore
'.svnignore', // SVN ignore
'.githistory' // Git history
]
],
[
'subtitle',
[
'.srt',
'.sub',
'.ass' // 字幕格式
]
],
[
'log',
[
'.log',
'.rpt' // 日志和报告 (移除了.out因为通常是二进制可执行文件)
]
],
[
'eda',
[
'.v',
'.sv',
'.svh', // Verilog/SystemVerilog
'.vhd',
'.vhdl', // VHDL
'.lef',
'.def', // LEF/DEF
'.edif', // EDIF
'.sdf', // SDF
'.sdc',
'.xdc', // 约束文件
'.sp',
'.spi',
'.cir',
'.net', // SPICE
'.scs', // Spectre
'.asc', // LTspice
'.tf', // Technology File
'.il',
'.ils' // SKILL
]
],
[
'game',
[
'.mtl', // Material Template Library
'.x3d', // X3D文件
'.gltf', // glTF JSON
'.prefab', // Unity预制体 (YAML格式)
'.meta' // Unity元数据文件 (YAML格式)
]
],
[
'other',
[
'.mcfunction', // Minecraft函数
'.jsp', // JSP
'.aspx', // ASP.NET
'.ipynb', // Jupyter Notebook
'.cake',
'.ctp', // CakePHP
'.cfm',
'.cfc' // ColdFusion
]
]
])
export const textExts = Array.from(textExtsByCategory.values()).flat()
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
@ -170,3 +403,8 @@ export const KB = 1024
export const MB = 1024 * KB
export const GB = 1024 * MB
export const defaultLanguage = 'en-US'
export enum FeedUrl {
PRODUCTION = 'https://releases.cherry-ai.com',
EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
}

42
playwright.config.ts Normal file
View File

@ -0,0 +1,42 @@
import { defineConfig, devices } from '@playwright/test'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// Look for test files, relative to this configuration file.
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@ -1,345 +0,0 @@
[
{
"id": "all",
"title": "Categories",
"items": [
{
"id": "featured",
"name": "Featured",
"count": 24
},
{
"id": "new",
"name": "New Releases",
"count": 18
},
{
"id": "top",
"name": "Top Rated",
"count": 32
}
]
},
{
"id": "Assistant",
"title": "助手",
"items": [
{
"id": "assistant-job",
"name": "职业",
"count": 274
},
{
"id": "assistant-business",
"name": "商业",
"count": 163
},
{
"id": "assistant-tools",
"name": "工具",
"count": 284
},
{
"id": "assistant-language",
"name": "语言",
"count": 29
},
{
"id": "assistant-office",
"name": "办公",
"count": 44
},
{
"id": "assistant-general",
"name": "通用",
"count": 37
},
{
"id": "assistant-writing",
"name": "写作",
"count": 128
},
{
"id": "assistant-coding",
"name": "编程",
"count": 61
},
{
"id": "assistant-emotion",
"name": "情感",
"count": 57
},
{
"id": "assistant-education",
"name": "教育",
"count": 275
},
{
"id": "assistant-creative",
"name": "创意",
"count": 166
},
{
"id": "assistant-academic",
"name": "学术",
"count": 54
},
{
"id": "assistant-design",
"name": "设计",
"count": 37
},
{
"id": "assistant-art",
"name": "艺术",
"count": 42
},
{
"id": "assistant-entertainment",
"name": "娱乐",
"count": 75
},
{
"id": "assistant-featured",
"name": "精选",
"count": 4
},
{
"id": "assistant-life",
"name": "生活",
"count": 83
},
{
"id": "assistant-medical",
"name": "医疗",
"count": 18
},
{
"id": "assistant-game",
"name": "游戏",
"count": 34
},
{
"id": "assistant-translation",
"name": "翻译",
"count": 51
},
{
"id": "assistant-music",
"name": "音乐",
"count": 5
},
{
"id": "assistant-review",
"name": "点评",
"count": 10
},
{
"id": "assistant-copywriting",
"name": "文案",
"count": 78
},
{
"id": "assistant-encyclopedia",
"name": "百科",
"count": 13
},
{
"id": "assistant-health",
"name": "健康",
"count": 18
},
{
"id": "assistant-marketing",
"name": "营销",
"count": 17
},
{
"id": "assistant-science",
"name": "科学",
"count": 12
},
{
"id": "assistant-analysis",
"name": "分析",
"count": 32
},
{
"id": "assistant-law",
"name": "法律",
"count": 11
},
{
"id": "assistant-consulting",
"name": "咨询",
"count": 18
},
{
"id": "assistant-finance",
"name": "金融",
"count": 6
},
{
"id": "assistant-travel",
"name": "旅游",
"count": 5
},
{
"id": "assistant-management",
"name": "管理",
"count": 21
}
]
},
{
"id": "Mini-App",
"title": "小程序",
"items": []
},
{
"id": "Knowledge",
"title": "知识库",
"items": [
{
"id": "knowledge-history",
"name": "历史"
},
{
"id": "knowledge-literature",
"name": "文学"
},
{
"id": "knowledge-education",
"name": "教育"
},
{
"id": "knowledge-law",
"name": "法律"
},
{
"id": "knowledge-science",
"name": "科学"
},
{
"id": "knowledge-medicine",
"name": "医学"
},
{
"id": "knowledge-economics",
"name": "经济"
},
{
"id": "knowledge-art",
"name": "艺术"
},
{
"id": "knowledge-geography",
"name": "地理"
},
{
"id": "knowledge-social",
"name": "社会"
}
]
},
{
"id": "MCP-Server",
"title": "MCP 服务器",
"items": [
{
"id": "mcp-dev-tools",
"name": "Developer Tools"
},
{
"id": "mcp-research-data",
"name": "Research And Data"
},
{
"id": "mcp-cloud",
"name": "Cloud Platforms"
},
{
"id": "mcp-communication",
"name": "Communication"
},
{
"id": "mcp-browser-auto",
"name": "Browser Automation"
},
{
"id": "mcp-finance",
"name": "Finance"
},
{
"id": "mcp-security",
"name": "Security"
},
{
"id": "mcp-os-auto",
"name": "Os Automation"
},
{
"id": "mcp-databases",
"name": "Databases"
},
{
"id": "mcp-cloud-storage",
"name": "Cloud Storage"
},
{
"id": "mcp-monitoring",
"name": "Monitoring"
},
{
"id": "mcp-media",
"name": "Entertainment And Media"
},
{
"id": "mcp-knowledge-mem",
"name": "Knowledge And Memory"
},
{
"id": "mcp-file-systems",
"name": "File Systems"
},
{
"id": "mcp-location",
"name": "Location Services"
},
{
"id": "mcp-calendar",
"name": "Calendar Management"
},
{
"id": "mcp-customer-data",
"name": "Customer Data Platforms"
},
{
"id": "mcp-ai-chatbot",
"name": "AI Chatbot"
},
{
"id": "mcp-virtualization",
"name": "Virtualization"
},
{
"id": "mcp-official-servers",
"name": "Official Servers"
},
{
"id": "mcp-database",
"name": "Database"
}
]
},
{
"id": "Model-Provider",
"title": "模型服务",
"items": []
},
{
"id": "Agent",
"title": "智能体",
"items": []
}
]

File diff suppressed because one or more lines are too long

View File

@ -1,691 +0,0 @@
[
{
"id": "mini-app-openai",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "ChatGPT",
"description": "",
"author": "openai",
"image": "OpenAiProviderLogo",
"tags": [],
"url": "https://chatgpt.com/",
"bodered": true
},
{
"id": "mini-app-gemini",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Gemini",
"description": "",
"author": "gemini",
"image": "GeminiAppLogo",
"tags": [],
"url": "https://gemini.google.com/"
},
{
"id": "mini-app-silicon",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "SiliconFlow",
"description": "",
"author": "silicon",
"image": "SiliconFlowProviderLogo",
"tags": [],
"url": "https://cloud.siliconflow.cn/playground/chat"
},
{
"id": "mini-app-deepseek",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "DeepSeek",
"description": "",
"author": "deepseek",
"image": "DeepSeekProviderLogo",
"tags": [],
"url": "https://chat.deepseek.com/"
},
{
"id": "mini-app-yi",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "万知",
"description": "",
"author": "yi",
"image": "WanZhiAppLogo",
"tags": [],
"url": "https://www.wanzhi.com/",
"bodered": true
},
{
"id": "mini-app-zhipu",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "智谱清言",
"description": "",
"author": "zhipu",
"image": "ZhipuProviderLogo",
"tags": [],
"url": "https://chatglm.cn/main/alltoolsdetail"
},
{
"id": "mini-app-moonshot",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Kimi",
"description": "",
"author": "moonshot",
"image": "KimiAppLogo",
"tags": [],
"url": "https://kimi.moonshot.cn/"
},
{
"id": "mini-app-baichuan",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "百小应",
"description": "",
"author": "baichuan",
"image": "BaicuanAppLogo",
"tags": [],
"url": "https://ying.baichuan-ai.com/chat"
},
{
"id": "mini-app-dashscope",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "通义千问",
"description": "",
"author": "dashscope",
"image": "QwenModelLogo",
"tags": [],
"url": "https://tongyi.aliyun.com/qianwen/"
},
{
"id": "mini-app-stepfun",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "跃问",
"description": "",
"author": "stepfun",
"image": "YuewenAppLogo",
"tags": [],
"url": "https://yuewen.cn/chats/new",
"bodered": true
},
{
"id": "mini-app-doubao",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "豆包",
"description": "",
"author": "doubao",
"image": "DoubaoAppLogo",
"tags": [],
"url": "https://www.doubao.com/chat/"
},
{
"id": "mini-app-cici",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Cici",
"description": "",
"author": "cici",
"image": "CiciAppLogo",
"tags": [],
"url": "https://www.cici.com/chat/"
},
{
"id": "mini-app-minimax",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "海螺",
"description": "",
"author": "minimax",
"image": "HailuoModelLogo",
"tags": [],
"url": "https://hailuoai.com/"
},
{
"id": "mini-app-groq",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Groq",
"description": "",
"author": "groq",
"image": "GroqProviderLogo",
"tags": [],
"url": "https://chat.groq.com/"
},
{
"id": "mini-app-anthropic",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Claude",
"description": "",
"author": "anthropic",
"image": "ClaudeAppLogo",
"tags": [],
"url": "https://claude.ai/"
},
{
"id": "mini-app-baidu-ai-chat",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "文心一言",
"description": "",
"author": "baidu-ai-chat",
"image": "BaiduAiAppLogo",
"tags": [],
"url": "https://yiyan.baidu.com/"
},
{
"id": "mini-app-baidu-ai-search",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "百度AI搜索",
"description": "",
"author": "baidu-ai-search",
"image": "BaiduAiSearchLogo",
"tags": [],
"url": "https://chat.baidu.com/",
"bodered": true,
"style": {
"padding": 5
}
},
{
"id": "mini-app-tencent-yuanbao",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "腾讯元宝",
"description": "",
"author": "tencent-yuanbao",
"image": "TencentYuanbaoAppLogo",
"tags": [],
"url": "https://yuanbao.tencent.com/chat",
"bodered": true
},
{
"id": "mini-app-sensetime-chat",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "商量",
"description": "",
"author": "sensetime-chat",
"image": "SensetimeAppLogo",
"tags": [],
"url": "https://chat.sensetime.com/wb/chat",
"bodered": true
},
{
"id": "mini-app-spark-desk",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "SparkDesk",
"description": "",
"author": "spark-desk",
"image": "SparkDeskAppLogo",
"tags": [],
"url": "https://xinghuo.xfyun.cn/desk"
},
{
"id": "mini-app-metaso",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "秘塔AI搜索",
"description": "",
"author": "metaso",
"image": "MetasoAppLogo",
"tags": [],
"url": "https://metaso.cn/"
},
{
"id": "mini-app-poe",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Poe",
"description": "",
"author": "poe",
"image": "PoeAppLogo",
"tags": [],
"url": "https://poe.com"
},
{
"id": "mini-app-perplexity",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Perplexity",
"description": "",
"author": "perplexity",
"image": "PerplexityAppLogo",
"tags": [],
"url": "https://www.perplexity.ai/"
},
{
"id": "mini-app-devv",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "DEVV_",
"description": "",
"author": "devv",
"image": "DevvAppLogo",
"tags": [],
"url": "https://devv.ai/"
},
{
"id": "mini-app-tiangong-ai",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "天工AI",
"description": "",
"author": "tiangong-ai",
"image": "TiangongAiLogo",
"tags": [],
"url": "https://www.tiangong.cn/",
"bodered": true
},
{
"id": "mini-app-hugging-chat",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "HuggingChat",
"description": "",
"author": "hugging-chat",
"image": "HuggingChatLogo",
"tags": [],
"url": "https://huggingface.co/chat/",
"bodered": true
},
{
"id": "mini-app-Felo",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Felo",
"description": "",
"author": "Felo",
"image": "FeloAppLogo",
"tags": [],
"url": "https://felo.ai/",
"bodered": true
},
{
"id": "mini-app-duckduckgo",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "DuckDuckGo",
"description": "",
"author": "duckduckgo",
"image": "DuckDuckGoAppLogo",
"tags": [],
"url": "https://duck.ai"
},
{
"id": "mini-app-bolt",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "bolt",
"description": "",
"author": "bolt",
"image": "BoltAppLogo",
"tags": [],
"url": "https://bolt.new/",
"bodered": true
},
{
"id": "mini-app-nm",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "纳米AI",
"description": "",
"author": "nm",
"image": "NamiAiLogo",
"tags": [],
"url": "https://bot.n.cn/",
"bodered": true
},
{
"id": "mini-app-nm-search",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "纳米AI搜索",
"description": "",
"author": "nm-search",
"image": "NamiAiSearchLogo",
"tags": [],
"url": "https://www.n.cn/",
"bodered": true
},
{
"id": "mini-app-thinkany",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "ThinkAny",
"description": "",
"author": "thinkany",
"image": "ThinkAnyLogo",
"tags": [],
"url": "https://thinkany.ai/",
"bodered": true,
"style": {
"padding": 5
}
},
{
"id": "mini-app-hika",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Hika",
"description": "",
"author": "hika",
"image": "HikaLogo",
"tags": [],
"url": "https://hika.fyi/",
"bodered": true
},
{
"id": "mini-app-github-copilot",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "GitHub Copilot",
"description": "",
"author": "github-copilot",
"image": "GithubCopilotLogo",
"tags": [],
"url": "https://github.com/copilot"
},
{
"id": "mini-app-genspark",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Genspark",
"description": "",
"author": "genspark",
"image": "GensparkLogo",
"tags": [],
"url": "https://www.genspark.ai/"
},
{
"id": "mini-app-grok",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Grok",
"description": "",
"author": "grok",
"image": "GrokAppLogo",
"tags": [],
"url": "https://grok.com",
"bodered": true
},
{
"id": "mini-app-grok-x",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Grok / X",
"description": "",
"author": "grok-x",
"image": "GrokXAppLogo",
"tags": [],
"url": "https://x.com/i/grok",
"bodered": true
},
{
"id": "mini-app-qwenlm",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "QwenLM",
"description": "",
"author": "qwenlm",
"image": "QwenlmAppLogo",
"tags": [],
"url": "https://qwenlm.ai/"
},
{
"id": "mini-app-flowith",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Flowith",
"description": "",
"author": "flowith",
"image": "FlowithAppLogo",
"tags": [],
"url": "https://www.flowith.io/",
"bodered": true
},
{
"id": "mini-app-3mintop",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "3MinTop",
"description": "",
"author": "3mintop",
"image": "ThreeMinTopAppLogo",
"tags": [],
"url": "https://3min.top",
"bodered": false
},
{
"id": "mini-app-aistudio",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "AI Studio",
"description": "",
"author": "aistudio",
"image": "AIStudioLogo",
"tags": [],
"url": "https://aistudio.google.com/"
},
{
"id": "mini-app-xiaoyi",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "小艺",
"description": "",
"author": "xiaoyi",
"image": "XiaoYiAppLogo",
"tags": [],
"url": "https://xiaoyi.huawei.com/chat/",
"bodered": true
},
{
"id": "mini-app-notebooklm",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "NotebookLM",
"description": "",
"author": "notebooklm",
"image": "NotebookLMAppLogo",
"tags": [],
"url": "https://notebooklm.google.com/"
},
{
"id": "mini-app-coze",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Coze",
"description": "",
"author": "coze",
"image": "CozeAppLogo",
"tags": [],
"url": "https://www.coze.com/space",
"bodered": true
},
{
"id": "mini-app-dify",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Dify",
"description": "",
"author": "dify",
"image": "DifyAppLogo",
"tags": [],
"url": "https://cloud.dify.ai/apps",
"bodered": true,
"style": {
"padding": 5
}
},
{
"id": "mini-app-wpslingxi",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "WPS灵犀",
"description": "",
"author": "wpslingxi",
"image": "WPSLingXiLogo",
"tags": [],
"url": "https://copilot.wps.cn/",
"bodered": true
},
{
"id": "mini-app-lechat",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "LeChat",
"description": "",
"author": "lechat",
"image": "LeChatLogo",
"tags": [],
"url": "https://chat.mistral.ai/chat",
"bodered": true
},
{
"id": "mini-app-abacus",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Abacus",
"description": "",
"author": "abacus",
"image": "AbacusLogo",
"tags": [],
"url": "https://apps.abacus.ai/chatllm",
"bodered": true
},
{
"id": "mini-app-lambdachat",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Lambda Chat",
"description": "",
"author": "lambdachat",
"image": "LambdaChatLogo",
"tags": [],
"url": "https://lambda.chat/",
"bodered": true
},
{
"id": "mini-app-monica",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Monica",
"description": "",
"author": "monica",
"image": "MonicaLogo",
"tags": [],
"url": "https://monica.im/home/",
"bodered": true
},
{
"id": "mini-app-you",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "You",
"description": "",
"author": "you",
"image": "YouLogo",
"tags": [],
"url": "https://you.com/"
},
{
"id": "mini-app-zhihu",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "知乎直答",
"description": "",
"author": "zhihu",
"image": "ZhihuAppLogo",
"tags": [],
"url": "https://zhida.zhihu.com/",
"bodered": true
},
{
"id": "mini-app-dangbei",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "当贝AI",
"description": "",
"author": "dangbei",
"image": "DangbeiLogo",
"tags": [],
"url": "https://ai.dangbei.com/",
"bodered": true
},
{
"id": "mini-app-zai",
"type": "Mini-App",
"categoryId": "mini-app",
"subcategoryId": "",
"title": "Z.ai",
"description": "",
"author": "zai",
"image": "ZaiAppLogo",
"tags": [],
"url": "https://chat.z.ai/",
"bodered": true,
"style": {
"padding": 10
}
}
]

View File

@ -1,134 +0,0 @@
// convert_agents.js
// 将 agents.json 转换为 list_assistant.json
// 一次性的(如何后面不扩展agents.json), 则不需要再运行这个脚本
const fs = require('fs')
const path = require('path')
// --- 配置路径 ---
const agentsJsonPath = path.resolve(__dirname, '../data/agents.json')
const outputDir = path.resolve(__dirname, '../data')
const outputJsonPath = path.resolve(outputDir, 'store_list_assistant.json')
// --- 映射和默认值配置 ---
const CATEGORY_ID_ASSISTANT = 'assistant'
// 映射 agents.json 的 group 名称 到 store_categories.json 中 "助手" 分类的二级分类 ID
// Key: agent.group 中的项 (请确保大小写和字符与 agents.json 中的 group 值一致)
// Value: 二级分类 ID (subcategoryId)
const groupToSubcategoryMap = {
职业: 'assistant-job',
商业: 'assistant-business',
工具: 'assistant-tools',
语言: 'assistant-language',
办公: 'assistant-office',
通用: 'assistant-general',
写作: 'assistant-writing',
编程: 'assistant-coding',
情感: 'assistant-emotion',
教育: 'assistant-education',
创意: 'assistant-creative',
学术: 'assistant-academic',
设计: 'assistant-design',
艺术: 'assistant-art',
娱乐: 'assistant-entertainment',
精选: 'assistant-featured',
生活: 'assistant-life',
医疗: 'assistant-medical',
文案: 'assistant-copywriting',
健康: 'assistant-health',
点评: 'assistant-review',
百科: 'assistant-encyclopedia',
旅游: 'assistant-travel',
翻译: 'assistant-translation',
游戏: 'assistant-game',
音乐: 'assistant-music',
营销: 'assistant-marketing',
科学: 'assistant-science',
分析: 'assistant-analysis',
法律: 'assistant-law',
咨询: 'assistant-consulting',
金融: 'assistant-finance',
管理: 'assistant-management'
}
// 从 agent.group 数组中获取 subcategoryId
// 策略:取第一个在 groupToSubcategoryMap 中能找到匹配的 group 名称
function getSubcategoryIdFromGroup(groupArray = []) {
if (!Array.isArray(groupArray)) return 'assistant-general'
for (const groupName of groupArray) {
const key = String(groupName)
if (groupToSubcategoryMap[key]) {
return groupToSubcategoryMap[key]
}
}
// 如果 group 中没有一项能精确映射,打印警告并返回通用默认值
// (避免为仅包含 "精选" 且 "精选" 本身无特定映射的情况重复打印警告, featured 字段会处理它)
if (!groupArray.includes('精选') || groupArray.length > 1 || !groupToSubcategoryMap['精选']) {
console.warn(
`No specific subcategory mapping found for group: ${JSON.stringify(groupArray)} (excluding '精选' if it has no specific map other than setting featured flag). Defaulting to 'assistant-general'.`
)
}
return 'assistant-general'
}
// --- 主转换逻辑 ---
try {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
console.log(`Created output directory: ${outputDir}`)
}
const agentsDataRaw = fs.readFileSync(agentsJsonPath, 'utf-8')
const agents = JSON.parse(agentsDataRaw)
// 假设 agents.json 的根是一个直接的数组
if (!Array.isArray(agents)) {
throw new Error(
`agents.json (path: ${agentsJsonPath}) is not an array. Please ensure it is a JSON array of agent objects.`
)
}
console.log(`Read ${agents.length} raw agent objects from ${agentsJsonPath}`)
const storeAssistants = agents
.map((agent) => {
if (!agent || typeof agent.id === 'undefined' || !agent.name) {
console.warn(
'Skipping invalid agent object (missing id or name):',
agent && agent.id ? `ID: ${agent.id}` : agent
)
return null
}
// 从 agent.group 获取 subcategoryId同时将 agent.group 用作 StoreItem.tags
const agentGroups = Array.isArray(agent.group) ? agent.group : []
const subcategoryId = getSubcategoryIdFromGroup(agentGroups)
// 检查 group 是否包含 "精选" 来设置 featured 标志
const isFeaturedByGroup = agentGroups.includes('精选')
return {
id: String(agent.id), // 使用 agent.id (顶层)
title: agent.name, // 使用 agent.name (顶层)
description: agent.description || 'No description available.', // 使用 agent.description (顶层)
type: 'Assistant', // 固定类型
categoryId: CATEGORY_ID_ASSISTANT, // 固定一级分类
subcategoryId: subcategoryId, // 从 agent.group 动态获取
author: 'Cherry Studio', // agent.author 可能不存在, 提供默认 'Cherry Studio'
icon: agent.emoji || '🤖', // 使用 agent.emoji (顶层), 若无则用默认
image: '',
tags: agentGroups, // 使用 agent.group (顶层) 作为 StoreItem.tags
// 如果 group 含 "精选",则 isFeaturedByGroup 为 true。
featured: isFeaturedByGroup,
// assistant
prompt: agent.prompt || ''
}
})
.filter((item) => item !== null)
fs.writeFileSync(outputJsonPath, JSON.stringify(storeAssistants, null, 2), 'utf-8')
console.log(`Successfully converted ${storeAssistants.length} agents to ${outputJsonPath}`)
} catch (error) {
console.error('Error during conversion:', error)
process.exit(1)
}

View File

@ -1,196 +0,0 @@
const fs = require('fs')
const path = require('path')
const assistantListPath = path.join(__dirname, '../data/store_list_assistant.json')
const categoriesPath = path.join(__dirname, '../data/store_categories.json')
// REMOVED: groupToSubcategoryMap loading logic
// const converAgentsPath = path.join(__dirname, '../js/conver_agents.json.js')
// let groupToSubcategoryMap = {}
// try {
// // This is a simplified way to get the map.
// // NOTE: This uses eval which is generally unsafe if the file content is not trusted.
// // For a safer approach, you might need to parse the JS file content
// // or export the map from conver_agents.json.js and require() it if it's a module.
// const converAgentsContent = fs.readFileSync(converAgentsPath, 'utf-8')
// const mapString = converAgentsContent.substring(
// converAgentsContent.indexOf('{'),
// converAgentsContent.lastIndexOf('}') + 1
// )
// if (mapString) {
// groupToSubcategoryMap = eval('(' + mapString + ')') // Using eval, ensure the content is safe
// console.log('Successfully loaded groupToSubcategoryMap.')
// } else {
// console.warn(
// 'Could not extract groupToSubcategoryMap from conver_agents.json.js. ID generation for new items might be affected.'
// )
// }
// } catch (error) {
// console.error('Error loading or parsing groupToSubcategoryMap from conver_agents.json.js:', error)
// // Continue without the map if it fails, IDs will be generated as assistant-name
// }
async function updateCounts() {
let assistantItems
let categoriesData // Declare categoriesData here alongside assistantItems
// Initialize tagCounts here, it will be populated after loading necessary data
const tagCounts = {}
try {
const assistantData = fs.readFileSync(assistantListPath, 'utf-8')
assistantItems = JSON.parse(assistantData)
console.log(`Successfully read ${path.basename(assistantListPath)}.`)
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`Error: ${assistantListPath} not found. Please check the path.`)
} else if (error instanceof SyntaxError) {
console.error(`Error: Could not decode JSON from ${assistantListPath}. Please check its format.`, error)
} else {
console.error(`An unexpected error occurred while processing ${assistantListPath}:`, error)
}
process.exit(1)
}
try {
const categoriesFileContent = fs.readFileSync(categoriesPath, 'utf-8')
categoriesData = JSON.parse(categoriesFileContent)
console.log(`Successfully read ${path.basename(categoriesPath)}.`)
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`Error: ${categoriesPath} not found. Please check the path.`)
} else if (error instanceof SyntaxError) {
console.error(`Error: Could not decode JSON from ${categoriesPath}. Please check its format.`, error)
} else {
console.error(`An unexpected error occurred while processing ${categoriesPath}:`, error)
}
process.exit(1)
}
// Determine all potential subcategory names
const potentialSubcategoryNames = new Set()
// Add from existing subcategories in store_categories.json (if assistant category exists)
const assistantCatFromData = categoriesData.find((c) => c.id === 'assistant')
if (assistantCatFromData && assistantCatFromData.items && Array.isArray(assistantCatFromData.items)) {
assistantCatFromData.items.forEach((subItem) => {
if (subItem.name) {
potentialSubcategoryNames.add(String(subItem.name).trim())
}
})
}
// Add from unique tags in store_list_assistant.json
// Ensure assistantItems is loaded and is an array before iterating
if (assistantItems && Array.isArray(assistantItems)) {
for (const item of assistantItems) {
if (item.tags && Array.isArray(item.tags)) {
for (const tag of item.tags) {
const trimmedTag = String(tag).trim()
if (trimmedTag) {
potentialSubcategoryNames.add(trimmedTag)
}
}
}
}
}
// Initialize tagCounts for all potential subcategory names
potentialSubcategoryNames.forEach((name) => {
tagCounts[name] = 0
})
// Calculate counts based on the new logic
// Ensure assistantItems is loaded and is an array
if (assistantItems && Array.isArray(assistantItems)) {
for (const item of assistantItems) {
const itemTitleLower = item.title?.toLowerCase() || ''
const itemAuthorLower = item.author?.toLowerCase() || ''
// Ensure item.tags is an array and tags are strings and trimmed, and filter out empty strings
const currentItemTags =
item.tags && Array.isArray(item.tags) ? item.tags.map((t) => String(t).trim()).filter(Boolean) : []
for (const subcategoryName of potentialSubcategoryNames) {
const normalizedSubcategoryName = subcategoryName.toLowerCase() // For case-insensitive matching in title/author
let foundInItem = false
// Condition 1: Subcategory name is in item.tags (exact, case-sensitive match using original subcategoryName)
if (currentItemTags.includes(subcategoryName)) {
foundInItem = true
}
// Condition 2: Subcategory name is in item.title (case-insensitive)
if (!foundInItem && itemTitleLower && itemTitleLower.includes(normalizedSubcategoryName)) {
foundInItem = true
}
// Condition 3: Subcategory name is in item.author (case-insensitive)
if (!foundInItem && itemAuthorLower && itemAuthorLower.includes(normalizedSubcategoryName)) {
foundInItem = true
}
if (foundInItem) {
tagCounts[subcategoryName]++
}
}
}
}
console.log('Tag counts calculated based on revised logic (OR condition).')
// console.log("Tag counts:", tagCounts); // For debugging
let assistantCategoryFound = false
for (const category of categoriesData) {
if (category.id === 'assistant') {
assistantCategoryFound = true
console.log("Found 'assistant' category. Updating and adding subcategories...")
if (!category.items || !Array.isArray(category.items)) {
category.items = [] // Initialize if items array is missing or not an array
console.warn(" Initialized 'items' array for 'assistant' category as it was missing or invalid.")
}
const existingSubCategoryNames = new Set(category.items.map((subItem) => subItem.name))
// Update existing subcategories
for (const subItem of category.items) {
if (subItem.name) {
// Match using original case name from categories.json with original case tag from tagCounts
const count = tagCounts[subItem.name] || 0
subItem.count = count
console.log(` Updated count for existing '${subItem.name}': ${count}`)
}
}
// Add new subcategories from tagCounts if they don't exist
for (const tagName in tagCounts) {
if (Object.prototype.hasOwnProperty.call(tagCounts, tagName) && !existingSubCategoryNames.has(tagName)) {
const count = tagCounts[tagName]
const subcategoryId = `assistant-${tagName.toLowerCase().replace(/\s+/g, '-')}`
category.items.push({
id: subcategoryId,
name: tagName,
count: count
})
console.log(` Added new subcategory '${tagName}' with id '${subcategoryId}' and count ${count}`)
}
}
break
}
}
if (!assistantCategoryFound) {
console.warn("Warning: Category with id 'assistant' not found. Cannot update or add subcategories.")
}
try {
fs.writeFileSync(categoriesPath, JSON.stringify(categoriesData, null, 2), 'utf-8')
console.log(`Successfully updated and wrote back to ${path.basename(categoriesPath)}.`)
} catch (error) {
console.error(`Error writing updated data to ${path.basename(categoriesPath)}:`, error)
process.exit(1)
}
console.log('Script finished.')
}
updateCounts()

View File

@ -15,6 +15,8 @@ const BUN_PACKAGES = {
'darwin-x64': 'bun-darwin-x64.zip',
'win32-x64': 'bun-windows-x64.zip',
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
'win32-arm64': 'bun-windows-x64.zip',
'win32-arm64-baseline': 'bun-windows-x64-baseline.zip',
'linux-x64': 'bun-linux-x64.zip',
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
'linux-arm64': 'bun-linux-aarch64.zip',

View File

@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true })
var fs = require('fs')
var path = require('path')
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
var baseLocale = 'zh-CN'
var baseLocale = 'en-us'
var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName)
/**

View File

@ -7,7 +7,6 @@ exports.default = async function notarizing(context) {
}
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
console.log('Skipping notarization')
return
}

View File

@ -1,16 +1,16 @@
/**
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
* Paratera_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
*/
// OCOOL API KEY
const OCOOL_API_KEY = process.env.OCOOL_API_KEY
const Paratera_API_KEY = process.env.Paratera_API_KEY
const INDEX = [
// 语言的名称 代码 用来翻译的模型
{ name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
{ name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
{ name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
{ name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
{ name: 'France', code: 'fr-fr', model: 'Qwen3-235B-A22B' },
{ name: 'Spanish', code: 'es-es', model: 'Qwen3-235B-A22B' },
{ name: 'Portuguese', code: 'pt-pt', model: 'Qwen3-235B-A22B' },
{ name: 'Greek', code: 'el-gr', model: 'Qwen3-235B-A22B' }
]
const fs = require('fs')
@ -19,8 +19,8 @@ import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const openai = new OpenAI({
apiKey: OCOOL_API_KEY,
baseURL: 'https://one.ocoolai.com/v1'
apiKey: Paratera_API_KEY,
baseURL: 'https://llmapi.paratera.com/v1'
})
// 递归遍历翻译

19
scripts/win-sign.js Normal file
View File

@ -0,0 +1,19 @@
const { execSync } = require('child_process')
exports.default = async function (configuration) {
if (process.env.WIN_SIGN) {
const { path } = configuration
if (configuration.path) {
try {
console.log('Start code signing...')
console.log('Signing file:', path)
const signCommand = `signtool sign /tr http://timestamp.comodoca.com /td sha256 /fd sha256 /a /v "${path}"`
execSync(signCommand, { stdio: 'inherit' })
console.log('Code signing completed')
} catch (error) {
console.error('Code signing failed:', error)
throw error
}
}
}
}

View File

@ -0,0 +1,58 @@
interface IFilterList {
WINDOWS: string[]
MAC?: string[]
}
interface IFinetunedList {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: IFilterList
INCLUDE_CLIPBOARD_DELAY_READ: IFilterList
}
/*************************************************************************
*
* Note: Do not modify this configuration unless you fully understand its meaning, implications, and intended behavior.
* -----------------------------------------------------------------------
* A predefined application filter list to include commonly used software
* that does not require text selection but may conflict with it, and disable them in advance.
* Only available in the selected mode.
*
* Specification: must be all lowercase, need to accurately find the actual running program name
*************************************************************************/
export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
WINDOWS: [
'explorer.exe',
// Screenshot
'snipaste.exe',
'pixpin.exe',
'sharex.exe',
// Office
'excel.exe',
'powerpnt.exe',
// Image Editor
'photoshop.exe',
'illustrator.exe',
// Video Editor
'adobe premiere pro.exe',
'afterfx.exe',
// Audio Editor
'adobe audition.exe',
// 3D Editor
'blender.exe',
'3dsmax.exe',
'maya.exe',
// CAD
'acad.exe',
'sldworks.exe',
// Remote Desktop
'mstsc.exe'
]
}
export const SELECTION_FINETUNED_LIST: IFinetunedList = {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe']
},
INCLUDE_CLIPBOARD_DELAY_READ: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe']
}
}

View File

@ -11,7 +11,6 @@ export default class VoyageEmbeddings extends BaseEmbeddings {
if (!this.configuration.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
console.log('VoyageEmbeddings', this.configuration)
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise<number> {

View File

@ -1,10 +1,12 @@
import '@main/config'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import { app } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
import { isDev, isWin } from './constant'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
@ -14,16 +16,45 @@ import {
registerProtocolClient,
setupAppImageDeepLink
} from './services/ProtocolClient'
import selectionService, { initSelectionService } from './services/SelectionService'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { setUserDataDir } from './utils/file'
Logger.initialize()
/**
* Disable chromium's window animations
* main purpose for this is to avoid the transparent window flashing when it is shown
* (especially on Windows for SelectionAssistant Toolbar)
* Know Issue: https://github.com/electron/electron/issues/12130#issuecomment-627198990
*/
if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled')
}
// in production mode, handle uncaught exception and unhandled rejection globally
if (!isDev) {
// handle uncaught exception
process.on('uncaughtException', (error) => {
Logger.error('Uncaught Exception:', error)
})
// handle unhandled rejection
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
})
}
// Check for single instance lock
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
} else {
// Portable dir must be setup before app ready
setUserDataDir()
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
@ -56,23 +87,17 @@ if (!app.requestSingleInstanceLock()) {
replaceDevtoolsFont(mainWindow)
setUserDataDir()
// Setup deep link for AppImage on Linux
await setupAppImageDeepLink()
if (process.env.NODE_ENV === 'development') {
if (isDev) {
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
}
ipcMain.handle(IpcChannel.System_GetDeviceType, () => {
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
})
ipcMain.handle(IpcChannel.System_GetHostname, () => {
return require('os').hostname()
})
//start selection assistant service
initSelectionService()
})
registerProtocolClient(app)
@ -99,6 +124,11 @@ if (!app.requestSingleInstanceLock()) {
app.on('before-quit', () => {
app.isQuitting = true
// quit selection service
if (selectionService) {
selectionService.quit()
}
})
app.on('will-quit', async () => {

View File

@ -3,12 +3,13 @@ import { arch } from 'node:os'
import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
@ -16,22 +17,25 @@ import CopilotService from './services/CopilotService'
import { ExportService } from './services/ExportService'
import FileService from './services/FileService'
import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService'
import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService'
import { TrayService } from './services/TrayService'
import { themeService } from './services/ThemeService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getConfigDir, getFilesDir } from './utils/file'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
import { FeedUrl } from '@shared/config/constant'
const fileManager = new FileStorage()
const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
@ -39,6 +43,7 @@ const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow)
const notificationService = new NotificationService(mainWindow)
ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(),
@ -108,10 +113,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setAutoUpdate(isActive)
})
ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => {
appUpdater.setFeedUrl(feedUrl)
})
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
configManager.set(key, value)
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify)
})
ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
@ -120,36 +127,13 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// theme
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
const notifyThemeChange = () => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((win) =>
win.webContents.send(IpcChannel.ThemeChange, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
)
}
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
nativeTheme.on('updated', notifyThemeChange)
} else {
nativeTheme.themeSource = theme
nativeTheme.removeAllListeners('updated')
}
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
configManager.setTheme(theme)
notifyThemeChange()
themeService.setTheme(theme)
})
// zoom factor
ipcMain.handle(IpcChannel.App_SetZoomFactor, (_, factor: number) => {
configManager.setZoomFactor(factor)
ipcMain.handle(IpcChannel.App_HandleZoomFactor, (_, delta: number, reset: boolean = false) => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.setZoomFactor(factor)
}
})
handleZoomFactor(windows, delta, reset)
return configManager.getZoomFactor()
})
// clear cache
@ -174,15 +158,46 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// get cache size
ipcMain.handle(IpcChannel.App_GetCacheSize, async () => {
const cachePath = getCacheDir()
log.info(`Calculating cache size for path: ${cachePath}`)
try {
const sizeInBytes = await calculateDirectorySize(cachePath)
const sizeInMB = (sizeInBytes / (1024 * 1024)).toFixed(2)
return `${sizeInMB}`
} catch (error: any) {
log.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
return '0'
}
})
// check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
await appUpdater.checkForUpdates()
return await appUpdater.checkForUpdates()
})
// notification
ipcMain.handle(IpcChannel.Notification_Send, async (_, notification: Notification) => {
await notificationService.sendNotification(notification)
})
ipcMain.handle(IpcChannel.Notification_OnClick, (_, notification: Notification) => {
mainWindow.webContents.send('notification-click', notification)
})
// zip
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
// system
ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux'))
ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname())
ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => {
const win = BrowserWindow.fromWebContents(e.sender)
win && win.webContents.toggleDevTools()
})
// backup
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
@ -206,8 +221,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile)
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
@ -256,13 +273,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// gemini
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
// mini window
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
@ -289,6 +299,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
@ -337,4 +348,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// store sync
storeSyncService.registerIpcHandler()
// selection assistant
SelectionService.registerIpcHandler()
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
}

View File

@ -56,7 +56,6 @@ class DifyKnowledgeServer {
private config: DifyKnowledgeServerConfig
constructor(difyKey: string, args: string[]) {
console.log('DifyKnowledgeServer args', args)
if (args.length === 0) {
throw new Error('DifyKnowledgeServer requires at least one argument')
}
@ -113,8 +112,6 @@ class DifyKnowledgeServer {
const errorDetails = JSON.stringify(parsed.error.format(), null, 2)
throw new Error(`无效的参数:\n${errorDetails}`)
}
console.log('DifyKnowledgeServer search_knowledge parsed', parsed.data)
return await this.performSearchKnowledge(
parsed.data.id,
parsed.data.query,

View File

@ -2,6 +2,7 @@ import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
import { Mutex } from 'async-mutex' // 引入 Mutex
import Logger from 'electron-log'
import { promises as fs } from 'fs'
import path from 'path'
@ -355,9 +356,9 @@ class MemoryServer {
private async _initializeManager(memoryPath: string): Promise<void> {
try {
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath)
console.log('KnowledgeGraphManager initialized successfully.')
Logger.log('KnowledgeGraphManager initialized successfully.')
} catch (error) {
console.error('Failed to initialize KnowledgeGraphManager:', error)
Logger.error('Failed to initialize KnowledgeGraphManager:', error)
// Server might be unusable, consider how to handle this state
// Maybe set a flag and return errors for all tool calls?
this.knowledgeGraphManager = null // Ensure it's null if init fails

View File

@ -17,6 +17,10 @@ export default abstract class BaseReranker {
* Get Rerank Request Url
*/
protected getRerankUrl() {
if (this.base.rerankModelProvider === 'dashscope') {
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
}
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
@ -28,6 +32,56 @@ export default abstract class BaseReranker {
return `${baseURL}/rerank`
}
/**
* Get Rerank Request Body
*/
protected getRerankRequestBody(query: string, searchResults: ExtractChunkData[]) {
const provider = this.base.rerankModelProvider
const documents = searchResults.map((doc) => doc.pageContent)
const topN = this.base.documentCount
if (provider === 'voyageai') {
return {
model: this.base.rerankModel,
query,
documents,
top_k: topN
}
} else if (provider === 'dashscope') {
return {
model: this.base.rerankModel,
input: {
query,
documents
},
parameters: {
top_n: topN
}
}
} else {
return {
model: this.base.rerankModel,
query,
documents,
top_n: topN
}
}
}
/**
* Extract Rerank Result
*/
protected extractRerankResult(data: any) {
const provider = this.base.rerankModelProvider
if (provider === 'dashscope') {
return data.output.results
} else if (provider === 'voyageai') {
return data.data
} else {
return data.results
}
}
/**
* Get Rerank Result
* @param searchResults

View File

@ -1,58 +0,0 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
interface DashscopeRerankResultItem {
document: {
text: string
}
index: number
relevance_score: number
}
interface DashscopeRerankResponse {
output: {
results: DashscopeRerankResultItem[]
}
usage: {
total_tokens: number
}
request_id: string
}
export default class DashscopeReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
const requestBody = {
model: this.base.rerankModel,
input: {
query,
documents: searchResults.map((doc) => doc.pageContent)
},
parameters: {
return_documents: true, // Recommended to be true to get document details if needed, though scores are primary
top_n: this.base.topN || 5 // Default to 5 if topN is not specified, as per API example
}
}
try {
const { data } = await axiosProxy.axios.post<DashscopeRerankResponse>(url, requestBody, {
headers: this.defaultHeaders()
})
const rerankResults = data.output.results
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Dashscope Reranker API 错误:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@ -1,14 +0,0 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class DefaultReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
async rerank(): Promise<ExtractChunkData[]> {
throw new Error('Method not implemented.')
}
}

View File

@ -4,7 +4,7 @@ import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class JinaReranker extends BaseReranker {
export default class GeneralReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
@ -12,21 +12,15 @@ export default class JinaReranker extends BaseReranker {
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_n: this.base.topN
}
const requestBody = this.getRerankRequestBody(query, searchResults)
try {
const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
const rerankResults = this.extractRerankResult(data)
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Jina Reranker API Error:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}

View File

@ -1,13 +1,12 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
import RerankerFactory from './RerankerFactory'
import GeneralReranker from './GeneralReranker'
export default class Reranker {
private sdk: BaseReranker
private sdk: GeneralReranker
constructor(base: KnowledgeBaseParams) {
this.sdk = RerankerFactory.create(base)
this.sdk = new GeneralReranker(base)
}
public async rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> {
return this.sdk.rerank(query, searchResults)

View File

@ -1,23 +0,0 @@
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
import DashscopeReranker from './DashscopeReranker'
import DefaultReranker from './DefaultReranker'
import JinaReranker from './JinaReranker'
import SiliconFlowReranker from './SiliconFlowReranker'
import VoyageReranker from './VoyageReranker'
export default class RerankerFactory {
static create(base: KnowledgeBaseParams): BaseReranker {
if (base.rerankModelProvider === 'silicon') {
return new SiliconFlowReranker(base)
} else if (base.rerankModelProvider === 'jina') {
return new JinaReranker(base)
} else if (base.rerankModelProvider === 'voyageai') {
return new VoyageReranker(base)
} else if (base.rerankModelProvider === 'dashscope') {
return new DashscopeReranker(base)
}
return new DefaultReranker(base)
}
}

View File

@ -1,36 +0,0 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class SiliconFlowReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_n: this.base.topN,
max_chunks_per_doc: this.base.chunkSize,
overlap_tokens: this.base.chunkOverlap
}
try {
const { data } = await axiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('SiliconFlow Reranker API 错误:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@ -1,40 +0,0 @@
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class VoyageReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_k: this.base.topN,
return_documents: false,
truncation: true
}
try {
const { data } = await axiosProxy.axios.post(url, requestBody, {
headers: {
...this.defaultHeaders()
}
})
const rerankResults = data.data
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Voyage Reranker API Error:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@ -1,5 +1,7 @@
import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales'
import { IpcChannel } from '@shared/IpcChannel'
import { FeedUrl } from '@shared/config/constant'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
@ -19,6 +21,7 @@ export default class AppUpdater {
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = configManager.getAutoUpdate()
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
autoUpdater.setFeedURL(configManager.getFeedUrl())
// 检测下载错误
autoUpdater.on('error', (error) => {
@ -61,6 +64,11 @@ export default class AppUpdater {
autoUpdater.autoInstallOnAppQuit = isActive
}
public setFeedUrl(feedUrl: FeedUrl) {
autoUpdater.setFeedURL(feedUrl)
configManager.setFeedUrl(feedUrl)
}
public async checkForUpdates() {
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
return {
@ -94,15 +102,22 @@ export default class AppUpdater {
if (!this.releaseInfo) {
return
}
const locale = locales[configManager.getLanguage()]
const { update: updateLocale } = locale.translation
let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes)
if (detail === '') {
detail = updateLocale.noReleaseNotes
}
dialog
.showMessageBox({
type: 'info',
title: '安装更新',
title: updateLocale.title,
icon,
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],
message: updateLocale.message.replace('{{version}}', this.releaseInfo.version),
detail,
buttons: [updateLocale.later, updateLocale.install],
defaultId: 1,
cancelId: 0
})
@ -118,7 +133,7 @@ export default class AppUpdater {
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
if (!releaseNotes) {
return '暂无更新说明'
return ''
}
if (typeof releaseNotes === 'string') {

View File

@ -4,10 +4,10 @@ import archiver from 'archiver'
import { exec } from 'child_process'
import { app } from 'electron'
import Logger from 'electron-log'
import extract from 'extract-zip'
import * as fs from 'fs-extra'
import StreamZip from 'node-stream-zip'
import * as path from 'path'
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
import { CreateDirectoryOptions, FileStat } from 'webdav'
import WebDav from './WebDav'
import { windowService } from './WindowService'
@ -77,7 +77,8 @@ class BackupManager {
_: Electron.IpcMainInvokeEvent,
fileName: string,
data: string,
destinationPath: string = this.backupDir
destinationPath: string = this.backupDir,
skipBackupFile: boolean = false
): Promise<string> {
const mainWindow = windowService.getMainWindow()
@ -104,23 +105,30 @@ class BackupManager {
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
Logger.log('[BackupManager IPC] ', skipBackupFile)
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
if (!skipBackupFile) {
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
} else {
Logger.log('[BackupManager] Skip the backup of the file')
await fs.promises.mkdir(path.join(this.tempDir, 'Data')) // 不创建空 Data 目录会导致 restore 失败
}
// 创建输出文件流
const backupedFilePath = path.join(destinationPath, fileName)
@ -231,15 +239,10 @@ class BackupManager {
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 使用 extract-zip 解压
await extract(backupPath, {
dir: this.tempDir,
onEntry: () => {
// 这里可以处理进度,但 extract-zip 不提供总条目数信息
onProgress({ stage: 'extracting', progress: 15, total: 100 })
}
})
onProgress({ stage: 'extracting', progress: 25, total: 100 })
const zip = new StreamZip.async({ file: backupPath })
onProgress({ stage: 'extracting', progress: 15, total: 100 })
await zip.extract(null, this.tempDir)
onProgress({ stage: 'extracted', progress: 25, total: 100 })
Logger.log('[backup] step 2: read data.json')
// 读取 data.json
@ -252,19 +255,26 @@ class BackupManager {
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
const dataExists = await fs.pathExists(sourcePath)
const dataFiles = dataExists ? await fs.readdir(sourcePath) : []
await this.setWritableRecursive(destPath)
await fs.remove(destPath)
if (dataExists && dataFiles.length > 0) {
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
// 使用流式复制
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
copiedSize += size
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(destPath)
await fs.remove(destPath)
// 使用流式复制
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
copiedSize += size
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
} else {
Logger.log('[backup] skipBackupFile is true, skip restoring Data directory')
}
Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录
@ -284,11 +294,13 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data)
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const contentLength = (await fs.stat(backupedFilePath)).size
const webdavClient = new WebDav(webdavConfig)
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true
overwrite: true,
contentLength
})
// 上传成功后删除本地备份文件
await fs.remove(backupedFilePath)
@ -330,12 +342,8 @@ class BackupManager {
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
try {
const client = createClient(config.webdavHost, {
username: config.webdavUser,
password: config.webdavPass
})
const response = await client.getDirectoryContents(config.webdavPath)
const client = new WebDav(config)
const response = await client.getDirectoryContents()
const files = Array.isArray(response) ? response : response.data
return files

View File

@ -1,11 +1,11 @@
import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
import { locales } from '../utils/locales'
enum ConfigKeys {
export enum ConfigKeys {
Language = 'language',
Theme = 'theme',
LaunchToTray = 'launchToTray',
@ -16,7 +16,14 @@ enum ConfigKeys {
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate',
EnableDataCollection = 'enableDataCollection'
FeedUrl = 'feedUrl',
EnableDataCollection = 'enableDataCollection',
SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar',
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList'
}
export class ConfigManager {
@ -32,12 +39,12 @@ export class ConfigManager {
return this.get(ConfigKeys.Language, locale) as LanguageVarious
}
setLanguage(theme: LanguageVarious) {
this.set(ConfigKeys.Language, theme)
setLanguage(lang: LanguageVarious) {
this.setAndNotify(ConfigKeys.Language, lang)
}
getTheme(): ThemeMode {
return this.get(ConfigKeys.Theme, ThemeMode.auto)
return this.get(ConfigKeys.Theme, ThemeMode.system)
}
setTheme(theme: ThemeMode) {
@ -57,8 +64,7 @@ export class ConfigManager {
}
setTray(value: boolean) {
this.set(ConfigKeys.Tray, value)
this.notifySubscribers(ConfigKeys.Tray, value)
this.setAndNotify(ConfigKeys.Tray, value)
}
getTrayOnClose(): boolean {
@ -74,8 +80,7 @@ export class ConfigManager {
}
setZoomFactor(factor: number) {
this.set(ConfigKeys.ZoomFactor, factor)
this.notifySubscribers(ConfigKeys.ZoomFactor, factor)
this.setAndNotify(ConfigKeys.ZoomFactor, factor)
}
subscribe<T>(key: string, callback: (newValue: T) => void) {
@ -107,11 +112,10 @@ export class ConfigManager {
}
setShortcuts(shortcuts: Shortcut[]) {
this.set(
this.setAndNotify(
ConfigKeys.Shortcuts,
shortcuts.filter((shortcut) => shortcut.system)
)
this.notifySubscribers(ConfigKeys.Shortcuts, shortcuts)
}
getClickTrayToShowQuickAssistant(): boolean {
@ -127,7 +131,7 @@ export class ConfigManager {
}
setEnableQuickAssistant(value: boolean) {
this.set(ConfigKeys.EnableQuickAssistant, value)
this.setAndNotify(ConfigKeys.EnableQuickAssistant, value)
}
getAutoUpdate(): boolean {
@ -138,6 +142,14 @@ export class ConfigManager {
this.set(ConfigKeys.AutoUpdate, value)
}
getFeedUrl(): string {
return this.get<string>(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION)
}
setFeedUrl(value: FeedUrl) {
this.set(ConfigKeys.FeedUrl, value)
}
getEnableDataCollection(): boolean {
return this.get<boolean>(ConfigKeys.EnableDataCollection, true)
}
@ -146,8 +158,64 @@ export class ConfigManager {
this.set(ConfigKeys.EnableDataCollection, value)
}
set(key: string, value: unknown) {
// Selection Assistant: is enabled the selection assistant
getSelectionAssistantEnabled(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, false)
}
setSelectionAssistantEnabled(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantEnabled, value)
}
// Selection Assistant: trigger mode (selected, ctrlkey)
getSelectionAssistantTriggerMode(): string {
return this.get<string>(ConfigKeys.SelectionAssistantTriggerMode, 'selected')
}
setSelectionAssistantTriggerMode(value: string) {
this.setAndNotify(ConfigKeys.SelectionAssistantTriggerMode, value)
}
// Selection Assistant: if action window position follow toolbar
getSelectionAssistantFollowToolbar(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantFollowToolbar, true)
}
setSelectionAssistantFollowToolbar(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantFollowToolbar, value)
}
getSelectionAssistantRemeberWinSize(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantRemeberWinSize, false)
}
setSelectionAssistantRemeberWinSize(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantRemeberWinSize, value)
}
getSelectionAssistantFilterMode(): string {
return this.get<string>(ConfigKeys.SelectionAssistantFilterMode, 'default')
}
setSelectionAssistantFilterMode(value: string) {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterMode, value)
}
getSelectionAssistantFilterList(): string[] {
return this.get<string[]>(ConfigKeys.SelectionAssistantFilterList, [])
}
setSelectionAssistantFilterList(value: string[]) {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterList, value)
}
setAndNotify(key: string, value: unknown) {
this.set(key, value, true)
}
set(key: string, value: unknown, isNotify: boolean = false) {
this.store.set(key, value)
isNotify && this.notifySubscribers(key, value)
}
get<T>(key: string, defaultValue?: T) {

View File

@ -9,12 +9,29 @@ class ContextMenu {
const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties)
const filtered = template.filter((item) => item.visible !== false)
if (filtered.length > 0) {
const menu = Menu.buildFromTemplate(filtered)
const menu = Menu.buildFromTemplate([...filtered, ...this.createInspectMenuItems(w)])
menu.popup()
}
})
}
private createInspectMenuItems(w: Electron.BrowserWindow): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
const template: MenuItemConstructorOptions[] = [
{
id: 'inspect',
label: common.inspect,
click: () => {
w.webContents.toggleDevTools()
},
enabled: true
}
]
return template
}
private createEditMenuItems(properties: Electron.ContextMenuParams): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation

View File

@ -1,5 +1,6 @@
import { AxiosRequestConfig } from 'axios'
import { app, safeStorage } from 'electron'
import Logger from 'electron-log'
import fs from 'fs/promises'
import path from 'path'
@ -227,10 +228,10 @@ class CopilotService {
try {
await fs.access(this.tokenFilePath)
await fs.unlink(this.tokenFilePath)
console.log('Successfully logged out from Copilot')
Logger.log('Successfully logged out from Copilot')
} catch (error) {
// 文件不存在不是错误,只是记录一下
console.log('Token file not found, nothing to delete')
Logger.log('Token file not found, nothing to delete')
}
} catch (error) {
console.error('Failed to logout:', error)

View File

@ -47,6 +47,8 @@ export class ExportService {
let linkText = ''
let linkUrl = ''
let insideLink = false
let boldStack = 0 // 跟踪嵌套的粗体标记
let italicStack = 0 // 跟踪嵌套的斜体标记
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
@ -82,17 +84,37 @@ export class ExportService {
insideLink = false
}
break
case 'strong_open':
boldStack++
break
case 'strong_close':
boldStack--
break
case 'em_open':
italicStack++
break
case 'em_close':
italicStack--
break
case 'text':
runs.push(new TextRun({ text: token.content, bold: isHeaderRow }))
break
case 'strong':
runs.push(new TextRun({ text: token.content, bold: true }))
break
case 'em':
runs.push(new TextRun({ text: token.content, italics: true }))
runs.push(
new TextRun({
text: token.content,
bold: isHeaderRow || boldStack > 0,
italics: italicStack > 0
})
)
break
case 'code_inline':
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
runs.push(
new TextRun({
text: token.content,
font: 'Consolas',
size: 20,
bold: isHeaderRow || boldStack > 0,
italics: italicStack > 0
})
)
break
}
}

View File

@ -1,7 +1,9 @@
import fs from 'node:fs'
import fs from 'fs/promises'
export default class FileService {
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
return fs.readFileSync(path, 'utf8')
public static async readFile(_: Electron.IpcMainInvokeEvent, pathOrUrl: string, encoding?: BufferEncoding) {
const path = pathOrUrl.startsWith('file://') ? new URL(pathOrUrl) : pathOrUrl
if (encoding) return fs.readFile(path, { encoding })
return fs.readFile(path)
}
}

View File

@ -28,11 +28,16 @@ class FileStorage {
}
private initStorageDir = (): void => {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
try {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
} catch (error) {
logger.error('[FileStorage] Failed to initialize storage directories:', error)
throw error
}
}
@ -263,6 +268,51 @@ class FileStorage {
}
}
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => {
try {
if (!base64Data) {
throw new Error('Base64 data is required')
}
// 移除 base64 头部信息(如果存在)
const base64String = base64Data.replace(/^data:.*;base64,/, '')
const buffer = Buffer.from(base64String, 'base64')
const uuid = uuidv4()
const ext = '.png'
const destPath = path.join(this.storageDir, uuid + ext)
logger.info('[FileStorage] Saving base64 image:', {
storageDir: this.storageDir,
destPath,
bufferSize: buffer.length
})
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(destPath, buffer)
const fileMetadata: FileType = {
id: uuid,
origin_name: uuid + ext,
name: uuid + ext,
path: destPath,
created_at: new Date().toISOString(),
size: buffer.length,
ext: ext.slice(1),
type: getFileType(ext),
count: 1
}
return fileMetadata
} catch (error) {
logger.error('[FileStorage] Failed to save base64 image:', error)
throw error
}
}
public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const buffer = await fs.promises.readFile(filePath)
@ -323,7 +373,7 @@ class FileStorage {
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<string | null> => {
): Promise<string> => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
@ -331,14 +381,18 @@ class FileStorage {
...options
})
if (result.canceled) {
return Promise.reject(new Error('User canceled the save dialog'))
}
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
return result.filePath
} catch (err) {
} catch (err: any) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
return null
return Promise.reject('An error occurred saving the file: ' + err?.message)
}
}
@ -377,7 +431,11 @@ class FileStorage {
}
}
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
public downloadFile = async (
_: Electron.IpcMainInvokeEvent,
url: string,
isUseContentType?: boolean
): Promise<FileType> => {
try {
const response = await fetch(url)
if (!response.ok) {
@ -402,7 +460,7 @@ class FileStorage {
}
// 如果文件名没有后缀根据Content-Type添加后缀
if (!filename.includes('.')) {
if (isUseContentType || !filename.includes('.')) {
const contentType = response.headers.get('Content-Type')
const ext = this.getExtensionFromMimeType(contentType)
filename += ext
@ -475,6 +533,25 @@ class FileStorage {
throw error
}
}
public writeFileWithId = async (_: Electron.IpcMainInvokeEvent, id: string, content: string): Promise<void> => {
try {
const filePath = path.join(this.storageDir, id)
logger.info('[FileStorage] Writing file:', filePath)
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
logger.info('[FileStorage] Creating storage directory:', this.storageDir)
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(filePath, content, 'utf8')
logger.info('[FileStorage] File written successfully:', filePath)
} catch (error) {
logger.error('[FileStorage] Failed to write file:', error)
throw error
}
}
}
export default FileStorage

View File

@ -1,68 +0,0 @@
import { File, FileState, GoogleGenAI, Pager } from '@google/genai'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
return await sdk.files.upload({
file: file.path,
config: {
mimeType: 'application/pdf',
name: file.id,
displayName: file.origin_name
}
})
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File | undefined> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await sdk.files.list()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static async processResponse(response: Pager<File>, file: FileType) {
for await (const f of response) {
if (f.state === FileState.ACTIVE) {
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
return f
}
}
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise<File[]> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const files: File[] = []
for await (const f of await sdk.files.list()) {
files.push(f)
}
return files
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
await sdk.files.delete({ name: fileId })
}
}

View File

@ -459,7 +459,7 @@ class KnowledgeService {
{ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }
): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
console.log(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
Logger.log(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
for (const id of uniqueIds) {
await ragApplication.deleteLoader(id)
}

View File

@ -4,6 +4,7 @@ import path from 'node:path'
import { createInMemoryMCPServer } from '@main/mcpServers/factory'
import { makeSureDirExists } from '@main/utils'
import { buildFunctionCallToolName } from '@main/utils/mcp'
import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
@ -69,17 +70,7 @@ function withCache<T extends unknown[], R>(
class McpService {
private clients: Map<string, Client> = new Map()
private getServerKey(server: MCPServer): string {
return JSON.stringify({
baseUrl: server.baseUrl,
command: server.command,
args: server.args,
registryUrl: server.registryUrl,
env: server.env,
id: server.id
})
}
private pendingClients: Map<string, Promise<Client>> = new Map()
constructor() {
this.initClient = this.initClient.bind(this)
@ -96,9 +87,26 @@ class McpService {
this.cleanup = this.cleanup.bind(this)
}
private getServerKey(server: MCPServer): string {
return JSON.stringify({
baseUrl: server.baseUrl,
command: server.command,
args: Array.isArray(server.args) ? server.args : [],
registryUrl: server.registryUrl,
env: server.env,
id: server.id
})
}
async initClient(server: MCPServer): Promise<Client> {
const serverKey = this.getServerKey(server)
// If there's a pending initialization, wait for it
const pendingClient = this.pendingClients.get(serverKey)
if (pendingClient) {
return pendingClient
}
// Check if we already have a client for this server configuration
const existingClient = this.clients.get(serverKey)
if (existingClient) {
@ -113,209 +121,232 @@ class McpService {
} else {
return existingClient
}
} catch (error) {
Logger.error(`[MCP] Error pinging server ${server.name}:`, error)
} catch (error: any) {
Logger.error(`[MCP] Error pinging server ${server.name}:`, error?.message)
this.clients.delete(serverKey)
}
}
// Create new client instance for each connection
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
const args = [...(server.args || [])]
// Create a promise for the initialization process
const initPromise = (async () => {
try {
// Create new client instance for each connection
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
// let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport
const authProvider = new McpOAuthClientProvider({
serverUrlHash: crypto
.createHash('md5')
.update(server.baseUrl || '')
.digest('hex')
})
const args = [...(server.args || [])]
const initTransport = async (): Promise<
StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport
> => {
// Create appropriate transport based on configuration
if (server.type === 'inMemory') {
Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
try {
await inMemoryServer.connect(serverTransport)
Logger.info(`[MCP] In-memory server started: ${server.name}`)
} catch (error: Error | any) {
Logger.error(`[MCP] Error starting in-memory server: ${error}`)
throw new Error(`Failed to start in-memory server: ${error.message}`)
}
// set the client transport to the client
return clientTransport
} else if (server.baseUrl) {
if (server.type === 'streamableHttp') {
const options: StreamableHTTPClientTransportOptions = {
requestInit: {
headers: server.headers || {}
},
authProvider
}
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: async (url, init) => {
const headers = { ...(server.headers || {}), ...(init?.headers || {}) }
// let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport
const authProvider = new McpOAuthClientProvider({
serverUrlHash: crypto
.createHash('md5')
.update(server.baseUrl || '')
.digest('hex')
})
// Get tokens from authProvider to make sure using the latest tokens
if (authProvider && typeof authProvider.tokens === 'function') {
try {
const tokens = await authProvider.tokens()
if (tokens && tokens.access_token) {
headers['Authorization'] = `Bearer ${tokens.access_token}`
const initTransport = async (): Promise<
StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport
> => {
// Create appropriate transport based on configuration
if (server.type === 'inMemory') {
Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
try {
await inMemoryServer.connect(serverTransport)
Logger.info(`[MCP] In-memory server started: ${server.name}`)
} catch (error: Error | any) {
Logger.error(`[MCP] Error starting in-memory server: ${error}`)
throw new Error(`Failed to start in-memory server: ${error.message}`)
}
// set the client transport to the client
return clientTransport
} else if (server.baseUrl) {
if (server.type === 'streamableHttp') {
const options: StreamableHTTPClientTransportOptions = {
requestInit: {
headers: server.headers || {}
},
authProvider
}
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: async (url, init) => {
const headers = { ...(server.headers || {}), ...(init?.headers || {}) }
// Get tokens from authProvider to make sure using the latest tokens
if (authProvider && typeof authProvider.tokens === 'function') {
try {
const tokens = await authProvider.tokens()
if (tokens && tokens.access_token) {
headers['Authorization'] = `Bearer ${tokens.access_token}`
}
} catch (error) {
Logger.error('Failed to fetch tokens:', error)
}
}
} catch (error) {
Logger.error('Failed to fetch tokens:', error)
return fetch(url, { ...init, headers })
}
},
requestInit: {
headers: server.headers || {}
},
authProvider
}
return new SSEClientTransport(new URL(server.baseUrl!), options)
} else {
throw new Error('Invalid server type')
}
} else if (server.command) {
let cmd = server.command
if (server.command === 'npx') {
cmd = await getBinaryPath('bun')
Logger.info(`[MCP] Using command: ${cmd}`)
// add -x to args if args exist
if (args && args.length > 0) {
if (!args.includes('-y')) {
args.unshift('-y')
}
if (!args.includes('x')) {
args.unshift('x')
}
}
if (server.registryUrl) {
server.env = {
...server.env,
NPM_CONFIG_REGISTRY: server.registryUrl
}
return fetch(url, { ...init, headers })
// if the server name is mcp-auto-install, use the mcp-registry.json file in the bin directory
if (server.name.includes('mcp-auto-install')) {
const binPath = await getBinaryPath()
makeSureDirExists(binPath)
server.env.MCP_REGISTRY_PATH = path.join(binPath, '..', 'config', 'mcp-registry.json')
}
}
} else if (server.command === 'uvx' || server.command === 'uv') {
cmd = await getBinaryPath(server.command)
if (server.registryUrl) {
server.env = {
...server.env,
UV_DEFAULT_INDEX: server.registryUrl,
PIP_INDEX_URL: server.registryUrl
}
}
},
requestInit: {
headers: server.headers || {}
},
authProvider
}
return new SSEClientTransport(new URL(server.baseUrl!), options)
} else {
throw new Error('Invalid server type')
}
} else if (server.command) {
let cmd = server.command
if (server.command === 'npx') {
cmd = await getBinaryPath('bun')
Logger.info(`[MCP] Using command: ${cmd}`)
// add -x to args if args exist
if (args && args.length > 0) {
if (!args.includes('-y')) {
args.unshift('-y')
}
if (!args.includes('x')) {
args.unshift('x')
}
}
if (server.registryUrl) {
server.env = {
...server.env,
NPM_CONFIG_REGISTRY: server.registryUrl
}
// if the server name is mcp-auto-install, use the mcp-registry.json file in the bin directory
if (server.name.includes('mcp-auto-install')) {
const binPath = await getBinaryPath()
makeSureDirExists(binPath)
server.env.MCP_REGISTRY_PATH = path.join(binPath, '..', 'config', 'mcp-registry.json')
}
}
} else if (server.command === 'uvx' || server.command === 'uv') {
cmd = await getBinaryPath(server.command)
if (server.registryUrl) {
server.env = {
...server.env,
UV_DEFAULT_INDEX: server.registryUrl,
PIP_INDEX_URL: server.registryUrl
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
// Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv()
// Bun not support proxy https://github.com/oven-sh/bun/issues/16812
if (cmd.includes('bun')) {
this.removeProxyEnv(loginShellEnv)
}
const stdioTransport = new StdioClientTransport({
command: cmd,
args,
env: {
...loginShellEnv,
...server.env
},
stderr: 'pipe'
})
stdioTransport.stderr?.on('data', (data) =>
Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString())
)
return stdioTransport
} else {
throw new Error('Either baseUrl or command must be provided')
}
}
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
// Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv()
const stdioTransport = new StdioClientTransport({
command: cmd,
args,
env: {
...loginShellEnv,
...server.env
},
stderr: 'pipe'
})
stdioTransport.stderr?.on('data', (data) =>
Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString())
)
return stdioTransport
} else {
throw new Error('Either baseUrl or command must be provided')
}
}
const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => {
Logger.info(`[MCP] Starting OAuth flow for server: ${server.name}`)
// Create an event emitter for the OAuth callback
const events = new EventEmitter()
const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => {
Logger.info(`[MCP] Starting OAuth flow for server: ${server.name}`)
// Create an event emitter for the OAuth callback
const events = new EventEmitter()
// Create a callback server
const callbackServer = new CallBackServer({
port: authProvider.config.callbackPort,
path: authProvider.config.callbackPath || '/oauth/callback',
events
})
// Create a callback server
const callbackServer = new CallBackServer({
port: authProvider.config.callbackPort,
path: authProvider.config.callbackPath || '/oauth/callback',
events
})
// Set a timeout to close the callback server
const timeoutId = setTimeout(() => {
Logger.warn(`[MCP] OAuth flow timed out for server: ${server.name}`)
callbackServer.close()
}, 300000) // 5 minutes timeout
// Set a timeout to close the callback server
const timeoutId = setTimeout(() => {
Logger.warn(`[MCP] OAuth flow timed out for server: ${server.name}`)
callbackServer.close()
}, 300000) // 5 minutes timeout
try {
// Wait for the authorization code
const authCode = await callbackServer.waitForAuthCode()
Logger.info(`[MCP] Received auth code: ${authCode}`)
try {
// Wait for the authorization code
const authCode = await callbackServer.waitForAuthCode()
Logger.info(`[MCP] Received auth code: ${authCode}`)
// Complete the OAuth flow
await transport.finishAuth(authCode)
// Complete the OAuth flow
await transport.finishAuth(authCode)
Logger.info(`[MCP] OAuth flow completed for server: ${server.name}`)
Logger.info(`[MCP] OAuth flow completed for server: ${server.name}`)
const newTransport = await initTransport()
// Try to connect again
await client.connect(newTransport)
const newTransport = await initTransport()
// Try to connect again
await client.connect(newTransport)
Logger.info(`[MCP] Successfully authenticated with server: ${server.name}`)
} catch (oauthError) {
Logger.error(`[MCP] OAuth authentication failed for server ${server.name}:`, oauthError)
throw new Error(
`OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}`
)
} finally {
// Clear the timeout and close the callback server
clearTimeout(timeoutId)
callbackServer.close()
}
}
Logger.info(`[MCP] Successfully authenticated with server: ${server.name}`)
} catch (oauthError) {
Logger.error(`[MCP] OAuth authentication failed for server ${server.name}:`, oauthError)
throw new Error(
`OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}`
)
try {
const transport = await initTransport()
try {
await client.connect(transport)
} catch (error: Error | any) {
if (
error instanceof Error &&
(error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))
) {
Logger.info(`[MCP] Authentication required for server: ${server.name}`)
await handleAuth(client, transport as SSEClientTransport | StreamableHTTPClientTransport)
} else {
throw error
}
}
// Store the new client in the cache
this.clients.set(serverKey, client)
Logger.info(`[MCP] Activated server: ${server.name}`)
return client
} catch (error: any) {
Logger.error(`[MCP] Error activating server ${server.name}:`, error?.message)
throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`)
}
} finally {
// Clear the timeout and close the callback server
clearTimeout(timeoutId)
callbackServer.close()
// Clean up the pending promise when done
this.pendingClients.delete(serverKey)
}
}
})()
try {
const transport = await initTransport()
try {
await client.connect(transport)
} catch (error: Error | any) {
if (error instanceof Error && (error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))) {
Logger.info(`[MCP] Authentication required for server: ${server.name}`)
await handleAuth(client, transport as SSEClientTransport | StreamableHTTPClientTransport)
} else {
throw error
}
}
// Store the pending promise
this.pendingClients.set(serverKey, initPromise)
// Store the new client in the cache
this.clients.set(serverKey, client)
Logger.info(`[MCP] Activated server: ${server.name}`)
return client
} catch (error: any) {
Logger.error(`[MCP] Error activating server ${server.name}:`, error)
throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`)
}
return initPromise
}
async closeClient(serverKey: string) {
@ -357,12 +388,32 @@ class McpService {
for (const [key] of this.clients) {
try {
await this.closeClient(key)
} catch (error) {
Logger.error(`[MCP] Failed to close client: ${error}`)
} catch (error: any) {
Logger.error(`[MCP] Failed to close client: ${error?.message}`)
}
}
}
/**
* Check connectivity for an MCP server
*/
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> {
Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
try {
const client = await this.initClient(server)
// Attempt to list tools as a way to check connectivity
await client.listTools()
Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`)
return true
} catch (error) {
Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error)
// Close the client if connectivity check fails to ensure a clean state for the next attempt
const serverKey = this.getServerKey(server)
await this.closeClient(serverKey)
return false
}
}
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
const client = await this.initClient(server)
@ -372,15 +423,15 @@ class McpService {
tools.map((tool: any) => {
const serverTool: MCPTool = {
...tool,
id: `f${nanoid()}`,
id: buildFunctionCallToolName(server.name, tool.name),
serverId: server.id,
serverName: server.name
}
serverTools.push(serverTool)
})
return serverTools
} catch (error) {
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error)
} catch (error: any) {
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error?.message)
return []
}
}
@ -439,8 +490,8 @@ class McpService {
* List prompts available on an MCP server
*/
private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> {
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
const client = await this.initClient(server)
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
try {
const { prompts } = await client.listPrompts()
return prompts.map((prompt: any) => ({
@ -449,8 +500,11 @@ class McpService {
serverId: server.id,
serverName: server.name
}))
} catch (error) {
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error)
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error?.message)
}
return []
}
}
@ -508,19 +562,21 @@ class McpService {
* List resources available on an MCP server (implementation)
*/
private async listResourcesImpl(server: MCPServer): Promise<MCPResource[]> {
Logger.info(`[MCP] Listing resources for server: ${server.name}`)
const client = await this.initClient(server)
Logger.info(`[MCP] Listing resources for server: ${server.name}`)
try {
const result = await client.listResources()
const resources = result.resources || []
const serverResources = (Array.isArray(resources) ? resources : []).map((resource: any) => ({
return (Array.isArray(resources) ? resources : []).map((resource: any) => ({
...resource,
serverId: server.id,
serverName: server.name
}))
return serverResources
} catch (error) {
Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error)
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error?.message)
}
return []
}
}
@ -563,7 +619,7 @@ class McpService {
contents: contents
}
} catch (error: Error | any) {
Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error)
Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error.message)
throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`)
}
}
@ -593,14 +649,21 @@ class McpService {
const pathSeparator = process.platform === 'win32' ? ';' : ':'
const cherryBinPath = path.join(os.homedir(), '.cherrystudio', 'bin')
loginEnv.PATH = `${loginEnv.PATH}${pathSeparator}${cherryBinPath}`
Logger.info('[MCP] Successfully fetched login shell environment variables:', loginEnv)
Logger.info('[MCP] Successfully fetched login shell environment variables:')
return loginEnv
} catch (error) {
Logger.error('[MCP] Failed to fetch login shell environment variables:', error)
return {}
}
})
private removeProxyEnv(env: Record<string, string>) {
delete env.HTTPS_PROXY
delete env.HTTP_PROXY
delete env.grpc_proxy
delete env.http_proxy
delete env.https_proxy
}
}
const mcpService = new McpService()
export default mcpService
export default new McpService()

View File

@ -0,0 +1,31 @@
import { BrowserWindow, Notification as ElectronNotification } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
import icon from '../../../build/icon.png?asset'
class NotificationService {
private window: BrowserWindow
constructor(window: BrowserWindow) {
// Initialize the service
this.window = window
}
public async sendNotification(notification: Notification) {
// 使用 Electron Notification API
const electronNotification = new ElectronNotification({
title: notification.title,
body: notification.message,
icon: icon
})
electronNotification.on('click', () => {
this.window.show()
this.window.webContents.send('notification-click', notification)
})
electronNotification.show()
}
}
export default NotificationService

View File

@ -1,4 +1,5 @@
import { app } from 'electron'
import Logger from 'electron-log'
import fs from 'fs'
import path from 'path'
@ -155,7 +156,7 @@ class ObsidianVaultService {
return []
}
console.log('获取Vault文件结构:', vault.name, vault.path)
Logger.log('获取Vault文件结构:', vault.name, vault.path)
return this.getVaultStructure(vault.path)
} catch (error) {
console.error('获取Vault文件结构时发生错误:', error)

View File

@ -6,6 +6,7 @@ import { promisify } from 'node:util'
import { app } from 'electron'
import Logger from 'electron-log'
import { handleProvidersProtocolUrl } from './urlschema/handle-providers'
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
import { windowService } from './WindowService'
@ -25,7 +26,6 @@ export function handleProtocolUrl(url: string) {
if (!url) return
// Process the URL that was used to open the app
// The url will be in the format: cherrystudio://data?param1=value1&param2=value2
console.log('Received URL:', url)
// Parse the URL and extract parameters
const urlObj = new URL(url)
@ -35,6 +35,9 @@ export function handleProtocolUrl(url: string) {
case 'mcp':
handleMcpProtocolUrl(urlObj)
return
case 'providers':
handleProvidersProtocolUrl(urlObj)
return
}
// You can send the data to your renderer process

View File

@ -1,8 +1,7 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { socksDispatcher } from 'fetch-socks'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
import { ProxyAgent, setGlobalDispatcher } from 'undici'
// import { ProxyAgent, setGlobalDispatcher } from 'undici'
type ProxyMode = 'system' | 'custom' | 'none'
@ -121,22 +120,22 @@ export class ProxyManager {
return this.config.url || ''
}
setGlobalProxy() {
const proxyUrl = this.config.url
if (proxyUrl) {
const [protocol, address] = proxyUrl.split('://')
const [host, port] = address.split(':')
if (!protocol.includes('socks')) {
setGlobalDispatcher(new ProxyAgent(proxyUrl))
} else {
global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
port: parseInt(port),
type: protocol === 'socks5' ? 5 : 4,
host: host
})
}
}
}
// setGlobalProxy() {
// const proxyUrl = this.config.url
// if (proxyUrl) {
// const [protocol, address] = proxyUrl.split('://')
// const [host, port] = address.split(':')
// if (!protocol.includes('socks')) {
// setGlobalDispatcher(new ProxyAgent(proxyUrl))
// } else {
// global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
// port: parseInt(port),
// type: protocol === 'socks5' ? 5 : 4,
// host: host
// })
// }
// }
// }
}
export const proxyManager = new ProxyManager()

View File

@ -191,7 +191,7 @@ export const reduxService = new ReduxService()
try {
// 读取状态
const settings = await reduxService.select('state.settings')
console.log('settings', settings)
Logger.log('settings', settings)
// 派发 action
await reduxService.dispatch({
@ -201,7 +201,7 @@ export const reduxService = new ReduxService()
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
console.log('API key changed:', newValue)
Logger.log('API key changed:', newValue)
})
// 批量执行 actions
@ -212,16 +212,16 @@ export const reduxService = new ReduxService()
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
console.log('apiKey', apiKey)
Logger.log('apiKey', apiKey)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
console.log('apiKey1', apiKey1)
Logger.log('apiKey1', apiKey1)
// 取消订阅
unsubscribe()
} catch (error) {
console.error('Error:', error)
Logger.error('Error:', error)
}
}
*/

View File

@ -1,57 +1,57 @@
import Logger from 'electron-log'
import { Operator } from 'opendal'
// import Logger from 'electron-log'
// import { Operator } from 'opendal'
export default class RemoteStorage {
public instance: Operator | undefined
// export default class RemoteStorage {
// public instance: Operator | undefined
/**
*
* @param scheme is the scheme for opendal services. Available value includes "azblob", "azdls", "cos", "gcs", "obs", "oss", "s3", "webdav", "webhdfs", "aliyun-drive", "alluxio", "azfile", "dropbox", "gdrive", "onedrive", "postgresql", "mysql", "redis", "swift", "mongodb", "alluxio", "b2", "seafile", "upyun", "koofr", "yandex-disk"
* @param options is the options for given opendal services. Valid options depend on the scheme. Checkout https://docs.rs/opendal/latest/opendal/services/index.html for all valid options.
*
* For example, use minio as remote storage:
*
* ```typescript
* const storage = new RemoteStorage('s3', {
* endpoint: 'http://localhost:9000',
* region: 'us-east-1',
* bucket: 'testbucket',
* access_key_id: 'user',
* secret_access_key: 'password',
* root: '/path/to/basepath',
* })
* ```
*/
constructor(scheme: string, options?: Record<string, string> | undefined | null) {
this.instance = new Operator(scheme, options)
// /**
// *
// * @param scheme is the scheme for opendal services. Available value includes "azblob", "azdls", "cos", "gcs", "obs", "oss", "s3", "webdav", "webhdfs", "aliyun-drive", "alluxio", "azfile", "dropbox", "gdrive", "onedrive", "postgresql", "mysql", "redis", "swift", "mongodb", "alluxio", "b2", "seafile", "upyun", "koofr", "yandex-disk"
// * @param options is the options for given opendal services. Valid options depend on the scheme. Checkout https://docs.rs/opendal/latest/opendal/services/index.html for all valid options.
// *
// * For example, use minio as remote storage:
// *
// * ```typescript
// * const storage = new RemoteStorage('s3', {
// * endpoint: 'http://localhost:9000',
// * region: 'us-east-1',
// * bucket: 'testbucket',
// * access_key_id: 'user',
// * secret_access_key: 'password',
// * root: '/path/to/basepath',
// * })
// * ```
// */
// constructor(scheme: string, options?: Record<string, string> | undefined | null) {
// this.instance = new Operator(scheme, options)
this.putFileContents = this.putFileContents.bind(this)
this.getFileContents = this.getFileContents.bind(this)
}
// this.putFileContents = this.putFileContents.bind(this)
// this.getFileContents = this.getFileContents.bind(this)
// }
public putFileContents = async (filename: string, data: string | Buffer) => {
if (!this.instance) {
return new Error('RemoteStorage client not initialized')
}
// public putFileContents = async (filename: string, data: string | Buffer) => {
// if (!this.instance) {
// return new Error('RemoteStorage client not initialized')
// }
try {
return await this.instance.write(filename, data)
} catch (error) {
Logger.error('[RemoteStorage] Error putting file contents:', error)
throw error
}
}
// try {
// return await this.instance.write(filename, data)
// } catch (error) {
// Logger.error('[RemoteStorage] Error putting file contents:', error)
// throw error
// }
// }
public getFileContents = async (filename: string) => {
if (!this.instance) {
throw new Error('RemoteStorage client not initialized')
}
// public getFileContents = async (filename: string) => {
// if (!this.instance) {
// throw new Error('RemoteStorage client not initialized')
// }
try {
return await this.instance.read(filename)
} catch (error) {
Logger.error('[RemoteStorage] Error getting file contents:', error)
throw error
}
}
}
// try {
// return await this.instance.read(filename)
// } catch (error) {
// Logger.error('[RemoteStorage] Error getting file contents:', error)
// throw error
// }
// }
// }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
import { ZOOM_LEVELS } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { handleZoomFactor } from '@main/utils/zoom'
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
@ -16,14 +15,11 @@ const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; on
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
return (window: BrowserWindow) => handleZoom(1)(window)
return (window: BrowserWindow) => handleZoomFactor([window], 0.1)
case 'zoom_out':
return (window: BrowserWindow) => handleZoom(-1)(window)
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset':
return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1)
configManager.setZoomFactor(1)
}
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
case 'show_app':
return () => {
windowService.toggleMainWindow()
@ -41,46 +37,6 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
function handleZoom(delta: number) {
return (window: BrowserWindow) => {
const currentZoom = configManager.getZoomFactor()
let currentIndex = ZOOM_LEVELS.indexOf(currentZoom)
// 如果当前缩放比例不在预设列表中,找到最接近的
if (currentIndex === -1) {
let closestIndex = 0
let minDiff = Math.abs(ZOOM_LEVELS[0] - currentZoom)
for (let i = 1; i < ZOOM_LEVELS.length; i++) {
const diff = Math.abs(ZOOM_LEVELS[i] - currentZoom)
if (diff < minDiff) {
minDiff = diff
closestIndex = i
}
}
currentIndex = closestIndex
}
let nextIndex = currentIndex + delta
// 边界检查
if (nextIndex < 0) {
nextIndex = 0 // 已经是最小值
} else if (nextIndex >= ZOOM_LEVELS.length) {
nextIndex = ZOOM_LEVELS.length - 1 // 已经是最大值
}
const newZoom = ZOOM_LEVELS[nextIndex]
if (newZoom !== currentZoom) {
// 只有在实际改变时才更新
configManager.setZoomFactor(newZoom)
// 通知所有渲染进程更新 zoomFactor
window.webContents.setZoomFactor(newZoom)
window.webContents.send(IpcChannel.ZoomFactorUpdated, newZoom)
}
}
}
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
shortcut: string | string[]
): string => {

View File

@ -0,0 +1,48 @@
import { IpcChannel } from '@shared/IpcChannel'
import { ThemeMode } from '@types'
import { BrowserWindow, nativeTheme } from 'electron'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { configManager } from './ConfigManager'
class ThemeService {
private theme: ThemeMode = ThemeMode.system
constructor() {
this.theme = configManager.getTheme()
if (this.theme === ThemeMode.dark || this.theme === ThemeMode.light || this.theme === ThemeMode.system) {
nativeTheme.themeSource = this.theme
} else {
// 兼容旧版本
configManager.setTheme(ThemeMode.system)
nativeTheme.themeSource = ThemeMode.system
}
nativeTheme.on('updated', this.themeUpdatadHandler.bind(this))
}
themeUpdatadHandler() {
BrowserWindow.getAllWindows().forEach((win) => {
if (win && !win.isDestroyed() && win.setTitleBarOverlay) {
try {
win.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
} catch (error) {
// don't throw error if setTitleBarOverlay failed
// Because it may be called with some windows have some title bar
}
}
win.webContents.send(IpcChannel.ThemeUpdated, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
})
}
setTheme(theme: ThemeMode) {
if (theme === this.theme) {
return
}
this.theme = theme
nativeTheme.themeSource = theme
configManager.setTheme(theme)
}
}
export const themeService = new ThemeService()

View File

@ -5,16 +5,17 @@ import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray }
import icon from '../../../build/tray_icon.png?asset'
import iconDark from '../../../build/tray_icon_dark.png?asset'
import iconLight from '../../../build/tray_icon_light.png?asset'
import { configManager } from './ConfigManager'
import { ConfigKeys, configManager } from './ConfigManager'
import { windowService } from './WindowService'
export class TrayService {
private static instance: TrayService
private tray: Tray | null = null
private contextMenu: Menu | null = null
constructor() {
this.watchConfigChanges()
this.updateTray()
this.watchTrayChanges()
TrayService.instance = this
}
@ -43,6 +44,30 @@ export class TrayService {
this.tray = tray
this.updateContextMenu()
if (process.platform === 'linux') {
this.tray.setContextMenu(this.contextMenu)
}
this.tray.setToolTip('Cherry Studio')
this.tray.on('right-click', () => {
if (this.contextMenu) {
this.tray?.popUpContextMenu(this.contextMenu)
}
})
this.tray.on('click', () => {
if (configManager.getEnableQuickAssistant() && configManager.getClickTrayToShowQuickAssistant()) {
windowService.showMiniWindow()
} else {
windowService.showMainWindow()
}
})
}
private updateContextMenu() {
const locale = locales[configManager.getLanguage()]
const { tray: trayLocale } = locale.translation
@ -64,25 +89,7 @@ export class TrayService {
}
].filter(Boolean) as MenuItemConstructorOptions[]
const contextMenu = Menu.buildFromTemplate(template)
if (process.platform === 'linux') {
this.tray.setContextMenu(contextMenu)
}
this.tray.setToolTip('Cherry Studio')
this.tray.on('right-click', () => {
this.tray?.popUpContextMenu(contextMenu)
})
this.tray.on('click', () => {
if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
windowService.showMiniWindow()
} else {
windowService.showMainWindow()
}
})
this.contextMenu = Menu.buildFromTemplate(template)
}
private updateTray() {
@ -94,13 +101,6 @@ export class TrayService {
}
}
public restartTray() {
if (configManager.getTray()) {
this.destroyTray()
this.createTray()
}
}
private destroyTray() {
if (this.tray) {
this.tray.destroy()
@ -108,8 +108,16 @@ export class TrayService {
}
}
private watchTrayChanges() {
configManager.subscribe<boolean>('tray', () => this.updateTray())
private watchConfigChanges() {
configManager.subscribe(ConfigKeys.Tray, () => this.updateTray())
configManager.subscribe(ConfigKeys.Language, () => {
this.updateContextMenu()
})
configManager.subscribe(ConfigKeys.EnableQuickAssistant, () => {
this.updateContextMenu()
})
}
private quit() {

View File

@ -1,5 +1,7 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
import https from 'https'
import path from 'path'
import Stream from 'stream'
import {
BufferLike,
@ -14,13 +16,14 @@ export default class WebDav {
private webdavPath: string
constructor(params: WebDavConfig) {
this.webdavPath = params.webdavPath
this.webdavPath = params.webdavPath || '/'
this.instance = createClient(params.webdavHost, {
username: params.webdavUser,
password: params.webdavPass,
maxBodyLength: Infinity,
maxContentLength: Infinity
maxContentLength: Infinity,
httpsAgent: new https.Agent({ rejectUnauthorized: false })
})
this.putFileContents = this.putFileContents.bind(this)
@ -49,7 +52,7 @@ export default class WebDav {
throw error
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
@ -64,7 +67,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.getFileContents(remoteFilePath, options)
@ -74,6 +77,19 @@ export default class WebDav {
}
}
public getDirectoryContents = async () => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')
}
try {
return await this.instance.getDirectoryContents(this.webdavPath)
} catch (error) {
Logger.error('[WebDAV] Error getting directory contents on WebDAV:', error)
throw error
}
}
public checkConnection = async () => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')
@ -105,7 +121,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.deleteFile(remoteFilePath)

View File

@ -6,12 +6,8 @@ import { session, shell, webContents } from 'electron'
*/
export function initSessionUserAgent() {
const wvSession = session.fromPartition('persist:webview')
const newChromeVersion = '135.0.7049.96'
const originUA = wvSession.getUserAgent()
const newUA = originUA
.replace(/CherryStudio\/\S+\s/, '')
.replace(/Electron\/\S+\s/, '')
.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${newChromeVersion}`)
const newUA = originUA.replace(/CherryStudio\/\S+\s/, '').replace(/Electron\/\S+\s/, '')
wvSession.setUserAgent(newUA)
}

View File

@ -1,8 +1,10 @@
// just import the themeService to ensure the theme is initialized
import './ThemeService'
import { is } from '@electron-toolkit/utils'
import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { getFilesDir } from '@main/utils/file'
import { IpcChannel } from '@shared/IpcChannel'
import { ThemeMode } from '@types'
import { app, BrowserWindow, nativeTheme, shell } from 'electron'
import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
@ -45,13 +47,6 @@ export class WindowService {
maximize: false
})
const theme = configManager.getTheme()
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
} else {
nativeTheme.themeSource = theme
}
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
@ -75,7 +70,9 @@ export class WindowService {
sandbox: false,
webSecurity: false,
webviewTag: true,
allowRunningInsecureContent: true
allowRunningInsecureContent: true,
zoomFactor: configManager.getZoomFactor(),
backgroundThrottling: false
}
})
@ -184,6 +181,12 @@ export class WindowService {
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
})
// set the zoom factor again when the window is going to restore
// minimize and restore will cause zoom reset
mainWindow.on('restore', () => {
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
})
// ARCH: as `will-resize` is only for Win & Mac,
// linux has the same problem, use `resize` listener instead
// but `resize` will fliker the ui
@ -198,10 +201,21 @@ export class WindowService {
// 当按下Escape键且窗口处于全屏状态时退出全屏
if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) {
if (mainWindow.isFullScreen()) {
event.preventDefault()
mainWindow.setFullScreen(false)
// 获取 shortcuts 配置
const shortcuts = configManager.getShortcuts()
const exitFullscreenShortcut = shortcuts.find((s) => s.key === 'exit_fullscreen')
if (exitFullscreenShortcut == undefined) {
mainWindow.setFullScreen(false)
return
}
if (exitFullscreenShortcut?.enabled) {
event.preventDefault()
mainWindow.setFullScreen(false)
return
}
}
}
return
})
}
@ -306,7 +320,7 @@ export class WindowService {
/**
* :
* win/linux: +
* win/linux: "开启托盘+设置关闭时最小化到托盘"
* mac: 任何情况都会到这里mac
*/
@ -429,7 +443,8 @@ export class WindowService {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true
webviewTag: true,
backgroundThrottling: false
}
})
@ -468,9 +483,9 @@ export class WindowService {
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/mini/index.html')
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/miniWindow.html')
} else {
this.miniWindow.loadFile(join(__dirname, '../renderer/src/windows/mini/index.html'))
this.miniWindow.loadFile(join(__dirname, '../renderer/miniWindow.html'))
}
return this.miniWindow
@ -529,6 +544,25 @@ export class WindowService {
public setPinMiniWindow(isPinned) {
this.isPinnedMiniWindow = isPinned
}
/**
*
* @param text
*/
public quoteToMainWindow(text: string): void {
try {
this.showMainWindow()
const mainWindow = this.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send(IpcChannel.App_QuoteToMain, text)
}, 100)
}
} catch (error) {
Logger.error('Failed to quote to main window:', error as Error)
}
}
}
export const windowService = WindowService.getInstance()

View File

@ -47,7 +47,7 @@ function getLoginShellEnvironment(): Promise<Record<string, string>> {
commandArgs = ['-ilc', shellCommandToGetEnv] // -i for interactive, -l for login, -c to execute command
}
Logger.log(`Spawning shell: ${shellPath} with args: ${commandArgs.join(' ')} in ${homeDirectory}`)
Logger.log(`[ShellEnv] Spawning shell: ${shellPath} with args: ${commandArgs.join(' ')} in ${homeDirectory}`)
const child = spawn(shellPath, commandArgs, {
cwd: homeDirectory, // Run the command in the user's home directory
@ -85,7 +85,7 @@ function getLoginShellEnvironment(): Promise<Record<string, string>> {
Logger.warn(`Shell process stderr output (even with exit code 0):\n${errorOutput.trim()}`)
}
const env = {}
const env: Record<string, string> = {}
const lines = output.split('\n')
lines.forEach((line) => {
@ -110,6 +110,8 @@ function getLoginShellEnvironment(): Promise<Record<string, string>> {
Logger.warn('Raw output from shell:\n', output)
}
env.PATH = env.Path || env.PATH || ''
resolve(env)
})
})

View File

@ -0,0 +1,37 @@
import { IpcChannel } from '@shared/IpcChannel'
import Logger from 'electron-log'
import { windowService } from '../WindowService'
export function handleProvidersProtocolUrl(url: URL) {
const params = new URLSearchParams(url.search)
switch (url.pathname) {
case '/api-keys': {
// jsonConfig example:
// {
// "id": "tokenflux",
// "baseUrl": "https://tokenflux.ai/v1",
// "apiKey": "sk-xxxx"
// }
// cherrystudio://providers/api-keys?data={base64Encode(JSON.stringify(jsonConfig))}
const data = params.get('data')
if (data) {
const stringify = Buffer.from(data, 'base64').toString('utf8')
Logger.info('get api keys from urlschema: ', stringify)
const jsonConfig = JSON.parse(stringify)
Logger.info('get api keys from urlschema: ', jsonConfig)
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.Provider_AddKey, jsonConfig)
mainWindow.webContents.executeJavaScript(`window.navigate('/settings/provider?id=${jsonConfig.id}')`)
}
} else {
Logger.error('No data found in URL')
}
break
}
default:
console.error(`Unknown MCP protocol URL: ${url}`)
break
}
}

View File

@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { decrypt, encrypt } from '../aes'
const key = '12345678901234567890123456789012' // 32字节
const iv = '1234567890abcdef1234567890abcdef' // 32字节hex实际应16字节hex
function getIv16() {
// 取前16字节作为 hex
return iv.slice(0, 32)
}
describe('aes utils', () => {
it('should encrypt and decrypt normal string', () => {
const text = 'hello world'
const { iv: outIv, encryptedData } = encrypt(text, key, getIv16())
expect(typeof encryptedData).toBe('string')
expect(outIv).toBe(getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should support unicode and special chars', () => {
const text = '你好,世界!🌟🚀'
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should handle empty string', () => {
const text = ''
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should encrypt and decrypt long string', () => {
const text = 'a'.repeat(100_000)
const { encryptedData } = encrypt(text, key, getIv16())
const decrypted = decrypt(encryptedData, getIv16(), key)
expect(decrypted).toBe(text)
})
it('should throw error for wrong key', () => {
const text = 'test'
const { encryptedData } = encrypt(text, key, getIv16())
expect(() => decrypt(encryptedData, getIv16(), 'wrongkeywrongkeywrongkeywrongkey')).toThrow()
})
it('should throw error for wrong iv', () => {
const text = 'test'
const { encryptedData } = encrypt(text, key, getIv16())
expect(() => decrypt(encryptedData, 'abcdefabcdefabcdefabcdefabcdefab', key)).toThrow()
})
it('should throw error for invalid key/iv length', () => {
expect(() => encrypt('test', 'shortkey', getIv16())).toThrow()
expect(() => encrypt('test', key, 'shortiv')).toThrow()
})
it('should throw error for invalid encrypted data', () => {
expect(() => decrypt('nothexdata', getIv16(), key)).toThrow()
})
it('should throw error for non-string input', () => {
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => encrypt(null, key, getIv16())).toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
expect(() => decrypt(null, getIv16(), key)).toThrow()
})
})

View File

@ -0,0 +1,243 @@
import * as fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { FileTypes } from '@types'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir } from '../file'
// Mock dependencies
vi.mock('node:fs')
vi.mock('node:os')
vi.mock('node:path')
vi.mock('uuid', () => ({
v4: () => 'mock-uuid'
}))
vi.mock('electron', () => ({
app: {
getPath: vi.fn((key) => {
if (key === 'temp') return '/mock/temp'
if (key === 'userData') return '/mock/userData'
return '/mock/unknown'
})
}
}))
describe('file', () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock path.extname
vi.mocked(path.extname).mockImplementation((file) => {
const parts = file.split('.')
return parts.length > 1 ? `.${parts[parts.length - 1]}` : ''
})
// Mock path.basename
vi.mocked(path.basename).mockImplementation((file) => {
const parts = file.split('/')
return parts[parts.length - 1]
})
// Mock path.join
vi.mocked(path.join).mockImplementation((...args) => args.join('/'))
// Mock os.homedir
vi.mocked(os.homedir).mockReturnValue('/mock/home')
})
afterEach(() => {
vi.resetAllMocks()
})
describe('getFileType', () => {
it('should return IMAGE for image extensions', () => {
expect(getFileType('.jpg')).toBe(FileTypes.IMAGE)
expect(getFileType('.jpeg')).toBe(FileTypes.IMAGE)
expect(getFileType('.png')).toBe(FileTypes.IMAGE)
expect(getFileType('.gif')).toBe(FileTypes.IMAGE)
expect(getFileType('.webp')).toBe(FileTypes.IMAGE)
expect(getFileType('.bmp')).toBe(FileTypes.IMAGE)
})
it('should return VIDEO for video extensions', () => {
expect(getFileType('.mp4')).toBe(FileTypes.VIDEO)
expect(getFileType('.avi')).toBe(FileTypes.VIDEO)
expect(getFileType('.mov')).toBe(FileTypes.VIDEO)
expect(getFileType('.mkv')).toBe(FileTypes.VIDEO)
expect(getFileType('.flv')).toBe(FileTypes.VIDEO)
})
it('should return AUDIO for audio extensions', () => {
expect(getFileType('.mp3')).toBe(FileTypes.AUDIO)
expect(getFileType('.wav')).toBe(FileTypes.AUDIO)
expect(getFileType('.ogg')).toBe(FileTypes.AUDIO)
expect(getFileType('.flac')).toBe(FileTypes.AUDIO)
expect(getFileType('.aac')).toBe(FileTypes.AUDIO)
})
it('should return TEXT for text extensions', () => {
expect(getFileType('.txt')).toBe(FileTypes.TEXT)
expect(getFileType('.md')).toBe(FileTypes.TEXT)
expect(getFileType('.html')).toBe(FileTypes.TEXT)
expect(getFileType('.json')).toBe(FileTypes.TEXT)
expect(getFileType('.js')).toBe(FileTypes.TEXT)
expect(getFileType('.ts')).toBe(FileTypes.TEXT)
expect(getFileType('.css')).toBe(FileTypes.TEXT)
expect(getFileType('.java')).toBe(FileTypes.TEXT)
expect(getFileType('.py')).toBe(FileTypes.TEXT)
})
it('should return DOCUMENT for document extensions', () => {
expect(getFileType('.pdf')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.pptx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.docx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.xlsx')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.odt')).toBe(FileTypes.DOCUMENT)
})
it('should return OTHER for unknown extensions', () => {
expect(getFileType('.unknown')).toBe(FileTypes.OTHER)
expect(getFileType('')).toBe(FileTypes.OTHER)
expect(getFileType('.')).toBe(FileTypes.OTHER)
expect(getFileType('...')).toBe(FileTypes.OTHER)
expect(getFileType('.123')).toBe(FileTypes.OTHER)
})
it('should handle case-insensitive extensions', () => {
expect(getFileType('.JPG')).toBe(FileTypes.IMAGE)
expect(getFileType('.PDF')).toBe(FileTypes.DOCUMENT)
expect(getFileType('.Mp3')).toBe(FileTypes.AUDIO)
expect(getFileType('.HtMl')).toBe(FileTypes.TEXT)
expect(getFileType('.Xlsx')).toBe(FileTypes.DOCUMENT)
})
it('should handle extensions without leading dot', () => {
expect(getFileType('jpg')).toBe(FileTypes.OTHER)
expect(getFileType('pdf')).toBe(FileTypes.OTHER)
expect(getFileType('mp3')).toBe(FileTypes.OTHER)
})
it('should handle extreme cases', () => {
expect(getFileType('.averylongfileextensionname')).toBe(FileTypes.OTHER)
expect(getFileType('.tar.gz')).toBe(FileTypes.OTHER)
expect(getFileType('.文件')).toBe(FileTypes.OTHER)
expect(getFileType('.файл')).toBe(FileTypes.OTHER)
})
})
describe('getAllFiles', () => {
it('should return all valid files recursively', () => {
// Mock file system
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockImplementation((dirPath) => {
if (dirPath === '/test') {
return ['file1.txt', 'file2.pdf', 'subdir']
} else if (dirPath === '/test/subdir') {
return ['file3.md', 'file4.docx']
}
return []
})
vi.mocked(fs.statSync).mockImplementation((filePath) => {
const isDir = String(filePath).endsWith('subdir')
return {
isDirectory: () => isDir,
size: 1024
} as fs.Stats
})
const result = getAllFiles('/test')
expect(result).toHaveLength(4)
expect(result[0].id).toBe('mock-uuid')
expect(result[0].name).toBe('file1.txt')
expect(result[0].type).toBe(FileTypes.TEXT)
expect(result[1].name).toBe('file2.pdf')
expect(result[1].type).toBe(FileTypes.DOCUMENT)
})
it('should skip hidden files', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue(['.hidden', 'visible.txt'])
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => false,
size: 1024
} as fs.Stats)
const result = getAllFiles('/test')
expect(result).toHaveLength(1)
expect(result[0].name).toBe('visible.txt')
})
it('should skip unsupported file types', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue(['image.jpg', 'video.mp4', 'audio.mp3', 'document.pdf'])
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => false,
size: 1024
} as fs.Stats)
const result = getAllFiles('/test')
// Should only include document.pdf as the others are excluded types
expect(result).toHaveLength(1)
expect(result[0].name).toBe('document.pdf')
expect(result[0].type).toBe(FileTypes.DOCUMENT)
})
it('should return empty array for empty directory', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockReturnValue([])
const result = getAllFiles('/empty')
expect(result).toHaveLength(0)
})
it('should handle file system errors', () => {
// @ts-ignore - override type for testing
vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
throw new Error('Directory not found')
})
// Since the function doesn't have error handling, we expect it to propagate
expect(() => getAllFiles('/nonexistent')).toThrow('Directory not found')
})
})
describe('getTempDir', () => {
it('should return correct temp directory path', () => {
const tempDir = getTempDir()
expect(tempDir).toBe('/mock/temp/CherryStudio')
})
})
describe('getFilesDir', () => {
it('should return correct files directory path', () => {
const filesDir = getFilesDir()
expect(filesDir).toBe('/mock/userData/Data/Files')
})
})
describe('getConfigDir', () => {
it('should return correct config directory path', () => {
const configDir = getConfigDir()
expect(configDir).toBe('/mock/home/.cherrystudio/config')
})
})
describe('getAppConfigDir', () => {
it('should return correct app config directory path', () => {
const appConfigDir = getAppConfigDir('test-app')
expect(appConfigDir).toBe('/mock/home/.cherrystudio/config/test-app')
})
it('should handle empty app name', () => {
const appConfigDir = getAppConfigDir('')
expect(appConfigDir).toBe('/mock/home/.cherrystudio/config/')
})
})
})

View File

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'
import { compress, decompress } from '../zip'
const jsonStr = JSON.stringify({ foo: 'bar', num: 42, arr: [1, 2, 3] })
// 辅助函数:生成大字符串
function makeLargeString(size: number) {
return 'a'.repeat(size)
}
describe('zip', () => {
describe('compress & decompress', () => {
it('should compress and decompress a normal JSON string', async () => {
const compressed = await compress(jsonStr)
expect(compressed).toBeInstanceOf(Buffer)
const decompressed = await decompress(compressed)
expect(decompressed).toBe(jsonStr)
})
it('should handle empty string', async () => {
const compressed = await compress('')
expect(compressed).toBeInstanceOf(Buffer)
const decompressed = await decompress(compressed)
expect(decompressed).toBe('')
})
it('should handle large string', async () => {
const largeStr = makeLargeString(100_000)
const compressed = await compress(largeStr)
expect(compressed).toBeInstanceOf(Buffer)
expect(compressed.length).toBeLessThan(largeStr.length)
const decompressed = await decompress(compressed)
expect(decompressed).toBe(largeStr)
})
it('should throw error when decompressing invalid buffer', async () => {
const invalidBuffer = Buffer.from('not a valid gzip', 'utf-8')
await expect(decompress(invalidBuffer)).rejects.toThrow()
})
it('should throw error when compress input is not string', async () => {
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(null)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(undefined)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(compress(123)).rejects.toThrow()
})
it('should throw error when decompress input is not buffer', async () => {
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress(null)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress(undefined)).rejects.toThrow()
// @ts-expect-error purposely pass wrong type to test error branch
await expect(decompress('string')).rejects.toThrow()
})
})
})

View File

@ -81,6 +81,10 @@ export function getConfigDir() {
return path.join(os.homedir(), '.cherrystudio', 'config')
}
export function getCacheDir() {
return path.join(app.getPath('userData'), 'Cache')
}
export function getAppConfigDir(name: string) {
return path.join(getConfigDir(), name)
}

View File

@ -1,4 +1,5 @@
import fs from 'node:fs'
import fsAsync from 'node:fs/promises'
import path from 'node:path'
import { app } from 'electron'
@ -52,3 +53,20 @@ export function makeSureDirExists(dir: string) {
fs.mkdirSync(dir, { recursive: true })
}
}
export async function calculateDirectorySize(directoryPath: string): Promise<number> {
let totalSize = 0
const items = await fsAsync.readdir(directoryPath)
for (const item of items) {
const itemPath = path.join(directoryPath, item)
const stats = await fsAsync.stat(itemPath)
if (stats.isFile()) {
totalSize += stats.size
} else if (stats.isDirectory()) {
totalSize += await calculateDirectorySize(itemPath)
}
}
return totalSize
}

34
src/main/utils/mcp.ts Normal file
View File

@ -0,0 +1,34 @@
export function buildFunctionCallToolName(serverName: string, toolName: string) {
const sanitizedServer = serverName.trim().replace(/-/g, '_')
const sanitizedTool = toolName.trim().replace(/-/g, '_')
// Combine server name and tool name
let name = sanitizedTool
if (!sanitizedTool.includes(sanitizedServer.slice(0, 7))) {
name = `${sanitizedServer.slice(0, 7) || ''}-${sanitizedTool || ''}`
}
// Replace invalid characters with underscores or dashes
// Keep a-z, A-Z, 0-9, underscores and dashes
name = name.replace(/[^a-zA-Z0-9_-]/g, '_')
// Ensure name starts with a letter or underscore (for valid JavaScript identifier)
if (!/^[a-zA-Z]/.test(name)) {
name = `tool-${name}`
}
// Remove consecutive underscores/dashes (optional improvement)
name = name.replace(/[_-]{2,}/g, '_')
// Truncate to 63 characters maximum
if (name.length > 63) {
name = name.slice(0, 63)
}
// Handle edge case: ensure we still have a valid name if truncation left invalid chars at edges
if (name.endsWith('_') || name.endsWith('-')) {
name = name.slice(0, -1)
}
return name
}

View File

@ -1,5 +1,7 @@
import { BrowserWindow } from 'electron'
import { isDev, isWin } from '../constant'
function isTilingWindowManager() {
if (process.platform === 'darwin') {
return false
@ -15,31 +17,59 @@ function isTilingWindowManager() {
return tilingSystems.some((system) => desktopEnv?.includes(system))
}
//see: https://github.com/electron/electron/issues/42055#issuecomment-2449365647
export const replaceDevtoolsFont = (browserWindow: BrowserWindow) => {
if (process.platform === 'win32') {
//only for windows and dev, don't do this in production to avoid performance issues
if (isWin && isDev) {
browserWindow.webContents.on('devtools-opened', () => {
const css = `
:root {
--sys-color-base: var(--ref-palette-neutral100);
--source-code-font-family: consolas;
--source-code-font-family: consolas !important;
--source-code-font-size: 12px;
--monospace-font-family: consolas;
--monospace-font-family: consolas !important;
--monospace-font-size: 12px;
--default-font-family: system-ui, sans-serif;
--default-font-size: 12px;
--ref-palette-neutral99: #ffffffff;
}
.-theme-with-dark-background {
.theme-with-dark-background {
--sys-color-base: var(--ref-palette-secondary25);
}
body {
--default-font-family: system-ui,sans-serif;
}`
--default-font-family: system-ui, sans-serif;
}
`
browserWindow.webContents.devToolsWebContents?.executeJavaScript(`
const overriddenStyle = document.createElement('style');
overriddenStyle.innerHTML = '${css.replaceAll('\n', ' ')}';
document.body.append(overriddenStyle);
document.body.classList.remove('platform-windows');`)
document.querySelectorAll('.platform-windows').forEach(el => el.classList.remove('platform-windows'));
addStyleToAutoComplete();
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
for (let i = 0; i < mutation.addedNodes.length; i++) {
const item = mutation.addedNodes[i];
if (item.classList.contains('editor-tooltip-host')) {
addStyleToAutoComplete();
}
}
}
}
});
observer.observe(document.body, {childList: true});
function addStyleToAutoComplete() {
document.querySelectorAll('.editor-tooltip-host').forEach(element => {
if (element.shadowRoot.querySelectorAll('[data-key="overridden-dev-tools-font"]').length === 0) {
const overriddenStyle = document.createElement('style');
overriddenStyle.setAttribute('data-key', 'overridden-dev-tools-font');
overriddenStyle.innerHTML = '.cm-tooltip-autocomplete ul[role=listbox] {font-family: consolas !important;}';
element.shadowRoot.append(overriddenStyle);
}
});
}
`)
})
}
}

View File

@ -9,10 +9,10 @@ const gunzipPromise = util.promisify(zlib.gunzip)
/**
*
* @param {string} str JSON
* @returns {Promise<Buffer>} Buffer
* @param str
*/
export async function compress(str) {
export async function compress(str: string): Promise<Buffer> {
try {
const buffer = Buffer.from(str, 'utf-8')
return await gzipPromise(buffer)
@ -27,7 +27,7 @@ export async function compress(str) {
* @param {Buffer} compressedBuffer - Buffer
* @returns {Promise<string>} JSON
*/
export async function decompress(compressedBuffer) {
export async function decompress(compressedBuffer: Buffer): Promise<string> {
try {
const buffer = await gunzipPromise(compressedBuffer)
return buffer.toString('utf-8')

26
src/main/utils/zoom.ts Normal file
View File

@ -0,0 +1,26 @@
import { BrowserWindow } from 'electron'
import { configManager } from '../services/ConfigManager'
export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
if (reset) {
wins.forEach((win) => {
win.webContents.setZoomFactor(1)
})
configManager.setZoomFactor(1)
return
}
if (delta === 0) {
return
}
const currentZoom = configManager.getZoomFactor()
const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.5 && newZoom <= 2.0) {
wins.forEach((win) => {
win.webContents.setZoomFactor(newZoom)
})
configManager.setZoomFactor(newZoom)
}
}

View File

@ -1,10 +1,14 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { electronAPI } from '@electron-toolkit/preload'
import { FeedUrl } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, ThemeMode, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
import { CreateDirectoryOptions } from 'webdav'
import type { ActionItem } from '../renderer/src/types/selectionTypes'
// Custom APIs for renderer
const api = {
getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
@ -17,23 +21,31 @@ const api = {
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
setZoomFactor: (factor: number) => ipcRenderer.invoke(IpcChannel.App_SetZoomFactor, factor),
setFeedUrl: (feedUrl: FeedUrl) => ipcRenderer.invoke(IpcChannel.App_SetFeedUrl, feedUrl),
setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
handleZoomFactor: (delta: number, reset: boolean = false) =>
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
notification: {
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
},
system: {
getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType),
getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname)
},
devTools: {
toggle: () => ipcRenderer.invoke(IpcChannel.System_ToggleDevTools)
},
zip: {
compress: (text: string) => ipcRenderer.invoke(IpcChannel.Zip_Compress, text),
decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
},
backup: {
backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath),
backup: (fileName: string, data: string, destinationPath?: string, skipBackupFile?: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath, skipBackupFile),
restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),
@ -57,6 +69,7 @@ const api = {
get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
create: (fileName: string) => ipcRenderer.invoke(IpcChannel.File_Create, fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data),
writeWithId: (id: string, content: string) => ipcRenderer.invoke(IpcChannel.File_WriteWithId, id, content),
open: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Open, options),
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
@ -64,13 +77,16 @@ const api = {
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data),
download: (url: string, isUseContentType?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId)
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
getPathForFile: (file: File) => webUtils.getPathForFile(file)
},
fs: {
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
@ -105,14 +121,16 @@ const api = {
resetMinimumSize: () => ipcRenderer.invoke(IpcChannel.Windows_ResetMinimumSize)
},
gemini: {
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_UploadFile, file, apiKey),
uploadFile: (file: FileType, { apiKey, baseURL }: { apiKey: string; baseURL: string }) =>
ipcRenderer.invoke(IpcChannel.Gemini_UploadFile, file, { apiKey, baseURL }),
base64File: (file: FileType) => ipcRenderer.invoke(IpcChannel.Gemini_Base64File, file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_RetrieveFile, file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
deleteFile: (fileId: string, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, fileId, apiKey)
},
config: {
set: (key: string, value: any) => ipcRenderer.invoke(IpcChannel.Config_Set, key, value),
set: (key: string, value: any, isNotify: boolean = false) =>
ipcRenderer.invoke(IpcChannel.Config_Set, key, value, isNotify),
get: (key: string) => ipcRenderer.invoke(IpcChannel.Config_Get, key)
},
miniWindow: {
@ -141,7 +159,8 @@ const api = {
listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server),
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server)
},
shell: {
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
@ -192,17 +211,25 @@ const api = {
unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe),
onUpdate: (action: any) => ipcRenderer.invoke(IpcChannel.StoreSync_OnUpdate, action)
},
// 新增:监听主进程的 zoom factor 更新
onZoomFactorUpdate: (callback: (factor: number) => void) => {
const listener = (_event: Electron.IpcRendererEvent, factor: number) => {
callback(factor)
}
ipcRenderer.on(IpcChannel.ZoomFactorUpdated, listener)
// 返回一个移除监听器的函数
return () => {
ipcRenderer.removeListener(IpcChannel.ZoomFactorUpdated, listener)
}
}
selection: {
hideToolbar: () => ipcRenderer.invoke(IpcChannel.Selection_ToolbarHide),
writeToClipboard: (text: string) => ipcRenderer.invoke(IpcChannel.Selection_WriteToClipboard, text),
determineToolbarSize: (width: number, height: number) =>
ipcRenderer.invoke(IpcChannel.Selection_ToolbarDetermineSize, width, height),
setEnabled: (enabled: boolean) => ipcRenderer.invoke(IpcChannel.Selection_SetEnabled, enabled),
setTriggerMode: (triggerMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetTriggerMode, triggerMode),
setFollowToolbar: (isFollowToolbar: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetFollowToolbar, isFollowToolbar),
setRemeberWinSize: (isRemeberWinSize: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetRemeberWinSize, isRemeberWinSize),
setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode),
setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList),
processAction: (actionItem: ActionItem) => ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem),
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
},
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' 'unsafe-inline' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
@ -21,7 +21,7 @@
flex-direction: row;
justify-content: center;
align-items: center;
display: none;
display: flex;
}
#spinner img {
@ -36,6 +36,9 @@
<div id="spinner">
<img src="/src/assets/images/logo.png" />
</div>
<script>
console.time('init')
</script>
<script type="module" src="/src/init.ts"></script>
<script type="module" src="/src/entryPoint.tsx"></script>
</body>

View File

@ -18,7 +18,7 @@
<body>
<div id="root"></div>
<script type="module" src="./entryPoint.tsx"></script>
<script type="module" src="/src/windows/mini/entryPoint.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio Selection Assistant</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/selection/action/entryPoint.tsx"></script>
<style>
html {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
</style>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More