Compare commits
131 Commits
v1.1.10
...
feat/varia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370cfd6e9f | ||
|
|
5cdf4eff77 | ||
|
|
b53dbcbb30 | ||
|
|
a42283e789 | ||
|
|
d2ed9972bd | ||
|
|
0fd9b6e56c | ||
|
|
d213bc1024 | ||
|
|
91b9a48c48 | ||
|
|
e572b3801b | ||
|
|
4bf15aed25 | ||
|
|
6d568688ed | ||
|
|
f20cbf31a8 | ||
|
|
bfbfba13fe | ||
|
|
8b9929cc7b | ||
|
|
a90be7e83f | ||
|
|
efa68c8519 | ||
|
|
d7bd240a9a | ||
|
|
95df69ff82 | ||
|
|
e41df917b4 | ||
|
|
0a33649b3c | ||
|
|
d1cb7258d2 | ||
|
|
8fbedb2bd0 | ||
|
|
750247aef8 | ||
|
|
32e1f428e7 | ||
|
|
aee6219a75 | ||
|
|
5329fa7ede | ||
|
|
ba640d4070 | ||
|
|
8c5f61d407 | ||
|
|
b43ecb75f5 | ||
|
|
3dc4947e26 | ||
|
|
a5b0480418 | ||
|
|
8a7db19e73 | ||
|
|
2da8a73124 | ||
|
|
5223a3c5a6 | ||
|
|
72c5de3b81 | ||
|
|
9f11e7c22b | ||
|
|
1ce86c11ca | ||
|
|
57c1b59a51 | ||
|
|
a2f9067908 | ||
|
|
2a4c512e49 | ||
|
|
94eb7f3a34 | ||
|
|
b363cb06a4 | ||
|
|
9e977f4b35 | ||
|
|
00de616958 | ||
|
|
1187a47698 | ||
|
|
83d0eb07aa | ||
|
|
8f6bf11320 | ||
|
|
22b0bd54b4 | ||
|
|
be39c5f40c | ||
|
|
8b00ff4b93 | ||
|
|
f5b675b356 | ||
|
|
de8dbb2646 | ||
|
|
7e67005e70 | ||
|
|
d6e66f3a4d | ||
|
|
e5aaec2129 | ||
|
|
464634d051 | ||
|
|
3698238e9e | ||
|
|
ae2a661201 | ||
|
|
d6dbe357fb | ||
|
|
e9dd795f9a | ||
|
|
03a18c1f3b | ||
|
|
e3ba44fc2c | ||
|
|
9976ad9ed0 | ||
|
|
3bb294e698 | ||
|
|
990b1651a9 | ||
|
|
11c070a1d7 | ||
|
|
57ba91072d | ||
|
|
433d562599 | ||
|
|
194ba1baa0 | ||
|
|
53ae427f2f | ||
|
|
3f40cc28ac | ||
|
|
d3584d2d39 | ||
|
|
da0db73916 | ||
|
|
21f1b8b373 | ||
|
|
f1a03916e7 | ||
|
|
45f0bfa0f9 | ||
|
|
f2102daf00 | ||
|
|
8f5c4483fc | ||
|
|
43adac3f74 | ||
|
|
7b8c5f185c | ||
|
|
eeb537048b | ||
|
|
5712a58a5e | ||
|
|
c4162bd9e3 | ||
|
|
eddbae6f5e | ||
|
|
29f7da1a4c | ||
|
|
403ed8cbf4 | ||
|
|
7263a682b7 | ||
|
|
29b5ba787b | ||
|
|
bb6fdd2db7 | ||
|
|
710171278a | ||
|
|
41191f6132 | ||
|
|
bbc7b20183 | ||
|
|
8bb8081f31 | ||
|
|
7ddd2cb9d5 | ||
|
|
06ff44f97c | ||
|
|
1a85b8bd5d | ||
|
|
fb9c23c500 | ||
|
|
7fb85dc311 | ||
|
|
2af15e4172 | ||
|
|
415f991143 | ||
|
|
c162242433 | ||
|
|
487d7a502e | ||
|
|
d64d6969ae | ||
|
|
cc32c36222 | ||
|
|
0d320120a4 | ||
|
|
3cbe45fc8d | ||
|
|
917943386e | ||
|
|
aee0f9ea3f | ||
|
|
2055615aca | ||
|
|
40cac47136 | ||
|
|
40d9629681 | ||
|
|
8acefaa907 | ||
|
|
e2d8b89ffd | ||
|
|
8d48824981 | ||
|
|
fd66881022 | ||
|
|
b321169ca2 | ||
|
|
123362b493 | ||
|
|
a1568808d4 | ||
|
|
6dff8b2725 | ||
|
|
c8b2e8dd79 | ||
|
|
8ac18934e9 | ||
|
|
6699b0902f | ||
|
|
9b98312775 | ||
|
|
1e14dd6ea2 | ||
|
|
0d612cb827 | ||
|
|
ccfac25a04 | ||
|
|
7447dfe771 | ||
|
|
0fe45a203c | ||
|
|
94942141b9 | ||
|
|
f08856ae42 | ||
|
|
a606f4b6c5 |
@@ -6,3 +6,4 @@ tsconfig.json
|
||||
tsconfig.*.json
|
||||
CHANGELOG*.md
|
||||
agents.json
|
||||
src/renderer/src/integration/nutstore/sso/lib
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||
index eaf30b114a273e68abbb92c8b07018495e63f4cb..4b06519bdb51845e4693fe877da9de01c7a81039 100644
|
||||
--- a/src/markdown-loader.js
|
||||
+++ b/src/markdown-loader.js
|
||||
@@ -21,7 +21,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||
: await streamToBuffer(fs.createReadStream(this.filePathOrUrl));
|
||||
this.debug('MarkdownLoader stream created');
|
||||
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
|
||||
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
|
||||
this.debug('Markdown parsed...');
|
||||
const webLoader = new WebLoader({
|
||||
urlOrContent: result,
|
||||
@@ -1,158 +0,0 @@
|
||||
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
|
||||
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..1c16d83bcbf9b7140292793d6cbb8c04281949d9 100644
|
||||
--- a/src/loaders/local-path-loader.d.ts
|
||||
+++ b/src/loaders/local-path-loader.d.ts
|
||||
@@ -4,8 +4,10 @@ export declare class LocalPathLoader extends BaseLoader<{
|
||||
}> {
|
||||
private readonly debug;
|
||||
private readonly path;
|
||||
- constructor({ path }: {
|
||||
+ constructor({ path, chunkSize, chunkOverlap }: {
|
||||
path: string;
|
||||
+ chunkSize?: number;
|
||||
+ chunkOverlap?: number;
|
||||
});
|
||||
getUnfilteredChunks(): AsyncGenerator<{
|
||||
metadata: {
|
||||
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
|
||||
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..ec8215b01195a21ef20f3c5d56ecc99f186bb596 100644
|
||||
--- a/src/loaders/local-path-loader.js
|
||||
+++ b/src/loaders/local-path-loader.js
|
||||
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
export class LocalPathLoader extends BaseLoader {
|
||||
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
|
||||
path;
|
||||
- constructor({ path }) {
|
||||
- super(`LocalPathLoader_${md5(path)}`, { path });
|
||||
+ constructor({ path, chunkSize, chunkOverlap }) {
|
||||
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
|
||||
this.path = path;
|
||||
}
|
||||
async *getUnfilteredChunks() {
|
||||
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
|
||||
const extension = currentPath.split('.').pop().toLowerCase();
|
||||
if (extension === 'md' || extension === 'mdx')
|
||||
mime = 'text/markdown';
|
||||
+ if (extension === 'txt')
|
||||
+ mime = 'text/plain';
|
||||
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
|
||||
}
|
||||
try {
|
||||
- const loader = await createLoaderFromMimeType(currentPath, mime);
|
||||
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
|
||||
for await (const result of await loader.getUnfilteredChunks()) {
|
||||
yield {
|
||||
pageContent: result.pageContent,
|
||||
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
|
||||
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..14be3b5727cff6eb1978838045e9a788f8f53bfb 100644
|
||||
--- a/src/util/mime.d.ts
|
||||
+++ b/src/util/mime.d.ts
|
||||
@@ -1,2 +1,2 @@
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
|
||||
+export declare function createLoaderFromMimeType(loaderData: string, mimeType: string, chunkSize?: number, chunkOverlap?: number): Promise<BaseLoader>;
|
||||
diff --git a/src/util/mime.js b/src/util/mime.js
|
||||
index b6426a859968e2bf6206795f70333e90ae27aeb7..16ae2adb863f8d7abfa757f1c5cc39f6bb1c44fa 100644
|
||||
--- a/src/util/mime.js
|
||||
+++ b/src/util/mime.js
|
||||
@@ -1,7 +1,9 @@
|
||||
import mime from 'mime';
|
||||
import createDebugMessages from 'debug';
|
||||
import { TextLoader } from '../loaders/text-loader.js';
|
||||
-export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
+import fs from 'node:fs'
|
||||
+
|
||||
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
|
||||
switch (mimeType) {
|
||||
case 'application/msword':
|
||||
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
|
||||
- return new DocxLoader({ filePathOrUrl: loaderData });
|
||||
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.ms-excel':
|
||||
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
||||
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
|
||||
- return new ExcelLoader({ filePathOrUrl: loaderData });
|
||||
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/pdf': {
|
||||
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
|
||||
- return new PdfLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
|
||||
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
|
||||
- return new PptLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/plain': {
|
||||
const fineType = mime.getType(loaderData);
|
||||
@@ -42,24 +44,24 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
- else
|
||||
- return new TextLoader({ text: loaderData });
|
||||
+ const content = fs.readFileSync(loaderData, 'utf-8');
|
||||
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/csv': {
|
||||
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/html': {
|
||||
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
|
||||
- return new WebLoader({ urlOrContent: loaderData });
|
||||
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/xml': {
|
||||
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
|
||||
@@ -67,14 +69,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
|
||||
if (await SitemapLoader.test(loaderData)) {
|
||||
- return new SitemapLoader({ url: loaderData });
|
||||
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
//This is not a Sitemap but is still XML
|
||||
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
|
||||
- return new XmlLoader({ filePathOrUrl: loaderData });
|
||||
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/x-markdown':
|
||||
case 'text/markdown': {
|
||||
@@ -82,7 +84,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
|
||||
- return new MarkdownLoader({ filePathOrUrl: loaderData });
|
||||
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'image/png':
|
||||
case 'image/jpeg': {
|
||||
@@ -1,26 +0,0 @@
|
||||
diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js
|
||||
index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644
|
||||
--- a/dist/cjs/client/stdio.js
|
||||
+++ b/dist/cjs/client/stdio.js
|
||||
@@ -68,7 +68,7 @@ class StdioClientTransport {
|
||||
this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||
- shell: false,
|
||||
+ shell: process.platform === 'win32' ? true : false,
|
||||
signal: this._abortController.signal,
|
||||
windowsHide: node_process_1.default.platform === "win32" && isElectron(),
|
||||
cwd: this._serverParams.cwd,
|
||||
diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js
|
||||
index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644
|
||||
--- a/dist/esm/client/stdio.js
|
||||
+++ b/dist/esm/client/stdio.js
|
||||
@@ -61,7 +61,7 @@ export class StdioClientTransport {
|
||||
this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||
- shell: false,
|
||||
+ shell: process.platform === 'win32' ? true : false,
|
||||
signal: this._abortController.signal,
|
||||
windowsHide: process.platform === "win32" && isElectron(),
|
||||
cwd: this._serverParams.cwd,
|
||||
18
.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch
vendored
Normal file
18
.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
diff --git a/dist/index.node.js b/dist/index.node.js
|
||||
index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644
|
||||
--- a/dist/index.node.js
|
||||
+++ b/dist/index.node.js
|
||||
@@ -1,8 +1,11 @@
|
||||
let crypto;
|
||||
crypto =
|
||||
globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto
|
||||
- globalThis.crypto ?? // Node.js 18+
|
||||
- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL
|
||||
+ globalThis.crypto ?? // Node.js 18+
|
||||
+ (async() => {
|
||||
+ const crypto = await import("node:crypto");
|
||||
+ return crypto.webcrypto;
|
||||
+ })();
|
||||
/**
|
||||
* Creates an array of length `size` of random bytes
|
||||
* @param size
|
||||
23
README.md
23
README.md
@@ -6,6 +6,7 @@
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
@@ -16,6 +17,10 @@ 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
|
||||
|
||||

|
||||
@@ -77,6 +82,14 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
- [ ] Voice input and output (AI call)
|
||||
- [ ] Data backup supports custom backup content
|
||||
|
||||
# 🌈 Theme
|
||||
|
||||
- Theme Gallery: https://cherrycss.com
|
||||
- Aero Theme: https://github.com/hakadao/CherryStudio-Aero
|
||||
- PaperMaterial Theme: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
|
||||
|
||||
Welcome PR for more themes
|
||||
|
||||
# 🖥️ Develop
|
||||
|
||||
Refer to the [development documentation](docs/dev.md)
|
||||
@@ -119,11 +132,7 @@ Thank you for your support and contributions!
|
||||
|
||||
# 🌐 Community
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# 📣 Product Hunt
|
||||
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ Sponsor
|
||||
|
||||
@@ -133,6 +142,10 @@ Thank you for your support and contributions!
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
|
||||
# ✉️ Contact
|
||||
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ Star History
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
# 🍒 Cherry Studio
|
||||
|
||||
@@ -17,6 +18,10 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
|
||||
|
||||
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||
|
||||
# 📖 ガイド
|
||||
|
||||
https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||

|
||||
@@ -78,6 +83,13 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
|
||||
- [ ] 音声入出力(AI コール)
|
||||
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||
|
||||
# 🌈 テーマ
|
||||
|
||||
テーマギャラリー: https://cherrycss.com
|
||||
Aero テーマ: https://github.com/hakadao/CherryStudio-Aero
|
||||
|
||||
より多くのテーマのPRを歓迎します
|
||||
|
||||
# 🖥️ 開発
|
||||
|
||||
参考[開発ドキュメント](dev.md)
|
||||
@@ -117,11 +129,7 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
|
||||
|
||||
# コミュニティ
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# 📣 プロダクトハント
|
||||
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# スポンサー
|
||||
|
||||
@@ -131,6 +139,10 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
|
||||
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ✉️ お問い合わせ
|
||||
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ スター履歴
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||
@@ -16,6 +18,10 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
|
||||
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||
|
||||
# 📖 使用教程
|
||||
|
||||
https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 界面
|
||||
|
||||

|
||||
@@ -77,6 +83,13 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
- [ ] 语音输入输出(AI 通话)
|
||||
- [ ] 数据备份支持自定义备份内容
|
||||
|
||||
# 🌈 主题
|
||||
|
||||
主题库:https://cherrycss.com
|
||||
Aero 主题:https://github.com/hakadao/CherryStudio-Aero
|
||||
|
||||
欢迎 PR 更多主题
|
||||
|
||||
# 🖥️ 开发
|
||||
|
||||
参考[开发文档](dev.md)
|
||||
@@ -117,11 +130,7 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
|
||||
# 🌐 社区
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# 📣 产品猎人
|
||||
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ 赞助
|
||||
|
||||
@@ -131,6 +140,10 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ✉️ 联系我们
|
||||
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ Star 记录
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
@@ -83,8 +83,7 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
知识库设置增加重排模型,提升知识库的准确性
|
||||
自定义服务商增加兼容模式
|
||||
增加 Github Copilot 服务商
|
||||
PlantUML 预览支持放大和缩小
|
||||
联网模式支持增强模式
|
||||
小程序支持多开
|
||||
支持 GPT-4o 图像生成
|
||||
修复 MCP 服务器无法使用问题
|
||||
修复升级导致旧版本数据丢失问题
|
||||
|
||||
@@ -12,16 +12,16 @@ export default defineConfig({
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: [
|
||||
'@llm-tools/embedjs',
|
||||
'@llm-tools/embedjs-openai',
|
||||
'@llm-tools/embedjs-loader-web',
|
||||
'@llm-tools/embedjs-loader-markdown',
|
||||
'@llm-tools/embedjs-loader-msoffice',
|
||||
'@llm-tools/embedjs-loader-xml',
|
||||
'@llm-tools/embedjs-loader-pdf',
|
||||
'@llm-tools/embedjs-loader-sitemap',
|
||||
'@llm-tools/embedjs-libsql',
|
||||
'@llm-tools/embedjs-loader-image',
|
||||
'@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'
|
||||
]
|
||||
|
||||
@@ -53,6 +53,16 @@ export default defineConfig([
|
||||
}
|
||||
],
|
||||
{
|
||||
ignores: ['node_modules/**', 'dist/**', 'out/**', 'local/**', '.gitignore', 'scripts/cloudflare-worker.js']
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'build/**',
|
||||
'dist/**',
|
||||
'out/**',
|
||||
'local/**',
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
47
package.json
47
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.1.10",
|
||||
"version": "1.1.17",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -50,21 +50,21 @@
|
||||
"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",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
|
||||
"@llm-tools/embedjs-libsql": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-csv": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.28-81647ffac6.patch",
|
||||
"@llm-tools/embedjs-loader-msoffice": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-pdf": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-sitemap": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-web": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-xml": "^0.1.28",
|
||||
"@llm-tools/embedjs-openai": "^0.1.28",
|
||||
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
@@ -76,6 +76,7 @@
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"fast-xml-parser": "^5.0.9",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
@@ -90,6 +91,7 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@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",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
@@ -101,7 +103,7 @@
|
||||
"@google/genai": "^0.4.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
@@ -113,8 +115,8 @@
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/pako": "^1.0.2",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
@@ -148,8 +150,9 @@
|
||||
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rc-virtual-list": "^3.18.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
@@ -163,6 +166,7 @@
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^7.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-cjk-friendly": "^1.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
@@ -176,15 +180,12 @@
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"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",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -88,7 +88,7 @@ export const textExts = [
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 源文件
|
||||
'.m', // Objective-C 或 MATLAB 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
@@ -106,7 +106,32 @@ export const textExts = [
|
||||
'.ixx', // C++20 模块实现文件
|
||||
'.f90', // Fortran 90 源文件
|
||||
'.f', // Fortran 固定格式源代码文件
|
||||
'.f03' // Fortran 2003+ 源代码文件
|
||||
'.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
|
||||
]
|
||||
|
||||
export const ZOOM_SHORTCUTS = [
|
||||
|
||||
1
packages/shared/config/nutstore.ts
Normal file
1
packages/shared/config/nutstore.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const NUTSTORE_HOST = 'https://dav.jianguoyun.com/dav'
|
||||
@@ -1,8 +1,5 @@
|
||||
const { ProxyAgent } = require('undici')
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||
const https = require('https')
|
||||
const fs = require('fs')
|
||||
const { pipeline } = require('stream/promises')
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL with redirect handling
|
||||
@@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises')
|
||||
* @returns {Promise<void>} Promise that resolves when download is complete
|
||||
*/
|
||||
async function downloadWithRedirects(url, destinationPath) {
|
||||
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
||||
if (proxyUrl.startsWith('socks')) {
|
||||
const proxyAgent = new SocksProxyAgent(proxyUrl)
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (url) => {
|
||||
https
|
||||
.get(url, { agent: proxyAgent }, (response) => {
|
||||
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||
request(response.headers.location)
|
||||
return
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
response.pipe(file)
|
||||
file.on('finish', () => resolve())
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
request(url)
|
||||
})
|
||||
} else {
|
||||
const proxyAgent = new ProxyAgent(proxyUrl)
|
||||
const response = await fetch(url, {
|
||||
dispatcher: proxyAgent
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (url) => {
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||
request(response.headers.location)
|
||||
return
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
response.pipe(file)
|
||||
file.on('finish', () => resolve())
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
await pipeline(response.body, file)
|
||||
}
|
||||
request(url)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { downloadWithRedirects }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import EmbeddingsFactory from './EmbeddingsFactory'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@llm-tools/embedjs-openai/src/azure-openai-embeddings'
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
|
||||
import { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
|
||||
export default class VoyageEmbeddings extends BaseEmbeddings {
|
||||
private model: _VoyageEmbeddings
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
@@ -47,7 +48,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
replaceDevtoolsFont(mainWindow)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
@@ -56,9 +57,30 @@ if (!app.requestSingleInstanceLock()) {
|
||||
})
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
|
||||
// macOS specific: handle protocol when app is already running
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
|
||||
// macOS specific: handle protocol when app is already running
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
windowService.showMainWindow()
|
||||
|
||||
// Protocol handler for Windows/Linux
|
||||
// The commandLine is an array of strings where the last item might be the URL
|
||||
const url = argv.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
|
||||
if (url) handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
|
||||
8
src/main/integration/nutstore/sso/lib/index.d.ts
vendored
Normal file
8
src/main/integration/nutstore/sso/lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare function decrypt(app: string, s: string): string;
|
||||
|
||||
interface Secret {
|
||||
app: string;
|
||||
}
|
||||
declare function createOAuthUrl(secret: Secret): string;
|
||||
|
||||
export { type Secret, createOAuthUrl, decrypt };
|
||||
9
src/main/integration/nutstore/sso/lib/index.js
Normal file
9
src/main/integration/nutstore/sso/lib/index.js
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
||||
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { MCPServer, Shortcut, ThemeMode } from '@types'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@@ -16,7 +16,9 @@ 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 mcpService from './services/MCPService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
@@ -29,7 +31,7 @@ import { compress, decompress } from './utils/zip'
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
const exportService = new ExportService(fileManager)
|
||||
const mcpService = new MCPService()
|
||||
const obsidianVaultService = new ObsidianVaultService()
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater(mainWindow)
|
||||
@@ -164,6 +166,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
|
||||
ipcMain.handle('backup:checkConnection', backupManager.checkConnection)
|
||||
ipcMain.handle('backup:createDirectory', backupManager.createDirectory)
|
||||
|
||||
// file
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
@@ -251,6 +255,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||
ipcMain.handle('miniwindow:set-pin', (_, isPinned) => windowService.setPinMiniWindow(isPinned))
|
||||
|
||||
// aes
|
||||
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
||||
@@ -259,36 +264,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
)
|
||||
|
||||
// Register MCP handlers
|
||||
ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers))
|
||||
ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices())
|
||||
ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server))
|
||||
ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server))
|
||||
ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName))
|
||||
ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) =>
|
||||
mcpService.setServerActive({ name, isActive })
|
||||
)
|
||||
|
||||
// According to preload, this should take no parameters, but our implementation accepts
|
||||
// an optional serverName for better flexibility
|
||||
ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName))
|
||||
ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) =>
|
||||
mcpService.callTool(params)
|
||||
)
|
||||
|
||||
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
||||
ipcMain.handle('mcp:remove-server', mcpService.removeServer)
|
||||
ipcMain.handle('mcp:restart-server', mcpService.restartServer)
|
||||
ipcMain.handle('mcp:stop-server', mcpService.stopServer)
|
||||
ipcMain.handle('mcp:list-tools', mcpService.listTools)
|
||||
ipcMain.handle('mcp:call-tool', mcpService.callTool)
|
||||
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo)
|
||||
|
||||
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
|
||||
|
||||
// Listen for changes in MCP servers and notify renderer
|
||||
mcpService.on('servers-updated', (servers) => {
|
||||
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => mcpService.cleanup())
|
||||
|
||||
//copilot
|
||||
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
|
||||
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
|
||||
@@ -296,4 +283,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||
|
||||
// Obsidian service
|
||||
ipcMain.handle('obsidian:get-vaults', () => {
|
||||
return obsidianVaultService.getVaults()
|
||||
})
|
||||
|
||||
ipcMain.handle('obsidian:get-files', (_event, vaultName) => {
|
||||
return obsidianVaultService.getFilesByVaultName(vaultName)
|
||||
})
|
||||
|
||||
// nutstore
|
||||
ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl)
|
||||
ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token))
|
||||
ipcMain.handle('nutstore:get-directory-contents', (_, token: string, path: string) =>
|
||||
NutstoreService.getDirectoryContents(token, path)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { JsonLoader } from '@llm-tools/embedjs'
|
||||
import { JsonLoader } from '@cherrystudio/embedjs'
|
||||
|
||||
/**
|
||||
* Drafts 应用导出的笔记文件加载器
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
|
||||
import { cleanString } from '@cherrystudio/embedjs-utils'
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||
import { getTempDir } from '@main/utils/file'
|
||||
import Logger from 'electron-log'
|
||||
import EPub from 'epub'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@cherrystudio/embedjs'
|
||||
import type { AddLoaderReturn } from '@cherrystudio/embedjs-interfaces'
|
||||
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
|
||||
import { LoaderReturn } from '@shared/config/types'
|
||||
import { FileType, KnowledgeBaseParams } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
|
||||
import { cleanString } from '@cherrystudio/embedjs-utils'
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
|
||||
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
|
||||
import { cleanString } from '@llm-tools/embedjs-utils'
|
||||
import md5 from 'md5'
|
||||
import { OfficeParserConfig, parseOfficeAsync } from 'officeparser'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
export default abstract class BaseReranker {
|
||||
@@ -17,4 +17,15 @@ export default abstract class BaseReranker {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
public formatErrorMessage(url: string, error: any, requestBody: any) {
|
||||
const errorDetails = {
|
||||
url: url,
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
requestBody: requestBody
|
||||
}
|
||||
return JSON.stringify(errorDetails, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import BaseReranker from './BaseReranker'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -47,8 +47,10 @@ export default class JinaReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Jina Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('Jina Reranker API Error:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import BaseReranker from './BaseReranker'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -49,8 +49,10 @@ export default class SiliconFlowReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('SiliconFlow Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('SiliconFlow Reranker API 错误:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
import axios from 'axios'
|
||||
|
||||
@@ -53,8 +53,10 @@ export default class VoyageReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Voyage Reranker API 错误:', error.message || error)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('Voyage Reranker API Error:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 353 KiB |
@@ -5,7 +5,7 @@ import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import { createClient, FileStat } from 'webdav'
|
||||
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
import { windowService } from './WindowService'
|
||||
@@ -15,6 +15,7 @@ class BackupManager {
|
||||
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
|
||||
|
||||
constructor() {
|
||||
this.checkConnection = this.checkConnection.bind(this)
|
||||
this.backup = this.backup.bind(this)
|
||||
this.restore = this.restore.bind(this)
|
||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||
@@ -86,9 +87,16 @@ class BackupManager {
|
||||
await fs.ensureDir(this.tempDir)
|
||||
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||
|
||||
// 将 data 写入临时文件
|
||||
// 使用流的方式写入 data.json
|
||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||
await fs.writeFile(tempDataPath, data)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(tempDataPath)
|
||||
writeStream.write(data)
|
||||
writeStream.end()
|
||||
|
||||
writeStream.on('finish', () => resolve())
|
||||
writeStream.on('error', (error) => reject(error))
|
||||
})
|
||||
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
|
||||
|
||||
// 复制 Data 目录到临时目录
|
||||
@@ -207,8 +215,15 @@ class BackupManager {
|
||||
fs.mkdirSync(this.backupDir, { recursive: true })
|
||||
}
|
||||
|
||||
// sync为同步写,无须await
|
||||
fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||
// 使用流的方式写入文件
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(backupedFilePath)
|
||||
writeStream.write(retrievedFile as Buffer)
|
||||
writeStream.end()
|
||||
|
||||
writeStream.on('finish', () => resolve())
|
||||
writeStream.on('error', (error) => reject(error))
|
||||
})
|
||||
|
||||
return await this.restore(_, backupedFilePath)
|
||||
} catch (error: any) {
|
||||
@@ -278,6 +293,21 @@ class BackupManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
return await webdavClient.checkConnection()
|
||||
}
|
||||
|
||||
async createDirectory(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
webdavConfig: WebDavConfig,
|
||||
path: string,
|
||||
options?: CreateDirectoryOptions
|
||||
) {
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
return await webdavClient.createDirectory(path, options)
|
||||
}
|
||||
}
|
||||
|
||||
export default BackupManager
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
import * as fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
|
||||
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@cherrystudio/embedjs'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { LibSqlDb } from '@cherrystudio/embedjs-libsql'
|
||||
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
|
||||
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
|
||||
import Embeddings from '@main/embeddings/Embeddings'
|
||||
import { addFileLoader } from '@main/loader'
|
||||
import Reranker from '@main/reranker/Reranker'
|
||||
@@ -475,6 +475,9 @@ class KnowledgeService {
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }
|
||||
): Promise<ExtractChunkData[]> => {
|
||||
if (results.length === 0) {
|
||||
return results
|
||||
}
|
||||
return await new Reranker(base).rerank(search, results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,559 +1,228 @@
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { isLinux, isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath } from '@main/utils/process'
|
||||
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { makeSureDirExists } from '@main/utils'
|
||||
import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { MCPServer, MCPTool } from '@types'
|
||||
import log from 'electron-log'
|
||||
import { EventEmitter } from 'events'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
/**
|
||||
* Service for managing Model Context Protocol servers and tools
|
||||
*/
|
||||
export default class MCPService extends EventEmitter {
|
||||
private servers: MCPServer[] = []
|
||||
private activeServers: Map<string, any> = new Map()
|
||||
private clients: { [key: string]: any } = {}
|
||||
private Client: typeof Client | undefined
|
||||
private stdioTransport: typeof StdioClientTransport | undefined
|
||||
private sseTransport: typeof SSEClientTransport | undefined
|
||||
private initialized = false
|
||||
private initPromise: Promise<void> | null = null
|
||||
class McpService {
|
||||
private clients: Map<string, Client> = new Map()
|
||||
|
||||
// Simplified server loading state management
|
||||
private readyState = {
|
||||
serversLoaded: false,
|
||||
promise: null as Promise<void> | null,
|
||||
resolve: null as ((value: void) => void) | null
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.createServerLoadingPromise()
|
||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a promise that resolves when servers are loaded
|
||||
*/
|
||||
private createServerLoadingPromise(): void {
|
||||
this.readyState.promise = new Promise<void>((resolve) => {
|
||||
this.readyState.resolve = resolve
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set servers received from Redux and trigger initialization if needed
|
||||
*/
|
||||
public setServers(servers: MCPServer[]): void {
|
||||
this.servers = servers
|
||||
log.info(`[MCP] Received ${servers.length} servers from Redux`)
|
||||
|
||||
// Mark servers as loaded and resolve the waiting promise
|
||||
if (!this.readyState.serversLoaded && this.readyState.resolve) {
|
||||
this.readyState.serversLoaded = true
|
||||
this.readyState.resolve()
|
||||
this.readyState.resolve = null
|
||||
}
|
||||
|
||||
// Initialize if not already initialized
|
||||
if (!this.initialized) {
|
||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||
}
|
||||
constructor() {
|
||||
this.initClient = this.initClient.bind(this)
|
||||
this.listTools = this.listTools.bind(this)
|
||||
this.callTool = this.callTool.bind(this)
|
||||
this.closeClient = this.closeClient.bind(this)
|
||||
this.removeServer = this.removeServer.bind(this)
|
||||
this.restartServer = this.restartServer.bind(this)
|
||||
this.stopServer = this.stopServer.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the MCP service if not already initialized
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
// If already initialized, return immediately
|
||||
if (this.initialized) return
|
||||
async initClient(server: MCPServer): Promise<Client> {
|
||||
const serverKey = this.getServerKey(server)
|
||||
|
||||
// If initialization is in progress, return that promise
|
||||
if (this.initPromise) return this.initPromise
|
||||
|
||||
this.initPromise = (async () => {
|
||||
try {
|
||||
log.info('[MCP] Starting initialization')
|
||||
|
||||
// Wait for servers to be loaded from Redux
|
||||
await this.waitForServers()
|
||||
|
||||
// Load SDK components in parallel for better performance
|
||||
const [Client, StdioTransport, SSETransport] = await Promise.all([
|
||||
this.importClient(),
|
||||
this.importStdioClientTransport(),
|
||||
this.importSSEClientTransport()
|
||||
])
|
||||
|
||||
this.Client = Client
|
||||
this.stdioTransport = StdioTransport
|
||||
this.sseTransport = SSETransport
|
||||
|
||||
// Mark as initialized before loading servers
|
||||
this.initialized = true
|
||||
|
||||
// Load active servers
|
||||
await this.loadActiveServers()
|
||||
log.info('[MCP] Initialization successfully')
|
||||
|
||||
return
|
||||
} catch (err) {
|
||||
this.initialized = false // Reset flag on error
|
||||
log.error('[MCP] Failed to initialize:', err)
|
||||
throw err
|
||||
} finally {
|
||||
this.initPromise = null
|
||||
// Check if we already have a client for this server configuration
|
||||
const existingClient = this.clients.get(serverKey)
|
||||
if (existingClient) {
|
||||
// Check if the existing client is still connected
|
||||
const pingResult = await existingClient.ping()
|
||||
Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult)
|
||||
// If the ping fails, remove the client from the cache
|
||||
// and create a new one
|
||||
if (!pingResult) {
|
||||
this.clients.delete(serverKey)
|
||||
} else {
|
||||
return existingClient
|
||||
}
|
||||
})()
|
||||
|
||||
return this.initPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for servers to be loaded from Redux
|
||||
*/
|
||||
private async waitForServers(): Promise<void> {
|
||||
if (!this.readyState.serversLoaded && this.readyState.promise) {
|
||||
log.info('[MCP] Waiting for servers data from Redux...')
|
||||
await this.readyState.promise
|
||||
log.info('[MCP] Servers received, continuing initialization')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create consistent error logging functions
|
||||
*/
|
||||
private logError(message: string, err?: any): void {
|
||||
log.error(`[MCP] ${message}`, err)
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the MCP client SDK
|
||||
*/
|
||||
private async importClient() {
|
||||
try {
|
||||
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
|
||||
return Client
|
||||
} catch (err) {
|
||||
this.logError('Failed to import Client:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the stdio transport
|
||||
*/
|
||||
private async importStdioClientTransport() {
|
||||
try {
|
||||
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
|
||||
return StdioClientTransport
|
||||
} catch (err) {
|
||||
log.error('[MCP] Failed to import StdioTransport:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the SSE transport
|
||||
*/
|
||||
private async importSSEClientTransport() {
|
||||
try {
|
||||
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')
|
||||
return SSEClientTransport
|
||||
} catch (err) {
|
||||
log.error('[MCP] Failed to import SSETransport:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available MCP servers
|
||||
*/
|
||||
public async listAvailableServices(): Promise<MCPServer[]> {
|
||||
await this.ensureInitialized()
|
||||
return this.servers
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the service is initialized before operations
|
||||
*/
|
||||
private async ensureInitialized() {
|
||||
if (!this.initialized) {
|
||||
log.debug('[MCP] Ensuring initialization')
|
||||
await this.init()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new MCP server
|
||||
*/
|
||||
public async addServer(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Check for duplicate name
|
||||
if (this.servers.some((s) => s.name === server.name)) {
|
||||
throw new Error(`Server with name ${server.name} already exists`)
|
||||
}
|
||||
|
||||
// Activate if needed
|
||||
if (server.isActive) {
|
||||
await this.activate(server)
|
||||
}
|
||||
// Create new client instance for each connection
|
||||
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
|
||||
|
||||
// Add to servers list
|
||||
this.servers = [...this.servers, server]
|
||||
this.notifyReduxServersChanged(this.servers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing MCP server
|
||||
*/
|
||||
public async updateServer(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const index = this.servers.findIndex((s) => s.name === server.name)
|
||||
if (index === -1) {
|
||||
throw new Error(`Server ${server.name} not found`)
|
||||
}
|
||||
|
||||
// Check activation status change
|
||||
const wasActive = this.servers[index].isActive
|
||||
if (wasActive && !server.isActive) {
|
||||
await this.deactivate(server.name)
|
||||
} else if (!wasActive && server.isActive) {
|
||||
await this.activate(server)
|
||||
} else {
|
||||
await this.restartServer(server)
|
||||
}
|
||||
|
||||
// Update servers list
|
||||
const updatedServers = [...this.servers]
|
||||
updatedServers[index] = server
|
||||
this.servers = updatedServers
|
||||
|
||||
// Notify Redux
|
||||
this.notifyReduxServersChanged(updatedServers)
|
||||
}
|
||||
|
||||
public async restartServer(_server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const server = this.servers.find((s) => s.name === _server.name)
|
||||
|
||||
if (server) {
|
||||
if (server.isActive) {
|
||||
await this.deactivate(server.name)
|
||||
}
|
||||
await this.activate(server)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete an MCP server
|
||||
*/
|
||||
public async deleteServer(serverName: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Deactivate if running
|
||||
if (this.clients[serverName]) {
|
||||
await this.deactivate(serverName)
|
||||
}
|
||||
|
||||
// Update servers list
|
||||
const filteredServers = this.servers.filter((s) => s.name !== serverName)
|
||||
this.servers = filteredServers
|
||||
this.notifyReduxServersChanged(filteredServers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a server's active state
|
||||
*/
|
||||
public async setServerActive(params: { name: string; isActive: boolean }): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { name, isActive } = params
|
||||
const server = this.servers.find((s) => s.name === name)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Server ${name} not found`)
|
||||
}
|
||||
|
||||
// Activate or deactivate as needed
|
||||
if (isActive) {
|
||||
await this.activate(server)
|
||||
} else {
|
||||
await this.deactivate(name)
|
||||
}
|
||||
|
||||
// Update server status
|
||||
server.isActive = isActive
|
||||
this.notifyReduxServersChanged([...this.servers])
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify Redux in the renderer process about server changes
|
||||
*/
|
||||
private notifyReduxServersChanged(servers: MCPServer[]): void {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('mcp:servers-changed', servers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate an MCP server
|
||||
*/
|
||||
public async activate(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { name, baseUrl, command, env } = server
|
||||
const args = [...(server.args || [])]
|
||||
|
||||
// Skip if already running
|
||||
if (this.clients[name]) {
|
||||
log.info(`[MCP] Server ${name} is already running`)
|
||||
return
|
||||
}
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
try {
|
||||
// Create appropriate transport based on configuration
|
||||
if (baseUrl) {
|
||||
transport = new this.sseTransport!(new URL(baseUrl))
|
||||
} else if (command) {
|
||||
let cmd: string = command
|
||||
if (command === 'npx') {
|
||||
if (server.baseUrl) {
|
||||
transport = new SSEClientTransport(new URL(server.baseUrl))
|
||||
} else if (server.command) {
|
||||
let cmd = server.command
|
||||
|
||||
if (server.command === 'npx' || server.command === 'bun' || server.command === 'bunx') {
|
||||
cmd = await getBinaryPath('bun')
|
||||
|
||||
if (cmd === 'bun') {
|
||||
cmd = 'npx'
|
||||
}
|
||||
|
||||
log.info(`[MCP] Using command: ${cmd}`)
|
||||
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')
|
||||
!args.includes('-y') && args.unshift('-y')
|
||||
}
|
||||
if (cmd.includes('bun') && !args.includes('x')) {
|
||||
if (!args.includes('x')) {
|
||||
args.unshift('x')
|
||||
}
|
||||
}
|
||||
} else if (command === 'uvx') {
|
||||
cmd = await getBinaryPath('uvx')
|
||||
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 === 'mcp-auto-install') {
|
||||
const binPath = await getBinaryPath()
|
||||
makeSureDirExists(binPath)
|
||||
server.env.MCP_REGISTRY_PATH = path.join(binPath, '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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
|
||||
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
|
||||
// Logger.info(`[MCP] Environment variables for server:`, server.env)
|
||||
|
||||
transport = new this.stdioTransport!({
|
||||
transport = new StdioClientTransport({
|
||||
command: cmd,
|
||||
args,
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
...getDefaultEnvironment(),
|
||||
PATH: this.getEnhancedPath(process.env.PATH || ''),
|
||||
...env
|
||||
...server.env
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Either baseUrl or command must be provided')
|
||||
}
|
||||
|
||||
// Create and connect client
|
||||
const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} })
|
||||
|
||||
await client.connect(transport)
|
||||
|
||||
// Store client and server info
|
||||
this.clients[name] = client
|
||||
this.activeServers.set(name, { client, server })
|
||||
// Store the new client in the cache
|
||||
this.clients.set(serverKey, client)
|
||||
|
||||
log.info(`[MCP] Activated server: ${server.name}`)
|
||||
this.emit('server-started', { name })
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error activating server ${name}:`, error)
|
||||
this.setServerActive({ name, isActive: false })
|
||||
Logger.info(`[MCP] Activated server: ${server.name}`)
|
||||
return client
|
||||
} catch (error: any) {
|
||||
Logger.error(`[MCP] Error activating server ${server.name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate an MCP server
|
||||
*/
|
||||
public async deactivate(name: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
if (!this.clients[name]) {
|
||||
log.warn(`[MCP] Server ${name} is not running`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(`[MCP] Stopping server: ${name}`)
|
||||
await this.clients[name].close()
|
||||
delete this.clients[name]
|
||||
this.activeServers.delete(name)
|
||||
this.emit('server-stopped', { name })
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error deactivating server ${name}:`, error)
|
||||
throw error
|
||||
async closeClient(serverKey: string) {
|
||||
const client = this.clients.get(serverKey)
|
||||
if (client) {
|
||||
// Remove the client from the cache
|
||||
await client.close()
|
||||
Logger.info(`[MCP] Closed server: ${serverKey}`)
|
||||
this.clients.delete(serverKey)
|
||||
CacheService.remove(`mcp:list_tool:${serverKey}`)
|
||||
Logger.info(`[MCP] Cleared cache for server: ${serverKey}`)
|
||||
} else {
|
||||
Logger.warn(`[MCP] No client found for server: ${serverKey}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tools from active MCP servers
|
||||
*/
|
||||
public async listTools(serverName?: string): Promise<MCPTool[]> {
|
||||
await this.ensureInitialized()
|
||||
log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`)
|
||||
async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const serverKey = this.getServerKey(server)
|
||||
Logger.info(`[MCP] Stopping server: ${server.name}`)
|
||||
await this.closeClient(serverKey)
|
||||
}
|
||||
|
||||
try {
|
||||
// If server name provided, list tools for that server only
|
||||
if (serverName) {
|
||||
return await this.listToolsFromServer(serverName)
|
||||
}
|
||||
|
||||
// Otherwise list tools from all active servers
|
||||
let allTools: MCPTool[] = []
|
||||
|
||||
for (const clientName in this.clients) {
|
||||
log.info(`[MCP] Listing tools from ${clientName}`)
|
||||
try {
|
||||
const tools = await this.listToolsFromServer(clientName)
|
||||
allTools = allTools.concat(tools)
|
||||
} catch (error) {
|
||||
this.logError(`Error listing tools for ${clientName}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[MCP] Total tools listed: ${allTools.length}`)
|
||||
return allTools
|
||||
} catch (error) {
|
||||
this.logError('Error listing tools:', error)
|
||||
return []
|
||||
async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const serverKey = this.getServerKey(server)
|
||||
const existingClient = this.clients.get(serverKey)
|
||||
if (existingClient) {
|
||||
await this.closeClient(serverKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to list tools from a specific server
|
||||
*/
|
||||
private async listToolsFromServer(serverName: string): Promise<MCPTool[]> {
|
||||
log.info(`[MCP] start list tools from ${serverName}:`)
|
||||
if (!this.clients[serverName]) {
|
||||
throw new Error(`MCP Client ${serverName} not found`)
|
||||
}
|
||||
const cacheKey = `mcp:list_tool:${serverName}`
|
||||
async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
Logger.info(`[MCP] Restarting server: ${server.name}`)
|
||||
const serverKey = this.getServerKey(server)
|
||||
await this.closeClient(serverKey)
|
||||
await this.initClient(server)
|
||||
}
|
||||
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const client = await this.initClient(server)
|
||||
const serverKey = this.getServerKey(server)
|
||||
const cacheKey = `mcp:list_tool:${serverKey}`
|
||||
if (CacheService.has(cacheKey)) {
|
||||
log.info(`[MCP] Tools from ${serverName} loaded from cache`)
|
||||
// Check if cache is still valid
|
||||
Logger.info(`[MCP] Tools from ${server.name} loaded from cache`)
|
||||
const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
|
||||
if (cachedTools && cachedTools.length > 0) {
|
||||
return cachedTools
|
||||
}
|
||||
CacheService.remove(cacheKey)
|
||||
}
|
||||
|
||||
const { tools } = await this.clients[serverName].listTools()
|
||||
|
||||
const transformedTools = tools.map((tool: any) => ({
|
||||
...tool,
|
||||
serverName,
|
||||
id: 'f' + uuidv4().replace(/-/g, '')
|
||||
}))
|
||||
|
||||
// Cache the tools for 5 minutes
|
||||
if (transformedTools.length > 0) {
|
||||
CacheService.set(cacheKey, transformedTools, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
log.info(`[MCP] Tools from ${serverName}:`, transformedTools)
|
||||
return transformedTools
|
||||
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
||||
const { tools } = await client.listTools()
|
||||
const serverTools: MCPTool[] = []
|
||||
tools.map((tool: any) => {
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: `f${nanoid()}`,
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}
|
||||
serverTools.push(serverTool)
|
||||
})
|
||||
CacheService.set(cacheKey, serverTools, 5 * 60 * 1000)
|
||||
return serverTools
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on an MCP server
|
||||
*/
|
||||
public async callTool(params: { client: string; name: string; args: any }): Promise<any> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { client, name, args } = params
|
||||
|
||||
if (!this.clients[client]) {
|
||||
throw new Error(`MCP Client ${client} not found`)
|
||||
}
|
||||
|
||||
log.info('[MCP] Calling:', client, name, args)
|
||||
|
||||
public async callTool(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args }: { server: MCPServer; name: string; args: any }
|
||||
): Promise<any> {
|
||||
try {
|
||||
return await this.clients[client].callTool({
|
||||
name,
|
||||
arguments: args
|
||||
})
|
||||
Logger.info('[MCP] Calling:', server.name, name, args)
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args })
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error calling tool ${name} on ${client}:`, error)
|
||||
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all MCP resources
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
const clientNames = Object.keys(this.clients)
|
||||
|
||||
if (clientNames.length === 0) {
|
||||
log.info('[MCP] No active servers to clean up')
|
||||
return
|
||||
}
|
||||
|
||||
log.info(`[MCP] Cleaning up ${clientNames.length} active servers`)
|
||||
|
||||
// Deactivate all clients
|
||||
await Promise.allSettled(
|
||||
clientNames.map((name) =>
|
||||
this.deactivate(name).catch((err) => {
|
||||
log.error(`[MCP] Error during cleanup of ${name}:`, err)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
this.clients = {}
|
||||
this.activeServers.clear()
|
||||
log.info('[MCP] All servers cleaned up')
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all active servers
|
||||
*/
|
||||
private async loadActiveServers(): Promise<void> {
|
||||
const activeServers = this.servers.filter((server) => server.isActive)
|
||||
|
||||
if (activeServers.length === 0) {
|
||||
log.info('[MCP] No active servers to load')
|
||||
return
|
||||
}
|
||||
|
||||
log.info(`[MCP] Start loading ${activeServers.length} active servers`)
|
||||
|
||||
// Activate servers in parallel for better performance
|
||||
await Promise.allSettled(
|
||||
activeServers.map(async (server) => {
|
||||
try {
|
||||
await this.activate(server)
|
||||
} catch (error) {
|
||||
this.logError(`Failed to activate server ${server.name}`, error)
|
||||
this.emit('server-error', { name: server.name, error })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
log.info(`[MCP] End loading ${Object.keys(this.clients).length} active servers`)
|
||||
public async getInstallInfo() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const uvName = await getBinaryName('uv')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const uvPath = path.join(dir, uvName)
|
||||
const bunPath = path.join(dir, bunName)
|
||||
return { dir, uvPath, bunPath }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,6 +250,7 @@ export default class MCPService extends EventEmitter {
|
||||
`${homeDir}/.npm-global/bin`,
|
||||
`${homeDir}/.yarn/bin`,
|
||||
`${homeDir}/.cargo/bin`,
|
||||
`${homeDir}/.cherrystudio/bin`,
|
||||
'/opt/local/bin'
|
||||
)
|
||||
}
|
||||
@@ -594,12 +264,18 @@ export default class MCPService extends EventEmitter {
|
||||
`${homeDir}/.npm-global/bin`,
|
||||
`${homeDir}/.yarn/bin`,
|
||||
`${homeDir}/.cargo/bin`,
|
||||
`${homeDir}/.cherrystudio/bin`,
|
||||
'/snap/bin'
|
||||
)
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
newPaths.push(`${process.env.APPDATA}\\npm`, `${homeDir}\\AppData\\Local\\Yarn\\bin`, `${homeDir}\\.cargo\\bin`)
|
||||
newPaths.push(
|
||||
`${process.env.APPDATA}\\npm`,
|
||||
`${homeDir}\\AppData\\Local\\Yarn\\bin`,
|
||||
`${homeDir}\\.cargo\\bin`,
|
||||
`${homeDir}\\.cherrystudio\\bin`
|
||||
)
|
||||
}
|
||||
|
||||
// 只添加不存在的路径
|
||||
@@ -613,3 +289,5 @@ export default class MCPService extends EventEmitter {
|
||||
return Array.from(existingPaths).join(pathSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
export default new McpService()
|
||||
|
||||
134
src/main/services/NutstoreService.ts
Normal file
134
src/main/services/NutstoreService.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { NUTSTORE_HOST } from '@shared/config/nutstore'
|
||||
import { XMLParser } from 'fast-xml-parser'
|
||||
import { isNil, partial } from 'lodash'
|
||||
import { type FileStat } from 'webdav'
|
||||
|
||||
interface OAuthResponse {
|
||||
username: string
|
||||
userid: string
|
||||
access_token: string
|
||||
}
|
||||
|
||||
interface WebDAVResponse {
|
||||
multistatus: {
|
||||
response: Array<{
|
||||
href: string
|
||||
propstat: {
|
||||
prop: {
|
||||
displayname: string
|
||||
resourcetype: { collection?: any }
|
||||
getlastmodified?: string
|
||||
getcontentlength?: string
|
||||
getcontenttype?: string
|
||||
}
|
||||
status: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNutstoreSSOUrl() {
|
||||
const { createOAuthUrl } = await import('../integration/nutstore/sso/lib')
|
||||
|
||||
const url = createOAuthUrl({
|
||||
app: 'cherrystudio'
|
||||
})
|
||||
return url
|
||||
}
|
||||
|
||||
export async function decryptToken(token: string) {
|
||||
const { decrypt } = await import('../integration/nutstore/sso/lib')
|
||||
try {
|
||||
const decrypted = decrypt('cherrystudio', token)
|
||||
return JSON.parse(decrypted) as OAuthResponse
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDirectoryContents(token: string, target: string): Promise<FileStat[]> {
|
||||
const contents: FileStat[] = []
|
||||
if (!target.startsWith('/')) {
|
||||
target = '/' + target
|
||||
}
|
||||
|
||||
let currentUrl = `${NUTSTORE_HOST}${target}`
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(currentUrl, {
|
||||
method: 'PROPFIND',
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/xml',
|
||||
Depth: '1'
|
||||
},
|
||||
body: `<?xml version="1.0" encoding="utf-8"?>
|
||||
<propfind xmlns="DAV:">
|
||||
<prop>
|
||||
<displayname/>
|
||||
<resourcetype/>
|
||||
<getlastmodified/>
|
||||
<getcontentlength/>
|
||||
<getcontenttype/>
|
||||
</prop>
|
||||
</propfind>`
|
||||
})
|
||||
|
||||
const text = await response.text()
|
||||
|
||||
const result = parseXml<WebDAVResponse>(text)
|
||||
const items = Array.isArray(result.multistatus.response)
|
||||
? result.multistatus.response
|
||||
: [result.multistatus.response]
|
||||
|
||||
// 跳过第一个条目(当前目录)
|
||||
contents.push(...items.slice(1).map(partial(convertToFileStat, '/dav')))
|
||||
|
||||
const linkHeader = response.headers['link'] || response.headers['Link']
|
||||
if (!linkHeader) {
|
||||
break
|
||||
}
|
||||
|
||||
const nextLink = extractNextLink(linkHeader)
|
||||
if (!nextLink) {
|
||||
break
|
||||
}
|
||||
|
||||
currentUrl = decodeURI(nextLink)
|
||||
}
|
||||
|
||||
return contents
|
||||
}
|
||||
|
||||
function extractNextLink(linkHeader: string): string | null {
|
||||
const matches = linkHeader.match(/<([^>]+)>;\s*rel="next"/)
|
||||
return matches ? matches[1] : null
|
||||
}
|
||||
|
||||
function convertToFileStat(serverBase: string, item: WebDAVResponse['multistatus']['response'][number]): FileStat {
|
||||
const props = item.propstat.prop
|
||||
const isDir = !isNil(props.resourcetype?.collection)
|
||||
const href = decodeURIComponent(item.href)
|
||||
const filename = serverBase === '/' ? href : path.posix.join('/', href.replace(serverBase, ''))
|
||||
|
||||
return {
|
||||
filename: filename.endsWith('/') ? filename.slice(0, -1) : filename,
|
||||
basename: path.basename(filename),
|
||||
lastmod: props.getlastmodified || '',
|
||||
size: props.getcontentlength ? parseInt(props.getcontentlength, 10) : 0,
|
||||
type: isDir ? 'directory' : 'file',
|
||||
etag: null,
|
||||
mime: props.getcontenttype
|
||||
}
|
||||
}
|
||||
|
||||
function parseXml<T>(xml: string) {
|
||||
const parser = new XMLParser({
|
||||
attributeNamePrefix: '',
|
||||
removeNSPrefix: true
|
||||
})
|
||||
return parser.parse(xml) as T
|
||||
}
|
||||
167
src/main/services/ObsidianVaultService.ts
Normal file
167
src/main/services/ObsidianVaultService.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
interface VaultInfo {
|
||||
path: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
class ObsidianVaultService {
|
||||
private obsidianConfigPath: string
|
||||
|
||||
constructor() {
|
||||
// 根据操作系统获取Obsidian配置文件路径
|
||||
if (process.platform === 'win32') {
|
||||
this.obsidianConfigPath = path.join(app.getPath('appData'), 'obsidian', 'obsidian.json')
|
||||
} else if (process.platform === 'darwin') {
|
||||
this.obsidianConfigPath = path.join(
|
||||
app.getPath('home'),
|
||||
'Library',
|
||||
'Application Support',
|
||||
'obsidian',
|
||||
'obsidian.json'
|
||||
)
|
||||
} else {
|
||||
// Linux
|
||||
this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的Obsidian Vault
|
||||
*/
|
||||
getVaults(): VaultInfo[] {
|
||||
try {
|
||||
if (!fs.existsSync(this.obsidianConfigPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(this.obsidianConfigPath, 'utf8')
|
||||
const config = JSON.parse(configContent)
|
||||
|
||||
if (!config.vaults) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(config.vaults).map(([, vault]: [string, any]) => ({
|
||||
path: vault.path,
|
||||
name: vault.name || path.basename(vault.path)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian Vault失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Vault中的文件夹和Markdown文件结构
|
||||
*/
|
||||
getVaultStructure(vaultPath: string): FileInfo[] {
|
||||
const results: FileInfo[] = []
|
||||
|
||||
try {
|
||||
// 检查vault路径是否存在
|
||||
if (!fs.existsSync(vaultPath)) {
|
||||
console.error('Vault路径不存在:', vaultPath)
|
||||
return []
|
||||
}
|
||||
|
||||
// 检查是否是目录
|
||||
const stats = fs.statSync(vaultPath)
|
||||
if (!stats.isDirectory()) {
|
||||
console.error('Vault路径不是一个目录:', vaultPath)
|
||||
return []
|
||||
}
|
||||
|
||||
this.traverseDirectory(vaultPath, '', results)
|
||||
} catch (error) {
|
||||
console.error('读取Vault文件夹结构失败:', error)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历目录获取所有文件夹和Markdown文件
|
||||
*/
|
||||
private traverseDirectory(dirPath: string, relativePath: string, results: FileInfo[]) {
|
||||
try {
|
||||
// 首先添加当前文件夹
|
||||
if (relativePath) {
|
||||
results.push({
|
||||
path: relativePath,
|
||||
type: 'folder',
|
||||
name: path.basename(relativePath)
|
||||
})
|
||||
}
|
||||
|
||||
// 确保目录存在且可访问
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
console.error('目录不存在:', dirPath)
|
||||
return
|
||||
}
|
||||
|
||||
let items
|
||||
try {
|
||||
items = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
} catch (err) {
|
||||
console.error(`无法读取目录 ${dirPath}:`, err)
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
// 忽略以.开头的隐藏文件夹和文件
|
||||
if (item.name.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const newRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
|
||||
const fullPath = path.join(dirPath, item.name)
|
||||
|
||||
if (item.isDirectory()) {
|
||||
this.traverseDirectory(fullPath, newRelativePath, results)
|
||||
} else if (item.isFile() && item.name.endsWith('.md')) {
|
||||
// 收集.md文件
|
||||
results.push({
|
||||
path: newRelativePath,
|
||||
type: 'markdown',
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`遍历目录出错 ${dirPath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定Vault的文件夹和Markdown文件结构
|
||||
* @param vaultName vault名称
|
||||
*/
|
||||
getFilesByVaultName(vaultName: string): FileInfo[] {
|
||||
try {
|
||||
const vaults = this.getVaults()
|
||||
const vault = vaults.find((v) => v.name === vaultName)
|
||||
|
||||
if (!vault) {
|
||||
console.error('未找到指定名称的Vault:', vaultName)
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('获取Vault文件结构:', vault.name, vault.path)
|
||||
return this.getVaultStructure(vault.path)
|
||||
} catch (error) {
|
||||
console.error('获取Vault文件结构时发生错误:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ObsidianVaultService
|
||||
34
src/main/services/ProtocolClient.ts
Normal file
34
src/main/services/ProtocolClient.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio'
|
||||
|
||||
export function registerProtocolClient(app: Electron.App) {
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(CHERRY_STUDIO_PROTOCOL, process.execPath, [process.argv[1]])
|
||||
}
|
||||
}
|
||||
|
||||
app.setAsDefaultProtocolClient('cherrystudio')
|
||||
}
|
||||
|
||||
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¶m2=value2
|
||||
console.log('Received URL:', url)
|
||||
|
||||
// Parse the URL and extract parameters
|
||||
const urlObj = new URL(url)
|
||||
const params = new URLSearchParams(urlObj.search)
|
||||
|
||||
// You can send the data to your renderer process
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('protocol-data', {
|
||||
url,
|
||||
params: Object.fromEntries(params.entries())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,17 +23,8 @@ function getShortcutHandler(shortcut: Shortcut) {
|
||||
configManager.setZoomFactor(1)
|
||||
}
|
||||
case 'show_app':
|
||||
return (window: BrowserWindow) => {
|
||||
if (window.isVisible()) {
|
||||
if (window.isFocused()) {
|
||||
window.hide()
|
||||
} else {
|
||||
window.focus()
|
||||
}
|
||||
} else {
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
return () => {
|
||||
windowService.toggleMainWindow()
|
||||
}
|
||||
case 'mini_window':
|
||||
return () => {
|
||||
@@ -221,9 +212,13 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
|
||||
// only register the event handlers once
|
||||
if (undefined === windowOnHandlers.get(window)) {
|
||||
window.on('focus', register)
|
||||
// pass register() directly to listener, the func will receive Event as argument, it's not expected
|
||||
const registerHandler = () => {
|
||||
register()
|
||||
}
|
||||
window.on('focus', registerHandler)
|
||||
window.on('blur', unregister)
|
||||
windowOnHandlers.set(window, { onFocusHandler: register, onBlurHandler: unregister })
|
||||
windowOnHandlers.set(window, { onFocusHandler: registerHandler, onBlurHandler: unregister })
|
||||
}
|
||||
|
||||
if (!window.isDestroyed() && window.isFocused()) {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import Stream from 'stream'
|
||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||
import {
|
||||
BufferLike,
|
||||
createClient,
|
||||
CreateDirectoryOptions,
|
||||
GetFileContentsOptions,
|
||||
PutFileContentsOptions,
|
||||
WebDAVClient
|
||||
} from 'webdav'
|
||||
export default class WebDav {
|
||||
public instance: WebDAVClient | undefined
|
||||
private webdavPath: string
|
||||
@@ -18,6 +25,7 @@ export default class WebDav {
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
this.getFileContents = this.getFileContents.bind(this)
|
||||
this.createDirectory = this.createDirectory.bind(this)
|
||||
}
|
||||
|
||||
public putFileContents = async (
|
||||
@@ -64,4 +72,30 @@ export default class WebDav {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public checkConnection = async () => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.instance.exists('/')
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error checking connection:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public createDirectory = async (path: string, options?: CreateDirectoryOptions) => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.instance.createDirectory(path, options)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private miniWindow: BrowserWindow | null = null
|
||||
private isPinnedMiniWindow: boolean = false
|
||||
private wasFullScreen: boolean = false
|
||||
//hacky-fix: store the focused status of mainWindow before miniWindow shows
|
||||
//to restore the focus status when miniWindow hides
|
||||
private wasMainWindowFocused: boolean = false
|
||||
private selectionMenuWindow: BrowserWindow | null = null
|
||||
private lastSelectedText: string = ''
|
||||
private contextMenu: Menu | null = null
|
||||
@@ -30,6 +34,7 @@ export class WindowService {
|
||||
public createMainWindow(): BrowserWindow {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@@ -56,7 +61,7 @@ export class WindowService {
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
...(isLinux ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
@@ -68,6 +73,12 @@ export class WindowService {
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
|
||||
//preload miniWindow to resolve series of issues about miniWindow in Mac
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
if (enableQuickAssistant && !this.miniWindow) {
|
||||
this.miniWindow = this.createMiniWindow(true)
|
||||
}
|
||||
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@@ -148,6 +159,8 @@ export class WindowService {
|
||||
// show window only when laucn to tray not set
|
||||
const isLaunchToTray = configManager.getLaunchToTray()
|
||||
if (!isLaunchToTray) {
|
||||
//[mac]hacky-fix: miniWindow set visibleOnFullScreen:true will cause dock icon disappeared
|
||||
app.dock?.show()
|
||||
mainWindow.show()
|
||||
}
|
||||
})
|
||||
@@ -163,6 +176,25 @@ export class WindowService {
|
||||
mainWindow.webContents.send('fullscreen-status-changed', false)
|
||||
})
|
||||
|
||||
// set the zoom factor again when the window is going to resize
|
||||
//
|
||||
// this is a workaround for the known bug that
|
||||
// the zoom factor is reset to cached value when window is resized after routing to other page
|
||||
// see: https://github.com/electron/electron/issues/10572
|
||||
//
|
||||
mainWindow.on('will-resize', () => {
|
||||
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
|
||||
if (isLinux) {
|
||||
mainWindow.on('resize', () => {
|
||||
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
})
|
||||
}
|
||||
|
||||
// 添加Escape键退出全屏的支持
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// 当按下Escape键且窗口处于全屏状态时退出全屏
|
||||
@@ -286,9 +318,8 @@ export class WindowService {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
|
||||
if (isMac && isTrayOnClose) {
|
||||
app.dock?.hide() //for mac to hide to tray
|
||||
}
|
||||
//for mac users, should hide dock icon if close to tray
|
||||
app.dock?.hide()
|
||||
})
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
@@ -309,44 +340,52 @@ export class WindowService {
|
||||
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
if (this.mainWindow.isMinimized()) {
|
||||
return this.mainWindow.restore()
|
||||
this.mainWindow.restore()
|
||||
return
|
||||
}
|
||||
//[macOS] Known Issue
|
||||
// setVisibleOnAllWorkspaces true/false will NOT bring window to current desktop in Mac (works fine with Windows)
|
||||
// AppleScript may be a solution, but it's not worth
|
||||
this.mainWindow.setVisibleOnAllWorkspaces(true)
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
this.mainWindow.setVisibleOnAllWorkspaces(false)
|
||||
} else {
|
||||
this.mainWindow = this.createMainWindow()
|
||||
this.mainWindow.focus()
|
||||
}
|
||||
|
||||
//for mac users, when window is shown, should show dock icon (dock may be set to hide when launch)
|
||||
app.dock?.show()
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
if (!enableQuickAssistant) {
|
||||
public toggleMainWindow() {
|
||||
// should not toggle main window when in full screen
|
||||
if (this.wasFullScreen) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.hide()
|
||||
}
|
||||
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
if (this.miniWindow.isMinimized()) {
|
||||
this.miniWindow.restore()
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.isVisible()) {
|
||||
if (this.mainWindow.isFocused()) {
|
||||
// if tray is enabled, hide the main window, else do nothing
|
||||
if (configManager.getTray()) {
|
||||
this.mainWindow.hide()
|
||||
app.dock?.hide()
|
||||
}
|
||||
} else {
|
||||
this.mainWindow.focus()
|
||||
}
|
||||
this.miniWindow.show()
|
||||
this.miniWindow.center()
|
||||
this.miniWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
this.showMainWindow()
|
||||
}
|
||||
|
||||
public createMiniWindow(isPreload: boolean = false): BrowserWindow {
|
||||
this.miniWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 520,
|
||||
show: true,
|
||||
width: 550,
|
||||
height: 400,
|
||||
minWidth: 350,
|
||||
minHeight: 380,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 768,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
@@ -354,8 +393,13 @@ export class WindowService {
|
||||
center: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
resizable: true,
|
||||
useContentSize: true,
|
||||
...(isMac ? { type: 'panel' } : {}),
|
||||
skipTaskbar: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
@@ -364,8 +408,25 @@ export class WindowService {
|
||||
}
|
||||
})
|
||||
|
||||
//miniWindow should show in current desktop
|
||||
this.miniWindow?.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
|
||||
//make miniWindow always on top of fullscreen apps with level set
|
||||
this.miniWindow.setAlwaysOnTop(true, 'screen-saver', 1)
|
||||
|
||||
this.miniWindow.on('ready-to-show', () => {
|
||||
if (isPreload) {
|
||||
return
|
||||
}
|
||||
|
||||
this.wasMainWindowFocused = this.mainWindow?.isFocused() || false
|
||||
this.miniWindow?.center()
|
||||
this.miniWindow?.show()
|
||||
})
|
||||
|
||||
this.miniWindow.on('blur', () => {
|
||||
this.miniWindow?.hide()
|
||||
if (!this.isPinnedMiniWindow) {
|
||||
this.hideMiniWindow()
|
||||
}
|
||||
})
|
||||
|
||||
this.miniWindow.on('closed', () => {
|
||||
@@ -391,9 +452,48 @@ export class WindowService {
|
||||
hash: '#/mini'
|
||||
})
|
||||
}
|
||||
|
||||
return this.miniWindow
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
if (!enableQuickAssistant) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.hide()
|
||||
}
|
||||
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
this.wasMainWindowFocused = this.mainWindow?.isFocused() || false
|
||||
|
||||
if (this.miniWindow.isMinimized()) {
|
||||
this.miniWindow.restore()
|
||||
}
|
||||
this.miniWindow.show()
|
||||
return
|
||||
}
|
||||
|
||||
this.miniWindow = this.createMiniWindow()
|
||||
}
|
||||
|
||||
public hideMiniWindow() {
|
||||
//hacky-fix:[mac/win] previous window(not self-app) should be focused again after miniWindow hide
|
||||
if (isWin) {
|
||||
this.miniWindow?.minimize()
|
||||
this.miniWindow?.hide()
|
||||
return
|
||||
} else if (isMac) {
|
||||
this.miniWindow?.hide()
|
||||
if (!this.wasMainWindowFocused) {
|
||||
app.hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.miniWindow?.hide()
|
||||
}
|
||||
|
||||
@@ -402,11 +502,16 @@ export class WindowService {
|
||||
}
|
||||
|
||||
public toggleMiniWindow() {
|
||||
if (this.miniWindow) {
|
||||
this.miniWindow.isVisible() ? this.miniWindow.hide() : this.miniWindow.show()
|
||||
} else {
|
||||
this.showMiniWindow()
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed() && this.miniWindow.isVisible()) {
|
||||
this.hideMiniWindow()
|
||||
return
|
||||
}
|
||||
|
||||
this.showMiniWindow()
|
||||
}
|
||||
|
||||
public setPinMiniWindow(isPinned) {
|
||||
this.isPinnedMiniWindow = isPinned
|
||||
}
|
||||
|
||||
public showSelectionMenu(bounds: { x: number; y: number }) {
|
||||
|
||||
@@ -42,3 +42,13 @@ export function dumpPersistState() {
|
||||
}
|
||||
return JSON.stringify(persistState)
|
||||
}
|
||||
|
||||
export const runAsyncFunction = async (fn: () => void) => {
|
||||
await fn()
|
||||
}
|
||||
|
||||
export function makeSureDirExists(dir: string) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,22 @@ export function runInstallScript(scriptPath: string): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBinaryPath(name: string): Promise<string> {
|
||||
let cmd = process.platform === 'win32' ? `${name}.exe` : name
|
||||
export async function getBinaryName(name: string): Promise<string> {
|
||||
if (process.platform === 'win32') {
|
||||
return `${name}.exe`
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
if (!name) {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name)
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binariesDirExists = await fs.existsSync(binariesDir)
|
||||
cmd = binariesDirExists ? path.join(binariesDir, cmd) : name
|
||||
return cmd
|
||||
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
||||
}
|
||||
|
||||
export async function isBinaryExists(name: string): Promise<boolean> {
|
||||
|
||||
30
src/preload/index.d.ts
vendored
30
src/preload/index.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
|
||||
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import type { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious, WebDavConfig } from '@renderer/types'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
@@ -45,6 +45,8 @@ declare global {
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => Promise<BackupFile[]>
|
||||
checkConnection: (webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) => Promise<void>
|
||||
}
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||
@@ -135,6 +137,7 @@ declare global {
|
||||
hide: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
toggle: () => Promise<void>
|
||||
setPin: (isPinned: boolean) => Promise<void>
|
||||
}
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||
@@ -144,17 +147,12 @@ declare global {
|
||||
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
|
||||
}
|
||||
mcp: {
|
||||
// servers
|
||||
listServers: () => Promise<MCPServer[]>
|
||||
addServer: (server: MCPServer) => Promise<void>
|
||||
updateServer: (server: MCPServer) => Promise<void>
|
||||
deleteServer: (serverName: string) => Promise<void>
|
||||
setServerActive: (name: string, isActive: boolean) => Promise<void>
|
||||
// tools
|
||||
listTools: () => Promise<MCPTool[]>
|
||||
callTool: ({ client, name, args }: { client: string; name: string; args: any }) => Promise<any>
|
||||
// status
|
||||
cleanup: () => Promise<void>
|
||||
removeServer: (server: MCPServer) => Promise<void>
|
||||
restartServer: (server: MCPServer) => Promise<void>
|
||||
stopServer: (server: MCPServer) => Promise<void>
|
||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
}
|
||||
copilot: {
|
||||
getAuthMessage: (
|
||||
@@ -170,6 +168,14 @@ declare global {
|
||||
getBinaryPath: (name: string) => Promise<string>
|
||||
installUVBinary: () => Promise<void>
|
||||
installBunBinary: () => Promise<void>
|
||||
protocol: {
|
||||
onReceiveData: (callback: (data: { url: string; params: any }) => void) => () => void
|
||||
}
|
||||
nutstore: {
|
||||
getSSOUrl: () => Promise<string>
|
||||
decryptToken: (token: string) => Promise<{ username: string; access_token: string }>
|
||||
getDirectoryContents: (token: string, path: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
|
||||
import { CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@@ -34,7 +35,10 @@ const api = {
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig)
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig),
|
||||
checkConnection: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:checkConnection', webdavConfig),
|
||||
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) =>
|
||||
ipcRenderer.invoke('backup:createDirectory', webdavConfig, path, options)
|
||||
},
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||
@@ -108,7 +112,8 @@ const api = {
|
||||
show: () => ipcRenderer.invoke('miniwindow:show'),
|
||||
hide: () => ipcRenderer.invoke('miniwindow:hide'),
|
||||
close: () => ipcRenderer.invoke('miniwindow:close'),
|
||||
toggle: () => ipcRenderer.invoke('miniwindow:toggle')
|
||||
toggle: () => ipcRenderer.invoke('miniwindow:toggle'),
|
||||
setPin: (isPinned: boolean) => ipcRenderer.invoke('miniwindow:set-pin', isPinned)
|
||||
},
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
||||
@@ -116,15 +121,13 @@ const api = {
|
||||
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
||||
},
|
||||
mcp: {
|
||||
listServers: () => ipcRenderer.invoke('mcp:list-servers'),
|
||||
addServer: (server: MCPServer) => ipcRenderer.invoke('mcp:add-server', server),
|
||||
updateServer: (server: MCPServer) => ipcRenderer.invoke('mcp:update-server', server),
|
||||
deleteServer: (serverName: string) => ipcRenderer.invoke('mcp:delete-server', serverName),
|
||||
setServerActive: (name: string, isActive: boolean) =>
|
||||
ipcRenderer.invoke('mcp:set-server-active', { name, isActive }),
|
||||
listTools: (serverName?: string) => ipcRenderer.invoke('mcp:list-tools', serverName),
|
||||
callTool: (params: { client: string; name: string; args: any }) => ipcRenderer.invoke('mcp:call-tool', params),
|
||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||
removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server),
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke('mcp:restart-server', server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke('mcp:stop-server', server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
||||
ipcRenderer.invoke('mcp:call-tool', { server, name, args }),
|
||||
getInstallInfo: () => ipcRenderer.invoke('mcp:get-install-info')
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
@@ -143,7 +146,24 @@ const api = {
|
||||
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name),
|
||||
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
|
||||
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'),
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary')
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary'),
|
||||
protocol: {
|
||||
onReceiveData: (callback: (data: { url: string; params: any }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => {
|
||||
callback(data)
|
||||
}
|
||||
ipcRenderer.on('protocol-data', listener)
|
||||
return () => {
|
||||
ipcRenderer.off('protocol-data', listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
nutstore: {
|
||||
getSSOUrl: () => ipcRenderer.invoke('nutstore:get-sso-url'),
|
||||
decryptToken: (token: string) => ipcRenderer.invoke('nutstore:decrypt-token', token),
|
||||
getDirectoryContents: (token: string, path: string) =>
|
||||
ipcRenderer.invoke('nutstore:get-directory-contents', token, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
@@ -153,6 +173,11 @@ if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld('obsidian', {
|
||||
getVaults: () => ipcRenderer.invoke('obsidian:get-vaults'),
|
||||
getFolders: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName),
|
||||
getFiles: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@@ -39,5 +39,4 @@
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -21,7 +21,7 @@ import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
function App(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StyleSheetManager>
|
||||
|
||||
18
src/renderer/src/assets/styles/animation.scss
Normal file
18
src/renderer/src/assets/styles/animation.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
@keyframes animation-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0.5);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 var(--pulse-size) rgba(var(--pulse-color), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--pulse-color), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 电磁波扩散效果
|
||||
.animation-pulse {
|
||||
--pulse-color: 59, 130, 246;
|
||||
--pulse-size: 8px;
|
||||
animation: animation-pulse 1.5s infinite;
|
||||
}
|
||||
@@ -192,3 +192,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-dropdown-menu .ant-dropdown-menu-sub {
|
||||
max-height: 350px;
|
||||
width: max-content;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@use './ant.scss';
|
||||
@use './scrollbar.scss';
|
||||
@use './container.scss';
|
||||
@use './animation.scss';
|
||||
@import '../fonts/icon-fonts/iconfont.css';
|
||||
@import '../fonts/ubuntu/ubuntu.css';
|
||||
|
||||
@@ -18,7 +19,7 @@
|
||||
--color-gray-2: #414853;
|
||||
--color-gray-3: #32363f;
|
||||
|
||||
--color-text-1: rgba(255, 255, 245, 0.86);
|
||||
--color-text-1: rgba(255, 255, 245, 0.9);
|
||||
--color-text-2: rgba(235, 235, 245, 0.6);
|
||||
--color-text-3: rgba(235, 235, 245, 0.38);
|
||||
|
||||
|
||||
@@ -294,3 +294,11 @@
|
||||
emoji-picker {
|
||||
--border-size: 0;
|
||||
}
|
||||
|
||||
.katex-display{
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
mjx-container{
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -8,9 +8,10 @@ interface Props {
|
||||
model: Model
|
||||
size: number
|
||||
props?: AvatarProps
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
||||
const ModelAvatar: FC<Props> = ({ model, size, props, className }) => {
|
||||
return (
|
||||
<Avatar
|
||||
src={getModelLogo(model?.id || '')}
|
||||
@@ -23,7 +24,8 @@ const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
{...props}>
|
||||
{...props}
|
||||
className={className}>
|
||||
{first(model?.name)}
|
||||
</Avatar>
|
||||
)
|
||||
|
||||
43
src/renderer/src/components/CustomCollapse.tsx
Normal file
43
src/renderer/src/components/CustomCollapse.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Collapse } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
label: React.ReactNode
|
||||
extra: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const CustomCollapse: FC<CustomCollapseProps> = ({ label, extra, children }) => {
|
||||
const CollapseStyle = {
|
||||
background: 'transparent',
|
||||
border: '0.5px solid var(--color-border)'
|
||||
}
|
||||
const CollapseItemStyles = {
|
||||
header: {
|
||||
padding: '8px 16px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
body: {
|
||||
borderTop: '0.5px solid var(--color-border)'
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
style={CollapseStyle}
|
||||
defaultActiveKey={['1']}
|
||||
items={[
|
||||
{
|
||||
styles: CollapseItemStyles,
|
||||
key: '1',
|
||||
label,
|
||||
extra,
|
||||
children
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CustomCollapse)
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ResponderProvided
|
||||
} from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props<T> {
|
||||
@@ -47,26 +48,28 @@ const DragableList: FC<Props<any>> = ({
|
||||
<Droppable droppableId="droppable" {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
||||
{list.map((item, index) => {
|
||||
const id = item.id || item
|
||||
return (
|
||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...listStyle,
|
||||
...provided.draggableProps.style,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
})}
|
||||
<VirtualList data={list} itemKey="id">
|
||||
{(item, index) => {
|
||||
const id = item.id || item
|
||||
return (
|
||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...listStyle,
|
||||
...provided.draggableProps.style,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}}
|
||||
</VirtualList>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
50
src/renderer/src/components/Icons/NutstoreIcons.tsx
Normal file
50
src/renderer/src/components/Icons/NutstoreIcons.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
const IconSpan = styled.span`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export function NutstoreIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<IconSpan>
|
||||
<svg
|
||||
{...props}
|
||||
width="16px"
|
||||
height="16px"
|
||||
viewBox="0 0 20 20"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink">
|
||||
<title>线性单坚果</title>
|
||||
<g id="线性单坚果" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M10.1590439,0.886175571 C10.1753674,0.890326544 10.291709,0.910777855 10.428428,0.935202765 L10.6388345,0.973279488 C10.7074276,0.985937901 10.77116,0.998048871 10.8200766,1.00807156 C11.2437905,1.09488771 11.6662387,1.21011472 12.1133986,1.37210166 C13.2580363,1.78675499 14.3714894,2.43940777 15.4224927,3.39703693 L15.621,3.584 L15.6351722,3.57092562 C16.53166,2.76294504 17.6751418,2.31986999 18.4291849,2.58060734 L18.5580792,2.63399481 C18.9455012,2.81584984 19.2328582,3.16284846 19.437028,3.61729231 C19.5709871,3.91546021 19.6526725,4.21929758 19.6985752,4.50662941 C19.7148596,4.80478115 19.5904581,5.0358501 19.4098118,5.1582622 C19.3815042,5.17858714 19.3523426,5.19648783 19.3224017,5.21197531 C19.1152073,5.31915066 18.9086763,5.30466603 18.6939183,5.22086872 C18.6620576,5.20843687 18.6328325,5.19564599 18.6006654,5.18105502 C18.4394695,5.11546938 18.2846309,5.06753532 18.1365915,5.04232952 C17.7415971,4.96197402 17.3578102,5.06378907 17.051656,5.32621284 L17.046624,5.33098744 L17.1856424,5.55157847 C18.0964209,7.0577136 18.6880009,8.98631362 18.5914984,10.988329 L18.5672508,11.3423168 C18.518886,12.3590196 18.336046,13.2889191 17.9959883,14.1391815 C17.4227031,15.6418626 16.5311196,16.5912538 15.4105898,16.2529712 L15.278,16.207 C15.204042,16.2889459 15.1247235,16.3618831 15.0410669,16.4278107 L14.9126231,16.5212291 C13.2906651,17.9150353 10.9315401,19.0281897 7.99389616,19.2 L7.17106258,19.2 C3.43360072,19.2 1.02132454,17.63803 0.534391412,16.0333683 L0.513,15.954 L0.504265285,15.9449232 C-0.110228462,15.1972878 0.264421351,10.4760569 2.09599684,6.99794495 L2.22026541,6.76796973 C2.29571954,6.63016882 2.43695112,6.39220857 2.63659846,6.08729923 C2.9688861,5.57981633 3.34471126,5.07232148 3.75709487,4.59788661 C4.2749895,4.0020645 4.81413532,3.50121679 5.3386949,3.15177019 C5.36355777,3.12648036 5.4278064,3.07827062 5.50910569,3.02364741 L5.559,2.991 L5.5530361,2.96941337 C5.48899059,2.69876461 5.47862138,2.4784725 5.54146387,2.2521942 L5.58811106,2.11525813 C5.68308256,1.86409186 5.94349142,1.57994703 6.25873284,1.38755406 C6.58654657,1.18748816 7.23187921,0.95895859 7.69473739,0.883035787 C8.37505518,0.763266442 9.38159553,0.78076773 10.1590439,0.886175571 Z M6.59801776,3.85068129 C6.46732353,3.85068129 6.2240354,3.97828097 6.07844768,4.1001814 C5.59811888,4.42589962 5.12194443,4.87010868 4.65860433,5.40361803 C4.52372819,5.55892011 4.37448327,5.74624534 4.22515758,5.94252901 L4.04684241,6.18089332 C3.57610889,6.82012555 3.16307203,7.45661922 3.27592159,7.33459023 C1.39280393,10.7336939 1.18786427,14.1190682 1.66513528,15.5784041 C1.72944314,15.8645824 2.24255786,16.4352772 2.98506717,16.8902532 C4.03558482,17.5339627 5.43381914,17.9303112 7.15636912,17.9630362 L7.95282724,17.9633776 C10.5671194,17.8104156 12.6011819,16.8513512 14.1270746,15.5866906 L14.2005419,15.5269075 L14.2189125,15.5136158 C14.591184,15.2751975 14.6855045,14.9945722 14.5299888,14.3127204 C14.1480256,12.8500475 13.2023047,10.9705228 11.4802274,8.76564869 C10.6761315,7.73569508 9.84271439,6.77270459 8.9812637,5.88185595 C8.26651717,5.13999817 7.48191474,4.46126051 6.65303256,3.86947602 C6.6343697,3.85523851 6.62003281,3.85068129 6.59801776,3.85068129 Z M8.0520431,2.14478343 C7.34750556,2.24716005 6.81392621,2.48276912 6.75769294,2.58286729 C6.75315545,2.59094425 6.75172186,2.59912409 6.75788522,2.63367631 L6.761,2.653 C6.92447955,2.67441039 7.07755879,2.72514333 7.22081781,2.80306173 L7.36053304,2.88992896 C8.25106173,3.52400396 9.08393795,4.2496146 9.84209216,5.05104835 C10.7498631,5.98954517 11.620838,6.99715009 12.4127624,8.02643665 C14.2357617,10.3660968 15.255676,12.4067536 15.6810213,14.0171728 C15.7810435,14.3986973 15.8140553,14.7531702 15.7838468,15.0855202 L15.779624,15.1139874 L15.7923351,15.1170186 C16.0195271,15.1453183 16.2337261,14.9383655 16.4514,14.5090146 L16.5168229,14.3735502 C16.5998938,14.1934825 16.8522658,13.5389313 16.8131724,13.6336744 L16.800624,13.6629874 L16.8933423,13.4088509 C17.1021765,12.7846983 17.2487406,12.0003637 17.2861365,11.2776414 C17.4525549,9.34169753 16.8847303,7.51332101 15.9618076,5.9792161 C15.8725231,5.8278532 15.7620551,5.66138642 15.6942132,5.57820575 C14.7595226,4.31701776 13.5999579,3.42705248 12.3136888,2.84260842 C11.4827868,2.46507019 10.794487,2.2853603 10.1559862,2.18983638 C9.43796126,2.09113972 8.59553714,2.05880421 8.0520431,2.14478343 Z M16.4823653,4.32067121 L16.364,4.418 L16.393,4.454 L16.5100007,4.3621392 C17.0306065,3.97118443 17.6106194,3.7900296 18.1665334,3.88918284 L18.233,3.904 L18.2063581,3.87419362 C18.1376794,3.79892884 18.0675642,3.72412847 18.0165076,3.68190508 L17.972563,3.65173005 C17.800955,3.56958653 17.0606024,3.86572493 16.4823653,4.32067121 Z"
|
||||
id="形状结合"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</IconSpan>
|
||||
)
|
||||
}
|
||||
|
||||
export function FolderIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<IconSpan>
|
||||
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" {...props}>
|
||||
<title>folder</title>
|
||||
<path
|
||||
d="M396.5,185.7l22.7,27.2a36.1,36.1,0,0,0,27.7,12.7H906.8c29.4,0,53.2,22.8,53.2,50.9V800.1c0,28.1-23.8,50.9-53.2,50.9H117.2C87.8,851,64,828.2,64,800.1V223.9c0-28.1,23.8-50.9,53.2-50.9H368.8A36.1,36.1,0,0,1,396.5,185.7Z"
|
||||
style={{ fill: '#9fddff' }}
|
||||
/>
|
||||
<path
|
||||
d="M64,342.5V797.8c0,29.4,24,53.2,53.6,53.2H906.4c29.6,0,53.6-23.8,53.6-53.2V342.5Z"
|
||||
style={{ fill: '#74c6ff' }}
|
||||
/>
|
||||
</svg>
|
||||
</IconSpan>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const Icon = styled(ToolOutlined)`
|
||||
color: #d97757;
|
||||
color: var(--color-primary);
|
||||
font-size: 15px;
|
||||
margin-right: 6px;
|
||||
`
|
||||
|
||||
@@ -4,15 +4,25 @@ import styled from 'styled-components'
|
||||
|
||||
interface IndicatorLightProps {
|
||||
color: string
|
||||
size?: number
|
||||
shadow?: boolean
|
||||
style?: React.CSSProperties
|
||||
animation?: boolean
|
||||
}
|
||||
|
||||
const Light = styled.div<{ color: string }>`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
const Light = styled.div<{
|
||||
color: string
|
||||
size: number
|
||||
shadow?: boolean
|
||||
style?: React.CSSProperties
|
||||
animation?: boolean
|
||||
}>`
|
||||
width: ${({ size }) => size}px;
|
||||
height: ${({ size }) => size}px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ color }) => color};
|
||||
box-shadow: 0 0 6px ${({ color }) => color};
|
||||
animation: pulse 2s infinite;
|
||||
box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')};
|
||||
animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')};
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
@@ -27,9 +37,9 @@ const Light = styled.div<{ color: string }>`
|
||||
}
|
||||
`
|
||||
|
||||
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => {
|
||||
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color, size = 8, shadow = true, style, animation = true }) => {
|
||||
const actualColor = color === 'green' ? '#22c55e' : color
|
||||
return <Light color={actualColor} />
|
||||
return <Light color={actualColor} size={size} shadow={shadow} style={style} animation={animation} />
|
||||
}
|
||||
|
||||
export default IndicatorLight
|
||||
|
||||
@@ -8,17 +8,20 @@ interface ListItemProps {
|
||||
subtitle?: string
|
||||
titleStyle?: React.CSSProperties
|
||||
onClick?: () => void
|
||||
rightContent?: ReactNode
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick }: ListItemProps) => {
|
||||
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
|
||||
return (
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={style}>
|
||||
<ListItemContent>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<TextContainer>
|
||||
<TitleText style={titleStyle}>{title}</TitleText>
|
||||
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
|
||||
</TextContainer>
|
||||
{rightContent && <RightContentWrapper>{rightContent}</RightContentWrapper>}
|
||||
</ListItemContent>
|
||||
</ListItemContainer>
|
||||
)
|
||||
@@ -84,4 +87,8 @@ const SubtitleText = styled.div`
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const RightContentWrapper = styled.div`
|
||||
margin-left: auto;
|
||||
`
|
||||
|
||||
export default ListItem
|
||||
|
||||
429
src/renderer/src/components/MinApp/MinappPopupContainer.tsx
Normal file
429
src/renderer/src/components/MinApp/MinappPopupContainer.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
import {
|
||||
CloseOutlined,
|
||||
CodeOutlined,
|
||||
CopyOutlined,
|
||||
ExportOutlined,
|
||||
MinusOutlined,
|
||||
PushpinOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { delay } from '@renderer/utils'
|
||||
import { Avatar, Drawer, Tooltip } from 'antd'
|
||||
import { WebviewTag } from 'electron'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import WebviewContainer from './WebviewContainer'
|
||||
|
||||
interface AppExtraInfo {
|
||||
canPinned: boolean
|
||||
isPinned: boolean
|
||||
canOpenExternalLink: boolean
|
||||
}
|
||||
|
||||
type AppInfo = MinAppType & AppExtraInfo
|
||||
|
||||
/** The main container for MinApp popup */
|
||||
const MinappPopupContainer: React.FC = () => {
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime()
|
||||
const { closeMinapp, hideMinappPopup } = useMinappPopup()
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
|
||||
/** control the drawer open or close */
|
||||
const [isPopupShow, setIsPopupShow] = useState(true)
|
||||
/** whether the current minapp is ready */
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
/** the current REAL url of the minapp
|
||||
* different from the app preset url, because user may navigate in minapp */
|
||||
const [currentUrl, setCurrentUrl] = useState<string | null>(null)
|
||||
|
||||
/** store the last minapp id and show status */
|
||||
const lastMinappId = useRef<string | null>(null)
|
||||
const lastMinappShow = useRef<boolean>(false)
|
||||
|
||||
/** store the webview refs, one of the key to make them keepalive */
|
||||
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
|
||||
/** indicate whether the webview has loaded */
|
||||
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
|
||||
|
||||
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
useBridge()
|
||||
|
||||
/** set the popup display status */
|
||||
useEffect(() => {
|
||||
if (minappShow) {
|
||||
// init the current url
|
||||
if (currentMinappId && currentAppInfo) {
|
||||
setCurrentUrl(currentAppInfo.url)
|
||||
}
|
||||
|
||||
setIsPopupShow(true)
|
||||
|
||||
if (webviewLoadedRefs.current.get(currentMinappId)) {
|
||||
setIsReady(true)
|
||||
/** the case that open the minapp from sidebar */
|
||||
} else if (lastMinappId.current !== currentMinappId && lastMinappShow.current === minappShow) {
|
||||
setIsReady(false)
|
||||
}
|
||||
} else {
|
||||
setIsPopupShow(false)
|
||||
setIsReady(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
/** renew the last minapp id and show status */
|
||||
lastMinappId.current = currentMinappId
|
||||
lastMinappShow.current = minappShow
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minappShow, currentMinappId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewRefs.current) return
|
||||
|
||||
/** set the webview display status
|
||||
* DO NOT use the state to set the display status,
|
||||
* to AVOID the re-render of the webview container
|
||||
*/
|
||||
webviewRefs.current.forEach((webviewRef, appid) => {
|
||||
if (!webviewRef) return
|
||||
webviewRef.style.display = appid === currentMinappId ? 'inline-flex' : 'none'
|
||||
})
|
||||
|
||||
//delete the extra webviewLoadedRefs
|
||||
webviewLoadedRefs.current.forEach((_, appid) => {
|
||||
if (!webviewRefs.current.has(appid)) {
|
||||
webviewLoadedRefs.current.delete(appid)
|
||||
}
|
||||
})
|
||||
}, [currentMinappId])
|
||||
|
||||
/** only the keepalive minapp can be minimized */
|
||||
const canMinimize = !(openedOneOffMinapp && openedOneOffMinapp.id == currentMinappId)
|
||||
|
||||
/** combine the openedKeepAliveMinapps and openedOneOffMinapp */
|
||||
const combinedApps = useMemo(() => {
|
||||
return [...openedKeepAliveMinapps, ...(openedOneOffMinapp ? [openedOneOffMinapp] : [])]
|
||||
}, [openedKeepAliveMinapps, openedOneOffMinapp])
|
||||
|
||||
/** get the extra info of the apps */
|
||||
const appsExtraInfo = useMemo(() => {
|
||||
return combinedApps.reduce(
|
||||
(acc, app) => ({
|
||||
...acc,
|
||||
[app.id]: {
|
||||
canPinned: DEFAULT_MIN_APPS.some((item) => item.id === app.id),
|
||||
isPinned: pinned.some((item) => item.id === app.id),
|
||||
canOpenExternalLink: app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
}
|
||||
}),
|
||||
{} as Record<string, AppExtraInfo>
|
||||
)
|
||||
}, [combinedApps, pinned])
|
||||
|
||||
/** get the current app info with extra info */
|
||||
let currentAppInfo: AppInfo | null = null
|
||||
if (currentMinappId) {
|
||||
const currentApp = combinedApps.find((item) => item.id === currentMinappId) as MinAppType
|
||||
currentAppInfo = { ...currentApp, ...appsExtraInfo[currentApp.id] }
|
||||
}
|
||||
|
||||
/** will close the popup and delete the webview */
|
||||
const handlePopupClose = async (appid: string) => {
|
||||
setIsPopupShow(false)
|
||||
await delay(0.3)
|
||||
webviewLoadedRefs.current.delete(appid)
|
||||
closeMinapp(appid)
|
||||
}
|
||||
|
||||
/** will hide the popup and remain the webviews */
|
||||
const handlePopupMinimize = async () => {
|
||||
setIsPopupShow(false)
|
||||
await delay(0.3)
|
||||
hideMinappPopup()
|
||||
}
|
||||
|
||||
/** the callback function to set the webviews ref */
|
||||
const handleWebviewSetRef = (appid: string, element: WebviewTag | null) => {
|
||||
webviewRefs.current.set(appid, element)
|
||||
|
||||
if (!webviewRefs.current.has(appid)) {
|
||||
webviewRefs.current.set(appid, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (element) {
|
||||
webviewRefs.current.set(appid, element)
|
||||
} else {
|
||||
webviewRefs.current.delete(appid)
|
||||
}
|
||||
}
|
||||
|
||||
/** the callback function to set the webviews loaded indicator */
|
||||
const handleWebviewLoaded = (appid: string) => {
|
||||
webviewLoadedRefs.current.set(appid, true)
|
||||
if (appid == currentMinappId) {
|
||||
setTimeout(() => setIsReady(true), 200)
|
||||
}
|
||||
}
|
||||
|
||||
/** the callback function to handle the webview navigate to new url */
|
||||
const handleWebviewNavigate = (appid: string, url: string) => {
|
||||
if (appid === currentMinappId) {
|
||||
setCurrentUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
/** will open the devtools of the minapp */
|
||||
const handleOpenDevTools = (appid: string) => {
|
||||
const webview = webviewRefs.current.get(appid)
|
||||
if (webview) {
|
||||
webview.openDevTools()
|
||||
}
|
||||
}
|
||||
|
||||
/** only reload the original url */
|
||||
const handleReload = (appid: string) => {
|
||||
const webview = webviewRefs.current.get(appid)
|
||||
if (webview) {
|
||||
const url = combinedApps.find((item) => item.id === appid)?.url
|
||||
if (url) {
|
||||
webview.src = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** open the giving url in browser */
|
||||
const handleOpenLink = (url: string) => {
|
||||
window.api.openWebsite(url)
|
||||
}
|
||||
|
||||
/** toggle the pin status of the minapp */
|
||||
const handleTogglePin = (appid: string) => {
|
||||
const app = combinedApps.find((item) => item.id === appid)
|
||||
if (!app) return
|
||||
|
||||
const newPinned = appsExtraInfo[appid].isPinned ? pinned.filter((item) => item.id !== appid) : [...pinned, app]
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
|
||||
/** Title bar of the popup */
|
||||
const Title = ({ appInfo, url }: { appInfo: AppInfo | null; url: string | null }) => {
|
||||
if (!appInfo) return null
|
||||
|
||||
const handleCopyUrl = (event: any, url: string) => {
|
||||
//don't show app-wide context menu
|
||||
event.preventDefault()
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
window.message.success('URL ' + t('message.copy.success'))
|
||||
})
|
||||
.catch(() => {
|
||||
window.message.error('URL ' + t('message.copy.failed'))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<TitleTextTooltip>
|
||||
{url ?? appInfo.url} <br />
|
||||
<CopyOutlined className="icon-copy" />
|
||||
{t('minapp.popup.rightclick_copyurl')}
|
||||
</TitleTextTooltip>
|
||||
}
|
||||
mouseEnterDelay={0.8}
|
||||
placement="rightBottom"
|
||||
styles={{
|
||||
root: {
|
||||
maxWidth: '400px'
|
||||
}
|
||||
}}>
|
||||
<TitleText onContextMenu={(e) => handleCopyUrl(e, url ?? appInfo.url)}>{appInfo.name}</TitleText>
|
||||
</Tooltip>
|
||||
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
||||
<Tooltip title={t('minapp.popup.refresh')} mouseEnterDelay={0.8} placement="bottom">
|
||||
<Button onClick={() => handleReload(appInfo.id)}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{appInfo.canPinned && (
|
||||
<Tooltip
|
||||
title={appInfo.isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title')}
|
||||
mouseEnterDelay={0.8}
|
||||
placement="bottom">
|
||||
<Button onClick={() => handleTogglePin(appInfo.id)} className={appInfo.isPinned ? 'pinned' : ''}>
|
||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{appInfo.canOpenExternalLink && (
|
||||
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
|
||||
<Button onClick={() => handleOpenLink(url ?? appInfo.url)}>
|
||||
<ExportOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isInDevelopment && (
|
||||
<Tooltip title={t('minapp.popup.devtools')} mouseEnterDelay={0.8} placement="bottom">
|
||||
<Button onClick={() => handleOpenDevTools(appInfo.id)}>
|
||||
<CodeOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canMinimize && (
|
||||
<Tooltip title={t('minapp.popup.minimize')} mouseEnterDelay={0.8} placement="bottom">
|
||||
<Button onClick={() => handlePopupMinimize()}>
|
||||
<MinusOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('minapp.popup.close')} mouseEnterDelay={0.8} placement="bottom">
|
||||
<Button onClick={() => handlePopupClose(appInfo.id)}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonsGroup>
|
||||
</TitleContainer>
|
||||
)
|
||||
}
|
||||
|
||||
/** group the webview containers with Memo, one of the key to make them keepalive */
|
||||
const WebviewContainerGroup = useMemo(() => {
|
||||
return combinedApps.map((app) => (
|
||||
<WebviewContainer
|
||||
key={app.id}
|
||||
appid={app.id}
|
||||
url={app.url}
|
||||
onSetRefCallback={handleWebviewSetRef}
|
||||
onLoadedCallback={handleWebviewLoaded}
|
||||
onNavigateCallback={handleWebviewNavigate}
|
||||
/>
|
||||
))
|
||||
|
||||
// because the combinedApps is enough
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [combinedApps])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={<Title appInfo={currentAppInfo} url={currentUrl} />}
|
||||
placement="bottom"
|
||||
onClose={handlePopupMinimize}
|
||||
open={isPopupShow}
|
||||
destroyOnClose={false}
|
||||
mask={false}
|
||||
rootClassName="minapp-drawer"
|
||||
maskClassName="minapp-mask"
|
||||
height={'100%'}
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||
{!isReady && (
|
||||
<EmptyView>
|
||||
<Avatar
|
||||
src={currentAppInfo?.logo}
|
||||
size={80}
|
||||
style={{ border: '1px solid var(--color-border)', marginTop: -150 }}
|
||||
/>
|
||||
<BeatLoader color="var(--color-text-2)" size="10px" style={{ marginTop: 15 }} />
|
||||
</EmptyView>
|
||||
)}
|
||||
{WebviewContainerGroup}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '20px' : '10px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
const TitleText = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
margin-right: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
`
|
||||
|
||||
const TitleTextTooltip = styled.span`
|
||||
font-size: 0.8rem;
|
||||
|
||||
.icon-copy {
|
||||
font-size: 0.7rem;
|
||||
padding-right: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonsGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
-webkit-app-region: no-drag;
|
||||
&.windows {
|
||||
margin-right: ${isWindows ? '130px' : 0};
|
||||
background-color: var(--color-background-mute);
|
||||
border-radius: 50px;
|
||||
padding: 0 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.div`
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-2);
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
&.pinned {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-bg);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyView = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
export default MinappPopupContainer
|
||||
@@ -0,0 +1,11 @@
|
||||
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
|
||||
const TopViewMinappContainer = () => {
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
|
||||
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
|
||||
|
||||
return <>{isCreate && <MinappPopupContainer />}</>
|
||||
}
|
||||
|
||||
export default TopViewMinappContainer
|
||||
92
src/renderer/src/components/MinApp/WebviewContainer.tsx
Normal file
92
src/renderer/src/components/MinApp/WebviewContainer.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { WebviewTag } from 'electron'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* WebviewContainer is a component that renders a webview element.
|
||||
* It is used in the MinAppPopupContainer component.
|
||||
* The webcontent can be remain in memory
|
||||
*/
|
||||
const WebviewContainer = memo(
|
||||
({
|
||||
appid,
|
||||
url,
|
||||
onSetRefCallback,
|
||||
onLoadedCallback,
|
||||
onNavigateCallback
|
||||
}: {
|
||||
appid: string
|
||||
url: string
|
||||
onSetRefCallback: (appid: string, element: WebviewTag | null) => void
|
||||
onLoadedCallback: (appid: string) => void
|
||||
onNavigateCallback: (appid: string, url: string) => void
|
||||
}) => {
|
||||
const webviewRef = useRef<WebviewTag | null>(null)
|
||||
|
||||
const setRef = (appid: string) => {
|
||||
onSetRefCallback(appid, null)
|
||||
|
||||
return (element: WebviewTag | null) => {
|
||||
onSetRefCallback(appid, element)
|
||||
if (element) {
|
||||
webviewRef.current = element
|
||||
} else {
|
||||
webviewRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewRef.current) return
|
||||
|
||||
const handleNewWindow = (event: any) => {
|
||||
event.preventDefault()
|
||||
if (webviewRef.current?.loadURL) {
|
||||
webviewRef.current.loadURL(event.url)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoaded = () => {
|
||||
onLoadedCallback(appid)
|
||||
}
|
||||
|
||||
const handleNavigate = (event: any) => {
|
||||
onNavigateCallback(appid, event.url)
|
||||
}
|
||||
|
||||
webviewRef.current.addEventListener('new-window', handleNewWindow)
|
||||
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
|
||||
webviewRef.current.addEventListener('did-navigate-in-page', handleNavigate)
|
||||
|
||||
// we set the url when the webview is ready
|
||||
webviewRef.current.src = url
|
||||
|
||||
return () => {
|
||||
webviewRef.current?.removeEventListener('new-window', handleNewWindow)
|
||||
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
|
||||
webviewRef.current?.removeEventListener('did-navigate-in-page', handleNavigate)
|
||||
}
|
||||
// because the appid and url are enough, no need to add onLoadedCallback
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appid, url])
|
||||
|
||||
return (
|
||||
<webview
|
||||
key={appid}
|
||||
ref={setRef(appid)}
|
||||
style={WebviewStyle}
|
||||
allowpopups={'true' as any}
|
||||
partition="persist:webview"
|
||||
nodeintegration={'true' as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: 'calc(100vw - var(--sidebar-width))',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
backgroundColor: 'white',
|
||||
display: 'inline-flex'
|
||||
}
|
||||
|
||||
export default WebviewContainer
|
||||
@@ -1,282 +0,0 @@
|
||||
import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import store from '@renderer/store'
|
||||
import { setMinappShow } from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { delay } from '@renderer/utils'
|
||||
import { Avatar, Drawer } from 'antd'
|
||||
import { WebviewTag } from 'electron'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface Props {
|
||||
app: MinAppType
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const isPinned = pinned.some((p) => p.id === app.id)
|
||||
const [open, setOpen] = useState(true)
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const webviewRef = useRef<WebviewTag | null>(null)
|
||||
|
||||
useBridge()
|
||||
|
||||
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
|
||||
|
||||
const onClose = async (_delay = 0.3) => {
|
||||
setOpen(false)
|
||||
await delay(_delay)
|
||||
resolve({})
|
||||
}
|
||||
|
||||
MinApp.onClose = onClose
|
||||
const openDevTools = () => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.openDevTools()
|
||||
}
|
||||
}
|
||||
const onReload = () => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.src = app.url
|
||||
}
|
||||
}
|
||||
|
||||
const onOpenLink = () => {
|
||||
if (webviewRef.current) {
|
||||
const currentUrl = webviewRef.current.getURL()
|
||||
window.api.openWebsite(currentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const onTogglePin = () => {
|
||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
|
||||
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||
<TitleText>{app.name}</TitleText>
|
||||
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
||||
<Button onClick={onReload}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
{canPinned && (
|
||||
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
|
||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
)}
|
||||
{canOpenExternalLink && (
|
||||
<Button onClick={onOpenLink}>
|
||||
<ExportOutlined />
|
||||
</Button>
|
||||
)}
|
||||
{isInDevelopment && (
|
||||
<Button onClick={openDevTools}>
|
||||
<CodeOutlined />
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => onClose()}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
</ButtonsGroup>
|
||||
</TitleContainer>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const webview = webviewRef.current
|
||||
|
||||
if (webview) {
|
||||
const handleNewWindow = (event: any) => {
|
||||
event.preventDefault()
|
||||
if (webview.loadURL) {
|
||||
webview.loadURL(event.url)
|
||||
}
|
||||
}
|
||||
|
||||
const onLoaded = () => setIsReady(true)
|
||||
|
||||
webview.addEventListener('new-window', handleNewWindow)
|
||||
webview.addEventListener('did-finish-load', onLoaded)
|
||||
|
||||
return () => {
|
||||
webview.removeEventListener('new-window', handleNewWindow)
|
||||
webview.removeEventListener('did-finish-load', onLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}, [opened])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setOpened(true), 350)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={<Title />}
|
||||
placement="bottom"
|
||||
onClose={() => onClose()}
|
||||
open={open}
|
||||
mask={true}
|
||||
rootClassName="minapp-drawer"
|
||||
maskClassName="minapp-mask"
|
||||
height={'100%'}
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||
{!isReady && (
|
||||
<EmptyView>
|
||||
<Avatar src={app.logo} size={80} style={{ border: '1px solid var(--color-border)', marginTop: -150 }} />
|
||||
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
|
||||
</EmptyView>
|
||||
)}
|
||||
{opened && (
|
||||
<webview
|
||||
src={app.url}
|
||||
ref={webviewRef}
|
||||
style={WebviewStyle}
|
||||
allowpopups={'true' as any}
|
||||
partition="persist:webview"
|
||||
nodeintegration={true}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: 'calc(100vw - var(--sidebar-width))',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
backgroundColor: 'white',
|
||||
display: 'inline-flex'
|
||||
}
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '20px' : '10px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
const TitleText = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const ButtonsGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
-webkit-app-region: no-drag;
|
||||
&.windows {
|
||||
margin-right: ${isWindows ? '130px' : 0};
|
||||
background-color: var(--color-background-mute);
|
||||
border-radius: 50px;
|
||||
padding: 0 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.div`
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-2);
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
&.pinned {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-bg);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyView = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
export default class MinApp {
|
||||
static topviewId = 0
|
||||
static onClose = () => {}
|
||||
static app: MinAppType | null = null
|
||||
|
||||
static async start(app: MinAppType) {
|
||||
if (app?.id && MinApp.app?.id === app?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (MinApp.app) {
|
||||
// @ts-ignore delay params
|
||||
await MinApp.onClose(0)
|
||||
await delay(0)
|
||||
}
|
||||
|
||||
if (!app.logo) {
|
||||
app.logo = AppLogo
|
||||
}
|
||||
|
||||
MinApp.app = app
|
||||
store.dispatch(setMinappShow(true))
|
||||
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
app={app}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.close()
|
||||
}}
|
||||
/>,
|
||||
'MinApp'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
static close() {
|
||||
TopView.hide('MinApp')
|
||||
store.dispatch(setMinappShow(false))
|
||||
MinApp.app = null
|
||||
MinApp.onClose = () => {}
|
||||
}
|
||||
}
|
||||
250
src/renderer/src/components/NutstorePathSelector.tsx
Normal file
250
src/renderer/src/components/NutstorePathSelector.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { FolderIcon as NutstoreFolderIcon } from '@renderer/components/Icons/NutstoreIcons'
|
||||
import { Button, Input } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { HStack } from './Layout'
|
||||
|
||||
interface NewFolderProps {
|
||||
onConfirm: (name: string) => void
|
||||
onCancel: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NewFolderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
`
|
||||
|
||||
const FolderIcon = styled(NutstoreFolderIcon)`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
function NewFolder(props: NewFolderProps) {
|
||||
const { onConfirm, onCancel } = props
|
||||
const [name, setName] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<NewFolderContainer>
|
||||
<FolderIcon className={props.className}></FolderIcon>
|
||||
<Input type="text" style={{ flex: 1 }} autoFocus value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<Button type="primary" size="small" onClick={() => onConfirm(name)}>
|
||||
{t('settings.data.nutstore.new_folder.button.confirm')}
|
||||
</Button>
|
||||
<Button type="default" size="small" onClick={() => onCancel()}>
|
||||
{t('settings.data.nutstore.new_folder.button.cancel')}
|
||||
</Button>
|
||||
</NewFolderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface FolderProps {
|
||||
name: string
|
||||
path: string
|
||||
onClick: (path: string) => void
|
||||
}
|
||||
|
||||
const FolderContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 0 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
.nutstore-pathname {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
function Folder(props: FolderProps) {
|
||||
return (
|
||||
<FolderContainer onClick={() => props.onClick(props.path)}>
|
||||
<FolderIcon></FolderIcon>
|
||||
<span className="nutstore-pathname">{props.name}</span>
|
||||
</FolderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface FileListProps {
|
||||
path: string
|
||||
fs: Nutstore.Fs
|
||||
onClick: (file: Nutstore.FileStat) => void
|
||||
}
|
||||
|
||||
function FileList(props: FileListProps) {
|
||||
const [files, setFiles] = useState<Nutstore.FileStat[]>([])
|
||||
|
||||
const folders = files.filter((file) => file.isDir).sort((a, b) => a.basename.localeCompare(b.basename, ['zh']))
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchFiles() {
|
||||
try {
|
||||
const items = await props.fs.ls(props.path)
|
||||
setFiles(items)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error)
|
||||
window.modal.error({
|
||||
content: error.message,
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchFiles()
|
||||
}, [props.path, props.fs])
|
||||
|
||||
return (
|
||||
<>
|
||||
{folders.map((folder) => (
|
||||
<Folder key={folder.path} name={folder.basename} path={folder.path} onClick={() => props.onClick(folder)} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SingleFileListContainer = styled.div`
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new-folder {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.nutstore-current-path-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.nutstore-current-path {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.nutstore-path-operater {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
interface Props {
|
||||
fs: Nutstore.Fs
|
||||
onConfirm: (path: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function NutstorePathSelector(props: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [stack, setStack] = useState<string[]>(['/'])
|
||||
const [showNewFolder, setShowNewFolder] = useState(false)
|
||||
|
||||
const cwd = stack.at(-1)
|
||||
|
||||
const enter = useCallback((path: string) => {
|
||||
setStack((prev) => [...prev, path])
|
||||
}, [])
|
||||
|
||||
const pop = useCallback(() => {
|
||||
setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev))
|
||||
}, [])
|
||||
|
||||
const handleNewFolder = useCallback(
|
||||
async (name: string) => {
|
||||
const target = (cwd ?? '/') + (cwd && cwd !== '/' ? '/' : '') + name
|
||||
await props.fs.mkdirs(target)
|
||||
setShowNewFolder(false)
|
||||
enter(target)
|
||||
},
|
||||
[cwd, props.fs, enter]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<SingleFileListContainer>
|
||||
<div className="scroll-container">
|
||||
{showNewFolder && (
|
||||
<NewFolder className="new-folder" onConfirm={handleNewFolder} onCancel={() => setShowNewFolder(false)} />
|
||||
)}
|
||||
<FileList path={cwd ?? ''} fs={props.fs} onClick={(f) => enter(f.path)} />
|
||||
</div>
|
||||
</SingleFileListContainer>
|
||||
<div className="nutstore-current-path-container">
|
||||
<span>{t('settings.data.nutstore.pathSelector.currentPath')}</span>
|
||||
<span className="nutstore-current-path">{cwd ?? '/'}</span>
|
||||
</div>
|
||||
</Container>
|
||||
<NustorePathSelectorFooter
|
||||
returnPrev={pop}
|
||||
mkdir={() => setShowNewFolder(true)}
|
||||
cancel={props.onCancel}
|
||||
confirm={() => props.onConfirm(cwd ?? '')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const FooterContainer = styled(HStack)`
|
||||
background: transparent;
|
||||
margin-top: 12px;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
`
|
||||
|
||||
interface FooterProps {
|
||||
returnPrev: () => void
|
||||
mkdir: () => void
|
||||
cancel: () => void
|
||||
confirm: () => void
|
||||
}
|
||||
|
||||
export function NustorePathSelectorFooter(props: FooterProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<FooterContainer justifyContent="space-between">
|
||||
<HStack gap={8} alignItems="center">
|
||||
<Button onClick={props.returnPrev}>{t('settings.data.nutstore.pathSelector.return')}</Button>
|
||||
<Button size="small" type="link" onClick={props.mkdir}>
|
||||
{t('settings.data.nutstore.new_folder.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
<HStack gap={8} alignItems="center">
|
||||
<Button type="default" onClick={props.cancel}>
|
||||
{t('settings.data.nutstore.new_folder.button.cancel')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={props.confirm}>
|
||||
{t('backup.confirm.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</FooterContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +1,223 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
import { Form, Input, Modal, Select } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface ObsidianExportDialogProps {
|
||||
title: string
|
||||
markdown: string
|
||||
open: boolean // 使用 open 属性替代 visible
|
||||
open: boolean
|
||||
onClose: (success: boolean) => void
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
// 转换文件信息数组为树形结构
|
||||
const convertToTreeData = (files: FileInfo[]) => {
|
||||
const treeData: any[] = [
|
||||
{
|
||||
title: i18n.t('chat.topics.export.obsidian_root_directory'),
|
||||
value: '',
|
||||
isLeaf: false,
|
||||
selectable: true
|
||||
}
|
||||
]
|
||||
|
||||
// 记录已创建的节点路径
|
||||
const pathMap: Record<string, any> = {
|
||||
'': treeData[0]
|
||||
}
|
||||
|
||||
// 先按类型分组,确保先处理文件夹
|
||||
const folders = files.filter((file) => file.type === 'folder')
|
||||
const mdFiles = files.filter((file) => file.type === 'markdown')
|
||||
|
||||
// 按路径排序,确保父文件夹先被创建
|
||||
const sortedFolders = [...folders].sort((a, b) => a.path.split('/').length - b.path.split('/').length)
|
||||
|
||||
// 先处理所有文件夹,构建目录结构
|
||||
for (const folder of sortedFolders) {
|
||||
const parts = folder.path.split('/')
|
||||
let currentPath = ''
|
||||
let parentPath = ''
|
||||
|
||||
// 遍历文件夹路径的每一部分,确保创建完整路径
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
|
||||
// 构建当前路径
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||
|
||||
// 如果这个路径节点还没创建
|
||||
if (!pathMap[currentPath]) {
|
||||
const node = {
|
||||
title: part,
|
||||
value: currentPath,
|
||||
key: currentPath,
|
||||
isLeaf: false,
|
||||
selectable: true,
|
||||
children: []
|
||||
}
|
||||
|
||||
// 获取父节点,将当前节点添加到父节点的children中
|
||||
const parentNode = pathMap[parentPath]
|
||||
if (parentNode) {
|
||||
if (!parentNode.children) {
|
||||
parentNode.children = []
|
||||
}
|
||||
parentNode.children.push(node)
|
||||
}
|
||||
|
||||
pathMap[currentPath] = node
|
||||
}
|
||||
|
||||
// 更新父路径为当前路径,为下一级做准备
|
||||
parentPath = currentPath
|
||||
}
|
||||
}
|
||||
|
||||
// 然后处理md文件
|
||||
for (const file of mdFiles) {
|
||||
const fullPath = file.path
|
||||
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'))
|
||||
const fileName = file.name
|
||||
|
||||
// 获取父文件夹节点
|
||||
const parentNode = pathMap[dirPath] || pathMap['']
|
||||
|
||||
// 创建文件节点
|
||||
const fileNode = {
|
||||
title: fileName,
|
||||
value: fullPath,
|
||||
isLeaf: true,
|
||||
selectable: true,
|
||||
icon: <span style={{ marginRight: 4 }}>📄</span>
|
||||
}
|
||||
|
||||
// 添加到父节点
|
||||
if (!parentNode.children) {
|
||||
parentNode.children = []
|
||||
}
|
||||
parentNode.children.push(fileNode)
|
||||
}
|
||||
|
||||
return treeData
|
||||
}
|
||||
|
||||
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
title,
|
||||
markdown,
|
||||
obsidianTags,
|
||||
processingMethod,
|
||||
open,
|
||||
onClose
|
||||
onClose,
|
||||
obsidianTags,
|
||||
processingMethod
|
||||
}) => {
|
||||
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
||||
const [state, setState] = useState({
|
||||
title: title,
|
||||
title,
|
||||
tags: obsidianTags || '',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
source: 'Cherry Studio',
|
||||
processingMethod: processingMethod
|
||||
processingMethod: processingMethod,
|
||||
folder: ''
|
||||
})
|
||||
|
||||
const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
const [fileTreeData, setFileTreeData] = useState<any[]>([])
|
||||
const [selectedVault, setSelectedVault] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 处理文件数据转为树形结构
|
||||
useEffect(() => {
|
||||
if (files.length > 0) {
|
||||
const treeData = convertToTreeData(files)
|
||||
setFileTreeData(treeData)
|
||||
} else {
|
||||
setFileTreeData([
|
||||
{
|
||||
title: i18n.t('chat.topics.export.obsidian_root_directory'),
|
||||
value: '',
|
||||
isLeaf: false,
|
||||
selectable: true
|
||||
}
|
||||
])
|
||||
}
|
||||
}, [files])
|
||||
|
||||
// 组件加载时获取Vault列表
|
||||
useEffect(() => {
|
||||
const fetchVaults = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const vaultsData = await window.obsidian.getVaults()
|
||||
|
||||
if (vaultsData.length === 0) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vaults'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setVaults(vaultsData)
|
||||
|
||||
// 如果没有选择的vault,使用默认值或第一个
|
||||
const vaultToUse = defaultObsidianVault || vaultsData[0]?.name
|
||||
if (vaultToUse) {
|
||||
setSelectedVault(vaultToUse)
|
||||
|
||||
// 获取选中vault的文件和文件夹
|
||||
const filesData = await window.obsidian.getFiles(vaultToUse)
|
||||
setFiles(filesData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian Vault失败:', error)
|
||||
setError(i18n.t('chat.topics.export.obsidian_fetch_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchVaults()
|
||||
}, [defaultObsidianVault])
|
||||
|
||||
// 当选择的vault变化时,获取其文件和文件夹
|
||||
useEffect(() => {
|
||||
if (selectedVault) {
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const filesData = await window.obsidian.getFiles(selectedVault)
|
||||
setFiles(filesData)
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian文件失败:', error)
|
||||
setError(i18n.t('chat.topics.export.obsidian_fetch_folders_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchFiles()
|
||||
}
|
||||
}, [selectedVault])
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!selectedVault) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vault_selected'))
|
||||
return
|
||||
}
|
||||
|
||||
//构建content 并复制到粘贴板
|
||||
let content = ''
|
||||
if (state.processingMethod !== '3') {
|
||||
@@ -45,10 +232,18 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
}
|
||||
if (content === '') {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||
return
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(content)
|
||||
markdown = ''
|
||||
exportMarkdownToObsidian(state)
|
||||
|
||||
// 导出到Obsidian
|
||||
exportMarkdownToObsidian({
|
||||
...state,
|
||||
folder: state.folder,
|
||||
vault: selectedVault
|
||||
})
|
||||
|
||||
onClose(true)
|
||||
}
|
||||
|
||||
@@ -60,18 +255,56 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
const handleVaultChange = (value: string) => {
|
||||
setSelectedVault(value)
|
||||
// 文件夹会通过useEffect自动获取
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
folder: ''
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (value: string) => {
|
||||
// 更新folder值
|
||||
handleChange('folder', value)
|
||||
|
||||
// 检查是否选中md文件
|
||||
if (value) {
|
||||
const selectedFile = files.find((file) => file.path === value)
|
||||
if (selectedFile) {
|
||||
if (selectedFile.type === 'markdown') {
|
||||
// 如果是md文件,自动设置标题为文件名并设置处理方式为1(追加)
|
||||
const fileName = selectedFile.name
|
||||
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
|
||||
handleChange('title', titleWithoutExt)
|
||||
handleChange('processingMethod', '1')
|
||||
} else {
|
||||
// 如果是文件夹,自动设置标题为话题名并设置处理方式为3(新建)
|
||||
handleChange('processingMethod', '3')
|
||||
handleChange('title', title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||
open={open} // 使用 open 属性
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
closable
|
||||
maskClosable
|
||||
centered
|
||||
okButtonProps={{ type: 'primary' }}
|
||||
okButtonProps={{
|
||||
type: 'primary',
|
||||
disabled: vaults.length === 0 || loading || !!error
|
||||
}}
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||
{error && <Alert message={error} type="error" showIcon style={{ marginBottom: 16 }} />}
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||
<Input
|
||||
@@ -80,6 +313,55 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_vault')}>
|
||||
{vaults.length > 0 ? (
|
||||
<Select
|
||||
loading={loading}
|
||||
value={selectedVault}
|
||||
onChange={handleVaultChange}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_vault_placeholder')}
|
||||
style={{ width: '100%' }}>
|
||||
{vaults.map((vault) => (
|
||||
<Option key={vault.name} value={vault.name}>
|
||||
{vault.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Empty
|
||||
description={
|
||||
loading
|
||||
? i18n.t('chat.topics.export.obsidian_loading')
|
||||
: i18n.t('chat.topics.export.obsidian_no_vaults')
|
||||
}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_path')}>
|
||||
<Spin spinning={loading}>
|
||||
{selectedVault ? (
|
||||
<TreeSelect
|
||||
value={state.folder}
|
||||
onChange={handleFileSelect}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_path_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeDefaultExpandAll={false}
|
||||
treeNodeFilterProp="title"
|
||||
treeData={fileTreeData}></TreeSelect>
|
||||
) : (
|
||||
<Empty
|
||||
description={i18n.t('chat.topics.export.obsidian_select_vault_first')}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||
<Input
|
||||
value={state.tags}
|
||||
@@ -101,6 +383,7 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
|
||||
<Select
|
||||
value={state.processingMethod}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Agent, Assistant } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
|
||||
import { take } from 'lodash'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -30,6 +30,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const systemAgents = useSystemAgents()
|
||||
const loadingRef = useRef(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||
@@ -52,25 +54,80 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
return filtered
|
||||
}, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
|
||||
|
||||
const onCreateAssistant = async (agent: Agent) => {
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
// 重置选中索引当搜索或列表内容变更时
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [agents.length, searchText])
|
||||
|
||||
const onCreateAssistant = useCallback(
|
||||
async (agent: Agent) => {
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
let assistant: Assistant
|
||||
|
||||
if (agent.id === 'default') {
|
||||
assistant = { ...agent, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
} else {
|
||||
assistant = await createAssistantFromAgent(agent)
|
||||
}
|
||||
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
resolve(assistant)
|
||||
setOpen(false)
|
||||
},
|
||||
[resolve, addAssistant, setOpen]
|
||||
) // 添加函数内使用的依赖项
|
||||
// 键盘导航处理
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const displayedAgents = take(agents, 100)
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev >= displayedAgents.length - 1 ? 0 : prev + 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev <= 0 ? displayedAgents.length - 1 : prev - 1))
|
||||
break
|
||||
case 'Enter':
|
||||
// 如果焦点在输入框且有搜索内容,则默认选择第一项
|
||||
if (document.activeElement === inputRef.current?.input && searchText.trim()) {
|
||||
e.preventDefault()
|
||||
onCreateAssistant(displayedAgents[selectedIndex])
|
||||
}
|
||||
// 否则选择当前选中项
|
||||
else if (selectedIndex >= 0 && selectedIndex < displayedAgents.length) {
|
||||
e.preventDefault()
|
||||
onCreateAssistant(displayedAgents[selectedIndex])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
let assistant: Assistant
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open, selectedIndex, agents, searchText, onCreateAssistant])
|
||||
|
||||
if (agent.id === 'default') {
|
||||
assistant = { ...agent, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
} else {
|
||||
assistant = await createAssistantFromAgent(agent)
|
||||
// 确保选中项在可视区域
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const agentItems = containerRef.current.querySelectorAll('.agent-item')
|
||||
if (agentItems[selectedIndex]) {
|
||||
agentItems[selectedIndex].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
resolve(assistant)
|
||||
setOpen(false)
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
@@ -121,12 +178,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
/>
|
||||
</HStack>
|
||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||
<Container>
|
||||
{take(agents, 100).map((agent) => (
|
||||
<Container ref={containerRef}>
|
||||
{take(agents, 100).map((agent, index) => (
|
||||
<AgentItem
|
||||
key={agent.id}
|
||||
onClick={() => onCreateAssistant(agent)}
|
||||
className={agent.id === 'default' ? 'default' : ''}>
|
||||
className={`agent-item ${agent.id === 'default' ? 'default' : ''} ${index === selectedIndex ? 'keyboard-selected' : ''}`}
|
||||
onMouseEnter={() => setSelectedIndex(index)}>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
gap={5}
|
||||
@@ -161,9 +219,14 @@ const AgentItem = styled.div`
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
&.default {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
&.keyboard-selected {
|
||||
background-color: var(--color-background-mute);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--color-icon);
|
||||
|
||||
60
src/renderer/src/components/Popups/NutsorePathPopup.tsx
Normal file
60
src/renderer/src/components/Popups/NutsorePathPopup.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { NutstorePathSelector } from '../NutstorePathSelector'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface Props {
|
||||
fs: Nutstore.Fs
|
||||
resolve: (data: string | null) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve, fs }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={t('settings.data.nutstore.pathSelector.title')}
|
||||
transitionName="ant-move-down"
|
||||
afterClose={onClose}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
centered>
|
||||
<NutstorePathSelector fs={fs} onConfirm={resolve} onCancel={onCancel} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'NutstorePathPopup'
|
||||
|
||||
export default class NutstorePathPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(fs: Nutstore.Fs) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
fs={fs}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
interface ObsidianExportOptions {
|
||||
@@ -17,14 +15,6 @@ interface ObsidianExportOptions {
|
||||
* @returns
|
||||
*/
|
||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||
const obsidianValut = store.getState().settings.obsidianValut
|
||||
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||
|
||||
if (!obsidianValut || !obsidianFolder) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||
return false
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
@@ -35,12 +25,12 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
|
||||
document.body.removeChild(div)
|
||||
resolve(success)
|
||||
}
|
||||
const obsidianTags = store.getState().settings.obsidianTages
|
||||
// 不再从store中获取tag配置
|
||||
root.render(
|
||||
<ObsidianExportDialog
|
||||
title={options.title}
|
||||
markdown={options.markdown}
|
||||
obsidianTags={obsidianTags}
|
||||
obsidianTags=""
|
||||
processingMethod={options.processingMethod}
|
||||
open={true}
|
||||
onClose={handleClose}
|
||||
@@ -49,8 +39,6 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
|
||||
})
|
||||
}
|
||||
|
||||
const ObsidianExportPopup = {
|
||||
export default {
|
||||
show: showObsidianExportDialog
|
||||
}
|
||||
|
||||
export default ObsidianExportPopup
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { throttle } from 'lodash'
|
||||
import { FC, forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -7,7 +7,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
ref?: any
|
||||
}
|
||||
|
||||
const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const Scrollbar: FC<Props> = ({ ref, ...props }: Props & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||
const [isScrolling, setIsScrolling] = useState(false)
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
@@ -35,7 +35,7 @@ const Scrollbar: FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
{props.children}
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const Container = styled.div<{ isScrolling: boolean; right?: boolean }>`
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { message, Modal } from 'antd'
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
||||
@@ -76,6 +77,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
{children}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
<TopViewMinappContainer />
|
||||
{elements.map(({ element: Element, id }) => (
|
||||
<FullScreenContainer key={`TOPVIEW_${id}`}>
|
||||
{typeof Element === 'function' ? <Element /> : Element}
|
||||
|
||||
103
src/renderer/src/components/VariableList.tsx
Normal file
103
src/renderer/src/components/VariableList.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { DeleteOutlined, ImportOutlined } from '@ant-design/icons'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import { Variable } from '@renderer/types'
|
||||
import { Button, Input, Tooltip } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface VariableListProps {
|
||||
variables: Variable[]
|
||||
setVariables: (variables: Variable[]) => void
|
||||
onUpdate?: (variables: Variable[]) => void
|
||||
onInsertVariable?: (name: string) => void
|
||||
}
|
||||
|
||||
const VariableList: React.FC<VariableListProps> = ({ variables, setVariables, onUpdate, onInsertVariable }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const deleteVariable = (id: string) => {
|
||||
const updatedVariables = variables.filter((v) => v.id !== id)
|
||||
setVariables(updatedVariables)
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate(updatedVariables)
|
||||
}
|
||||
}
|
||||
|
||||
const updateVariable = (id: string, field: 'name' | 'value', value: string) => {
|
||||
// Only update the local state when typing, don't call the parent's onUpdate
|
||||
const updatedVariables = variables.map((v) => (v.id === id ? { ...v, [field]: value } : v))
|
||||
setVariables(updatedVariables)
|
||||
}
|
||||
|
||||
// This function will be called when input loses focus
|
||||
const handleInputBlur = () => {
|
||||
if (onUpdate) {
|
||||
onUpdate(variables)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VariablesContainer>
|
||||
{variables.length === 0 ? (
|
||||
<EmptyText>{t('common.no_variables_added')}</EmptyText>
|
||||
) : (
|
||||
<VStack gap={8} width="100%">
|
||||
{variables.map((variable) => (
|
||||
<VariableItem key={variable.id}>
|
||||
<Input
|
||||
placeholder={t('common.variable_name')}
|
||||
value={variable.name}
|
||||
onChange={(e) => updateVariable(variable.id, 'name', e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
style={{ width: '30%' }}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('common.value')}
|
||||
value={variable.value}
|
||||
onChange={(e) => updateVariable(variable.id, 'value', e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
{onInsertVariable && (
|
||||
<Tooltip title={t('common.insert_variable_into_prompt')}>
|
||||
<Button type="text" onClick={() => onInsertVariable(variable.name)}>
|
||||
<ImportOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => deleteVariable(variable.id)} />
|
||||
</VariableItem>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</VariablesContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const VariablesContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
`
|
||||
|
||||
const VariableItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const EmptyText = styled.div`
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
export default VariableList
|
||||
237
src/renderer/src/components/WebdavModals.tsx
Normal file
237
src/renderer/src/components/WebdavModals.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { backupToWebdav, restoreFromWebdav } from '@renderer/services/BackupService'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Input, Modal, Select, Spin } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface BackupFile {
|
||||
fileName: string
|
||||
modifiedTime: string
|
||||
size: number
|
||||
}
|
||||
|
||||
interface WebdavModalProps {
|
||||
isModalVisible: boolean
|
||||
handleBackup: () => void
|
||||
handleCancel: () => void
|
||||
backuping: boolean
|
||||
customFileName: string
|
||||
setCustomFileName: (value: string) => void
|
||||
}
|
||||
|
||||
export function useWebdavBackupModal({ backupMethod }: { backupMethod?: typeof backupToWebdav } = {}) {
|
||||
const [customFileName, setCustomFileName] = useState('')
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [backuping, setBackuping] = useState(false)
|
||||
|
||||
const handleBackup = async () => {
|
||||
setBackuping(true)
|
||||
try {
|
||||
await (backupMethod ?? backupToWebdav)({ showMessage: true, customFileName })
|
||||
} finally {
|
||||
setBackuping(false)
|
||||
setIsModalVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false)
|
||||
}
|
||||
|
||||
const showBackupModal = useCallback(async () => {
|
||||
// 获取默认文件名
|
||||
const deviceType = await window.api.system.getDeviceType()
|
||||
const timestamp = dayjs().format('YYYYMMDDHHmmss')
|
||||
const defaultFileName = `cherry-studio.${timestamp}.${deviceType}.zip`
|
||||
setCustomFileName(defaultFileName)
|
||||
setIsModalVisible(true)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
isModalVisible,
|
||||
handleBackup,
|
||||
handleCancel,
|
||||
backuping,
|
||||
customFileName,
|
||||
setCustomFileName,
|
||||
showBackupModal
|
||||
}
|
||||
}
|
||||
|
||||
export function WebdavBackupModal({
|
||||
isModalVisible,
|
||||
handleBackup,
|
||||
handleCancel,
|
||||
backuping,
|
||||
customFileName,
|
||||
setCustomFileName
|
||||
}: WebdavModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.data.webdav.backup.modal.title')}
|
||||
open={isModalVisible}
|
||||
onOk={handleBackup}
|
||||
onCancel={handleCancel}
|
||||
okButtonProps={{ loading: backuping }}>
|
||||
<Input
|
||||
value={customFileName}
|
||||
onChange={(e) => setCustomFileName(e.target.value)}
|
||||
placeholder={t('settings.data.webdav.backup.modal.filename.placeholder')}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
interface WebdavRestoreModalProps {
|
||||
isRestoreModalVisible: boolean
|
||||
handleRestore: () => void
|
||||
handleCancel: () => void
|
||||
restoring: boolean
|
||||
selectedFile: string | null
|
||||
setSelectedFile: (value: string | null) => void
|
||||
loadingFiles: boolean
|
||||
backupFiles: BackupFile[]
|
||||
}
|
||||
|
||||
interface UseWebdavRestoreModalProps {
|
||||
webdavHost: string | undefined
|
||||
webdavUser: string | undefined
|
||||
webdavPass: string | undefined
|
||||
webdavPath: string | undefined
|
||||
restoreMethod?: typeof restoreFromWebdav
|
||||
}
|
||||
|
||||
export function useWebdavRestoreModal({
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath,
|
||||
restoreMethod
|
||||
}: UseWebdavRestoreModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isRestoreModalVisible, setIsRestoreModalVisible] = useState(false)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
||||
const [loadingFiles, setLoadingFiles] = useState(false)
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
|
||||
|
||||
const showRestoreModal = useCallback(async () => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||
return
|
||||
}
|
||||
|
||||
setIsRestoreModalVisible(true)
|
||||
setLoadingFiles(true)
|
||||
try {
|
||||
const files = await window.api.backup.listWebdavFiles({
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath
|
||||
})
|
||||
setBackupFiles(files)
|
||||
} catch (error: any) {
|
||||
window.message.error({ content: error.message, key: 'list-files-error' })
|
||||
} finally {
|
||||
setLoadingFiles(false)
|
||||
}
|
||||
}, [webdavHost, webdavUser, webdavPass, webdavPath, t])
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
window.message.error({
|
||||
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
|
||||
key: 'restore-error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.webdav.restore.confirm.title'),
|
||||
content: t('settings.data.webdav.restore.confirm.content'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
setRestoring(true)
|
||||
try {
|
||||
await (restoreMethod ?? restoreFromWebdav)(selectedFile)
|
||||
setIsRestoreModalVisible(false)
|
||||
} catch (error: any) {
|
||||
window.message.error({ content: error.message, key: 'restore-error' })
|
||||
} finally {
|
||||
setRestoring(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [selectedFile, webdavHost, webdavUser, webdavPass, webdavPath, t, restoreMethod])
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsRestoreModalVisible(false)
|
||||
}
|
||||
|
||||
return {
|
||||
isRestoreModalVisible,
|
||||
handleRestore,
|
||||
handleCancel,
|
||||
restoring,
|
||||
selectedFile,
|
||||
setSelectedFile,
|
||||
loadingFiles,
|
||||
backupFiles,
|
||||
showRestoreModal
|
||||
}
|
||||
}
|
||||
|
||||
export function WebdavRestoreModal({
|
||||
isRestoreModalVisible,
|
||||
handleRestore,
|
||||
handleCancel,
|
||||
restoring,
|
||||
selectedFile,
|
||||
setSelectedFile,
|
||||
loadingFiles,
|
||||
backupFiles
|
||||
}: WebdavRestoreModalProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.data.webdav.restore.modal.title')}
|
||||
open={isRestoreModalVisible}
|
||||
onOk={handleRestore}
|
||||
onCancel={handleCancel}
|
||||
okButtonProps={{ loading: restoring }}
|
||||
width={600}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('settings.data.webdav.restore.modal.select.placeholder')}
|
||||
value={selectedFile}
|
||||
onChange={setSelectedFile}
|
||||
options={backupFiles.map(formatFileOption)}
|
||||
loading={loadingFiles}
|
||||
showSearch
|
||||
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
{loadingFiles && (
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function formatFileOption(file: BackupFile) {
|
||||
const date = dayjs(file.modifiedTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
const size = formatFileSize(file.size)
|
||||
return {
|
||||
label: `${file.fileName} (${date}, ${size})`,
|
||||
value: file.fileName
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
@@ -62,4 +63,6 @@ const NavbarRightContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
padding-right: ${isWindows ? '140px' : 12};
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
@@ -9,35 +9,36 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { isEmoji } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Avatar } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import DragableList from '../DragableList'
|
||||
import MinAppIcon from '../Icons/MinAppIcon'
|
||||
import MinApp from '../MinApp'
|
||||
import UserPopup from '../Popups/UserPopup'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const avatar = useAvatar()
|
||||
const { minappShow } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { hideMinappPopup, openMinapp } = useMinappPopup()
|
||||
const { minappShow, currentMinappId } = useRuntime()
|
||||
const { sidebarIcons } = useSettings()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { pinned } = useMinapps()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { theme, settingTheme, toggleTheme } = useTheme()
|
||||
const avatar = useAvatar()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onEditUser = () => UserPopup.show()
|
||||
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
@@ -49,9 +50,10 @@ const Sidebar: FC = () => {
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const docsId = 'cherrystudio-docs'
|
||||
const onOpenDocs = () => {
|
||||
MinApp.start({
|
||||
id: 'docs',
|
||||
openMinapp({
|
||||
id: docsId,
|
||||
name: t('docs.title'),
|
||||
url: 'https://docs.cherry-ai.com/',
|
||||
logo: AppLogo
|
||||
@@ -66,9 +68,10 @@ const Sidebar: FC = () => {
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
)}
|
||||
<MainMenusContainer>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Menus onClick={hideMinappPopup}>
|
||||
<MainMenus />
|
||||
</Menus>
|
||||
<SidebarOpenedMinappTabs />
|
||||
{showPinnedApps && (
|
||||
<AppsContainer>
|
||||
<Divider />
|
||||
@@ -80,14 +83,14 @@ const Sidebar: FC = () => {
|
||||
</MainMenusContainer>
|
||||
<Menus>
|
||||
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Icon
|
||||
theme={theme}
|
||||
onClick={onOpenDocs}
|
||||
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
|
||||
<Icon theme={theme} onClick={onOpenDocs} className={minappShow && currentMinappId === docsId ? 'active' : ''}>
|
||||
<QuestionCircleOutlined />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Tooltip
|
||||
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settingTheme}`)}
|
||||
mouseEnterDelay={0.8}
|
||||
placement="right">
|
||||
<Icon theme={theme} onClick={() => toggleTheme()}>
|
||||
{theme === 'dark' ? (
|
||||
<i className="iconfont icon-theme icon-dark1" />
|
||||
@@ -99,7 +102,7 @@ const Sidebar: FC = () => {
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink
|
||||
onClick={async () => {
|
||||
minappShow && (await MinApp.close())
|
||||
hideMinappPopup()
|
||||
await modelGenerating()
|
||||
await to('/settings/provider')
|
||||
}}>
|
||||
@@ -114,6 +117,7 @@ const Sidebar: FC = () => {
|
||||
}
|
||||
|
||||
const MainMenus: FC = () => {
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { pathname } = useLocation()
|
||||
const { sidebarIcons } = useSettings()
|
||||
@@ -152,7 +156,7 @@ const MainMenus: FC = () => {
|
||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink
|
||||
onClick={async () => {
|
||||
minappShow && (await MinApp.close())
|
||||
hideMinappPopup()
|
||||
await modelGenerating()
|
||||
navigate(path)
|
||||
}}>
|
||||
@@ -165,11 +169,103 @@ const MainMenus: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** Tabs of opened minapps in sidebar */
|
||||
const SidebarOpenedMinappTabs: FC = () => {
|
||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
||||
const { showOpenedMinappsInSidebar } = useSettings() // 获取控制显示的设置
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnClick = (app) => {
|
||||
if (minappShow && currentMinappId === app.id) {
|
||||
hideMinappPopup()
|
||||
} else {
|
||||
openMinappKeepAlive(app)
|
||||
}
|
||||
}
|
||||
|
||||
// animation for minapp switch indicator
|
||||
useEffect(() => {
|
||||
//hacky way to get the height of the icon
|
||||
const iconDefaultHeight = 40
|
||||
const iconDefaultOffset = 17
|
||||
const container = document.querySelector('.TabsContainer') as HTMLElement
|
||||
const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
|
||||
|
||||
let indicatorTop = 0,
|
||||
indicatorRight = 0
|
||||
if (minappShow && activeIcon && container) {
|
||||
indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4 // 4 is half of the indicator's height (8px)
|
||||
indicatorRight = 0
|
||||
} else {
|
||||
indicatorTop =
|
||||
((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
|
||||
iconDefaultOffset -
|
||||
4
|
||||
indicatorRight = -50
|
||||
}
|
||||
container.style.setProperty('--indicator-top', `${indicatorTop}px`)
|
||||
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
|
||||
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
|
||||
|
||||
// 检查是否需要显示已打开小程序组件
|
||||
const isShowOpened = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0
|
||||
|
||||
// 如果不需要显示,返回空容器保持动画效果但不显示内容
|
||||
if (!isShowOpened) return <TabsContainer className="TabsContainer" />
|
||||
|
||||
return (
|
||||
<TabsContainer className="TabsContainer">
|
||||
<Divider />
|
||||
<TabsWrapper>
|
||||
<Menus>
|
||||
{openedKeepAliveMinapps.map((app) => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'closeApp',
|
||||
label: t('minapp.sidebar.close.title'),
|
||||
onClick: () => {
|
||||
closeMinapp(app.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'closeAllApp',
|
||||
label: t('minapp.sidebar.closeall.title'),
|
||||
onClick: () => {
|
||||
closeAllMinapps()
|
||||
}
|
||||
}
|
||||
]
|
||||
const isActive = minappShow && currentMinappId === app.id
|
||||
|
||||
return (
|
||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||
<Icon
|
||||
theme={theme}
|
||||
onClick={() => handleOnClick(app)}
|
||||
className={`${isActive ? 'opened-active' : ''}`}>
|
||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||
</Icon>
|
||||
</Dropdown>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</Menus>
|
||||
</TabsWrapper>
|
||||
</TabsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const PinnedApps: FC = () => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
const { minappShow } = useRuntime()
|
||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||
const { theme } = useTheme()
|
||||
const { openMinappKeepAlive } = useMinappPopup()
|
||||
|
||||
return (
|
||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||
@@ -184,12 +280,15 @@ const PinnedApps: FC = () => {
|
||||
}
|
||||
}
|
||||
]
|
||||
const isActive = minappShow && MinApp.app?.id === app.id
|
||||
const isActive = minappShow && currentMinappId === app.id
|
||||
return (
|
||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Icon theme={theme} onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||
<Icon
|
||||
theme={theme}
|
||||
onClick={() => openMinappKeepAlive(app)}
|
||||
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-animation' : ''}`}>
|
||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||
</Icon>
|
||||
</Dropdown>
|
||||
@@ -262,6 +361,7 @@ const Icon = styled.div<{ theme: string }>`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
-webkit-app-region: none;
|
||||
border: 0.5px solid transparent;
|
||||
.iconfont,
|
||||
@@ -290,6 +390,39 @@ const Icon = styled.div<{ theme: string }>`
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderBreath {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&.opened-animation {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.opened-animation::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
border: 0.5px solid var(--color-primary);
|
||||
/* NOTICE: although we have optimized for the performance,
|
||||
* the infinite animation will still consume a little GPU resources,
|
||||
* it's a trade-off balance between performance and animation smoothness*/
|
||||
animation: borderBreath 4s ease-in-out infinite;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLink = styled.div`
|
||||
@@ -320,4 +453,37 @@ const Divider = styled.div`
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const TabsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
-webkit-app-region: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: var(--indicator-right, 0);
|
||||
top: var(--indicator-top, 0);
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
background-color: var(--color-primary);
|
||||
transition:
|
||||
top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
right 0.3s ease-in-out;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const TabsWrapper = styled.div`
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default Sidebar
|
||||
|
||||
@@ -49,9 +49,7 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
{
|
||||
id: 'openai',
|
||||
@@ -395,8 +393,3 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
bodered: true
|
||||
}
|
||||
]
|
||||
|
||||
export function startMinAppById(id: string) {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
||||
app && MinApp.start(app)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ const visionAllowedModels = [
|
||||
'minicpm',
|
||||
'gemini-1\\.5',
|
||||
'gemini-2\\.0',
|
||||
'gemini-2\\.5',
|
||||
'gemini-exp',
|
||||
'claude-3',
|
||||
'vision',
|
||||
@@ -149,12 +150,14 @@ const visionAllowedModels = [
|
||||
'qwen-vl',
|
||||
'qwen2-vl',
|
||||
'qwen2.5-vl',
|
||||
'qwen2.5-omni',
|
||||
'qvq',
|
||||
'internvl2',
|
||||
'grok-vision-beta',
|
||||
'pixtral',
|
||||
'gpt-4(?:-[\\w-]+)',
|
||||
'gpt-4o(?:-[\\w-]+)?',
|
||||
'gpt-4.5(?:-[\\w-]+)',
|
||||
'chatgpt-4o(?:-[\\w-]+)?',
|
||||
'o1(?:-[\\w-]+)?',
|
||||
'deepseek-vl(?:[\\w-]+)?',
|
||||
@@ -162,7 +165,15 @@ const visionAllowedModels = [
|
||||
'gemma-3(?:-[\\w-]+)'
|
||||
]
|
||||
|
||||
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
||||
const visionExcludedModels = [
|
||||
'gpt-4-\\d+-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-32k',
|
||||
'gpt-4-\\d+',
|
||||
'o1-mini',
|
||||
'o1-preview',
|
||||
'AIDC-AI/Marco-o1'
|
||||
]
|
||||
export const VISION_REGEX = new RegExp(
|
||||
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
|
||||
'i'
|
||||
@@ -173,7 +184,7 @@ export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|
|
||||
|
||||
// Reasoning models
|
||||
export const REASONING_REGEX =
|
||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*)$/i
|
||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*)$/i
|
||||
|
||||
// Embedding models
|
||||
export const EMBEDDING_REGEX =
|
||||
@@ -190,15 +201,23 @@ export const FUNCTION_CALLING_MODELS = [
|
||||
'gpt-4o-mini',
|
||||
'gpt-4',
|
||||
'gpt-4.5',
|
||||
'o1(?:-[\\w-]+)?',
|
||||
'claude',
|
||||
'qwen',
|
||||
'hunyuan',
|
||||
'deepseek',
|
||||
'glm-4(?:-[\\w-]+)?',
|
||||
'learnlm(?:-[\\w-]+)?',
|
||||
'gemini(?:-[\\w-]+)?' // 提前排除了gemini的嵌入模型
|
||||
]
|
||||
|
||||
const FUNCTION_CALLING_EXCLUDED_MODELS = ['aqa(?:-[\\w-]+)?', 'imagen(?:-[\\w-]+)?']
|
||||
const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
||||
'aqa(?:-[\\w-]+)?',
|
||||
'imagen(?:-[\\w-]+)?',
|
||||
'o1-mini',
|
||||
'o1-preview',
|
||||
'AIDC-AI/Marco-o1'
|
||||
]
|
||||
|
||||
export const FUNCTION_CALLING_REGEX = new RegExp(
|
||||
`\\b(?!(?:${FUNCTION_CALLING_EXCLUDED_MODELS.join('|')})\\b)(?:${FUNCTION_CALLING_MODELS.join('|')})\\b`,
|
||||
@@ -1212,7 +1231,140 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'Step 1'
|
||||
}
|
||||
],
|
||||
doubao: [],
|
||||
doubao: [
|
||||
{
|
||||
id: 'doubao-1-5-vision-pro-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-vision-pro',
|
||||
group: 'Doubao-1.5-vision-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-pro-32k',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-32k-character-250228',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-pro-32k-character',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-256k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-1.5-pro-256k',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-distill-qwen-32b-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1-Distill-Qwen-32B',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-distill-qwen-7b-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1-Distill-Qwen-7B',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v3-250324',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-V3',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v3-250324',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-V3',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-functioncall-241028',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k-functioncall-241028',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-character-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k-character-241215',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-256k-241115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-256k',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-4k-character-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-4k-character-240828',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-32k-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-32k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-32k-character-241015',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-32k-character-241015',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-128k-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-128k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-lite-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-1.5-lite-32k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-large-text-240915',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding-large',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-text-240715',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-vision-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding-vision',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-vision-lite-32k-241015',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-vision-lite-32k',
|
||||
group: 'Doubao-vision-lite-32k'
|
||||
}
|
||||
],
|
||||
minimax: [
|
||||
{
|
||||
id: 'abab6.5s-chat',
|
||||
@@ -1541,15 +1693,15 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'Llama3'
|
||||
},
|
||||
{
|
||||
id: 'mixtral-8x7b-32768',
|
||||
id: 'mistral-saba-24b',
|
||||
provider: 'groq',
|
||||
name: 'Mixtral 8x7B',
|
||||
group: 'Mixtral'
|
||||
name: 'Mistral Saba 24B',
|
||||
group: 'Mistral'
|
||||
},
|
||||
{
|
||||
id: 'gemma-7b-it',
|
||||
id: 'gemma-9b-it',
|
||||
provider: 'groq',
|
||||
name: 'Gemma 7B',
|
||||
name: 'Gemma 9B',
|
||||
group: 'Gemma'
|
||||
}
|
||||
],
|
||||
@@ -1939,6 +2091,17 @@ export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
|
||||
|
||||
export const GENERATE_IMAGE_MODELS = ['gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-exp']
|
||||
|
||||
export const GEMINI_SEARCH_MODELS = [
|
||||
'gemini-2.0-flash',
|
||||
'gemini-2.0-flash-lite',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-001',
|
||||
'gemini-2.0-pro-exp-02-05',
|
||||
'gemini-2.0-pro-exp',
|
||||
'gemini-2.5-pro-exp',
|
||||
'gemini-2.5-pro-exp-03-25'
|
||||
]
|
||||
|
||||
export function isTextToImageModel(model: Model): boolean {
|
||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||
}
|
||||
@@ -1986,6 +2149,18 @@ export function isOpenAIoSeries(model: Model): boolean {
|
||||
return ['o1', 'o1-2024-12-17'].includes(model.id) || model.id.includes('o3')
|
||||
}
|
||||
|
||||
export function isSupportedResoningEffortModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet') || isOpenAIoSeries(model)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function isReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
@@ -1999,6 +2174,10 @@ export function isReasoningModel(model?: Model): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
if (model.id.includes('gemini-2.5-pro-exp')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false
|
||||
}
|
||||
|
||||
@@ -2027,32 +2206,25 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
const models = ['gemini-2.0-flash-search', 'gemini-2.0-flash-exp-search', 'gemini-2.0-pro-exp-02-05-search']
|
||||
return models.includes(model?.id)
|
||||
}
|
||||
|
||||
if (provider?.type === 'openai') {
|
||||
if (model?.id?.includes('gemini-2.0-flash-exp')) {
|
||||
if (GEMINI_SEARCH_MODELS.includes(model?.id)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini') {
|
||||
const models = [
|
||||
'gemini-2.0-flash',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-001',
|
||||
'gemini-2.0-pro-exp-02-05',
|
||||
'gemini-2.0-pro-exp'
|
||||
]
|
||||
return models.includes(model?.id)
|
||||
return GEMINI_SEARCH_MODELS.includes(model?.id)
|
||||
}
|
||||
|
||||
if (provider.id === 'hunyuan') {
|
||||
return model?.id !== 'hunyuan-lite'
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
const models = ['gemini-2.0-flash-search', 'gemini-2.0-flash-exp-search', 'gemini-2.0-pro-exp-02-05-search']
|
||||
return models.includes(model?.id)
|
||||
}
|
||||
|
||||
if (provider.id === 'zhipu') {
|
||||
return model?.id?.startsWith('glm-4-')
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import isPropValid from '@emotion/is-prop-valid'
|
||||
import { ReactNode } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { StyleSheetManager as StyledComponentsStyleSheetManager } from 'styled-components'
|
||||
|
||||
interface StyleSheetManagerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StyleSheetManager = ({ children }: StyleSheetManagerProps): JSX.Element => {
|
||||
const StyleSheetManager = ({ children }: StyleSheetManagerProps): React.ReactElement => {
|
||||
return (
|
||||
<StyledComponentsStyleSheetManager
|
||||
shouldForwardProp={(prop, element) => {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
BundledLanguage,
|
||||
bundledLanguages,
|
||||
BundledTheme,
|
||||
bundledThemes,
|
||||
createHighlighter,
|
||||
HighlighterGeneric
|
||||
} from 'shiki'
|
||||
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import type React from 'react'
|
||||
import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
|
||||
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki'
|
||||
|
||||
interface SyntaxHighlighterContextType {
|
||||
codeToHtml: (code: string, language: string) => Promise<string>
|
||||
@@ -51,42 +46,47 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
||||
initHighlighter()
|
||||
}, [highlighterTheme])
|
||||
|
||||
const codeToHtml = async (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
const codeToHtml = useCallback(
|
||||
async (_code: string, language: string) => {
|
||||
{
|
||||
if (!highlighter) return ''
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
vab: 'vb'
|
||||
}
|
||||
const languageMap: Record<string, string> = {
|
||||
vab: 'vb'
|
||||
}
|
||||
|
||||
const mappedLanguage = languageMap[language] || language
|
||||
const mappedLanguage = languageMap[language] || language
|
||||
|
||||
code = code?.trimEnd() ?? ''
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
const code = _code?.trimEnd() ?? ''
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage as BundledLanguage)) {
|
||||
if (mappedLanguage in bundledLanguages || mappedLanguage === 'text') {
|
||||
await highlighter.loadLanguage(mappedLanguage as BundledLanguage)
|
||||
} else {
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage as BundledLanguage)) {
|
||||
if (mappedLanguage in bundledLanguages || mappedLanguage === 'text') {
|
||||
await highlighter.loadLanguage(mappedLanguage as BundledLanguage)
|
||||
} else {
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: mappedLanguage,
|
||||
theme: highlighterTheme
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error highlighting code for language '${mappedLanguage}':`, error)
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
},
|
||||
[highlighter, highlighterTheme]
|
||||
)
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: mappedLanguage,
|
||||
theme: highlighterTheme
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error highlighting code for language '${mappedLanguage}':`, error)
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
|
||||
return <SyntaxHighlighterContext value={{ codeToHtml }}>{children}</SyntaxHighlighterContext>
|
||||
}
|
||||
|
||||
export const useSyntaxHighlighter = () => {
|
||||
const context = useContext(SyntaxHighlighterContext)
|
||||
const context = use(SyntaxHighlighterContext)
|
||||
if (!context) {
|
||||
throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider')
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: ThemeMode
|
||||
settingTheme: ThemeMode
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>({
|
||||
theme: ThemeMode.light,
|
||||
settingTheme: ThemeMode.light,
|
||||
toggleTheme: () => {}
|
||||
})
|
||||
|
||||
@@ -55,7 +57,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
||||
}
|
||||
})
|
||||
|
||||
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
|
||||
return <ThemeContext value={{ theme: _theme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext>
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
export const useTheme = () => use(ThemeContext)
|
||||
|
||||
@@ -11,14 +11,13 @@ import { useEffect } from 'react'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
import { useInitMCPServers } from './useMCPServers'
|
||||
import { useRuntime } from './useRuntime'
|
||||
import { useSettings } from './useSettings'
|
||||
import useUpdateHandler from './useUpdateHandler'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
|
||||
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss } = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@@ -26,7 +25,6 @@ export function useAppInit() {
|
||||
|
||||
useUpdateHandler()
|
||||
useFullScreenNotice()
|
||||
useInitMCPServers()
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
@@ -36,13 +34,13 @@ export function useAppInit() {
|
||||
document.getElementById('spinner')?.remove()
|
||||
runAsyncFunction(async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
if (isPackaged && !manualUpdateCheck) {
|
||||
if (isPackaged && autoCheckUpdate) {
|
||||
await delay(2)
|
||||
const { updateInfo } = await window.api.checkForUpdate()
|
||||
dispatch(setUpdateState({ info: updateInfo }))
|
||||
}
|
||||
})
|
||||
}, [dispatch, manualUpdateCheck])
|
||||
}, [dispatch, autoCheckUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyMode === 'system') {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@renderer/store/assistants'
|
||||
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { TopicManager } from './useTopic'
|
||||
|
||||
@@ -69,7 +70,10 @@ export function useAssistant(id: string) {
|
||||
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
||||
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
||||
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
|
||||
setModel: useCallback(
|
||||
(model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
|
||||
[dispatch, assistant.id]
|
||||
),
|
||||
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
|
||||
updateAssistantSettings: (settings: Partial<AssistantSettings>) => {
|
||||
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setMCPServers } from '@renderer/store/mcp'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
|
||||
@@ -12,83 +11,17 @@ ipcRenderer.on('mcp:servers-changed', (_event, servers) => {
|
||||
|
||||
export const useMCPServers = () => {
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive))
|
||||
|
||||
const addMCPServer = async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.addServer(server)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to add MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const updateMCPServer = async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.updateServer(server)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to update MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMCPServer = async (name: string) => {
|
||||
try {
|
||||
await window.api.mcp.deleteServer(name)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to delete MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const setMCPServerActive = async (name: string, isActive: boolean) => {
|
||||
try {
|
||||
await window.api.mcp.setServerActive(name, isActive)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to set MCP server active status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const getActiveMCPServers = () => {
|
||||
return mcpServers.filter((server) => server.isActive)
|
||||
}
|
||||
const activedMcpServers = mcpServers.filter((server) => server.isActive)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
mcpServers,
|
||||
activedMcpServers,
|
||||
addMCPServer,
|
||||
updateMCPServer,
|
||||
deleteMCPServer,
|
||||
setMCPServerActive,
|
||||
getActiveMCPServers
|
||||
addMCPServer: (server: MCPServer) => dispatch(addMCPServer(server)),
|
||||
updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)),
|
||||
deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id)),
|
||||
setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })),
|
||||
getActiveMCPServers: () => mcpServers.filter((server) => server.isActive),
|
||||
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
|
||||
}
|
||||
}
|
||||
|
||||
export const useInitMCPServers = () => {
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
// const dispatch = useAppDispatch()
|
||||
|
||||
// Send servers to main process when they change in Redux
|
||||
useEffect(() => {
|
||||
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
|
||||
}, [mcpServers])
|
||||
|
||||
// Initial load of MCP servers from main process
|
||||
// useEffect(() => {
|
||||
// const loadServers = async () => {
|
||||
// try {
|
||||
// const servers = await window.api.mcp.listServers()
|
||||
// dispatch(setMCPServers(servers))
|
||||
// } catch (error) {
|
||||
// console.error('Failed to load MCP servers:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// loadServers()
|
||||
// }, [dispatch])
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearStreamMessage,
|
||||
clearTopicMessages,
|
||||
commitStreamMessage,
|
||||
deleteMessageAction,
|
||||
resendMessage,
|
||||
selectDisplayCount,
|
||||
selectTopicLoading,
|
||||
selectTopicMessages,
|
||||
setStreamMessage,
|
||||
setTopicLoading,
|
||||
updateMessage,
|
||||
updateMessages
|
||||
updateMessages,
|
||||
updateMessageThunk
|
||||
} from '@renderer/store/messages'
|
||||
import type { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
@@ -26,17 +28,15 @@ import { TopicManager } from './useTopic'
|
||||
*/
|
||||
export function useMessageOperations(topic: Topic) {
|
||||
const dispatch = useAppDispatch()
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
|
||||
/**
|
||||
* 删除单个消息
|
||||
*/
|
||||
const deleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const newMessages = messages.filter((m) => m.id !== message.id)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
async (id: string) => {
|
||||
await dispatch(deleteMessageAction(topic, id))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -44,10 +44,9 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const deleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const newMessages = messages.filter((m) => m.askId !== askId)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
await dispatch(deleteMessageAction(topic, askId, 'askId'))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -55,13 +54,17 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const editMessage = useCallback(
|
||||
async (messageId: string, updates: Partial<Message>) => {
|
||||
await dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId,
|
||||
updates
|
||||
})
|
||||
)
|
||||
// 如果更新包含内容变更,重新计算 token
|
||||
if ('content' in updates) {
|
||||
const messages = store.getState().messages.messagesByTopic[topic.id]
|
||||
const message = messages?.find((m) => m.id === messageId)
|
||||
if (message) {
|
||||
const updatedMessage = { ...message, ...updates }
|
||||
const usage = await estimateMessageUsage(updatedMessage)
|
||||
updates.usage = usage
|
||||
}
|
||||
}
|
||||
await dispatch(updateMessageThunk(topic.id, messageId, updates))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
@@ -148,7 +151,6 @@ export function useMessageOperations(topic: Topic) {
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||
}, [])
|
||||
|
||||
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
|
||||
const displayCount = useAppSelector(selectDisplayCount)
|
||||
// /**
|
||||
// * 获取当前消息列表
|
||||
@@ -200,8 +202,6 @@ export function useMessageOperations(topic: Topic) {
|
||||
)
|
||||
|
||||
return {
|
||||
messages,
|
||||
loading,
|
||||
displayCount,
|
||||
updateMessages: updateMessagesAction,
|
||||
deleteMessage,
|
||||
@@ -219,3 +219,13 @@ export function useMessageOperations(topic: Topic) {
|
||||
resumeMessage
|
||||
}
|
||||
}
|
||||
|
||||
export const useTopicMessages = (topic: Topic) => {
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
return messages
|
||||
}
|
||||
|
||||
export const useTopicLoading = (topic: Topic) => {
|
||||
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
|
||||
return loading
|
||||
}
|
||||
|
||||
117
src/renderer/src/hooks/useMinappPopup.ts
Normal file
117
src/renderer/src/hooks/useMinappPopup.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setCurrentMinappId,
|
||||
setMinappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setOpenedOneOffMinapp
|
||||
} from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* To control the minapp popup, you can use the following hooks:
|
||||
* import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
*
|
||||
* in the component:
|
||||
* const { openMinapp, openMinappKeepAlive, openMinappById,
|
||||
* closeMinapp, hideMinappPopup, closeAllMinapps } = useMinappPopup()
|
||||
*
|
||||
* To use some key states of the minapp popup:
|
||||
* import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
* const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||
*/
|
||||
export const useMinappPopup = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||
const { maxKeepAliveMinapps } = useSettings() // 使用设置中的值
|
||||
|
||||
/** Open a minapp (popup shows and minapp loaded) */
|
||||
const openMinapp = (app: MinAppType, keepAlive: boolean = false) => {
|
||||
if (keepAlive) {
|
||||
// 如果小程序已经打开,只切换显示
|
||||
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果缓存数量未达上限,添加到缓存列表
|
||||
if (openedKeepAliveMinapps.length < maxKeepAliveMinapps) {
|
||||
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps]))
|
||||
} else {
|
||||
// 缓存数量达到上限,移除最后一个,添加新的
|
||||
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps.slice(0, maxKeepAliveMinapps - 1)]))
|
||||
}
|
||||
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
return
|
||||
}
|
||||
|
||||
//if the minapp is not keep alive, open it as one-off minapp
|
||||
dispatch(setOpenedOneOffMinapp(app))
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
return
|
||||
}
|
||||
|
||||
/** a wrapper of openMinapp(app, true) */
|
||||
const openMinappKeepAlive = (app: MinAppType) => {
|
||||
openMinapp(app, true)
|
||||
}
|
||||
|
||||
/** Open a minapp by id (look up the minapp in DEFAULT_MIN_APPS) */
|
||||
const openMinappById = (id: string, keepAlive: boolean = false) => {
|
||||
import('@renderer/config/minapps').then(({ DEFAULT_MIN_APPS }) => {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
||||
if (app) {
|
||||
openMinapp(app, keepAlive)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Close a minapp immediately (popup hides and minapp unloaded) */
|
||||
const closeMinapp = (appid: string) => {
|
||||
if (openedKeepAliveMinapps.some((item) => item.id === appid)) {
|
||||
dispatch(setOpenedKeepAliveMinapps(openedKeepAliveMinapps.filter((item) => item.id !== appid)))
|
||||
} else if (openedOneOffMinapp?.id === appid) {
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
}
|
||||
|
||||
dispatch(setCurrentMinappId(''))
|
||||
dispatch(setMinappShow(false))
|
||||
return
|
||||
}
|
||||
|
||||
/** Close all minapps (popup hides and all minapps unloaded) */
|
||||
const closeAllMinapps = () => {
|
||||
dispatch(setOpenedKeepAliveMinapps([]))
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(''))
|
||||
dispatch(setMinappShow(false))
|
||||
}
|
||||
|
||||
/** Hide the minapp popup (only one-off minapp unloaded) */
|
||||
const hideMinappPopup = () => {
|
||||
if (!minappShow) return
|
||||
|
||||
if (openedOneOffMinapp) {
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(''))
|
||||
}
|
||||
dispatch(setMinappShow(false))
|
||||
}
|
||||
|
||||
return {
|
||||
openMinapp,
|
||||
openMinappKeepAlive,
|
||||
openMinappById,
|
||||
closeMinapp,
|
||||
hideMinappPopup,
|
||||
closeAllMinapps
|
||||
}
|
||||
}
|
||||
24
src/renderer/src/hooks/useNutstoreSSO.ts
Normal file
24
src/renderer/src/hooks/useNutstoreSSO.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export function useNutstoreSSO() {
|
||||
const nutstoreSSOHandler = useCallback(() => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const removeListener = window.api.protocol.onReceiveData(async (data) => {
|
||||
try {
|
||||
const url = new URL(data.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const encryptedToken = params.get('s')
|
||||
if (!encryptedToken) return reject(null)
|
||||
resolve(encryptedToken)
|
||||
} catch (error) {
|
||||
console.error('解析URL失败:', error)
|
||||
reject(null)
|
||||
} finally {
|
||||
removeListener()
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return nutstoreSSOHandler
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import { useEffect, useState } from 'react'
|
||||
import { useAssistant } from './useAssistant'
|
||||
import { getStoreSetting } from './useSettings'
|
||||
|
||||
const renamingTopics = new Set<string>()
|
||||
|
||||
let _activeTopic: Topic
|
||||
let _setActiveTopic: (topic: Topic) => void
|
||||
|
||||
@@ -54,35 +56,45 @@ export async function getTopicById(topicId: string) {
|
||||
}
|
||||
|
||||
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
|
||||
const topic = await getTopicById(topicId)
|
||||
const enableTopicNaming = getStoreSetting('enableTopicNaming')
|
||||
|
||||
if (isEmpty(topic.messages)) {
|
||||
if (renamingTopics.has(topicId)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (topic.isNameManuallyEdited) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
renamingTopics.add(topicId)
|
||||
|
||||
if (!enableTopicNaming) {
|
||||
const topicName = topic.messages[0]?.content.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ...topic, name: topicName } as Topic
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
const topic = await getTopicById(topicId)
|
||||
const enableTopicNaming = getStoreSetting('enableTopicNaming')
|
||||
|
||||
if (isEmpty(topic.messages)) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
|
||||
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
|
||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||
if (summaryText) {
|
||||
const data = { ...topic, name: summaryText }
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
if (topic.isNameManuallyEdited) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!enableTopicNaming) {
|
||||
const topicName = topic.messages[0]?.content.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ...topic, name: topicName } as Topic
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
|
||||
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
|
||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||
if (summaryText) {
|
||||
const data = { ...topic, name: summaryText }
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
renamingTopics.delete(topicId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"title": "Agents"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "Assistants",
|
||||
"abbr": "Assistant",
|
||||
"settings.title": "Assistant Settings",
|
||||
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
|
||||
"clear.title": "Clear topics",
|
||||
"copy.title": "Copy Assistant",
|
||||
@@ -44,6 +46,11 @@
|
||||
"search": "Search assistants...",
|
||||
"settings.default_model": "Default Model",
|
||||
"settings.knowledge_base": "Knowledge Base Settings",
|
||||
"settings.mcp": "MCP Servers",
|
||||
"settings.mcp.enableFirst": "Enable this server in MCP settings first",
|
||||
"settings.mcp.title": "MCP Settings",
|
||||
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
|
||||
"settings.mcp.description": "Default enabled MCP servers",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
@@ -53,7 +60,7 @@
|
||||
"settings.reasoning_effort.medium": "medium",
|
||||
"settings.reasoning_effort.off": "off",
|
||||
"settings.reasoning_effort.tip": "Only supports OpenAI o-series and Anthropic reasoning models",
|
||||
"title": "Assistants"
|
||||
"settings.more": "Assistant Settings"
|
||||
},
|
||||
"auth": {
|
||||
"error": "API key automatically obtained failed, please get it manually",
|
||||
@@ -143,7 +150,10 @@
|
||||
"history": "Chat History",
|
||||
"last": "Already at the last message",
|
||||
"next": "Next Message",
|
||||
"prev": "Previous Message"
|
||||
"prev": "Previous Message",
|
||||
"top": "Back to top",
|
||||
"bottom": "Back to bottom",
|
||||
"close": "Close"
|
||||
},
|
||||
"resend": "Resend",
|
||||
"save": "Save",
|
||||
@@ -180,13 +190,16 @@
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.notion": "Export to Notion",
|
||||
"topics.export.obsidian": "Export to Obsidian",
|
||||
"topics.export.obsidian_vault": "Vault",
|
||||
"topics.export.obsidian_vault_placeholder": "Please select the vault name",
|
||||
"topics.export.obsidian_path": "Path",
|
||||
"topics.export.obsidian_path_placeholder": "Please select the path",
|
||||
"topics.export.obsidian_atributes": "Configure Note Attributes",
|
||||
"topics.export.obsidian_btn": "Confirm",
|
||||
"topics.export.obsidian_created": "Creation Time",
|
||||
"topics.export.obsidian_created_placeholder": "Please select the creation time",
|
||||
"topics.export.obsidian_export_failed": "Export failed",
|
||||
"topics.export.obsidian_export_success": "Export success",
|
||||
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
||||
"topics.export.obsidian_operate": "Operation Method",
|
||||
"topics.export.obsidian_operate_append": "Append",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)",
|
||||
@@ -199,6 +212,13 @@
|
||||
"topics.export.obsidian_title": "Title",
|
||||
"topics.export.obsidian_title_placeholder": "Please enter the title",
|
||||
"topics.export.obsidian_title_required": "The title cannot be empty",
|
||||
"topics.export.obsidian_no_vaults": "No Obsidian vaults found",
|
||||
"topics.export.obsidian_loading": "Loading...",
|
||||
"topics.export.obsidian_fetch_error": "Failed to fetch Obsidian vaults",
|
||||
"topics.export.obsidian_fetch_folders_error": "Failed to fetch folder structure",
|
||||
"topics.export.obsidian_no_vault_selected": "Please select a vault first",
|
||||
"topics.export.obsidian_select_vault_first": "Please select a vault first",
|
||||
"topics.export.obsidian_root_directory": "Root Directory",
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.word": "Export as Word",
|
||||
"topics.export.yuque": "Export to Yuque",
|
||||
@@ -211,7 +231,11 @@
|
||||
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
||||
"topics.title": "Topics",
|
||||
"topics.unpinned": "Unpinned Topics",
|
||||
"translate": "Translate"
|
||||
"translate": "Translate",
|
||||
"topics.export.siyuan": "Export to Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Generating title...",
|
||||
"topics.export.title_naming_success": "Title generated successfully",
|
||||
"topics.export.title_naming_failed": "Failed to generate title, using default title"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Collapse",
|
||||
@@ -262,7 +286,13 @@
|
||||
"select": "Select",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
"you": "You"
|
||||
"you": "You",
|
||||
"variable_name": "Variable Name",
|
||||
"value": "Value",
|
||||
"no_variables_added": "No variables added",
|
||||
"insert_variable_into_prompt": "Insert variable into prompt",
|
||||
"variables": "Variables",
|
||||
"variables_help": "Add variables that need to be replaced in the text, triggered by {{variable_name}} in the replacement document"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
@@ -288,7 +318,8 @@
|
||||
"description": "Failed to render formula. Please check if the formula format is correct",
|
||||
"title": "Render Error"
|
||||
},
|
||||
"user_message_not_found": "Cannot find original user message to resend"
|
||||
"user_message_not_found": "Cannot find original user message to resend",
|
||||
"unknown": "Unknown error"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "Assistant",
|
||||
@@ -469,6 +500,8 @@
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"error.joplin.export": "Failed to export to Joplin. Please keep Joplin running and check connection status or configuration",
|
||||
"error.joplin.no_config": "Joplin Authorization Token or URL is not configured",
|
||||
"error.invalid.nutstore": "Invalid Nutstore settings",
|
||||
"error.invalid.nutstore_token": "Invalid Nutstore Token",
|
||||
"error.markdown.export.preconf": "Failed to export the Markdown file to the preconfigured path",
|
||||
"error.markdown.export.specified": "Failed to export the Markdown file",
|
||||
"error.notion.export": "Failed to export to Notion. Please check connection status and configuration according to documentation",
|
||||
@@ -518,11 +551,27 @@
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"upgrade.success.title": "Upgrade successfully",
|
||||
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again."
|
||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again.",
|
||||
"error.siyuan.export": "Failed to export to Siyuan Note, please check connection status and configuration according to documentation",
|
||||
"error.siyuan.no_config": "Siyuan Note API address or token is not configured",
|
||||
"success.siyuan.export": "Successfully exported to Siyuan Note",
|
||||
"warn.yuque.exporting": "Exporting to Yuque, please do not request export repeatedly!",
|
||||
"warn.siyuan.exporting": "Exporting to Siyuan Note, please do not request export repeatedly!"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
"refresh": "Refresh",
|
||||
"close": "Close MinApp",
|
||||
"minimize": "Minimize MinApp",
|
||||
"devtools": "Developer Tools",
|
||||
"openExternal": "Open in Browser",
|
||||
"rightclick_copyurl": "Right-click to copy URL"
|
||||
},
|
||||
"sidebar.add.title": "Add to sidebar",
|
||||
"sidebar.remove.title": "Remove from sidebar",
|
||||
"sidebar.close.title": "Close",
|
||||
"sidebar.closeall.title": "Close All",
|
||||
"sidebar.hide.title": "Hide MinApp",
|
||||
"title": "MinApp"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -537,15 +586,19 @@
|
||||
},
|
||||
"footer": {
|
||||
"copy_last_message": "Press C to copy",
|
||||
"esc": "Press ESC {{action}}",
|
||||
"esc_back": "back",
|
||||
"esc_close": "close the window"
|
||||
"backspace_clear": "Backspace to clear",
|
||||
"esc": "ESC to {{action}}",
|
||||
"esc_back": "return",
|
||||
"esc_close": "close"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "Ask {{model}} for help...",
|
||||
"title": "What do you want to do with this text?"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "Keep Window on Top"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -786,15 +839,6 @@
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.title": "Notion Configuration",
|
||||
"obsidian": {
|
||||
"folder": "Folder",
|
||||
"folder_placeholder": "Please enter the folder name",
|
||||
"tags": "Global Tags",
|
||||
"tags_placeholder": "Please enter the tag name, separate multiple tags with commas",
|
||||
"title": "Obsidian Configuration",
|
||||
"vault": "Vault",
|
||||
"vault_placeholder": "Please enter the vault name"
|
||||
},
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
@@ -839,16 +883,66 @@
|
||||
"title": "Yuque Configuration",
|
||||
"token": "Yuque Token",
|
||||
"token_placeholder": "Please enter the Yuque Token"
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian Configuration",
|
||||
"default_vault": "Default Obsidian Vault",
|
||||
"default_vault_placeholder": "Please select the default Obsidian vault",
|
||||
"default_vault_loading": "Loading Obsidian vault...",
|
||||
"default_vault_no_vaults": "No Obsidian vaults found",
|
||||
"default_vault_fetch_error": "Failed to fetch Obsidian vault",
|
||||
"default_vault_export_failed": "Export failed"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "Siyuan Note Configuration",
|
||||
"api_url": "Siyuan Note API URL",
|
||||
"api_url_placeholder": "e.g.: http://127.0.0.1:6806",
|
||||
"token": "Siyuan Note Token",
|
||||
"token.help": "Get Siyuan Note Token",
|
||||
"token_placeholder": "Please enter Siyuan Note Token",
|
||||
"box_id": "Siyuan Note Box ID",
|
||||
"box_id_placeholder": "Please enter Siyuan Note Box ID",
|
||||
"root_path": "Siyuan Note Root Path",
|
||||
"root_path_placeholder": "e.g.: /CherryStudio",
|
||||
"check": {
|
||||
"title": "Connection Check",
|
||||
"button": "Check",
|
||||
"empty_config": "Please fill in the API address and token",
|
||||
"success": "Connection successful",
|
||||
"fail": "Connection failed, please check API address and token",
|
||||
"error": "Connection error, please check network connection"
|
||||
}
|
||||
},
|
||||
"nutstore": {
|
||||
"title": "Nutstore Configuration",
|
||||
"isLogin": "Logged in",
|
||||
"notLogin": "Not logged in",
|
||||
"login.button": "Login",
|
||||
"logout.button": "Logout",
|
||||
"logout.title": "Are you sure you want to logout from Nutstore?",
|
||||
"logout.content": "After logout, you will not be able to backup to Nutstore or restore from Nutstore.",
|
||||
"checkConnection.name": "Check Connection",
|
||||
"checkConnection.success": "Connected to Nutstore",
|
||||
"checkConnection.fail": "Nutstore connection failed",
|
||||
"username": "Nutstore Username",
|
||||
"path": "Nutstore Storage Path",
|
||||
"path.placeholder": "Enter Nutstore storage path",
|
||||
"backup.button": "Backup to Nutstore",
|
||||
"restore.button": "Restore from Nutstore",
|
||||
"pathSelector.title": "Nutstore Storage Path",
|
||||
"pathSelector.return": "Return",
|
||||
"pathSelector.currentPath": "Current Path",
|
||||
"new_folder.button.confirm": "Confirm",
|
||||
"new_folder.button.cancel": "Cancel",
|
||||
"new_folder.button": "New Folder"
|
||||
},
|
||||
"message_title.use_topic_naming.title": "Use topic naming model to create titles for exported messages",
|
||||
"message_title.use_topic_naming.help": "When enabled, use topic naming model to create titles for exported messages. This will also affect all Markdown export methods."
|
||||
},
|
||||
"display.assistant.title": "Assistant Settings",
|
||||
"display.custom.css": "Custom CSS",
|
||||
"display.custom.css.cherrycss": "Get from cherrycss.com",
|
||||
"display.custom.css.placeholder": "/* Put custom CSS here */",
|
||||
"display.minApp.disabled": "Hidden MinApp",
|
||||
"display.minApp.empty": "Drag minApp from the left to hide them here",
|
||||
"display.minApp.title": "MinApp Settings",
|
||||
"display.minApp.visible": "Visible MinApp",
|
||||
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
|
||||
"display.sidebar.disabled": "Hide icons",
|
||||
"display.sidebar.empty": "Drag the hidden feature from the left side here",
|
||||
@@ -861,6 +955,20 @@
|
||||
"display.sidebar.visible": "Show icons",
|
||||
"display.title": "Display Settings",
|
||||
"display.topic.title": "Topic Settings",
|
||||
"miniapps": {
|
||||
"title": "Mini Apps Settings",
|
||||
"disabled": "Hidden Mini Apps",
|
||||
"empty": "Drag mini apps from the left to hide them",
|
||||
"visible": "Visible Mini Apps",
|
||||
"cache_settings": "Cache Settings",
|
||||
"cache_title": "Mini App Cache Limit",
|
||||
"cache_description": "Set the maximum number of active mini apps to keep in memory",
|
||||
"reset_tooltip": "Reset to default",
|
||||
"display_title": "Mini App Display Settings",
|
||||
"sidebar_title": "Sidebar Active Mini Apps Display",
|
||||
"sidebar_description": "Show active mini apps in the sidebar",
|
||||
"cache_change_notice": "Changes will take effect when the number of open mini apps reaches the set value"
|
||||
},
|
||||
"font_size.title": "Message font size",
|
||||
"general": "General Settings",
|
||||
"general.avatar.reset": "Reset Avatar",
|
||||
@@ -869,7 +977,7 @@
|
||||
"general.display.title": "Display Settings",
|
||||
"general.emoji_picker": "Emoji Picker",
|
||||
"general.image_upload": "Image Upload",
|
||||
"general.manually_check_update.title": "Turn off update checking",
|
||||
"general.auto_check_update.title": "Auto update checking",
|
||||
"general.reset.button": "Reset",
|
||||
"general.reset.title": "Data Reset",
|
||||
"general.restore.button": "Restore",
|
||||
@@ -897,10 +1005,7 @@
|
||||
"argsTooltip": "Each argument on a new line",
|
||||
"baseUrlTooltip": "Remote server base URL",
|
||||
"command": "Command",
|
||||
"commandRequired": "Please enter a command",
|
||||
"config_description": "Configure Model Context Protocol servers",
|
||||
"confirmDelete": "Delete Server",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete the server?",
|
||||
"deleteError": "Failed to delete server",
|
||||
"deleteSuccess": "Server deleted successfully",
|
||||
"dependenciesInstall": "Install Dependencies",
|
||||
@@ -911,7 +1016,8 @@
|
||||
"editServer": "Edit Server",
|
||||
"env": "Environment Variables",
|
||||
"envTooltip": "Format: KEY=value, one per line",
|
||||
"findMore": "Find More MCP Servers",
|
||||
"findMore": "Find More MCP",
|
||||
"searchNpx": "Search MCP",
|
||||
"install": "Install",
|
||||
"installError": "Failed to install dependencies",
|
||||
"installSuccess": "Dependencies installed successfully",
|
||||
@@ -921,8 +1027,8 @@
|
||||
"jsonSaveSuccess": "JSON configuration has been saved.",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"name": "Name",
|
||||
"nameRequired": "Please enter a server name",
|
||||
"noServers": "No servers configured",
|
||||
"newServer": "MCP Server",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
@@ -938,14 +1044,29 @@
|
||||
"usage": "Usage",
|
||||
"version": "Version"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP server failed to start, please check the parameters according to the tutorial"
|
||||
},
|
||||
"serverPlural": "servers",
|
||||
"serverSingular": "server",
|
||||
"title": "MCP Servers",
|
||||
"toggleError": "Toggle failed",
|
||||
"startError": "Start failed",
|
||||
"type": "Type",
|
||||
"updateError": "Failed to update server",
|
||||
"updateSuccess": "Server updated successfully",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
"installHelp": "Get Installation Help",
|
||||
"tools": {
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available"
|
||||
},
|
||||
"deleteServer": "Delete Server",
|
||||
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
||||
"registry": "Package Registry",
|
||||
"registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.",
|
||||
"registryDefault": "Default"
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
@@ -1057,7 +1178,7 @@
|
||||
"docs_more_details": "for more details",
|
||||
"get_api_key": "Get API Key",
|
||||
"is_not_support_array_content": "Enable compatible mode",
|
||||
"no_models": "Please add models first before checking the API connection",
|
||||
"no_models_for_check": "No models available for checking (e.g. chat models)",
|
||||
"not_checked": "Not Checked",
|
||||
"remove_duplicate_keys": "Remove Duplicate Keys",
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
@@ -1095,7 +1216,7 @@
|
||||
"reset_defaults_confirm": "Are you sure you want to reset all shortcuts?",
|
||||
"reset_to_default": "Reset to Default",
|
||||
"search_message": "Search Message",
|
||||
"show_app": "Show App",
|
||||
"show_app": "Show/Hide App",
|
||||
"show_settings": "Open Settings",
|
||||
"title": "Keyboard Shortcuts",
|
||||
"toggle_new_context": "Clear Context",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"title": "エージェント"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "アシスタント",
|
||||
"abbr": "アシスタント",
|
||||
"settings.title": "アシスタント設定",
|
||||
"clear.content": "トピックをクリアすると、アシスタント内のすべてのトピックとファイルが削除されます。続行しますか?",
|
||||
"clear.title": "トピックをクリア",
|
||||
"copy.title": "アシスタントをコピー",
|
||||
@@ -42,6 +44,11 @@
|
||||
"save.success": "保存に成功しました",
|
||||
"save.title": "エージェントに保存",
|
||||
"search": "アシスタントを検索...",
|
||||
"settings.mcp": "MCP サーバー",
|
||||
"settings.mcp.enableFirst": "まず MCP 設定でこのサーバーを有効にしてください",
|
||||
"settings.mcp.title": "MCP 設定",
|
||||
"settings.mcp.noServersAvailable": "利用可能な MCP サーバーがありません。設定でサーバーを追加してください",
|
||||
"settings.mcp.description": "デフォルトで有効な MCP サーバー",
|
||||
"settings.default_model": "デフォルトモデル",
|
||||
"settings.knowledge_base": "ナレッジベース設定",
|
||||
"settings.model": "モデル設定",
|
||||
@@ -53,7 +60,7 @@
|
||||
"settings.reasoning_effort.medium": "中程度",
|
||||
"settings.reasoning_effort.off": "オフ",
|
||||
"settings.reasoning_effort.tip": "OpenAIのoシリーズとAnthropicの推論モデルのみサポートしています",
|
||||
"title": "アシスタント"
|
||||
"settings.more": "アシスタント設定"
|
||||
},
|
||||
"auth": {
|
||||
"error": "APIキーの自動取得に失敗しました。手動で取得してください",
|
||||
@@ -143,7 +150,10 @@
|
||||
"history": "チャット履歴",
|
||||
"last": "最後のメッセージです",
|
||||
"next": "次のメッセージ",
|
||||
"prev": "前のメッセージ"
|
||||
"prev": "前のメッセージ",
|
||||
"top": "トップに戻る",
|
||||
"bottom": "下部に戻る",
|
||||
"close": "閉じる"
|
||||
},
|
||||
"resend": "再送信",
|
||||
"save": "保存",
|
||||
@@ -180,13 +190,16 @@
|
||||
"topics.export.md": "Markdownとしてエクスポート",
|
||||
"topics.export.notion": "Notion にエクスポート",
|
||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||
"topics.export.obsidian_vault": "保管庫",
|
||||
"topics.export.obsidian_vault_placeholder": "保管庫名を選択してください",
|
||||
"topics.export.obsidian_path": "パス",
|
||||
"topics.export.obsidian_path_placeholder": "パスを選択してください",
|
||||
"topics.export.obsidian_atributes": "ノートの属性を設定",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "作成日時",
|
||||
"topics.export.obsidian_created_placeholder": "作成日時を選択してください",
|
||||
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||
"topics.export.obsidian_export_success": "エクスポート成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
||||
"topics.export.obsidian_operate": "処理方法",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)",
|
||||
@@ -199,6 +212,13 @@
|
||||
"topics.export.obsidian_title": "タイトル",
|
||||
"topics.export.obsidian_title_placeholder": "タイトルを入力してください",
|
||||
"topics.export.obsidian_title_required": "タイトルは空白にできません",
|
||||
"topics.export.obsidian_no_vaults": "Obsidianの保管庫が見つかりません",
|
||||
"topics.export.obsidian_loading": "読み込み中...",
|
||||
"topics.export.obsidian_fetch_error": "Obsidianの保管庫の取得に失敗しました",
|
||||
"topics.export.obsidian_fetch_folders_error": "フォルダ構造の取得に失敗しました",
|
||||
"topics.export.obsidian_no_vault_selected": "保管庫を選択してください",
|
||||
"topics.export.obsidian_select_vault_first": "最初に保管庫を選択してください",
|
||||
"topics.export.obsidian_root_directory": "ルートディレクトリ",
|
||||
"topics.export.title": "エクスポート",
|
||||
"topics.export.word": "Wordとしてエクスポート",
|
||||
"topics.export.yuque": "語雀にエクスポート",
|
||||
@@ -211,7 +231,11 @@
|
||||
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
||||
"topics.title": "トピック",
|
||||
"topics.unpinned": "固定解除",
|
||||
"translate": "翻訳"
|
||||
"translate": "翻訳",
|
||||
"topics.export.siyuan": "思源笔记にエクスポート",
|
||||
"topics.export.wait_for_title_naming": "タイトルを生成中...",
|
||||
"topics.export.title_naming_success": "タイトルの生成に成功しました",
|
||||
"topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折りたたむ",
|
||||
@@ -262,7 +286,13 @@
|
||||
"select": "選択",
|
||||
"topics": "トピック",
|
||||
"warning": "警告",
|
||||
"you": "あなた"
|
||||
"you": "あなた",
|
||||
"variable_name": "変数名",
|
||||
"value": "値",
|
||||
"no_variables_added": "変数がありません",
|
||||
"insert_variable_into_prompt": "プロンプトに変数を挿入",
|
||||
"variables": "変数",
|
||||
"variables_help": "テキスト内で置換が必要な変数を追加し、置換ドキュメント内で{{variable_name}}の形式でトリガーします"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
@@ -288,7 +318,8 @@
|
||||
"description": "数式のレンダリングに失敗しました。数式の形式が正しいか確認してください",
|
||||
"title": "レンダリングエラー"
|
||||
},
|
||||
"user_message_not_found": "元のユーザーメッセージを見つけることができませんでした"
|
||||
"user_message_not_found": "元のユーザーメッセージを見つけることができませんでした",
|
||||
"unknown": "不明なエラー"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "アシスタント",
|
||||
@@ -469,12 +500,13 @@
|
||||
"error.invalid.webdav": "無効なWebDAV設定",
|
||||
"error.joplin.export": "Joplin へのエクスポートに失敗しました。Joplin が実行中であることを確認してください",
|
||||
"error.joplin.no_config": "Joplin 認証トークン または URL が設定されていません",
|
||||
"error.invalid.nutstore": "無効なNutstore設定です",
|
||||
"error.invalid.nutstore_token": "無効なNutstoreトークンです",
|
||||
"error.markdown.export.preconf": "Markdown ファイルを事前設定されたパスにエクスポートできませんでした",
|
||||
"error.markdown.export.specified": "Markdown ファイルのエクスポートに失敗しました",
|
||||
"error.notion.export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください",
|
||||
"error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません",
|
||||
"error.yuque.export": "語雀へのエクスポートに失敗しました。接続状態と設定を確認してください",
|
||||
"error.yuque.no_config": "語雀Token または 知識ベースID が設定されていません",
|
||||
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
|
||||
"group.delete.title": "分組メッセージを削除",
|
||||
"ignore.knowledge.base": "インターネットモードが有効になっています。ナレッジベースを無視します",
|
||||
@@ -518,11 +550,28 @@
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
"upgrade.success.title": "アップグレードに成功しました",
|
||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。",
|
||||
"error.siyuan.export": "思源ノートのエクスポートに失敗しました。接続状態を確認し、ドキュメントに従って設定を確認してください",
|
||||
"error.siyuan.no_config": "思源ノートのAPIアドレスまたはトークンが設定されていません",
|
||||
"success.siyuan.export": "思源ノートへのエクスポートに成功しました",
|
||||
"warn.yuque.exporting": "語雀にエクスポート中です。重複してエクスポートしないでください!",
|
||||
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
|
||||
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
"refresh": "更新",
|
||||
"close": "ミニアプリを閉じる",
|
||||
"minimize": "ミニアプリを最小化",
|
||||
"devtools": "開発者ツール",
|
||||
"openExternal": "ブラウザで開く",
|
||||
"rightclick_copyurl": "右クリックでURLをコピー"
|
||||
},
|
||||
"sidebar.add.title": "サイドバーに追加",
|
||||
"sidebar.remove.title": "サイドバーから削除",
|
||||
"sidebar.close.title": "閉じる",
|
||||
"sidebar.closeall.title": "すべて閉じる",
|
||||
"sidebar.hide.title": "ミニアプリを隠す",
|
||||
"title": "ミニアプリ"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -539,13 +588,17 @@
|
||||
"copy_last_message": "C キーを押してコピー",
|
||||
"esc": "ESC キーを押して{{action}}",
|
||||
"esc_back": "戻る",
|
||||
"esc_close": "ウィンドウを閉じる"
|
||||
"esc_close": "ウィンドウを閉じる",
|
||||
"backspace_clear": "バックスペースを押してクリアします"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "{{model}} に質問してください...",
|
||||
"title": "下のテキストに対して何をしますか?"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "上部ウィンドウ"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -786,15 +839,6 @@
|
||||
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "フォルダー",
|
||||
"folder_placeholder": "フォルダーの名前を入力してください",
|
||||
"tags": "グローバルタグ",
|
||||
"tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください",
|
||||
"title": "Obsidian の設定",
|
||||
"vault": "ヴォールト(保管庫)",
|
||||
"vault_placeholder": "保管庫の名前を入力してください"
|
||||
},
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
@@ -839,16 +883,66 @@
|
||||
"title": "Yuque設定",
|
||||
"token": "Yuqueトークン",
|
||||
"token_placeholder": "Yuqueトークンを入力してください"
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 設定",
|
||||
"default_vault": "デフォルトの Obsidian 保管庫",
|
||||
"default_vault_placeholder": "デフォルトの Obsidian 保管庫を選択してください",
|
||||
"default_vault_loading": "Obsidian 保管庫を取得中...",
|
||||
"default_vault_no_vaults": "Obsidian 保管庫が見つかりません",
|
||||
"default_vault_fetch_error": "Obsidian 保管庫の取得に失敗しました",
|
||||
"default_vault_export_failed": "エクスポートに失敗しました"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源ノート設定",
|
||||
"api_url": "APIアドレス",
|
||||
"api_url_placeholder": "例:http://127.0.0.1:6806",
|
||||
"token": "APIトークン",
|
||||
"token.help": "思源ノート->設定->について で取得",
|
||||
"token_placeholder": "思源ノートトークンを入力してください",
|
||||
"box_id": "ノートブックID",
|
||||
"box_id_placeholder": "ノートブックIDを入力してください",
|
||||
"root_path": "ドキュメントルートパス",
|
||||
"root_path_placeholder": "例:/CherryStudio",
|
||||
"check": {
|
||||
"title": "接続チェック",
|
||||
"button": "チェック",
|
||||
"empty_config": "APIアドレスとトークンを入力してください",
|
||||
"success": "接続成功",
|
||||
"fail": "接続失敗、APIアドレスとトークンを確認してください",
|
||||
"error": "接続エラー、ネットワーク接続を確認してください"
|
||||
}
|
||||
},
|
||||
"nutstore": {
|
||||
"title": "Nutstore設定",
|
||||
"isLogin": "ログイン済み",
|
||||
"notLogin": "未ログイン",
|
||||
"login.button": "ログイン",
|
||||
"logout.button": "ログアウト",
|
||||
"logout.title": "Nutstoreからログアウトしますか?",
|
||||
"logout.content": "ログアウト後、Nutstoreへのバックアップや復元ができなくなります。",
|
||||
"checkConnection.name": "接続確認",
|
||||
"checkConnection.success": "Nutstoreに接続しました",
|
||||
"checkConnection.fail": "Nutstore接続に失敗しました",
|
||||
"username": "Nutstoreユーザー名",
|
||||
"path": "Nutstoreストレージパス",
|
||||
"path.placeholder": "Nutstoreストレージパスを入力",
|
||||
"backup.button": "Nutstoreにバックアップ",
|
||||
"restore.button": "Nutstoreから復元",
|
||||
"pathSelector.title": "Nutstoreストレージパス",
|
||||
"pathSelector.return": "戻る",
|
||||
"pathSelector.currentPath": "現在のパス",
|
||||
"new_folder.button.confirm": "確認",
|
||||
"new_folder.button.cancel": "キャンセル",
|
||||
"new_folder.button": "新しいフォルダー"
|
||||
},
|
||||
"message_title.use_topic_naming.title": "トピック命名モデルを使用してメッセージのタイトルを作成",
|
||||
"message_title.use_topic_naming.help": "この設定は、すべてのMarkdownエクスポート方法に影響します。"
|
||||
},
|
||||
"display.assistant.title": "アシスタント設定",
|
||||
"display.custom.css": "カスタムCSS",
|
||||
"display.custom.css.cherrycss": "cherrycss.comから取得",
|
||||
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
|
||||
"display.minApp.disabled": "非表示ミニプログラム",
|
||||
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
|
||||
"display.minApp.title": "ミニプログラム表示設定",
|
||||
"display.minApp.visible": "表示中ミニプログラム",
|
||||
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
|
||||
"display.sidebar.disabled": "アイコンを非表示",
|
||||
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
|
||||
@@ -861,6 +955,20 @@
|
||||
"display.sidebar.visible": "アイコンを表示",
|
||||
"display.title": "表示設定",
|
||||
"display.topic.title": "トピック設定",
|
||||
"miniapps": {
|
||||
"title": "ミニアプリ設定",
|
||||
"disabled": "非表示のミニアプリ",
|
||||
"empty": "非表示にするミニアプリを左側からここにドラッグしてください",
|
||||
"visible": "表示するミニアプリ",
|
||||
"cache_settings": "キャッシュ設定",
|
||||
"cache_title": "ミニアプリのキャッシュ数",
|
||||
"cache_description": "メモリに保持するアクティブなミニアプリの最大数を設定します",
|
||||
"reset_tooltip": "デフォルト値にリセット",
|
||||
"display_title": "ミニアプリ表示設定",
|
||||
"sidebar_title": "サイドバーのアクティブなミニアプリ表示",
|
||||
"sidebar_description": "サイドバーにアクティブなミニアプリを表示するかどうかを設定します",
|
||||
"cache_change_notice": "設定値に達するまでミニアプリの開閉が行われた後に変更が適用されます"
|
||||
},
|
||||
"font_size.title": "メッセージのフォントサイズ",
|
||||
"general": "一般設定",
|
||||
"general.avatar.reset": "アバターをリセット",
|
||||
@@ -869,7 +977,6 @@
|
||||
"general.display.title": "表示設定",
|
||||
"general.emoji_picker": "絵文字ピッカー",
|
||||
"general.image_upload": "画像アップロード",
|
||||
"general.manually_check_update.title": "更新チェックを無効にする",
|
||||
"general.reset.button": "リセット",
|
||||
"general.reset.title": "データをリセット",
|
||||
"general.restore.button": "復元",
|
||||
@@ -897,10 +1004,7 @@
|
||||
"argsTooltip": "1行に1つの引数を入力してください",
|
||||
"baseUrlTooltip": "リモートURLアドレス",
|
||||
"command": "コマンド",
|
||||
"commandRequired": "コマンドを入力してください",
|
||||
"config_description": "モデルコンテキストプロトコルサーバーの設定",
|
||||
"confirmDelete": "サーバーを削除",
|
||||
"confirmDeleteMessage": "本当にこのサーバーを削除しますか?",
|
||||
"deleteError": "サーバーの削除に失敗しました",
|
||||
"deleteSuccess": "サーバーが正常に削除されました",
|
||||
"dependenciesInstall": "依存関係をインストール",
|
||||
@@ -911,7 +1015,8 @@
|
||||
"editServer": "サーバーを編集",
|
||||
"env": "環境変数",
|
||||
"envTooltip": "形式: KEY=value, 1行に1つ",
|
||||
"findMore": "MCP サーバーを見つける",
|
||||
"findMore": "MCP を見つける",
|
||||
"searchNpx": "MCP を検索",
|
||||
"install": "インストール",
|
||||
"installError": "依存関係のインストールに失敗しました",
|
||||
"installSuccess": "依存関係のインストールに成功しました",
|
||||
@@ -921,8 +1026,8 @@
|
||||
"jsonSaveSuccess": "JSON設定が保存されました。",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"name": "名前",
|
||||
"nameRequired": "サーバー名を入力してください",
|
||||
"noServers": "サーバーが設定されていません",
|
||||
"newServer": "MCP サーバー",
|
||||
"npx_list": {
|
||||
"actions": "アクション",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
@@ -941,11 +1046,26 @@
|
||||
"serverPlural": "サーバー",
|
||||
"serverSingular": "サーバー",
|
||||
"title": "MCP サーバー",
|
||||
"toggleError": "切り替えに失敗しました",
|
||||
"startError": "起動に失敗しました",
|
||||
"type": "タイプ",
|
||||
"updateError": "サーバーの更新に失敗しました",
|
||||
"updateSuccess": "サーバーが正常に更新されました",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"errors": {
|
||||
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください"
|
||||
},
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
"installHelp": "インストールヘルプを取得",
|
||||
"tools": {
|
||||
"inputSchema": "入力スキーマ",
|
||||
"availableTools": "利用可能なツール",
|
||||
"noToolsAvailable": "利用可能なツールはありません"
|
||||
},
|
||||
"deleteServer": "サーバーを削除",
|
||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||
"registry": "パッケージ管理レジストリ",
|
||||
"registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。",
|
||||
"registryDefault": "デフォルト"
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
@@ -1057,7 +1177,7 @@
|
||||
"docs_more_details": "詳細を確認",
|
||||
"get_api_key": "APIキーを取得",
|
||||
"is_not_support_array_content": "互換モードを有効にする",
|
||||
"no_models": "API接続をチェックする前に、モデルを追加してください",
|
||||
"no_models_for_check": "チェックするモデルがありません(例:会話モデル)",
|
||||
"not_checked": "未チェック",
|
||||
"remove_duplicate_keys": "重複キーを削除",
|
||||
"remove_invalid_keys": "無効なキーを削除",
|
||||
@@ -1095,7 +1215,7 @@
|
||||
"reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?",
|
||||
"reset_to_default": "デフォルトにリセット",
|
||||
"search_message": "メッセージを検索",
|
||||
"show_app": "アプリを表示",
|
||||
"show_app": "アプリを表示/非表示",
|
||||
"show_settings": "設定を開く",
|
||||
"title": "ショートカット",
|
||||
"toggle_new_context": "コンテキストをクリア",
|
||||
@@ -1143,7 +1263,8 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "ウェブ検索"
|
||||
}
|
||||
},
|
||||
"general.auto_check_update.title": "自動更新チェックを有効にする"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意の言語",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"title": "Агенты"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "Ассистенты",
|
||||
"abbr": "Ассистент",
|
||||
"settings.title": "Настройки ассистента",
|
||||
"clear.content": "Очистка топика удалит все топики и файлы в ассистенте. Вы уверены, что хотите продолжить?",
|
||||
"clear.title": "Очистить топики",
|
||||
"copy.title": "Копировать ассистента",
|
||||
@@ -42,6 +44,11 @@
|
||||
"save.success": "Успешно сохранено",
|
||||
"save.title": "Сохранить в агента",
|
||||
"search": "Поиск ассистентов...",
|
||||
"settings.mcp": "Серверы MCP",
|
||||
"settings.mcp.enableFirst": "Сначала включите этот сервер в настройках MCP",
|
||||
"settings.mcp.title": "Настройки MCP",
|
||||
"settings.mcp.noServersAvailable": "Нет доступных серверов MCP. Добавьте серверы в настройках",
|
||||
"settings.mcp.description": "Серверы MCP, включенные по умолчанию",
|
||||
"settings.default_model": "Модель по умолчанию",
|
||||
"settings.knowledge_base": "Настройки базы знаний",
|
||||
"settings.model": "Настройки модели",
|
||||
@@ -53,7 +60,7 @@
|
||||
"settings.reasoning_effort.medium": "Средняя",
|
||||
"settings.reasoning_effort.off": "Выключено",
|
||||
"settings.reasoning_effort.tip": "Поддерживается только моделями с рассуждением OpenAI o-series и Anthropic",
|
||||
"title": "Ассистенты"
|
||||
"settings.more": "Настройки ассистента"
|
||||
},
|
||||
"auth": {
|
||||
"error": "Автоматический получение ключа API не удалось, пожалуйста, получите ключ вручную",
|
||||
@@ -143,7 +150,10 @@
|
||||
"history": "История чата",
|
||||
"last": "Уже последнее сообщение",
|
||||
"next": "Следующее сообщение",
|
||||
"prev": "Предыдущее сообщение"
|
||||
"prev": "Предыдущее сообщение",
|
||||
"top": "Вернуться наверх",
|
||||
"bottom": "Вернуться вниз",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"resend": "Переотправить",
|
||||
"save": "Сохранить",
|
||||
@@ -180,13 +190,16 @@
|
||||
"topics.export.md": "Экспорт как markdown",
|
||||
"topics.export.notion": "Экспорт в Notion",
|
||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||
"topics.export.obsidian_vault": "Хранилище",
|
||||
"topics.export.obsidian_vault_placeholder": "Выберите имя хранилища",
|
||||
"topics.export.obsidian_path": "Путь",
|
||||
"topics.export.obsidian_path_placeholder": "Выберите путь",
|
||||
"topics.export.obsidian_atributes": "Настроить атрибуты заметки",
|
||||
"topics.export.obsidian_btn": "Подтвердить",
|
||||
"topics.export.obsidian_created": "Дата создания",
|
||||
"topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания",
|
||||
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
||||
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
||||
"topics.export.obsidian_operate": "Метод обработки",
|
||||
"topics.export.obsidian_operate_append": "Добавить в конец",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)",
|
||||
@@ -199,6 +212,13 @@
|
||||
"topics.export.obsidian_title": "Заголовок",
|
||||
"topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок",
|
||||
"topics.export.obsidian_title_required": "Заголовок не может быть пустым",
|
||||
"topics.export.obsidian_no_vaults": "Хранилища Obsidian не найдены",
|
||||
"topics.export.obsidian_loading": "Загрузка...",
|
||||
"topics.export.obsidian_fetch_error": "Не удалось получить хранилища Obsidian",
|
||||
"topics.export.obsidian_fetch_folders_error": "Не удалось получить структуру папок",
|
||||
"topics.export.obsidian_no_vault_selected": "Пожалуйста, сначала выберите хранилище",
|
||||
"topics.export.obsidian_select_vault_first": "Пожалуйста, сначала выберите хранилище",
|
||||
"topics.export.obsidian_root_directory": "Корневая директория",
|
||||
"topics.export.title": "Экспорт",
|
||||
"topics.export.word": "Экспорт как Word",
|
||||
"topics.export.yuque": "Экспорт в Yuque",
|
||||
@@ -211,7 +231,11 @@
|
||||
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
||||
"topics.title": "Топики",
|
||||
"topics.unpinned": "Открепленные темы",
|
||||
"translate": "Перевести"
|
||||
"translate": "Перевести",
|
||||
"topics.export.siyuan": "Экспорт в Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Создание заголовка...",
|
||||
"topics.export.title_naming_success": "Заголовок успешно создан",
|
||||
"topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Свернуть",
|
||||
@@ -262,7 +286,13 @@
|
||||
"select": "Выбрать",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы"
|
||||
"you": "Вы",
|
||||
"variable_name": "Имя переменной",
|
||||
"value": "Значение",
|
||||
"no_variables_added": "Нет переменных",
|
||||
"insert_variable_into_prompt": "Вставить переменную в промпт",
|
||||
"variables": "Переменные",
|
||||
"variables_help": "Добавьте переменные, которые нужно заменить в тексте, замена срабатывает в формате {{variable_name}} в документе замены"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
@@ -288,7 +318,8 @@
|
||||
"description": "Не удалось рендерить формулу. Пожалуйста, проверьте, правильно ли формат формулы",
|
||||
"title": "Ошибка рендеринга"
|
||||
},
|
||||
"user_message_not_found": "Не удалось найти исходное сообщение пользователя"
|
||||
"user_message_not_found": "Не удалось найти исходное сообщение пользователя",
|
||||
"unknown": "Неизвестная ошибка"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "Ассистент",
|
||||
@@ -469,6 +500,8 @@
|
||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||
"error.joplin.export": "Не удалось экспортировать в Joplin, пожалуйста, убедитесь, что Joplin запущен и проверьте состояние подключения или настройки",
|
||||
"error.joplin.no_config": "Joplin Authorization Token или URL не настроен",
|
||||
"error.invalid.nutstore": "Неверные настройки Nutstore",
|
||||
"error.invalid.nutstore_token": "Неверный Nutstore токен",
|
||||
"error.markdown.export.preconf": "Не удалось экспортировать файл Markdown в предуказанный путь",
|
||||
"error.markdown.export.specified": "Не удалось экспортировать файл Markdown",
|
||||
"error.notion.export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации",
|
||||
@@ -518,11 +551,27 @@
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
"upgrade.success.title": "Обновление успешно",
|
||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.",
|
||||
"error.siyuan.export": "Ошибка экспорта в Siyuan, пожалуйста, проверьте состояние подключения и настройки в документации",
|
||||
"error.siyuan.no_config": "Не настроен API адрес или токен Siyuan",
|
||||
"success.siyuan.export": "Успешный экспорт в Siyuan",
|
||||
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
|
||||
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
"refresh": "Обновить",
|
||||
"close": "Закрыть встроенное приложение",
|
||||
"minimize": "Свернуть встроенное приложение",
|
||||
"devtools": "Инструменты разработчика",
|
||||
"openExternal": "Открыть в браузере",
|
||||
"rightclick_copyurl": "ПКМ → Копировать URL"
|
||||
},
|
||||
"sidebar.add.title": "Добавить в боковую панель",
|
||||
"sidebar.remove.title": "Удалить из боковой панели",
|
||||
"sidebar.close.title": "Закрыть",
|
||||
"sidebar.closeall.title": "Закрыть все",
|
||||
"sidebar.hide.title": "Скрыть приложение",
|
||||
"title": "Встроенные приложения"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -539,13 +588,17 @@
|
||||
"copy_last_message": "Нажмите C для копирования",
|
||||
"esc": "Нажмите ESC {{action}}",
|
||||
"esc_back": "возвращения",
|
||||
"esc_close": "закрытия окна"
|
||||
"esc_close": "закрытия окна",
|
||||
"backspace_clear": "Нажмите Backspace, чтобы очистить"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "Задайте вопрос {{model}}...",
|
||||
"title": "Что вы хотите сделать с этим текстом?"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "Верхнее окно"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -786,15 +839,6 @@
|
||||
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
|
||||
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
|
||||
"notion.title": "Настройки Notion",
|
||||
"obsidian": {
|
||||
"folder": "Папка",
|
||||
"folder_placeholder": "Пожалуйста, введите имя папки",
|
||||
"tags": "Глобальные Теги",
|
||||
"tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.",
|
||||
"title": "Конфигурация Obsidian",
|
||||
"vault": "Хранилище",
|
||||
"vault_placeholder": "Пожалуйста, введите имя хранилища"
|
||||
},
|
||||
"title": "Настройки данных",
|
||||
"webdav": {
|
||||
"autoSync": "Автоматическое резервное копирование",
|
||||
@@ -839,16 +883,66 @@
|
||||
"title": "Настройка Yuque",
|
||||
"token": "Токен Yuque",
|
||||
"token_placeholder": "Введите токен Yuque"
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Настройки Obsidian",
|
||||
"default_vault": "Хранилище Obsidian по умолчанию",
|
||||
"default_vault_placeholder": "Выберите хранилище Obsidian по умолчанию",
|
||||
"default_vault_loading": "Получение хранилищ Obsidian...",
|
||||
"default_vault_no_vaults": "Хранилища Obsidian не найдены",
|
||||
"default_vault_fetch_error": "Не удалось получить хранилища Obsidian",
|
||||
"default_vault_export_failed": "Ошибка экспорта"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "Конфигурация SiYuan Note",
|
||||
"api_url": "API адрес",
|
||||
"api_url_placeholder": "Например: http://127.0.0.1:6806",
|
||||
"token": "API токен",
|
||||
"token.help": "Получите в SiYuan Note -> Настройки -> О программе",
|
||||
"token_placeholder": "Введите токен SiYuan Note",
|
||||
"box_id": "ID блокнота",
|
||||
"box_id_placeholder": "Введите ID блокнота",
|
||||
"root_path": "Корневой путь документа",
|
||||
"root_path_placeholder": "Например: /CherryStudio",
|
||||
"check": {
|
||||
"title": "Проверка соединения",
|
||||
"button": "Проверить",
|
||||
"empty_config": "Пожалуйста, заполните API адрес и токен",
|
||||
"success": "Соединение успешно",
|
||||
"fail": "Не удалось подключиться, проверьте API адрес и токен",
|
||||
"error": "Ошибка соединения, проверьте сетевое подключение"
|
||||
}
|
||||
},
|
||||
"nutstore": {
|
||||
"title": "Настройки Nutstore",
|
||||
"isLogin": "Выполнен вход",
|
||||
"notLogin": "Вход не выполнен",
|
||||
"login.button": "Войти",
|
||||
"logout.button": "Выйти",
|
||||
"logout.title": "Вы уверены, что хотите выйти из Nutstore?",
|
||||
"logout.content": "После выхода вы не сможете создавать резервные копии в Nutstore или восстанавливать данные из Nutstore.",
|
||||
"checkConnection.name": "Проверить соединение",
|
||||
"checkConnection.success": "Подключение к Nutstore установлено",
|
||||
"checkConnection.fail": "Ошибка подключения к Nutstore",
|
||||
"username": "Имя пользователя Nutstore",
|
||||
"path": "Путь хранения Nutstore",
|
||||
"path.placeholder": "Введите путь хранения Nutstore",
|
||||
"backup.button": "Резервное копирование в Nutstore",
|
||||
"restore.button": "Восстановление из Nutstore",
|
||||
"pathSelector.title": "Путь хранения Nutstore",
|
||||
"pathSelector.return": "Назад",
|
||||
"pathSelector.currentPath": "Текущий путь",
|
||||
"new_folder.button.confirm": "Подтвердить",
|
||||
"new_folder.button.cancel": "Отмена",
|
||||
"new_folder.button": "Новая папка"
|
||||
},
|
||||
"message_title.use_topic_naming.title": "Использовать модель именования тем для создания заголовков сообщений",
|
||||
"message_title.use_topic_naming.help": "Этот параметр влияет на все методы экспорта в Markdown, такие как Notion, Yuque и т.д."
|
||||
},
|
||||
"display.assistant.title": "Настройки ассистентов",
|
||||
"display.custom.css": "Пользовательский CSS",
|
||||
"display.custom.css.cherrycss": "Получить из cherrycss.com",
|
||||
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
|
||||
"display.minApp.disabled": "скрытый апплет",
|
||||
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
|
||||
"display.minApp.title": "Настройки отображения мини программы",
|
||||
"display.minApp.visible": "Отображаемый апплет",
|
||||
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
|
||||
"display.sidebar.disabled": "Скрыть иконки",
|
||||
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
|
||||
@@ -861,6 +955,20 @@
|
||||
"display.sidebar.visible": "Показывать иконки",
|
||||
"display.title": "Настройки отображения",
|
||||
"display.topic.title": "Настройки топиков",
|
||||
"miniapps": {
|
||||
"title": "Настройки мини-приложений",
|
||||
"disabled": "Скрытые мини-приложения",
|
||||
"empty": "Перетащите мини-приложения слева, чтобы скрыть их",
|
||||
"visible": "Отображаемые мини-приложения",
|
||||
"cache_settings": "Настройки кэша",
|
||||
"cache_title": "Количество кэшируемых мини-приложений",
|
||||
"cache_description": "Установить максимальное количество активных мини-приложений в памяти",
|
||||
"reset_tooltip": "Сбросить до значения по умолчанию",
|
||||
"display_title": "Настройки отображения мини-приложений",
|
||||
"sidebar_title": "Отображение активных мини-приложений в боковой панели",
|
||||
"sidebar_description": "Настройка отображения активных мини-приложений в боковой панели",
|
||||
"cache_change_notice": "Изменения вступят в силу, когда количество открытых мини-приложений достигнет установленного значения"
|
||||
},
|
||||
"font_size.title": "Размер шрифта сообщений",
|
||||
"general": "Общие настройки",
|
||||
"general.avatar.reset": "Сброс аватара",
|
||||
@@ -869,7 +977,6 @@
|
||||
"general.display.title": "Настройки отображения",
|
||||
"general.emoji_picker": "Выбор эмодзи",
|
||||
"general.image_upload": "Загрузка изображений",
|
||||
"general.manually_check_update.title": "Отключить проверку обновлений",
|
||||
"general.reset.button": "Сброс",
|
||||
"general.reset.title": "Сброс данных",
|
||||
"general.restore.button": "Восстановление",
|
||||
@@ -897,10 +1004,7 @@
|
||||
"argsTooltip": "Каждый аргумент с новой строки",
|
||||
"baseUrlTooltip": "Адрес удаленного URL",
|
||||
"command": "Команда",
|
||||
"commandRequired": "Пожалуйста, введите команду",
|
||||
"config_description": "Настройка серверов протокола контекста модели",
|
||||
"confirmDelete": "Удалить сервер",
|
||||
"confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?",
|
||||
"deleteError": "Не удалось удалить сервер",
|
||||
"deleteSuccess": "Сервер успешно удален",
|
||||
"dependenciesInstall": "Установить зависимости",
|
||||
@@ -911,7 +1015,8 @@
|
||||
"editServer": "Редактировать сервер",
|
||||
"env": "Переменные окружения",
|
||||
"envTooltip": "Формат: KEY=value, по одной на строку",
|
||||
"findMore": "Найти больше MCP серверов",
|
||||
"findMore": "Найти больше MCP",
|
||||
"searchNpx": "Найти MCP",
|
||||
"install": "Установить",
|
||||
"installError": "Не удалось установить зависимости",
|
||||
"installSuccess": "Зависимости успешно установлены",
|
||||
@@ -921,8 +1026,8 @@
|
||||
"jsonSaveSuccess": "JSON конфигурация сохранена",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"name": "Имя",
|
||||
"nameRequired": "Пожалуйста, введите имя сервера",
|
||||
"noServers": "Серверы не настроены",
|
||||
"newServer": "MCP сервер",
|
||||
"npx_list": {
|
||||
"actions": "Действия",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
@@ -938,14 +1043,29 @@
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры"
|
||||
},
|
||||
"serverPlural": "серверы",
|
||||
"serverSingular": "сервер",
|
||||
"title": "Серверы MCP",
|
||||
"toggleError": "Переключение не удалось",
|
||||
"startError": "Запуск не удалось",
|
||||
"type": "Тип",
|
||||
"updateError": "Ошибка обновления сервера",
|
||||
"updateSuccess": "Сервер успешно обновлен",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "Редактировать MCP",
|
||||
"installHelp": "Получить помощь по установке",
|
||||
"tools": {
|
||||
"inputSchema": "входные параметры",
|
||||
"availableTools": "доступные инструменты",
|
||||
"noToolsAvailable": "нет доступных инструментов"
|
||||
},
|
||||
"deleteServer": "Удалить сервер",
|
||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||
"registry": "Реестр пакетов",
|
||||
"registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.",
|
||||
"registryDefault": "По умолчанию"
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
@@ -1057,7 +1177,7 @@
|
||||
"docs_more_details": "для получения дополнительной информации",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"is_not_support_array_content": "Включить совместимый режим",
|
||||
"no_models": "Пожалуйста, добавьте модели перед проверкой соединения с API",
|
||||
"no_models_for_check": "Нет моделей для проверки (например, диалоговые модели)",
|
||||
"not_checked": "Не проверено",
|
||||
"remove_duplicate_keys": "Удалить дубликаты ключей",
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
@@ -1095,7 +1215,7 @@
|
||||
"reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?",
|
||||
"reset_to_default": "Сбросить настройки по умолчанию",
|
||||
"search_message": "Поиск сообщения",
|
||||
"show_app": "Показать приложение",
|
||||
"show_app": "Показать/скрыть приложение",
|
||||
"show_settings": "Открыть настройки",
|
||||
"title": "Горячие клавиши",
|
||||
"toggle_new_context": "Очистить контекст",
|
||||
@@ -1143,7 +1263,8 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "Поиск в Интернете"
|
||||
}
|
||||
},
|
||||
"general.auto_check_update.title": "Включить автоматическую проверку обновлений"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"title": "智能体"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "助手",
|
||||
"abbr": "助手",
|
||||
"settings.title": "助手设置",
|
||||
"clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?",
|
||||
"clear.title": "清空话题",
|
||||
"copy.title": "复制助手",
|
||||
@@ -42,6 +44,11 @@
|
||||
"save.success": "保存成功",
|
||||
"save.title": "保存到智能体",
|
||||
"search": "搜索助手",
|
||||
"settings.mcp": "MCP 服务器",
|
||||
"settings.mcp.enableFirst": "请先在 MCP 设置中启用此服务器",
|
||||
"settings.mcp.title": "MCP 设置",
|
||||
"settings.mcp.noServersAvailable": "无可用 MCP 服务器。请在设置中添加服务器",
|
||||
"settings.mcp.description": "默认启用的 MCP 服务器",
|
||||
"settings.default_model": "默认模型",
|
||||
"settings.knowledge_base": "知识库设置",
|
||||
"settings.model": "模型设置",
|
||||
@@ -53,7 +60,7 @@
|
||||
"settings.reasoning_effort.medium": "中",
|
||||
"settings.reasoning_effort.off": "关",
|
||||
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series 和 Anthropic 推理模型",
|
||||
"title": "助手"
|
||||
"settings.more": "助手设置"
|
||||
},
|
||||
"auth": {
|
||||
"error": "自动获取密钥失败,请手动获取",
|
||||
@@ -143,7 +150,10 @@
|
||||
"history": "聊天历史",
|
||||
"last": "已经是最后一条消息",
|
||||
"next": "下一条消息",
|
||||
"prev": "上一条消息"
|
||||
"prev": "上一条消息",
|
||||
"top": "回到顶部",
|
||||
"bottom": "回到底部",
|
||||
"close": "关闭"
|
||||
},
|
||||
"resend": "重新发送",
|
||||
"save": "保存",
|
||||
@@ -180,13 +190,16 @@
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.notion": "导出到 Notion",
|
||||
"topics.export.obsidian": "导出到 Obsidian",
|
||||
"topics.export.obsidian_vault": "保管库",
|
||||
"topics.export.obsidian_vault_placeholder": "请选择保管库名称",
|
||||
"topics.export.obsidian_path": "路径",
|
||||
"topics.export.obsidian_path_placeholder": "请选择路径",
|
||||
"topics.export.obsidian_atributes": "配置笔记属性",
|
||||
"topics.export.obsidian_btn": "确定",
|
||||
"topics.export.obsidian_created": "创建时间",
|
||||
"topics.export.obsidian_created_placeholder": "请选择创建时间",
|
||||
"topics.export.obsidian_export_failed": "导出失败",
|
||||
"topics.export.obsidian_export_success": "导出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_export_failed": "导出到Obsidian失败",
|
||||
"topics.export.obsidian_export_success": "导出到Obsidian成功",
|
||||
"topics.export.obsidian_operate": "处理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)",
|
||||
@@ -199,6 +212,13 @@
|
||||
"topics.export.obsidian_title": "标题",
|
||||
"topics.export.obsidian_title_placeholder": "请输入标题",
|
||||
"topics.export.obsidian_title_required": "标题不能为空",
|
||||
"topics.export.obsidian_no_vaults": "未找到Obsidian保管库",
|
||||
"topics.export.obsidian_loading": "加载中...",
|
||||
"topics.export.obsidian_fetch_error": "获取Obsidian保管库失败",
|
||||
"topics.export.obsidian_fetch_folders_error": "获取文件夹结构失败",
|
||||
"topics.export.obsidian_no_vault_selected": "请先选择一个保管库",
|
||||
"topics.export.obsidian_select_vault_first": "请先选择保管库",
|
||||
"topics.export.obsidian_root_directory": "根目录",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.export.yuque": "导出到语雀",
|
||||
@@ -211,7 +231,11 @@
|
||||
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词",
|
||||
"topics.title": "话题",
|
||||
"topics.unpinned": "取消固定",
|
||||
"translate": "翻译"
|
||||
"translate": "翻译",
|
||||
"topics.export.siyuan": "导出到思源笔记",
|
||||
"topics.export.wait_for_title_naming": "正在生成标题...",
|
||||
"topics.export.title_naming_success": "标题生成成功",
|
||||
"topics.export.title_naming_failed": "标题生成失败,使用默认标题"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "收起",
|
||||
@@ -262,7 +286,13 @@
|
||||
"select": "选择",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
"you": "用户"
|
||||
"you": "用户",
|
||||
"variable_name": "变量名称",
|
||||
"value": "值",
|
||||
"no_variables_added": "没有添加变量",
|
||||
"insert_variable_into_prompt": "插入变量到提示词",
|
||||
"variables": "变量",
|
||||
"variables_help": "添加需要替换的变量名字和值即可"
|
||||
},
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
@@ -288,7 +318,8 @@
|
||||
"description": "渲染公式失败,请检查公式格式是否正确",
|
||||
"title": "渲染错误"
|
||||
},
|
||||
"user_message_not_found": "无法找到原始用户消息"
|
||||
"user_message_not_found": "无法找到原始用户消息",
|
||||
"unknown": "未知错误"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
@@ -469,6 +500,8 @@
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"error.joplin.export": "导出 Joplin 失败,请保持 Joplin 已运行并检查连接状态或检查配置",
|
||||
"error.joplin.no_config": "未配置 Joplin 授权令牌 或 URL",
|
||||
"error.invalid.nutstore": "无效的坚果云设置",
|
||||
"error.invalid.nutstore_token": "无效的坚果云 Token",
|
||||
"error.markdown.export.preconf": "导出Markdown文件到预先设定的路径失败",
|
||||
"error.markdown.export.specified": "导出Markdown文件失败",
|
||||
"error.notion.export": "导出 Notion 错误,请检查连接状态并对照文档检查配置",
|
||||
@@ -518,11 +551,27 @@
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试",
|
||||
"error.siyuan.export": "导出思源笔记失败,请检查连接状态并对照文档检查配置",
|
||||
"error.siyuan.no_config": "未配置思源笔记API地址或令牌",
|
||||
"success.siyuan.export": "导出到思源笔记成功",
|
||||
"warn.yuque.exporting": "正在导出语雀, 请勿重复请求导出!",
|
||||
"warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
"refresh": "刷新",
|
||||
"close": "关闭小程序",
|
||||
"minimize": "最小化小程序",
|
||||
"devtools": "开发者工具",
|
||||
"openExternal": "在浏览器中打开",
|
||||
"rightclick_copyurl": "右键复制URL"
|
||||
},
|
||||
"sidebar.add.title": "添加到侧边栏",
|
||||
"sidebar.remove.title": "从侧边栏移除",
|
||||
"sidebar.close.title": "关闭",
|
||||
"sidebar.closeall.title": "全部关闭",
|
||||
"sidebar.hide.title": "隐藏小程序",
|
||||
"title": "小程序"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -537,15 +586,19 @@
|
||||
},
|
||||
"footer": {
|
||||
"copy_last_message": "按 C 键复制",
|
||||
"backspace_clear": "按 Backspace 清空",
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_back": "返回",
|
||||
"esc_close": "关闭窗口"
|
||||
"esc_close": "关闭"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "询问 {{model}} 获取帮助...",
|
||||
"title": "你想对下方文字做什么"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "窗口置顶"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -679,7 +732,7 @@
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI",
|
||||
"voyageai":"Voyage AI"
|
||||
"voyageai": "Voyage AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "确定要恢复数据吗?",
|
||||
@@ -763,6 +816,8 @@
|
||||
"markdown_export.path_placeholder": "导出路径",
|
||||
"markdown_export.select": "选择",
|
||||
"markdown_export.title": "Markdown 导出",
|
||||
"message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题",
|
||||
"message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式。",
|
||||
"minute_interval_one": "{{count}} 分钟",
|
||||
"minute_interval_other": "{{count}} 分钟",
|
||||
"notion.api_key": "Notion 密钥",
|
||||
@@ -786,15 +841,6 @@
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.title": "Notion 配置",
|
||||
"obsidian": {
|
||||
"folder": "文件夹",
|
||||
"folder_placeholder": "请输入文件夹名称",
|
||||
"tags": "全局标签",
|
||||
"tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔",
|
||||
"title": "Obsidian 配置",
|
||||
"vault": "保管库",
|
||||
"vault_placeholder": "请输入保管库名称"
|
||||
},
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
@@ -839,16 +885,64 @@
|
||||
"title": "语雀配置",
|
||||
"token": "语雀 Token",
|
||||
"token_placeholder": "请输入语雀Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 配置",
|
||||
"default_vault": "默认 Obsidian 仓库",
|
||||
"default_vault_placeholder": "请选择默认 Obsidian 仓库",
|
||||
"default_vault_loading": "正在获取 Obsidian 仓库...",
|
||||
"default_vault_no_vaults": "未找到 Obsidian 仓库",
|
||||
"default_vault_fetch_error": "获取 Obsidian 仓库失败",
|
||||
"default_vault_export_failed": "导出失败"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源笔记配置",
|
||||
"api_url": "API地址",
|
||||
"api_url_placeholder": "例如:http://127.0.0.1:6806",
|
||||
"token": "API令牌",
|
||||
"token.help": "在思源笔记->设置->关于中获取",
|
||||
"token_placeholder": "请输入思源笔记令牌",
|
||||
"box_id": "笔记本ID",
|
||||
"box_id_placeholder": "请输入笔记本ID",
|
||||
"root_path": "文档根路径",
|
||||
"root_path_placeholder": "例如:/CherryStudio",
|
||||
"check": {
|
||||
"title": "连接检查",
|
||||
"button": "检查",
|
||||
"empty_config": "请填写API地址和令牌",
|
||||
"success": "连接成功",
|
||||
"fail": "连接失败,请检查API地址和令牌",
|
||||
"error": "连接异常,请检查网络连接"
|
||||
}
|
||||
},
|
||||
"nutstore": {
|
||||
"title": "坚果云配置",
|
||||
"isLogin": "已登录",
|
||||
"notLogin": "未登录",
|
||||
"login.button": "登录",
|
||||
"logout.button": "退出登录",
|
||||
"logout.title": "确定要退出坚果云登录?",
|
||||
"logout.content": "退出后将无法备份至坚果云和从坚果云恢复",
|
||||
"checkConnection.name": "检查连接",
|
||||
"checkConnection.success": "已连接坚果云",
|
||||
"checkConnection.fail": "坚果云连接失败",
|
||||
"username": "坚果云用户名",
|
||||
"path": "坚果云存储路径",
|
||||
"path.placeholder": "请输入坚果云的存储路径",
|
||||
"backup.button": "备份到坚果云",
|
||||
"restore.button": "从坚果云恢复",
|
||||
"pathSelector.title": "坚果云存储路径",
|
||||
"pathSelector.return": "返回",
|
||||
"pathSelector.currentPath": "当前路径",
|
||||
"new_folder.button.confirm": "确定",
|
||||
"new_folder.button.cancel": "取消",
|
||||
"new_folder.button": "新建文件夹"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "助手设置",
|
||||
"display.custom.css": "自定义 CSS",
|
||||
"display.custom.css.cherrycss": "从 cherrycss.com 获取",
|
||||
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
|
||||
"display.minApp.disabled": "隐藏的小程序",
|
||||
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
|
||||
"display.minApp.title": "小程序显示设置",
|
||||
"display.minApp.visible": "显示的小程序",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
|
||||
"display.sidebar.disabled": "隐藏的图标",
|
||||
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
|
||||
@@ -861,6 +955,20 @@
|
||||
"display.sidebar.visible": "显示的图标",
|
||||
"display.title": "显示设置",
|
||||
"display.topic.title": "话题设置",
|
||||
"miniapps": {
|
||||
"title": "小程序设置",
|
||||
"disabled": "隐藏的小程序",
|
||||
"empty": "把要隐藏的小程序从左侧拖拽到这里",
|
||||
"visible": "显示的小程序",
|
||||
"cache_settings": "缓存设置",
|
||||
"cache_title": "小程序缓存数量",
|
||||
"cache_description": "设置同时保持活跃状态的小程序最大数量",
|
||||
"reset_tooltip": "重置为默认值",
|
||||
"display_title": "小程序显示设置",
|
||||
"sidebar_title": "侧边栏活跃小程序显示设置",
|
||||
"sidebar_description": "设置侧边栏是否显示活跃的小程序",
|
||||
"cache_change_notice": "更改将在打开的小程序增减至设定值后生效"
|
||||
},
|
||||
"font_size.title": "消息字体大小",
|
||||
"general": "常规设置",
|
||||
"general.avatar.reset": "重置头像",
|
||||
@@ -869,7 +977,7 @@
|
||||
"general.display.title": "显示设置",
|
||||
"general.emoji_picker": "表情选择器",
|
||||
"general.image_upload": "图片上传",
|
||||
"general.manually_check_update.title": "关闭更新检测",
|
||||
"general.auto_check_update.title": "自动检测更新",
|
||||
"general.reset.button": "重置",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.restore.button": "恢复",
|
||||
@@ -897,10 +1005,7 @@
|
||||
"argsTooltip": "每个参数占一行",
|
||||
"baseUrlTooltip": "远程 URL 地址",
|
||||
"command": "命令",
|
||||
"commandRequired": "请输入命令",
|
||||
"config_description": "配置模型上下文协议服务器",
|
||||
"confirmDelete": "删除服务器",
|
||||
"confirmDeleteMessage": "您确定要删除该服务器吗?",
|
||||
"deleteError": "删除服务器失败",
|
||||
"deleteSuccess": "服务器删除成功",
|
||||
"dependenciesInstall": "安装依赖项",
|
||||
@@ -911,7 +1016,8 @@
|
||||
"editServer": "编辑服务器",
|
||||
"env": "环境变量",
|
||||
"envTooltip": "格式:KEY=value,每行一个",
|
||||
"findMore": "更多 MCP 服务器",
|
||||
"findMore": "更多 MCP",
|
||||
"searchNpx": "搜索 MCP",
|
||||
"install": "安装",
|
||||
"installError": "安装依赖项失败",
|
||||
"installSuccess": "依赖项安装成功",
|
||||
@@ -921,8 +1027,8 @@
|
||||
"jsonSaveSuccess": "JSON配置已保存",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入服务器名称",
|
||||
"noServers": "未配置服务器",
|
||||
"newServer": "MCP 服务器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
@@ -938,14 +1044,29 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整"
|
||||
},
|
||||
"serverPlural": "服务器",
|
||||
"serverSingular": "服务器",
|
||||
"title": "MCP 服务器",
|
||||
"toggleError": "切换失败",
|
||||
"startError": "启动失败",
|
||||
"type": "类型",
|
||||
"updateError": "更新服务器失败",
|
||||
"updateSuccess": "服务器更新成功",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
"installHelp": "获取安装帮助",
|
||||
"tools": {
|
||||
"inputSchema": "输入参数",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "没有可用工具"
|
||||
},
|
||||
"deleteServer": "删除服务器",
|
||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||
"registry": "包管理源",
|
||||
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题。",
|
||||
"registryDefault": "默认"
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
@@ -1057,7 +1178,7 @@
|
||||
"docs_more_details": "获取更多详情",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"is_not_support_array_content": "开启兼容模式",
|
||||
"no_models": "请先添加模型再检查 API 连接",
|
||||
"no_models_for_check": "没有可以被检查的模型(例如对话模型)",
|
||||
"not_checked": "未检查",
|
||||
"remove_duplicate_keys": "移除重复密钥",
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
@@ -1095,7 +1216,7 @@
|
||||
"reset_defaults_confirm": "确定要重置所有快捷键吗?",
|
||||
"reset_to_default": "重置为默认",
|
||||
"search_message": "搜索消息",
|
||||
"show_app": "显示应用",
|
||||
"show_app": "显示/隐藏应用",
|
||||
"show_settings": "打开设置",
|
||||
"title": "快捷方式",
|
||||
"toggle_new_context": "清除上下文",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"title": "智慧代理人"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "助手",
|
||||
"abbr": "助手",
|
||||
"settings.title": "助手設定",
|
||||
"clear.content": "清空話題會刪除助手下所有主題和檔案,確定要繼續嗎?",
|
||||
"clear.title": "清空話題",
|
||||
"copy.title": "複製助手",
|
||||
@@ -42,6 +44,11 @@
|
||||
"save.success": "儲存成功",
|
||||
"save.title": "儲存到智慧代理人",
|
||||
"search": "搜尋助手...",
|
||||
"settings.mcp": "MCP 伺服器",
|
||||
"settings.mcp.enableFirst": "請先在 MCP 設定中啟用此伺服器",
|
||||
"settings.mcp.title": "MCP 設定",
|
||||
"settings.mcp.noServersAvailable": "無可用 MCP 伺服器。請在設定中新增伺服器",
|
||||
"settings.mcp.description": "預設啟用的 MCP 伺服器",
|
||||
"settings.default_model": "預設模型",
|
||||
"settings.knowledge_base": "知識庫設定",
|
||||
"settings.model": "模型設定",
|
||||
@@ -52,8 +59,8 @@
|
||||
"settings.reasoning_effort.low": "短",
|
||||
"settings.reasoning_effort.medium": "中",
|
||||
"settings.reasoning_effort.off": "關",
|
||||
"settings.reasoning_effort.tip": "僅支援 OpenAI o 系列和 Anthropic 推理模型",
|
||||
"title": "助手"
|
||||
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series 和 Anthropic 推理模型",
|
||||
"settings.more": "助手設定"
|
||||
},
|
||||
"auth": {
|
||||
"error": "自動取得金鑰失敗,請手動取得",
|
||||
@@ -143,7 +150,10 @@
|
||||
"history": "聊天歷史",
|
||||
"last": "已經是最後一條訊息",
|
||||
"next": "下一條訊息",
|
||||
"prev": "上一條訊息"
|
||||
"prev": "上一條訊息",
|
||||
"top": "回到頂部",
|
||||
"bottom": "回到底部",
|
||||
"close": "關閉"
|
||||
},
|
||||
"resend": "重新傳送",
|
||||
"save": "儲存",
|
||||
@@ -180,13 +190,16 @@
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.notion": "匯出到 Notion",
|
||||
"topics.export.obsidian": "匯出到 Obsidian",
|
||||
"topics.export.obsidian_vault": "保管庫",
|
||||
"topics.export.obsidian_vault_placeholder": "請選擇保管庫名稱",
|
||||
"topics.export.obsidian_path": "路徑",
|
||||
"topics.export.obsidian_path_placeholder": "請選擇路徑",
|
||||
"topics.export.obsidian_atributes": "配置筆記屬性",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "建立時間",
|
||||
"topics.export.obsidian_created_placeholder": "請選擇建立時間",
|
||||
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||
"topics.export.obsidian_export_success": "匯出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_operate": "處理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)",
|
||||
@@ -199,6 +212,13 @@
|
||||
"topics.export.obsidian_title": "標題",
|
||||
"topics.export.obsidian_title_placeholder": "請輸入標題",
|
||||
"topics.export.obsidian_title_required": "標題不能為空",
|
||||
"topics.export.obsidian_no_vaults": "未找到Obsidian保管庫",
|
||||
"topics.export.obsidian_loading": "加載中...",
|
||||
"topics.export.obsidian_fetch_error": "獲取Obsidian保管庫失敗",
|
||||
"topics.export.obsidian_fetch_folders_error": "獲取文件夾結構失敗",
|
||||
"topics.export.obsidian_no_vault_selected": "請先選擇一個保管庫",
|
||||
"topics.export.obsidian_select_vault_first": "請先選擇保管庫",
|
||||
"topics.export.obsidian_root_directory": "根目錄",
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.word": "匯出為 Word",
|
||||
"topics.export.yuque": "匯出到語雀",
|
||||
@@ -211,7 +231,11 @@
|
||||
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
||||
"topics.title": "話題",
|
||||
"topics.unpinned": "取消固定",
|
||||
"translate": "翻譯"
|
||||
"translate": "翻譯",
|
||||
"topics.export.siyuan": "匯出到思源筆記",
|
||||
"topics.export.wait_for_title_naming": "正在生成標題...",
|
||||
"topics.export.title_naming_success": "標題生成成功",
|
||||
"topics.export.title_naming_failed": "標題生成失敗,使用預設標題"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折疊",
|
||||
@@ -262,7 +286,13 @@
|
||||
"select": "選擇",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您"
|
||||
"you": "您",
|
||||
"variable_name": "變量名稱",
|
||||
"value": "值",
|
||||
"no_variables_added": "沒有添加變量",
|
||||
"insert_variable_into_prompt": "插入變量到提示詞",
|
||||
"variables": "變量",
|
||||
"variables_help": "添加需要替換的變量名字和值即可"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
@@ -288,7 +318,8 @@
|
||||
"description": "渲染公式失敗,請檢查公式格式是否正確",
|
||||
"title": "渲染錯誤"
|
||||
},
|
||||
"user_message_not_found": "無法找到原始用戶訊息"
|
||||
"user_message_not_found": "無法找到原始用戶訊息",
|
||||
"unknown": "未知錯誤"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
@@ -469,6 +500,8 @@
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"error.joplin.export": "匯出 Joplin 失敗,請保持 Joplin 已運行並檢查連接狀態或檢查設定",
|
||||
"error.joplin.no_config": "未設定 Joplin 授權Token 或 URL",
|
||||
"error.invalid.nutstore": "無效的坚果云設定",
|
||||
"error.invalid.nutstore_token": "無效的坚果云 Token",
|
||||
"error.markdown.export.preconf": "導出 Markdown 文件到預先設定的路徑失敗",
|
||||
"error.markdown.export.specified": "導出 Markdown 文件失敗",
|
||||
"error.notion.export": "匯出 Notion 錯誤,請檢查連接狀態並對照文件檢查設定",
|
||||
@@ -518,11 +551,27 @@
|
||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||
"upgrade.success.title": "升級成功",
|
||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試",
|
||||
"error.siyuan.export": "導出思源筆記失敗,請檢查連接狀態並對照文檔檢查配置",
|
||||
"error.siyuan.no_config": "未配置思源筆記API地址或令牌",
|
||||
"success.siyuan.export": "導出到思源筆記成功",
|
||||
"warn.yuque.exporting": "正在導出語雀,請勿重複請求導出!",
|
||||
"warn.siyuan.exporting": "正在導出到思源筆記,請勿重複請求導出!"
|
||||
},
|
||||
"minapp": {
|
||||
"popup": {
|
||||
"refresh": "重新整理",
|
||||
"close": "關閉小工具",
|
||||
"minimize": "最小化小工具",
|
||||
"devtools": "開發者工具",
|
||||
"openExternal": "在瀏覽器中開啟",
|
||||
"rightclick_copyurl": "右鍵複製URL"
|
||||
},
|
||||
"sidebar.add.title": "新增到側邊欄",
|
||||
"sidebar.remove.title": "從側邊欄移除",
|
||||
"sidebar.close.title": "關閉",
|
||||
"sidebar.closeall.title": "全部關閉",
|
||||
"sidebar.hide.title": "隱藏小工具",
|
||||
"title": "小工具"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -539,13 +588,17 @@
|
||||
"copy_last_message": "按 C 鍵複製",
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_back": "返回",
|
||||
"esc_close": "關閉視窗"
|
||||
"esc_close": "關閉視窗",
|
||||
"backspace_clear": "按 Backspace 清空"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "詢問 {{model}} 取得幫助...",
|
||||
"title": "你想對下方文字做什麼"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "窗口置頂"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -786,15 +839,6 @@
|
||||
"notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "資料夾",
|
||||
"folder_placeholder": "請輸入資料夾名稱",
|
||||
"tags": "全域標籤",
|
||||
"tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。",
|
||||
"title": "Obsidian 設定",
|
||||
"vault": "保險庫",
|
||||
"vault_placeholder": "請輸入保險庫名稱"
|
||||
},
|
||||
"title": "資料設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
@@ -839,16 +883,66 @@
|
||||
"title": "語雀設定",
|
||||
"token": "語雀 Token",
|
||||
"token_placeholder": "請輸入語雀 Token"
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 設定",
|
||||
"default_vault": "預設 Obsidian 倉庫",
|
||||
"default_vault_placeholder": "請選擇預設 Obsidian 倉庫",
|
||||
"default_vault_loading": "正在獲取 Obsidian 倉庫...",
|
||||
"default_vault_no_vaults": "未找到 Obsidian 倉庫",
|
||||
"default_vault_fetch_error": "獲取 Obsidian 倉庫失敗",
|
||||
"default_vault_export_failed": "匯出失敗"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源筆記配置",
|
||||
"api_url": "API地址",
|
||||
"api_url_placeholder": "例如:http://127.0.0.1:6806",
|
||||
"token": "API令牌",
|
||||
"token.help": "在思源筆記->設置->關於中獲取",
|
||||
"token_placeholder": "請輸入思源筆記令牌",
|
||||
"box_id": "筆記本ID",
|
||||
"box_id_placeholder": "請輸入筆記本ID",
|
||||
"root_path": "文檔根路徑",
|
||||
"root_path_placeholder": "例如:/CherryStudio",
|
||||
"check": {
|
||||
"title": "連接檢查",
|
||||
"button": "檢查",
|
||||
"empty_config": "請填寫API地址和令牌",
|
||||
"success": "連接成功",
|
||||
"fail": "連接失敗,請檢查API地址和令牌",
|
||||
"error": "連接異常,請檢查網絡連接"
|
||||
}
|
||||
},
|
||||
"nutstore": {
|
||||
"title": "堅果雲設定",
|
||||
"isLogin": "已登入",
|
||||
"notLogin": "未登入",
|
||||
"login.button": "登入",
|
||||
"logout.button": "退出登入",
|
||||
"logout.title": "確定要退出堅果雲登入?",
|
||||
"logout.content": "退出後將無法備份至堅果雲和從堅果雲恢復",
|
||||
"checkConnection.name": "檢查連接",
|
||||
"checkConnection.success": "已連接堅果雲",
|
||||
"checkConnection.fail": "堅果雲連接失敗",
|
||||
"username": "堅果雲用戶名",
|
||||
"path": "堅果雲存儲路徑",
|
||||
"path.placeholder": "請輸入堅果雲的存儲路徑",
|
||||
"backup.button": "備份到堅果雲",
|
||||
"restore.button": "從堅果雲恢復",
|
||||
"pathSelector.title": "堅果雲存儲路徑",
|
||||
"pathSelector.return": "返回",
|
||||
"pathSelector.currentPath": "當前路徑",
|
||||
"new_folder.button.confirm": "確定",
|
||||
"new_folder.button.cancel": "取消",
|
||||
"new_folder.button": "新建文件夾"
|
||||
},
|
||||
"message_title.use_topic_naming.title": "使用話題命名模型為導出的消息創建標題",
|
||||
"message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式,如Notion、語雀等。"
|
||||
},
|
||||
"display.assistant.title": "助手設定",
|
||||
"display.custom.css": "自訂 CSS",
|
||||
"display.custom.css.cherrycss": "從 cherrycss.com 取得",
|
||||
"display.custom.css.placeholder": "/* 這裡寫自訂 CSS */",
|
||||
"display.minApp.disabled": "隱藏的小工具",
|
||||
"display.minApp.empty": "把要隱藏的小工具從左側拖拽到這裡",
|
||||
"display.minApp.title": "小工具顯示設定",
|
||||
"display.minApp.visible": "顯示的小工具",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
|
||||
"display.sidebar.disabled": "隱藏的圖示",
|
||||
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
|
||||
@@ -861,6 +955,20 @@
|
||||
"display.sidebar.visible": "顯示的圖示",
|
||||
"display.title": "顯示設定",
|
||||
"display.topic.title": "話題設定",
|
||||
"miniapps": {
|
||||
"title": "小程式設置",
|
||||
"disabled": "隱藏的小程式",
|
||||
"empty": "把要隱藏的小程式從左側拖拽到這裡",
|
||||
"visible": "顯示的小程式",
|
||||
"cache_settings": "緩存設置",
|
||||
"cache_title": "小程式緩存數量",
|
||||
"cache_description": "設置同時保持活躍狀態的小程式最大數量",
|
||||
"reset_tooltip": "重置為預設值",
|
||||
"display_title": "小程式顯示設置",
|
||||
"sidebar_title": "側邊欄活躍小程式顯示設置",
|
||||
"sidebar_description": "設置側邊欄是否顯示活躍的小程式",
|
||||
"cache_change_notice": "更改將在打開的小程式增減至設定值後生效"
|
||||
},
|
||||
"font_size.title": "訊息字型大小",
|
||||
"general": "一般設定",
|
||||
"general.avatar.reset": "重設頭像",
|
||||
@@ -869,7 +977,6 @@
|
||||
"general.display.title": "顯示設定",
|
||||
"general.emoji_picker": "表情選擇器",
|
||||
"general.image_upload": "圖片上傳",
|
||||
"general.manually_check_update.title": "關閉更新檢查",
|
||||
"general.reset.button": "重設",
|
||||
"general.reset.title": "資料重設",
|
||||
"general.restore.button": "復原",
|
||||
@@ -897,10 +1004,7 @@
|
||||
"argsTooltip": "每個參數佔一行",
|
||||
"baseUrlTooltip": "遠端 URL 地址",
|
||||
"command": "指令",
|
||||
"commandRequired": "請輸入指令",
|
||||
"config_description": "設定模型上下文協議伺服器",
|
||||
"confirmDelete": "刪除伺服器",
|
||||
"confirmDeleteMessage": "您確定要刪除該伺服器嗎?",
|
||||
"deleteError": "刪除伺服器失敗",
|
||||
"deleteSuccess": "伺服器刪除成功",
|
||||
"dependenciesInstall": "安裝相依套件",
|
||||
@@ -911,7 +1015,8 @@
|
||||
"editServer": "編輯伺服器",
|
||||
"env": "環境變數",
|
||||
"envTooltip": "格式:KEY=value,每行一個",
|
||||
"findMore": "更多 MCP 伺服器",
|
||||
"findMore": "更多 MCP",
|
||||
"searchNpx": "搜索 MCP",
|
||||
"install": "安裝",
|
||||
"installError": "安裝相依套件失敗",
|
||||
"installSuccess": "相依套件安裝成功",
|
||||
@@ -921,8 +1026,8 @@
|
||||
"jsonSaveSuccess": "JSON配置已儲存",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"name": "名稱",
|
||||
"nameRequired": "請輸入伺服器名稱",
|
||||
"noServers": "未設定伺服器",
|
||||
"newServer": "MCP 伺服器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
@@ -938,14 +1043,29 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整"
|
||||
},
|
||||
"serverPlural": "伺服器",
|
||||
"serverSingular": "伺服器",
|
||||
"title": "MCP 伺服器",
|
||||
"toggleError": "切換失敗",
|
||||
"startError": "啟動失敗",
|
||||
"type": "類型",
|
||||
"updateError": "更新伺服器失敗",
|
||||
"updateSuccess": "伺服器更新成功",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
"installHelp": "獲取安裝幫助",
|
||||
"tools": {
|
||||
"inputSchema": "輸入參數",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "沒有可用工具"
|
||||
},
|
||||
"deleteServer": "刪除伺服器",
|
||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||
"registry": "套件管理源",
|
||||
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題。",
|
||||
"registryDefault": "預設"
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
@@ -1057,7 +1177,7 @@
|
||||
"docs_more_details": "檢視更多細節",
|
||||
"get_api_key": "點選這裡取得金鑰",
|
||||
"is_not_support_array_content": "開啟相容模式",
|
||||
"no_models": "請先新增模型再檢查 API 連接",
|
||||
"no_models_for_check": "沒有可以被檢查的模型(例如對話模型)",
|
||||
"not_checked": "未檢查",
|
||||
"remove_duplicate_keys": "移除重複金鑰",
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
@@ -1095,7 +1215,7 @@
|
||||
"reset_defaults_confirm": "確定要重設所有快捷鍵嗎?",
|
||||
"reset_to_default": "重設為預設",
|
||||
"search_message": "搜尋訊息",
|
||||
"show_app": "顯示應用程式",
|
||||
"show_app": "顯示/隱藏應用程式",
|
||||
"show_settings": "開啟設定",
|
||||
"title": "快速方式",
|
||||
"toggle_new_context": "清除上下文",
|
||||
@@ -1143,7 +1263,8 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "網路搜尋"
|
||||
}
|
||||
},
|
||||
"general.auto_check_update.title": "啟用自動更新檢查"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意語言",
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
"first": "Ήδη το πρώτο μήνυμα",
|
||||
"last": "Ήδη το τελευταίο μήνυμα",
|
||||
"next": "Επόμενο μήνυμα",
|
||||
"prev": "Προηγούμενο μήνυμα"
|
||||
"prev": "Προηγούμενο μήνυμα",
|
||||
"top": "Επιστροφή στην κορυφή",
|
||||
"bottom": "Επιστροφή στο κάτω μέρος",
|
||||
"close": "Κλείσιμο"
|
||||
},
|
||||
"resend": "Ξαναστείλε",
|
||||
"save": "Αποθήκευση",
|
||||
@@ -512,6 +515,7 @@
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Προσθήκη στην πλευρή",
|
||||
"sidebar.remove.title": "Αφαίρεση από την πλευρή",
|
||||
"sidebar.hide.title": "Απόκρυψη μικροπρογράμματος",
|
||||
"title": "Μικρόπρογραμμα"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -885,10 +889,7 @@
|
||||
"argsTooltip": "Κάθε παράμετρος σε μια γραμμή",
|
||||
"baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL",
|
||||
"command": "Εντολή",
|
||||
"commandRequired": "Παρακαλώ εισάγετε την εντολή",
|
||||
"config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή",
|
||||
"confirmDelete": "Διαγραφή διακομιστή",
|
||||
"confirmDeleteMessage": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον διακομιστή;",
|
||||
"deleteError": "Αποτυχία διαγραφής διακομιστή",
|
||||
"deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς",
|
||||
"dependenciesInstall": "Εγκατάσταση εξαρτήσεων",
|
||||
@@ -909,7 +910,6 @@
|
||||
"jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς",
|
||||
"missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
|
||||
"name": "Όνομα",
|
||||
"nameRequired": "Παρακαλώ εισάγετε το όνομα του διακομιστή",
|
||||
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
||||
"npx_list": {
|
||||
"actions": "Ενέργειες",
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
"first": "Ya es el primer mensaje",
|
||||
"last": "Ya es el último mensaje",
|
||||
"next": "Siguiente mensaje",
|
||||
"prev": "Mensaje anterior"
|
||||
"prev": "Mensaje anterior",
|
||||
"top": "Volver arriba",
|
||||
"bottom": "Volver abajo",
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"resend": "Reenviar",
|
||||
"save": "Guardar",
|
||||
@@ -512,6 +515,7 @@
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Agregar al panel lateral",
|
||||
"sidebar.remove.title": "Quitar del panel lateral",
|
||||
"sidebar.hide.title": "Ocultar mini programa",
|
||||
"title": "Mini programa"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -885,10 +889,7 @@
|
||||
"argsTooltip": "Cada argumento en una línea",
|
||||
"baseUrlTooltip": "Dirección URL remota",
|
||||
"command": "Comando",
|
||||
"commandRequired": "Por favor ingrese el comando",
|
||||
"config_description": "Configurar modelo de contexto del protocolo del servidor",
|
||||
"confirmDelete": "Eliminar servidor",
|
||||
"confirmDeleteMessage": "¿Está seguro de que desea eliminar este servidor?",
|
||||
"deleteError": "Fallo al eliminar servidor",
|
||||
"deleteSuccess": "Servidor eliminado exitosamente",
|
||||
"dependenciesInstall": "Instalar dependencias",
|
||||
@@ -909,7 +910,6 @@
|
||||
"jsonSaveSuccess": "Configuración JSON guardada exitosamente",
|
||||
"missingDependencies": "Faltan, instalelas para continuar",
|
||||
"name": "Nombre",
|
||||
"nameRequired": "Por favor ingrese el nombre del servidor",
|
||||
"noServers": "No se han configurado servidores",
|
||||
"npx_list": {
|
||||
"actions": "Acciones",
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
"first": "Déjà premier message",
|
||||
"last": "Déjà dernier message",
|
||||
"next": "Prochain message",
|
||||
"prev": "Précédent message"
|
||||
"prev": "Précédent message",
|
||||
"top": "Retour en haut",
|
||||
"bottom": "Retour en bas",
|
||||
"close": "Fermer"
|
||||
},
|
||||
"resend": "Réenvoyer",
|
||||
"save": "Enregistrer",
|
||||
@@ -512,6 +515,7 @@
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Ajouter à la barre latérale",
|
||||
"sidebar.remove.title": "Supprimer de la barre latérale",
|
||||
"sidebar.hide.title": "Masquer le mini-programme",
|
||||
"title": "Mini-programme"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -885,10 +889,7 @@
|
||||
"argsTooltip": "Chaque argument sur une ligne",
|
||||
"baseUrlTooltip": "Adresse URL distante",
|
||||
"command": "Commande",
|
||||
"commandRequired": "Veuillez entrer une commande",
|
||||
"config_description": "Configurer le modèle du protocole de contexte du serveur",
|
||||
"confirmDelete": "Supprimer le serveur",
|
||||
"confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
|
||||
"deleteError": "Échec de la suppression du serveur",
|
||||
"deleteSuccess": "Serveur supprimé avec succès",
|
||||
"dependenciesInstall": "Installer les dépendances",
|
||||
@@ -909,7 +910,6 @@
|
||||
"jsonSaveSuccess": "Configuration JSON sauvegardée",
|
||||
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
|
||||
"name": "Nom",
|
||||
"nameRequired": "Veuillez entrer le nom du serveur",
|
||||
"noServers": "Aucun serveur configuré",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
"first": "Esta é a primeira mensagem",
|
||||
"last": "Esta é a última mensagem",
|
||||
"next": "Próxima mensagem",
|
||||
"prev": "Mensagem anterior"
|
||||
"prev": "Mensagem anterior",
|
||||
"top": "Voltar ao topo",
|
||||
"bottom": "Voltar ao fundo",
|
||||
"close": "Fechar"
|
||||
},
|
||||
"resend": "Reenviar",
|
||||
"save": "Salvar",
|
||||
@@ -512,6 +515,7 @@
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Adicionar à barra lateral",
|
||||
"sidebar.remove.title": "Remover da barra lateral",
|
||||
"sidebar.hide.title": "Ocultar aplicativo",
|
||||
"title": "Pequeno aplicativo"
|
||||
},
|
||||
"miniwindow": {
|
||||
@@ -885,10 +889,7 @@
|
||||
"argsTooltip": "Cada argumento em uma linha",
|
||||
"baseUrlTooltip": "Endereço de URL remoto",
|
||||
"command": "Comando",
|
||||
"commandRequired": "Digite o comando",
|
||||
"config_description": "Configurar modelo de protocolo de contexto do servidor",
|
||||
"confirmDelete": "Excluir servidor",
|
||||
"confirmDeleteMessage": "Tem certeza de que deseja excluir este servidor?",
|
||||
"deleteError": "Falha ao excluir servidor",
|
||||
"deleteSuccess": "Servidor excluído com sucesso",
|
||||
"dependenciesInstall": "Instalar dependências",
|
||||
@@ -909,7 +910,6 @@
|
||||
"jsonSaveSuccess": "Configuração JSON salva com sucesso",
|
||||
"missingDependencies": "Ausente, instale para continuar",
|
||||
"name": "Nome",
|
||||
"nameRequired": "Digite o nome do servidor",
|
||||
"noServers": "Nenhum servidor configurado",
|
||||
"npx_list": {
|
||||
"actions": "Ações",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import './assets/styles/index.scss'
|
||||
import '@ant-design/v5-patch-for-react-19'
|
||||
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
import MiniApp from './windows/mini/App'
|
||||
|
||||
if (location.hash === '#/mini') {
|
||||
document.getElementById('spinner')?.remove()
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<MiniApp />)
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<MiniApp />)
|
||||
} else {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<App />)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { EllipsisOutlined } from '@ant-design/icons'
|
||||
import { Agent } from '@renderer/types'
|
||||
import type { Agent } from '@renderer/types'
|
||||
import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import { type FC, memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
agent: Agent
|
||||
onClick: () => void
|
||||
contextMenu?: { label: string; onClick: () => void }[]
|
||||
contextMenu?: {
|
||||
key: string
|
||||
label: string
|
||||
icon?: React.ReactNode
|
||||
danger?: boolean
|
||||
onClick: () => void
|
||||
}[]
|
||||
menuItems?: {
|
||||
key: string
|
||||
label: string
|
||||
@@ -58,9 +64,14 @@ const AgentCard: FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: contextMenu.map((item) => ({
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
onClick: () => item.onClick()
|
||||
...item,
|
||||
onClick: (e) => {
|
||||
e.domEvent.stopPropagation()
|
||||
e.domEvent.preventDefault()
|
||||
setTimeout(() => {
|
||||
item.onClick()
|
||||
}, 0)
|
||||
}
|
||||
}))
|
||||
}}
|
||||
trigger={['contextMenu']}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } fro
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import type { Agent } from '@renderer/types'
|
||||
import { Col, Row } from 'antd'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -45,7 +45,7 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
|
||||
return (
|
||||
<Row gutter={[20, 20]}>
|
||||
{filteredAgents.map((agent) => {
|
||||
const dropdownMenuItems = [
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('agents.edit.title'),
|
||||
@@ -73,29 +73,9 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
|
||||
}
|
||||
]
|
||||
|
||||
const contextMenuItems = [
|
||||
{
|
||||
label: t('agents.edit.title'),
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
label: t('agents.add.button'),
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Col span={6} key={agent.id}>
|
||||
<AgentCard
|
||||
agent={agent}
|
||||
onClick={() => onClick?.(agent)}
|
||||
contextMenu={contextMenuItems}
|
||||
menuItems={dropdownMenuItems}
|
||||
/>
|
||||
<AgentCard agent={agent} onClick={() => onClick?.(agent)} contextMenu={menuItems} menuItems={menuItems} />
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import type { MenuProps } from 'antd'
|
||||
@@ -15,13 +15,14 @@ interface Props {
|
||||
}
|
||||
|
||||
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
||||
const { openMinappKeepAlive } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
||||
const isPinned = pinned.some((p) => p.id === app.id)
|
||||
const isVisible = minapps.some((m) => m.id === app.id)
|
||||
|
||||
const handleClick = () => {
|
||||
MinApp.start(app)
|
||||
openMinappKeepAlive(app)
|
||||
onClick?.()
|
||||
}
|
||||
|
||||
@@ -33,6 +34,18 @@ const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'hide',
|
||||
label: t('minapp.sidebar.hide.title'),
|
||||
onClick: () => {
|
||||
const newMinapps = minapps.filter((item) => item.id !== app.id)
|
||||
updateMinapps(newMinapps)
|
||||
const newDisabled = [...(disabled || []), app]
|
||||
updateDisabledMinapps(newDisabled)
|
||||
const newPinned = pinned.filter((item) => item.id !== app.id)
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
143
src/renderer/src/pages/files/FileItem.tsx
Normal file
143
src/renderer/src/pages/files/FileItem.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
FileExcelFilled,
|
||||
FileImageFilled,
|
||||
FileMarkdownFilled,
|
||||
FilePdfFilled,
|
||||
FilePptFilled,
|
||||
FileTextFilled,
|
||||
FileUnknownFilled,
|
||||
FileWordFilled,
|
||||
FileZipFilled,
|
||||
FolderOpenFilled,
|
||||
GlobalOutlined,
|
||||
LinkOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Flex } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface FileItemProps {
|
||||
fileInfo: {
|
||||
name: React.ReactNode | string
|
||||
ext: string
|
||||
extra?: React.ReactNode | string
|
||||
actions: React.ReactNode
|
||||
}
|
||||
}
|
||||
|
||||
const getFileIcon = (type?: string) => {
|
||||
if (!type) return <FileUnknownFilled />
|
||||
|
||||
const ext = type.toLowerCase()
|
||||
|
||||
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
|
||||
return <FileImageFilled />
|
||||
}
|
||||
|
||||
if (['.doc', '.docx'].includes(ext)) {
|
||||
return <FileWordFilled />
|
||||
}
|
||||
if (['.xls', '.xlsx'].includes(ext)) {
|
||||
return <FileExcelFilled />
|
||||
}
|
||||
if (['.ppt', '.pptx'].includes(ext)) {
|
||||
return <FilePptFilled />
|
||||
}
|
||||
if (ext === '.pdf') {
|
||||
return <FilePdfFilled />
|
||||
}
|
||||
if (['.md', '.markdown'].includes(ext)) {
|
||||
return <FileMarkdownFilled />
|
||||
}
|
||||
|
||||
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
|
||||
return <FileZipFilled />
|
||||
}
|
||||
|
||||
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
|
||||
return <FileTextFilled />
|
||||
}
|
||||
|
||||
if (['.url'].includes(ext)) {
|
||||
return <LinkOutlined />
|
||||
}
|
||||
|
||||
if (['.sitemap'].includes(ext)) {
|
||||
return <GlobalOutlined />
|
||||
}
|
||||
|
||||
if (['.folder'].includes(ext)) {
|
||||
return <FolderOpenFilled />
|
||||
}
|
||||
|
||||
return <FileUnknownFilled />
|
||||
}
|
||||
|
||||
const FileItem: React.FC<FileItemProps> = ({ fileInfo }) => {
|
||||
const { name, ext, extra, actions } = fileInfo
|
||||
|
||||
return (
|
||||
<FileItemCard>
|
||||
<CardContent>
|
||||
<FileIcon>{getFileIcon(ext)}</FileIcon>
|
||||
<Flex vertical gap={0} flex={1} style={{ width: '0px' }}>
|
||||
<FileName>{name}</FileName>
|
||||
{extra && <FileInfo>{extra}</FileInfo>}
|
||||
</Flex>
|
||||
{actions}
|
||||
</CardContent>
|
||||
</FileItemCard>
|
||||
)
|
||||
}
|
||||
|
||||
const FileItemCard = styled.div`
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 0.5px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
transition: box-shadow 0.2s ease;
|
||||
--shadow-color: rgba(0, 0, 0, 0.05);
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 10px 15px -3px var(--shadow-color),
|
||||
0 4px 6px -4px var(--shadow-color);
|
||||
}
|
||||
body[theme-mode='dark'] & {
|
||||
--shadow-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
`
|
||||
|
||||
const CardContent = styled.div`
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const FileIcon = styled.div`
|
||||
color: var(--color-text-3);
|
||||
font-size: 32px;
|
||||
`
|
||||
|
||||
const FileName = styled.div`
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
span {
|
||||
font-size: 15px;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
const FileInfo = styled.div`
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
export default memo(FileItem)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user