Compare commits
25 Commits
feat/messa
...
v1.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b0f0b6416 | ||
|
|
0e0c37dc17 | ||
|
|
9114bffb80 | ||
|
|
30d3afbf9b | ||
|
|
f62101e4c1 | ||
|
|
1223c39250 | ||
|
|
e8c14a0e4d | ||
|
|
6eb501a370 | ||
|
|
f0f0542a09 | ||
|
|
37c9829f43 | ||
|
|
b68a8d9dfc | ||
|
|
59a1506689 | ||
|
|
699eb2b384 | ||
|
|
470ce44316 | ||
|
|
33f636f7d8 | ||
|
|
a34955c4b6 | ||
|
|
267b1a92e8 | ||
|
|
f26bc19e34 | ||
|
|
ea605709e5 | ||
|
|
d606d0077d | ||
|
|
22c85781e5 | ||
|
|
f42fbb511e | ||
|
|
5a257cade2 | ||
|
|
e8f819fb46 | ||
|
|
7088a94489 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: main
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Get release tag
|
- name: Get release tag
|
||||||
id: get-tag
|
id: get-tag
|
||||||
@@ -149,4 +149,4 @@ jobs:
|
|||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: CherryHQ/cherry-studio-docs
|
repository: CherryHQ/cherry-studio-docs
|
||||||
event-type: update-download-version
|
event-type: update-download-version
|
||||||
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'
|
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'
|
||||||
|
|||||||
@@ -107,11 +107,7 @@ afterSign: scripts/notarize.js
|
|||||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能
|
服务商:新增端脑云、302.AI、蓝耘服务商
|
||||||
复制功能:新增纯文本复制(去除Markdown格式符号)
|
MCP: 新增蓝耘 MCP 服务器
|
||||||
知识库:支持设置向量维度,修复Ollama分数错误和维度编辑问题
|
实现话题重命名动画效果
|
||||||
多语言:增加模型名称多语言提示和翻译源语言手动选择
|
错误修复
|
||||||
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
|
|
||||||
模型:修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
|
|
||||||
图像功能:统一图片查看器,支持Base64图片渲染,修复图片预览相关问题
|
|
||||||
UI:实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.4.2",
|
"version": "1.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ exports.default = async function (context) {
|
|||||||
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === 'windows') {
|
||||||
|
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
|
||||||
|
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ export default abstract class BaseReranker {
|
|||||||
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
|
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
let baseURL = this.base.rerankBaseURL
|
||||||
? this.base.rerankBaseURL.slice(0, -1)
|
|
||||||
: this.base.rerankBaseURL
|
if (baseURL && baseURL.endsWith('/')) {
|
||||||
// 必须携带/v1,否则会404
|
// `/` 结尾强制使用rerankBaseURL
|
||||||
|
return `${baseURL}rerank`
|
||||||
|
}
|
||||||
|
|
||||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||||
baseURL = `${baseURL}/v1`
|
baseURL = `${baseURL}/v1`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { FeedUrl } from '@shared/config/constant'
|
|||||||
import { UpdateInfo } from 'builder-util-runtime'
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
import { app, BrowserWindow, dialog } from 'electron'
|
import { app, BrowserWindow, dialog } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
|
import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater } from 'electron-updater'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
import icon from '../../../build/icon.png?asset'
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
@@ -56,6 +57,10 @@ export default class AppUpdater {
|
|||||||
logger.info('下载完成', releaseInfo)
|
logger.info('下载完成', releaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isWin) {
|
||||||
|
;(autoUpdater as NsisUpdater).installDirectory = path.dirname(app.getPath('exe'))
|
||||||
|
}
|
||||||
|
|
||||||
this.autoUpdater = autoUpdater
|
this.autoUpdater = autoUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export class SelectionService {
|
|||||||
this.processTriggerMode()
|
this.processTriggerMode()
|
||||||
|
|
||||||
this.started = true
|
this.started = true
|
||||||
this.logInfo('SelectionService Started')
|
this.logInfo('SelectionService Started', true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ export class SelectionService {
|
|||||||
this.closePreloadedActionWindows()
|
this.closePreloadedActionWindows()
|
||||||
|
|
||||||
this.started = false
|
this.started = false
|
||||||
this.logInfo('SelectionService Stopped')
|
this.logInfo('SelectionService Stopped', true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ export class SelectionService {
|
|||||||
this.selectionHook = null
|
this.selectionHook = null
|
||||||
this.initStatus = false
|
this.initStatus = false
|
||||||
SelectionService.instance = null
|
SelectionService.instance = null
|
||||||
this.logInfo('SelectionService Quitted')
|
this.logInfo('SelectionService Quitted', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -456,8 +456,18 @@ export class SelectionService {
|
|||||||
x: posX,
|
x: posX,
|
||||||
y: posY
|
y: posY
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//set the window to always on top (highest level)
|
||||||
|
//should set every time the window is shown
|
||||||
|
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
|
||||||
this.toolbarWindow!.show()
|
this.toolbarWindow!.show()
|
||||||
this.toolbarWindow!.setOpacity(1)
|
|
||||||
|
/**
|
||||||
|
* In Windows 10, setOpacity(1) will make the window completely transparent
|
||||||
|
* It's a strange behavior, so we don't use it for compatibility
|
||||||
|
*/
|
||||||
|
// this.toolbarWindow!.setOpacity(1)
|
||||||
|
|
||||||
this.startHideByMouseKeyListener()
|
this.startHideByMouseKeyListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +477,7 @@ export class SelectionService {
|
|||||||
public hideToolbar(): void {
|
public hideToolbar(): void {
|
||||||
if (!this.isToolbarAlive()) return
|
if (!this.isToolbarAlive()) return
|
||||||
|
|
||||||
this.toolbarWindow!.setOpacity(0)
|
// this.toolbarWindow!.setOpacity(0)
|
||||||
this.toolbarWindow!.hide()
|
this.toolbarWindow!.hide()
|
||||||
|
|
||||||
this.stopHideByMouseKeyListener()
|
this.stopHideByMouseKeyListener()
|
||||||
@@ -1264,8 +1274,10 @@ export class SelectionService {
|
|||||||
this.isIpcHandlerRegistered = true
|
this.isIpcHandlerRegistered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private logInfo(message: string) {
|
private logInfo(message: string, forceShow: boolean = false) {
|
||||||
isDev && Logger.info('[SelectionService] Info: ', message)
|
if (isDev || forceShow) {
|
||||||
|
Logger.info('[SelectionService] Info: ', message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private logError(...args: [...string[], Error]) {
|
private logError(...args: [...string[], Error]) {
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/renderer/src/assets/images/providers/lanyun.png
Normal file
BIN
src/renderer/src/assets/images/providers/lanyun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -136,6 +136,10 @@ ul {
|
|||||||
display: flow-root;
|
display: flow-root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-wrapper:last-child > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.message-content-container > *:last-child {
|
.message-content-container > *:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,13 +295,16 @@ emoji-picker {
|
|||||||
--border-size: 0;
|
--border-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.katex-display {
|
.katex,
|
||||||
|
mjx-container {
|
||||||
|
display: inline-block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
overflow-wrap: break-word;
|
||||||
|
vertical-align: middle;
|
||||||
mjx-container {
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
padding: 1px 2px;
|
||||||
|
margin-top: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CodeMirror 相关样式 */
|
/* CodeMirror 相关样式 */
|
||||||
@@ -318,6 +321,7 @@ mjx-container {
|
|||||||
|
|
||||||
.cm-gutters {
|
.cm-gutters {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-content {
|
.cm-content {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [isRendering, setIsRendering] = useState(false)
|
const [isRendering, setIsRendering] = useState(false)
|
||||||
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
// 使用通用图像工具
|
// 使用通用图像工具
|
||||||
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
|
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
|
||||||
@@ -75,10 +76,55 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
[renderMermaid]
|
[renderMermaid]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听可见性变化,用于触发重新渲染。
|
||||||
|
* 这是为了解决 `MessageGroup` 组件的 `fold` 布局中被 `display: none` 隐藏的图标无法正确渲染的问题。
|
||||||
|
* 监听时向上遍历到第一个有 `fold` className 的父节点为止(也就是目前的 `MessageWrapper`)。
|
||||||
|
* FIXME: 将来 mermaid-js 修复此问题后可以移除这里的相关逻辑。
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mermaidRef.current) return
|
||||||
|
|
||||||
|
const checkVisibility = () => {
|
||||||
|
const element = mermaidRef.current
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const currentlyVisible = element.offsetParent !== null
|
||||||
|
setIsVisible(currentlyVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始检查
|
||||||
|
checkVisibility()
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
checkVisibility()
|
||||||
|
})
|
||||||
|
|
||||||
|
let targetElement = mermaidRef.current.parentElement
|
||||||
|
while (targetElement) {
|
||||||
|
observer.observe(targetElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class', 'style']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetElement.className?.includes('fold')) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
targetElement = targetElement.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 触发渲染
|
// 触发渲染
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingMermaid) return
|
if (isLoadingMermaid) return
|
||||||
|
|
||||||
|
if (mermaidRef.current?.offsetParent === null) return
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
setIsRendering(true)
|
setIsRendering(true)
|
||||||
debouncedRender(children)
|
debouncedRender(children)
|
||||||
@@ -90,7 +136,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
debouncedRender.cancel()
|
debouncedRender.cancel()
|
||||||
}
|
}
|
||||||
}, [children, isLoadingMermaid, debouncedRender])
|
}, [children, isLoadingMermaid, debouncedRender, isVisible])
|
||||||
|
|
||||||
const isLoading = isLoadingMermaid || isRendering
|
const isLoading = isLoadingMermaid || isRendering
|
||||||
|
|
||||||
|
|||||||
221
src/renderer/src/components/__tests__/MermaidPreview.test.tsx
Normal file
221
src/renderer/src/components/__tests__/MermaidPreview.test.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { act } from 'react'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||||
|
|
||||||
|
import MermaidPreview from '../CodeBlockView/MermaidPreview'
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
useMermaid: vi.fn(),
|
||||||
|
usePreviewToolHandlers: vi.fn(),
|
||||||
|
usePreviewTools: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock hooks
|
||||||
|
vi.mock('@renderer/hooks/useMermaid', () => ({
|
||||||
|
useMermaid: () => mocks.useMermaid()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@renderer/components/CodeToolbar', () => ({
|
||||||
|
usePreviewToolHandlers: () => mocks.usePreviewToolHandlers(),
|
||||||
|
usePreviewTools: () => mocks.usePreviewTools()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock nanoid
|
||||||
|
vi.mock('@reduxjs/toolkit', () => ({
|
||||||
|
nanoid: () => 'test-id-123456'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock lodash debounce
|
||||||
|
vi.mock('lodash', async () => {
|
||||||
|
const actual = await import('lodash')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
debounce: vi.fn((fn) => {
|
||||||
|
const debounced = (...args: any[]) => fn(...args)
|
||||||
|
debounced.cancel = vi.fn()
|
||||||
|
return debounced
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock antd components
|
||||||
|
vi.mock('antd', () => ({
|
||||||
|
Flex: ({ children, vertical, ...props }: any) => (
|
||||||
|
<div data-testid="flex" data-vertical={vertical} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
Spin: ({ children, spinning, indicator }: any) => (
|
||||||
|
<div data-testid="spin" data-spinning={spinning}>
|
||||||
|
{spinning && indicator}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MermaidPreview', () => {
|
||||||
|
const mockMermaid = {
|
||||||
|
parse: vi.fn(),
|
||||||
|
render: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
mocks.useMermaid.mockReturnValue({
|
||||||
|
mermaid: mockMermaid,
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.usePreviewToolHandlers.mockReturnValue({
|
||||||
|
handleZoom: vi.fn(),
|
||||||
|
handleCopyImage: vi.fn(),
|
||||||
|
handleDownload: vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.usePreviewTools.mockReturnValue({})
|
||||||
|
|
||||||
|
mockMermaid.parse.mockResolvedValue(true)
|
||||||
|
mockMermaid.render.mockResolvedValue({
|
||||||
|
svg: '<svg class="flowchart" viewBox="0 0 100 100"><g>test diagram</g></svg>'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock MutationObserver
|
||||||
|
global.MutationObserver = vi.fn().mockImplementation(() => ({
|
||||||
|
observe: vi.fn(),
|
||||||
|
disconnect: vi.fn(),
|
||||||
|
takeRecords: vi.fn()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('visibility detection', () => {
|
||||||
|
it('should not render mermaid when element has display: none', async () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
const { container } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Mock offsetParent to be null (simulating display: none)
|
||||||
|
const mermaidElement = container.querySelector('.mermaid')
|
||||||
|
if (mermaidElement) {
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => null,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render to trigger the effect
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not call mermaid render when offsetParent is null
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const svgElement = mermaidElement?.querySelector('svg.flowchart')
|
||||||
|
expect(svgElement).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should setup MutationObserver to monitor parent elements', () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
expect(global.MutationObserver).toHaveBeenCalledWith(expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should observe parent elements up to fold className', () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
// Create a DOM structure that simulates MessageGroup fold layout
|
||||||
|
const foldContainer = document.createElement('div')
|
||||||
|
foldContainer.className = 'fold selected'
|
||||||
|
|
||||||
|
const messageWrapper = document.createElement('div')
|
||||||
|
messageWrapper.className = 'message-wrapper'
|
||||||
|
|
||||||
|
const codeBlock = document.createElement('div')
|
||||||
|
codeBlock.className = 'code-block'
|
||||||
|
|
||||||
|
foldContainer.appendChild(messageWrapper)
|
||||||
|
messageWrapper.appendChild(codeBlock)
|
||||||
|
document.body.appendChild(foldContainer)
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>, {
|
||||||
|
container: codeBlock
|
||||||
|
})
|
||||||
|
|
||||||
|
const observerInstance = (global.MutationObserver as Mock).mock.results[0]?.value
|
||||||
|
expect(observerInstance.observe).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(foldContainer)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should trigger re-render when visibility changes from hidden to visible', async () => {
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
const { container, rerender } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
const mermaidElement = container.querySelector('.mermaid')
|
||||||
|
|
||||||
|
// Initially hidden (offsetParent is null)
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => null,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockMermaid.render.mockClear()
|
||||||
|
|
||||||
|
// Re-render with hidden state
|
||||||
|
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not render when hidden
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Now make it visible
|
||||||
|
Object.defineProperty(mermaidElement, 'offsetParent', {
|
||||||
|
get: () => document.body,
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate MutationObserver callback
|
||||||
|
const observerCallback = (global.MutationObserver as Mock).mock.calls[0][0]
|
||||||
|
act(() => {
|
||||||
|
observerCallback([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Re-render to trigger visibility change effect
|
||||||
|
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockMermaid.render).toHaveBeenCalledWith('mermaid-test-id-123456', mermaidCode, expect.any(Object))
|
||||||
|
|
||||||
|
const svgElement = mermaidElement?.querySelector('svg.flowchart')
|
||||||
|
expect(svgElement).toBeInTheDocument()
|
||||||
|
expect(svgElement).toHaveClass('flowchart')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mermaid loading state', () => {
|
||||||
|
mocks.useMermaid.mockReturnValue({
|
||||||
|
mermaid: mockMermaid,
|
||||||
|
isLoading: true,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const mermaidCode = 'graph TD\nA-->B'
|
||||||
|
|
||||||
|
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
|
||||||
|
|
||||||
|
// Should not render when mermaid is loading
|
||||||
|
expect(mockMermaid.render).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Should show loading state
|
||||||
|
expect(screen.getByTestId('spin')).toHaveAttribute('data-spinning', 'true')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -429,7 +429,86 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
group: 'deepseek-ai'
|
group: 'deepseek-ai'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
'302ai': [
|
||||||
|
{
|
||||||
|
id: 'deepseek-chat',
|
||||||
|
name: 'deepseek-chat',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'DeepSeek'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek-reasoner',
|
||||||
|
name: 'deepseek-reasoner',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'DeepSeek'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chatgpt-4o-latest',
|
||||||
|
name: 'chatgpt-4o-latest',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'OpenAI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gpt-4.1',
|
||||||
|
name: 'gpt-4.1',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'OpenAI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'o3',
|
||||||
|
name: 'o3',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'OpenAI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'o4-mini',
|
||||||
|
name: 'o4-mini',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'OpenAI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qwen3-235b-a22b',
|
||||||
|
name: 'qwen3-235b-a22b',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'Qwen'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2.5-flash-preview-05-20',
|
||||||
|
name: 'gemini-2.5-flash-preview-05-20',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'Gemini'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2.5-pro-preview-06-05',
|
||||||
|
name: 'gemini-2.5-pro-preview-06-05',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'Gemini'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4-20250514',
|
||||||
|
provider: '302ai',
|
||||||
|
name: 'claude-sonnet-4-20250514',
|
||||||
|
group: 'Anthropic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-opus-4-20250514',
|
||||||
|
provider: '302ai',
|
||||||
|
name: 'claude-opus-4-20250514',
|
||||||
|
group: 'Anthropic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jina-clip-v2',
|
||||||
|
name: 'jina-clip-v2',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'Jina AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jina-reranker-m0',
|
||||||
|
name: 'jina-reranker-m0',
|
||||||
|
provider: '302ai',
|
||||||
|
group: 'Jina AI'
|
||||||
|
}
|
||||||
|
],
|
||||||
aihubmix: [
|
aihubmix: [
|
||||||
{
|
{
|
||||||
id: 'gpt-4o',
|
id: 'gpt-4o',
|
||||||
@@ -2082,7 +2161,16 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
name: 'Qwen Plus',
|
name: 'Qwen Plus',
|
||||||
group: 'Qwen'
|
group: 'Qwen'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
cephalon: [
|
||||||
|
{
|
||||||
|
id: 'DeepSeek-R1',
|
||||||
|
provider: 'cephalon',
|
||||||
|
name: 'DeepSeek-R1满血版',
|
||||||
|
group: 'DeepSeek'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
lanyun: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TEXT_TO_IMAGES_MODELS = [
|
export const TEXT_TO_IMAGES_MODELS = [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
|
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
|
||||||
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png'
|
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png'
|
||||||
import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png'
|
import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png'
|
||||||
|
import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp'
|
||||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
|
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
|
||||||
import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
|
import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
|
||||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
|
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
|
||||||
@@ -8,6 +9,7 @@ import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png
|
|||||||
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
|
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
|
||||||
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
|
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
|
||||||
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
|
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
|
||||||
|
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
|
||||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||||
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
|
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
|
||||||
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
|
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
|
||||||
@@ -20,6 +22,7 @@ import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
|||||||
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
|
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
|
||||||
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
|
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
|
||||||
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
|
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
|
||||||
|
import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png'
|
||||||
import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png'
|
import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png'
|
||||||
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
||||||
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
|
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
|
||||||
@@ -48,6 +51,7 @@ import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
|||||||
import { TOKENFLUX_HOST } from './constant'
|
import { TOKENFLUX_HOST } from './constant'
|
||||||
|
|
||||||
const PROVIDER_LOGO_MAP = {
|
const PROVIDER_LOGO_MAP = {
|
||||||
|
'302ai': Ai302ProviderLogo,
|
||||||
openai: OpenAiProviderLogo,
|
openai: OpenAiProviderLogo,
|
||||||
silicon: SiliconFlowProviderLogo,
|
silicon: SiliconFlowProviderLogo,
|
||||||
deepseek: DeepSeekProviderLogo,
|
deepseek: DeepSeekProviderLogo,
|
||||||
@@ -94,7 +98,9 @@ const PROVIDER_LOGO_MAP = {
|
|||||||
alayanew: AlayaNewProviderLogo,
|
alayanew: AlayaNewProviderLogo,
|
||||||
voyageai: VoyageAIProviderLogo,
|
voyageai: VoyageAIProviderLogo,
|
||||||
qiniu: QiniuProviderLogo,
|
qiniu: QiniuProviderLogo,
|
||||||
tokenflux: TokenFluxProviderLogo
|
tokenflux: TokenFluxProviderLogo,
|
||||||
|
cephalon: CephalonProviderLogo,
|
||||||
|
lanyun: LanyunProviderLogo
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function getProviderLogo(providerId: string) {
|
export function getProviderLogo(providerId: string) {
|
||||||
@@ -106,6 +112,17 @@ export const NOT_SUPPORTED_REANK_PROVIDERS = ['ollama']
|
|||||||
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
|
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
|
||||||
|
|
||||||
export const PROVIDER_CONFIG = {
|
export const PROVIDER_CONFIG = {
|
||||||
|
'302ai': {
|
||||||
|
api: {
|
||||||
|
url: 'https://api.302.ai'
|
||||||
|
},
|
||||||
|
websites: {
|
||||||
|
official: 'https://302.ai',
|
||||||
|
apiKey: 'https://share.302.ai/F1B71g',
|
||||||
|
docs: 'https://302ai.apifox.cn/api-147522039',
|
||||||
|
models: 'https://302.ai/pricing/'
|
||||||
|
}
|
||||||
|
},
|
||||||
openai: {
|
openai: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.openai.com'
|
url: 'https://api.openai.com'
|
||||||
@@ -612,5 +629,27 @@ export const PROVIDER_CONFIG = {
|
|||||||
docs: `${TOKENFLUX_HOST}/docs`,
|
docs: `${TOKENFLUX_HOST}/docs`,
|
||||||
models: `${TOKENFLUX_HOST}/models`
|
models: `${TOKENFLUX_HOST}/models`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
cephalon: {
|
||||||
|
api: {
|
||||||
|
url: 'https://cephalon.cloud/user-center/v1/model'
|
||||||
|
},
|
||||||
|
websites: {
|
||||||
|
official: 'https://cephalon.cloud/share/register-landing?invite_id=jSdOYA',
|
||||||
|
apiKey: 'https://cephalon.cloud/api',
|
||||||
|
docs: 'https://cephalon.cloud/apitoken/1864244127731589124',
|
||||||
|
models: 'https://cephalon.cloud/model'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lanyun: {
|
||||||
|
api: {
|
||||||
|
url: 'https://maas-api.lanyun.net'
|
||||||
|
},
|
||||||
|
websites: {
|
||||||
|
official: 'https://lanyun.net',
|
||||||
|
apiKey: 'https://maas.lanyun.net/api/#/system/apiKey',
|
||||||
|
docs: 'https://archive.lanyun.net/maas/doc/',
|
||||||
|
models: 'https://maas.lanyun.net/api/#/model/modelSquare'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|||||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { updateTopic } from '@renderer/store/assistants'
|
import { updateTopic } from '@renderer/store/assistants'
|
||||||
|
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||||
@@ -13,8 +14,6 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useAssistant } from './useAssistant'
|
import { useAssistant } from './useAssistant'
|
||||||
import { getStoreSetting } from './useSettings'
|
import { getStoreSetting } from './useSettings'
|
||||||
|
|
||||||
const renamingTopics = new Set<string>()
|
|
||||||
|
|
||||||
let _activeTopic: Topic
|
let _activeTopic: Topic
|
||||||
let _setActiveTopic: (topic: Topic) => void
|
let _setActiveTopic: (topic: Topic) => void
|
||||||
|
|
||||||
@@ -58,13 +57,46 @@ export async function getTopicById(topicId: string) {
|
|||||||
return { ...topic, messages } as Topic
|
return { ...topic, messages } as Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始重命名指定话题
|
||||||
|
*/
|
||||||
|
export const startTopicRenaming = (topicId: string) => {
|
||||||
|
const currentIds = store.getState().runtime.chat.renamingTopics
|
||||||
|
if (!currentIds.includes(topicId)) {
|
||||||
|
store.dispatch(setRenamingTopics([...currentIds, topicId]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成重命名指定话题
|
||||||
|
*/
|
||||||
|
export const finishTopicRenaming = (topicId: string) => {
|
||||||
|
const state = store.getState()
|
||||||
|
|
||||||
|
// 1. 立即从 renamingTopics 移除
|
||||||
|
const currentRenaming = state.runtime.chat.renamingTopics
|
||||||
|
store.dispatch(setRenamingTopics(currentRenaming.filter((id) => id !== topicId)))
|
||||||
|
|
||||||
|
// 2. 立即添加到 newlyRenamedTopics
|
||||||
|
const currentNewlyRenamed = state.runtime.chat.newlyRenamedTopics
|
||||||
|
store.dispatch(setNewlyRenamedTopics([...currentNewlyRenamed, topicId]))
|
||||||
|
|
||||||
|
// 3. 延迟从 newlyRenamedTopics 移除
|
||||||
|
setTimeout(() => {
|
||||||
|
const current = store.getState().runtime.chat.newlyRenamedTopics
|
||||||
|
store.dispatch(setNewlyRenamedTopics(current.filter((id) => id !== topicId)))
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicRenamingLocks = new Set<string>()
|
||||||
|
|
||||||
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
|
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
|
||||||
if (renamingTopics.has(topicId)) {
|
if (topicRenamingLocks.has(topicId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
renamingTopics.add(topicId)
|
topicRenamingLocks.add(topicId)
|
||||||
|
|
||||||
const topic = await getTopicById(topicId)
|
const topic = await getTopicById(topicId)
|
||||||
const enableTopicNaming = getStoreSetting('enableTopicNaming')
|
const enableTopicNaming = getStoreSetting('enableTopicNaming')
|
||||||
@@ -85,24 +117,36 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
|||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
.substring(0, 50)
|
.substring(0, 50)
|
||||||
if (topicName) {
|
if (topicName) {
|
||||||
const data = { ...topic, name: topicName } as Topic
|
try {
|
||||||
_setActiveTopic(data)
|
startTopicRenaming(topicId)
|
||||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
|
||||||
|
const data = { ...topic, name: topicName } as Topic
|
||||||
|
_setActiveTopic(data)
|
||||||
|
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||||
|
} finally {
|
||||||
|
finishTopicRenaming(topicId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
|
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
|
||||||
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
|
try {
|
||||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
startTopicRenaming(topicId)
|
||||||
if (summaryText) {
|
|
||||||
const data = { ...topic, name: summaryText }
|
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
|
||||||
_setActiveTopic(data)
|
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
if (summaryText) {
|
||||||
|
const data = { ...topic, name: summaryText }
|
||||||
|
_setActiveTopic(data)
|
||||||
|
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
finishTopicRenaming(topicId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
renamingTopics.delete(topicId)
|
topicRenamingLocks.delete(topicId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +161,18 @@ export const TopicManager = {
|
|||||||
return await db.topics.toArray()
|
return await db.topics.toArray()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载并返回指定话题的消息
|
||||||
|
*/
|
||||||
async getTopicMessages(id: string) {
|
async getTopicMessages(id: string) {
|
||||||
const topic = await TopicManager.getTopic(id)
|
const topic = await TopicManager.getTopic(id)
|
||||||
return topic ? topic.messages : []
|
if (!topic) return []
|
||||||
|
|
||||||
|
await store.dispatch(loadTopicMessagesThunk(id))
|
||||||
|
|
||||||
|
// 获取更新后的话题
|
||||||
|
const updatedTopic = await TopicManager.getTopic(id)
|
||||||
|
return updatedTopic?.messages || []
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeTopic(id: string) {
|
async removeTopic(id: string) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Enter prompt",
|
"add.prompt.placeholder": "Enter prompt",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Available variables",
|
"title": "Available variables",
|
||||||
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
|
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name\n{{username}}:\tUsername"
|
||||||
},
|
},
|
||||||
"add.title": "Create Agent",
|
"add.title": "Create Agent",
|
||||||
"import": {
|
"import": {
|
||||||
@@ -978,6 +978,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "Baichuan",
|
"baichuan": "Baichuan",
|
||||||
"baidu-cloud": "Baidu Cloud",
|
"baidu-cloud": "Baidu Cloud",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"dashscope": "Alibaba Cloud",
|
"dashscope": "Alibaba Cloud",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
@@ -1018,7 +1019,9 @@
|
|||||||
"zhipu": "ZHIPU AI",
|
"zhipu": "ZHIPU AI",
|
||||||
"voyageai": "Voyage AI",
|
"voyageai": "Voyage AI",
|
||||||
"qiniu": "Qiniu AI",
|
"qiniu": "Qiniu AI",
|
||||||
"tokenflux": "TokenFlux"
|
"tokenflux": "TokenFlux",
|
||||||
|
"302ai": "302.AI",
|
||||||
|
"lanyun": "LANYUN"
|
||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"confirm": "Are you sure you want to restore data?",
|
"confirm": "Are you sure you want to restore data?",
|
||||||
@@ -1960,6 +1963,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "Actions",
|
"title": "Actions",
|
||||||
|
"custom": "Custom Action",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "Reset",
|
"button": "Reset",
|
||||||
"tooltip": "Reset to default actions. Custom actions will not be deleted.",
|
"tooltip": "Reset to default actions. Custom actions will not be deleted.",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "プロンプトを入力",
|
"add.prompt.placeholder": "プロンプトを入力",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "利用可能な変数",
|
"title": "利用可能な変数",
|
||||||
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
|
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名\n{{username}}:\tユーザー名"
|
||||||
},
|
},
|
||||||
"add.title": "エージェントを作成",
|
"add.title": "エージェントを作成",
|
||||||
"import": {
|
"import": {
|
||||||
@@ -703,7 +703,8 @@
|
|||||||
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
|
"warn.siyuan.exporting": "思源ノートにエクスポート中です。重複してエクスポートしないでください!",
|
||||||
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
|
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
|
||||||
"download.success": "ダウンロードに成功しました",
|
"download.success": "ダウンロードに成功しました",
|
||||||
"download.failed": "ダウンロードに失敗しました"
|
"download.failed": "ダウンロードに失敗しました",
|
||||||
|
"error.fetchTopicName": "トピック名の取得に失敗しました"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"popup": {
|
"popup": {
|
||||||
@@ -1017,7 +1018,10 @@
|
|||||||
"zhipu": "智譜AI",
|
"zhipu": "智譜AI",
|
||||||
"voyageai": "Voyage AI",
|
"voyageai": "Voyage AI",
|
||||||
"qiniu": "七牛云 AI 推理",
|
"qiniu": "七牛云 AI 推理",
|
||||||
"tokenflux": "TokenFlux"
|
"tokenflux": "TokenFlux",
|
||||||
|
"302ai": "302.AI",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
|
"lanyun": "LANYUN"
|
||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"confirm": "データを復元しますか?",
|
"confirm": "データを復元しますか?",
|
||||||
@@ -1959,6 +1963,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "機能設定",
|
"title": "機能設定",
|
||||||
|
"custom": "カスタム機能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "リセット",
|
"button": "リセット",
|
||||||
"tooltip": "デフォルト機能にリセット(カスタム機能は保持)",
|
"tooltip": "デフォルト機能にリセット(カスタム機能は保持)",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Введите промпт",
|
"add.prompt.placeholder": "Введите промпт",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Доступные переменные",
|
"title": "Доступные переменные",
|
||||||
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
|
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели\n{{username}}:\tИмя пользователя"
|
||||||
},
|
},
|
||||||
"add.title": "Создать агента",
|
"add.title": "Создать агента",
|
||||||
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
|
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
|
||||||
@@ -703,7 +703,8 @@
|
|||||||
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
|
"warn.yuque.exporting": "Экспортируется в Yuque, пожалуйста, не отправляйте повторные запросы!",
|
||||||
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
|
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
|
||||||
"download.success": "Скачано успешно",
|
"download.success": "Скачано успешно",
|
||||||
"download.failed": "Скачивание не удалось"
|
"download.failed": "Скачивание не удалось",
|
||||||
|
"error.fetchTopicName": "Не удалось назвать топик"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"popup": {
|
"popup": {
|
||||||
@@ -977,6 +978,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "Baichuan",
|
"baichuan": "Baichuan",
|
||||||
"baidu-cloud": "Baidu Cloud",
|
"baidu-cloud": "Baidu Cloud",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"dashscope": "Alibaba Cloud",
|
"dashscope": "Alibaba Cloud",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
@@ -1017,7 +1019,9 @@
|
|||||||
"zhipu": "ZHIPU AI",
|
"zhipu": "ZHIPU AI",
|
||||||
"voyageai": "Voyage AI",
|
"voyageai": "Voyage AI",
|
||||||
"qiniu": "Qiniu AI",
|
"qiniu": "Qiniu AI",
|
||||||
"tokenflux": "TokenFlux"
|
"tokenflux": "TokenFlux",
|
||||||
|
"302ai": "302.AI",
|
||||||
|
"lanyun": "LANYUN"
|
||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"confirm": "Вы уверены, что хотите восстановить данные?",
|
"confirm": "Вы уверены, что хотите восстановить данные?",
|
||||||
@@ -1959,6 +1963,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "Действия",
|
"title": "Действия",
|
||||||
|
"custom": "Пользовательское действие",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "Сбросить",
|
"button": "Сбросить",
|
||||||
"tooltip": "Сбросить стандартные действия. Пользовательские останутся.",
|
"tooltip": "Сбросить стандартные действия. Пользовательские останутся.",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "输入提示词",
|
"add.prompt.placeholder": "输入提示词",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "可用的变量",
|
"title": "可用的变量",
|
||||||
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
|
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称\n{{username}}:\t用户名"
|
||||||
},
|
},
|
||||||
"add.title": "创建智能体",
|
"add.title": "创建智能体",
|
||||||
"import": {
|
"import": {
|
||||||
@@ -978,6 +978,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "百川",
|
"baichuan": "百川",
|
||||||
"baidu-cloud": "百度云千帆",
|
"baidu-cloud": "百度云千帆",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"dashscope": "阿里云百炼",
|
"dashscope": "阿里云百炼",
|
||||||
"deepseek": "深度求索",
|
"deepseek": "深度求索",
|
||||||
@@ -1018,7 +1019,9 @@
|
|||||||
"zhipu": "智谱AI",
|
"zhipu": "智谱AI",
|
||||||
"voyageai": "Voyage AI",
|
"voyageai": "Voyage AI",
|
||||||
"qiniu": "七牛云 AI 推理",
|
"qiniu": "七牛云 AI 推理",
|
||||||
"tokenflux": "TokenFlux"
|
"tokenflux": "TokenFlux",
|
||||||
|
"302ai": "302.AI",
|
||||||
|
"lanyun": "蓝耘科技"
|
||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"confirm": "确定要恢复数据吗?",
|
"confirm": "确定要恢复数据吗?",
|
||||||
@@ -1925,7 +1928,7 @@
|
|||||||
"selected": "划词",
|
"selected": "划词",
|
||||||
"selected_note": "划词后立即显示工具栏",
|
"selected_note": "划词后立即显示工具栏",
|
||||||
"ctrlkey": "Ctrl 键",
|
"ctrlkey": "Ctrl 键",
|
||||||
"ctrlkey_note": "划词后,再 按住 Ctrl键,才显示工具栏",
|
"ctrlkey_note": "划词后,再 长按 Ctrl键,才显示工具栏",
|
||||||
"shortcut": "快捷键",
|
"shortcut": "快捷键",
|
||||||
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
|
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
|
||||||
"shortcut_link": "前往快捷键设置"
|
"shortcut_link": "前往快捷键设置"
|
||||||
@@ -1960,6 +1963,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "功能",
|
"title": "功能",
|
||||||
|
"custom": "自定义功能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "重置",
|
"button": "重置",
|
||||||
"tooltip": "重置为默认功能,自定义功能不会被删除",
|
"tooltip": "重置为默认功能,自定义功能不会被删除",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "輸入提示詞",
|
"add.prompt.placeholder": "輸入提示詞",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "可用的變數",
|
"title": "可用的變數",
|
||||||
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
|
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱\n{{username}}:\t使用者名稱"
|
||||||
},
|
},
|
||||||
"add.title": "建立智慧代理人",
|
"add.title": "建立智慧代理人",
|
||||||
"import": {
|
"import": {
|
||||||
@@ -978,6 +978,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "百川",
|
"baichuan": "百川",
|
||||||
"baidu-cloud": "百度雲千帆",
|
"baidu-cloud": "百度雲千帆",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"dashscope": "阿里雲百鍊",
|
"dashscope": "阿里雲百鍊",
|
||||||
"deepseek": "深度求索",
|
"deepseek": "深度求索",
|
||||||
@@ -1018,7 +1019,9 @@
|
|||||||
"zhipu": "智譜 AI",
|
"zhipu": "智譜 AI",
|
||||||
"voyageai": "Voyage AI",
|
"voyageai": "Voyage AI",
|
||||||
"qiniu": "七牛雲 AI 推理",
|
"qiniu": "七牛雲 AI 推理",
|
||||||
"tokenflux": "TokenFlux"
|
"tokenflux": "TokenFlux",
|
||||||
|
"302ai": "302.AI",
|
||||||
|
"lanyun": "藍耘"
|
||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"confirm": "確定要復原資料嗎?",
|
"confirm": "確定要復原資料嗎?",
|
||||||
@@ -1960,6 +1963,7 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "功能",
|
"title": "功能",
|
||||||
|
"custom": "自訂功能",
|
||||||
"reset": {
|
"reset": {
|
||||||
"button": "重設",
|
"button": "重設",
|
||||||
"tooltip": "重設為預設功能,自訂功能不會被刪除",
|
"tooltip": "重設為預設功能,自訂功能不會被刪除",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
|
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Διαθέσιμες μεταβλητές",
|
"title": "Διαθέσιμες μεταβλητές",
|
||||||
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
|
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου\n{{username}}:\tΌνομα χρήστη"
|
||||||
},
|
},
|
||||||
"add.title": "Δημιουργία νέου ειδικού",
|
"add.title": "Δημιουργία νέου ειδικού",
|
||||||
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
|
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
|
||||||
@@ -840,6 +840,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "Παράκειμαι",
|
"baichuan": "Παράκειμαι",
|
||||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilot",
|
"copilot": "GitHub Copilot",
|
||||||
"dashscope": "AliCloud Bailian",
|
"dashscope": "AliCloud Bailian",
|
||||||
"deepseek": "Βαθιά Αναζήτηση",
|
"deepseek": "Βαθιά Αναζήτηση",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Ingrese la palabra clave",
|
"add.prompt.placeholder": "Ingrese la palabra clave",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variables disponibles",
|
"title": "Variables disponibles",
|
||||||
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
|
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo\n{{username}}:\tNombre de usuario"
|
||||||
},
|
},
|
||||||
"add.title": "Crear agente inteligente",
|
"add.title": "Crear agente inteligente",
|
||||||
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
|
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
|
||||||
@@ -841,6 +841,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "BaiChuan",
|
"baichuan": "BaiChuan",
|
||||||
"baidu-cloud": "Baidu Nube Qiánfān",
|
"baidu-cloud": "Baidu Nube Qiánfān",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copiloto",
|
"copilot": "GitHub Copiloto",
|
||||||
"dashscope": "Álibaba Nube BaiLiàn",
|
"dashscope": "Álibaba Nube BaiLiàn",
|
||||||
"deepseek": "Profundo Buscar",
|
"deepseek": "Profundo Buscar",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Entrer le mot-clé",
|
"add.prompt.placeholder": "Entrer le mot-clé",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variables disponibles",
|
"title": "Variables disponibles",
|
||||||
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
|
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle\n{{username}}:\tNom d'utilisateur"
|
||||||
},
|
},
|
||||||
"add.title": "Créer un agent intelligent",
|
"add.title": "Créer un agent intelligent",
|
||||||
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
|
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
|
||||||
@@ -840,6 +840,7 @@
|
|||||||
"azure-openai": "Azure OpenAI",
|
"azure-openai": "Azure OpenAI",
|
||||||
"baichuan": "BaiChuan",
|
"baichuan": "BaiChuan",
|
||||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||||
|
"cephalon": "Cephalon",
|
||||||
"copilot": "GitHub Copilote",
|
"copilot": "GitHub Copilote",
|
||||||
"dashscope": "AliCloud BaiLian",
|
"dashscope": "AliCloud BaiLian",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"add.prompt.placeholder": "Digite o Prompt",
|
"add.prompt.placeholder": "Digite o Prompt",
|
||||||
"add.prompt.variables.tip": {
|
"add.prompt.variables.tip": {
|
||||||
"title": "Variáveis disponíveis",
|
"title": "Variáveis disponíveis",
|
||||||
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
|
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo\n{{username}}:\tNome de utilizador"
|
||||||
},
|
},
|
||||||
"add.title": "Criar Agente Inteligente",
|
"add.title": "Criar Agente Inteligente",
|
||||||
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
|
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
|
||||||
|
|||||||
@@ -190,16 +190,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topic.prompt) {
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
assistant.prompt = assistant.prompt ? `${assistant.prompt}\n${topic.prompt}` : topic.prompt
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
}
|
: assistant
|
||||||
|
|
||||||
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
|
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
|
||||||
|
|
||||||
const { message, blocks } = getUserMessage(baseUserMessage)
|
const { message, blocks } = getUserMessage(baseUserMessage)
|
||||||
|
|
||||||
currentMessageId.current = message.id
|
currentMessageId.current = message.id
|
||||||
dispatch(_sendMessage(message, blocks, assistant, topic.id))
|
dispatch(_sendMessage(message, blocks, assistantWithTopicPrompt, topic.id))
|
||||||
|
|
||||||
// Clear input
|
// Clear input
|
||||||
setText('')
|
setText('')
|
||||||
|
|||||||
@@ -80,14 +80,17 @@ const MessageItem: FC<Props> = ({
|
|||||||
|
|
||||||
const handleEditResend = useCallback(
|
const handleEditResend = useCallback(
|
||||||
async (blocks: MessageBlock[]) => {
|
async (blocks: MessageBlock[]) => {
|
||||||
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
try {
|
try {
|
||||||
await resendUserMessageWithEdit(message, blocks, assistant)
|
await resendUserMessageWithEdit(message, blocks, assistantWithTopicPrompt)
|
||||||
stopEditing()
|
stopEditing()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resend message:', error)
|
console.error('Failed to resend message:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[message, resendUserMessageWithEdit, assistant, stopEditing]
|
[message, resendUserMessageWithEdit, assistant, stopEditing, topic.prompt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEditCancel = useCallback(() => {
|
const handleEditCancel = useCallback(() => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { Model } from '@renderer/types'
|
|||||||
import type { Assistant, Topic } from '@renderer/types'
|
import type { Assistant, Topic } from '@renderer/types'
|
||||||
import type { Message } from '@renderer/types/newMessage'
|
import type { Message } from '@renderer/types/newMessage'
|
||||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
|
||||||
|
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
||||||
import {
|
import {
|
||||||
exportMarkdownToJoplin,
|
exportMarkdownToJoplin,
|
||||||
exportMarkdownToSiyuan,
|
exportMarkdownToSiyuan,
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
exportMessageToNotion,
|
exportMessageToNotion,
|
||||||
messageToMarkdown
|
messageToMarkdown
|
||||||
} from '@renderer/utils/export'
|
} from '@renderer/utils/export'
|
||||||
import { copyMessageAsPlainText } from '@renderer/utils/copy'
|
|
||||||
// import { withMessageThought } from '@renderer/utils/formats'
|
// import { withMessageThought } from '@renderer/utils/formats'
|
||||||
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
||||||
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||||
@@ -124,10 +124,13 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const handleResendUserMessage = useCallback(
|
const handleResendUserMessage = useCallback(
|
||||||
async (messageUpdate?: Message) => {
|
async (messageUpdate?: Message) => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
await resendMessage(messageUpdate ?? message, assistant)
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
|
await resendMessage(messageUpdate ?? message, assistantWithTopicPrompt)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[assistant, loading, message, resendMessage]
|
[assistant, loading, message, resendMessage, topic.prompt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { startEditing } = useMessageEditing()
|
const { startEditing } = useMessageEditing()
|
||||||
@@ -316,8 +319,12 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
// const _message = resetAssistantMessage(message, selectedModel)
|
// const _message = resetAssistantMessage(message, selectedModel)
|
||||||
// editMessage(message.id, { ..._message }) // REMOVED
|
// editMessage(message.id, { ..._message }) // REMOVED
|
||||||
|
|
||||||
|
const assistantWithTopicPrompt = topic.prompt
|
||||||
|
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
|
||||||
|
: assistant
|
||||||
|
|
||||||
// Call the function from the hook
|
// Call the function from the hook
|
||||||
regenerateAssistantMessage(message, assistant)
|
regenerateAssistantMessage(message, assistantWithTopicPrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = async (e: React.MouseEvent) => {
|
const onMentionModel = async (e: React.MouseEvent) => {
|
||||||
@@ -399,7 +406,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
menu={{
|
menu={{
|
||||||
style: {
|
style: {
|
||||||
maxHeight: 250,
|
maxHeight: 250,
|
||||||
overflowY: 'auto'
|
overflowY: 'auto',
|
||||||
|
backgroundClip: 'border-box'
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
...TranslateLanguageOptions.map((item) => ({
|
...TranslateLanguageOptions.map((item) => ({
|
||||||
|
|||||||
@@ -53,15 +53,17 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
showTokens && (
|
||||||
{hasMetrics ? (
|
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||||
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
|
{hasMetrics ? (
|
||||||
{showTokens && tokensInfo}
|
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
|
||||||
</Popover>
|
{tokensInfo}
|
||||||
) : (
|
</Popover>
|
||||||
tokensInfo
|
) : (
|
||||||
)}
|
tokensInfo
|
||||||
</MessageMetadata>
|
)}
|
||||||
|
</MessageMetadata>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { isMac } from '@renderer/config/constant'
|
|||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
|
||||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
@@ -57,6 +57,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
|
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
|
||||||
|
|
||||||
|
const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics)
|
||||||
|
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
|
||||||
|
|
||||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||||
|
|
||||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||||
@@ -84,6 +87,20 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
[activeTopic.id, pendingTopics]
|
[activeTopic.id, pendingTopics]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isRenaming = useCallback(
|
||||||
|
(topicId: string) => {
|
||||||
|
return renamingTopics.includes(topicId)
|
||||||
|
},
|
||||||
|
[renamingTopics]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isNewlyRenamed = useCallback(
|
||||||
|
(topicId: string) => {
|
||||||
|
return newlyRenamedTopics.includes(topicId)
|
||||||
|
},
|
||||||
|
[newlyRenamedTopics]
|
||||||
|
)
|
||||||
|
|
||||||
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
|
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@@ -170,16 +187,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
label: t('chat.topics.auto_rename'),
|
label: t('chat.topics.auto_rename'),
|
||||||
key: 'auto-rename',
|
key: 'auto-rename',
|
||||||
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
|
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
|
||||||
|
disabled: isRenaming(topic.id),
|
||||||
async onClick() {
|
async onClick() {
|
||||||
const messages = await TopicManager.getTopicMessages(topic.id)
|
const messages = await TopicManager.getTopicMessages(topic.id)
|
||||||
if (messages.length >= 2) {
|
if (messages.length >= 2) {
|
||||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
startTopicRenaming(topic.id)
|
||||||
if (summaryText) {
|
try {
|
||||||
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
|
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||||
updateTopic(updatedTopic)
|
if (summaryText) {
|
||||||
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
|
||||||
} else {
|
updateTopic(updatedTopic)
|
||||||
window.message?.error(t('message.error.fetchTopicName'))
|
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
|
||||||
|
} else {
|
||||||
|
window.message?.error(t('message.error.fetchTopicName'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
finishTopicRenaming(topic.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,6 +211,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
label: t('chat.topics.edit.title'),
|
label: t('chat.topics.edit.title'),
|
||||||
key: 'rename',
|
key: 'rename',
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
|
disabled: isRenaming(topic.id),
|
||||||
async onClick() {
|
async onClick() {
|
||||||
const name = await PromptPopup.show({
|
const name = await PromptPopup.show({
|
||||||
title: t('chat.topics.edit.title'),
|
title: t('chat.topics.edit.title'),
|
||||||
@@ -388,6 +412,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
}, [
|
}, [
|
||||||
targetTopic,
|
targetTopic,
|
||||||
t,
|
t,
|
||||||
|
isRenaming,
|
||||||
exportMenuOptions.image,
|
exportMenuOptions.image,
|
||||||
exportMenuOptions.markdown,
|
exportMenuOptions.markdown,
|
||||||
exportMenuOptions.markdown_reason,
|
exportMenuOptions.markdown_reason,
|
||||||
@@ -430,6 +455,13 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
const topicName = topic.name.replace('`', '')
|
const topicName = topic.name.replace('`', '')
|
||||||
const topicPrompt = topic.prompt
|
const topicPrompt = topic.prompt
|
||||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||||
|
|
||||||
|
const getTopicNameClassName = () => {
|
||||||
|
if (isRenaming(topic.id)) return 'shimmer'
|
||||||
|
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopicListItem
|
<TopicListItem
|
||||||
onContextMenu={() => setTargetTopic(topic)}
|
onContextMenu={() => setTargetTopic(topic)}
|
||||||
@@ -438,7 +470,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
style={{ borderRadius }}>
|
style={{ borderRadius }}>
|
||||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||||
<TopicNameContainer>
|
<TopicNameContainer>
|
||||||
<TopicName className="name" title={topicName}>
|
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||||
{topicName}
|
{topicName}
|
||||||
</TopicName>
|
</TopicName>
|
||||||
{isActive && !topic.pinned && (
|
{isActive && !topic.pinned && (
|
||||||
@@ -544,6 +576,46 @@ const TopicName = styled.div`
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
position: relative;
|
||||||
|
will-change: background-position, width;
|
||||||
|
|
||||||
|
--color-shimmer-mid: var(--color-text-1);
|
||||||
|
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||||
|
|
||||||
|
&.shimmer {
|
||||||
|
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||||
|
background-size: 200% 100%;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
animation: shimmer 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.typing {
|
||||||
|
display: block;
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
-webkit-box-orient: unset;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: typewriter 0.5s steps(40, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typewriter {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const PendingIndicator = styled.div.attrs({
|
const PendingIndicator = styled.div.attrs({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SyncOutlined } from '@ant-design/icons'
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -7,13 +8,14 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
const UpdateAppButton: FC = () => {
|
const UpdateAppButton: FC = () => {
|
||||||
const { update } = useRuntime()
|
const { update } = useRuntime()
|
||||||
|
const { autoCheckUpdate } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!update.downloaded) {
|
if (!update.downloaded || !autoCheckUpdate) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
|
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
|
||||||
|
import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './providers/lanyun'
|
||||||
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
|
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
|
||||||
|
|
||||||
// Provider configuration interface
|
// Provider configuration interface
|
||||||
@@ -45,6 +46,17 @@ const providers: ProviderConfig[] = [
|
|||||||
getToken: getTokenFluxToken,
|
getToken: getTokenFluxToken,
|
||||||
saveToken: saveTokenFluxToken,
|
saveToken: saveTokenFluxToken,
|
||||||
syncServers: syncTokenFluxServers
|
syncServers: syncTokenFluxServers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lanyun',
|
||||||
|
name: '蓝耘科技',
|
||||||
|
description: '蓝耘科技云平台 MCP 服务',
|
||||||
|
discoverUrl: 'https://mcp.lanyun.net',
|
||||||
|
apiKeyUrl: LANYUN_KEY_HOST,
|
||||||
|
tokenFieldName: 'tokenLanyunToken',
|
||||||
|
getToken: getTokenLanYunToken,
|
||||||
|
saveToken: saveTokenLanYunToken,
|
||||||
|
syncServers: syncTokenLanYunServers
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
178
src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts
Normal file
178
src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import type { MCPServer } from '@renderer/types'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
// Token storage constants and utilities
|
||||||
|
const TOKEN_STORAGE_KEY = 'tokenLanyunToken'
|
||||||
|
export const TOKENLANYUN_HOST = 'https://mcp.lanyun.net'
|
||||||
|
export const LANYUN_MCP_HOST = TOKENLANYUN_HOST + '/mcp/manager/selectListByApiKey'
|
||||||
|
export const LANYUN_KEY_HOST = TOKENLANYUN_HOST + '/#/manage/apiKey'
|
||||||
|
|
||||||
|
export const saveTokenLanYunToken = (token: string): void => {
|
||||||
|
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokenLanYunToken = (): string | null => {
|
||||||
|
return localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearTokenLanYunToken = (): void => {
|
||||||
|
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasTokenLanYunToken = (): boolean => {
|
||||||
|
return !!getTokenLanYunToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenLanYunServer {
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* locales 字段用于存储多语言信息。
|
||||||
|
* 其中 key(lang)为语言代码(如 'zh', 'en'),
|
||||||
|
* value 为该语言下的 name 和 description。
|
||||||
|
* 例如:
|
||||||
|
* {
|
||||||
|
* "zh": { name: "文档处理工具", description: "..." },
|
||||||
|
* "en": { name: "Document Processor", description: "..." }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
locales?: {
|
||||||
|
[lang: string]: {
|
||||||
|
description?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chineseName?: string
|
||||||
|
description?: string
|
||||||
|
operationalUrls?: { url: string }[]
|
||||||
|
tags?: string[]
|
||||||
|
logoUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenLanYunSyncResult {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
addedServers: MCPServer[]
|
||||||
|
errorDetails?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch and process TokenLanYun servers
|
||||||
|
export const syncTokenLanYunServers = async (
|
||||||
|
token: string,
|
||||||
|
existingServers: MCPServer[]
|
||||||
|
): Promise<TokenLanYunSyncResult> => {
|
||||||
|
const t = i18next.t
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(LANYUN_MCP_HOST, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle authentication errors
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
clearTokenLanYunToken()
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||||
|
addedServers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle server errors
|
||||||
|
if (response.status === 500 || !response.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process successful response
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.code === 401) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.code === 500) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: `Status: ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers: TokenLanYunServer[] = data.data || []
|
||||||
|
|
||||||
|
if (servers.length === 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
|
||||||
|
addedServers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform Token servers to MCP servers format
|
||||||
|
const addedServers: MCPServer[] = []
|
||||||
|
console.log('TokenLanYun servers:', servers)
|
||||||
|
for (const server of servers) {
|
||||||
|
try {
|
||||||
|
if (!server.operationalUrls?.[0]?.url) continue
|
||||||
|
|
||||||
|
// If any existing server id contains '@lanyun', clear them before adding new ones
|
||||||
|
// if (existingServers.some((s) => s.id.startsWith('@lanyun'))) {
|
||||||
|
// for (let i = existingServers.length - 1; i >= 0; i--) {
|
||||||
|
// if (existingServers[i].id.startsWith('@lanyun')) {
|
||||||
|
// existingServers.splice(i, 1)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Skip if server already exists after clearing
|
||||||
|
if (existingServers.some((s) => s.id === `@lanyun/${server.id}`)) continue
|
||||||
|
|
||||||
|
const mcpServer: MCPServer = {
|
||||||
|
id: `@lanyun/${server.id}`,
|
||||||
|
name:
|
||||||
|
server.chineseName || server.locales?.zh?.name || server.locales?.en?.name || `LanYun Server ${server.id}`,
|
||||||
|
description: server.description || '',
|
||||||
|
type: 'sse',
|
||||||
|
baseUrl: server.operationalUrls[0].url,
|
||||||
|
command: '',
|
||||||
|
args: [],
|
||||||
|
env: {},
|
||||||
|
isActive: true,
|
||||||
|
provider: '蓝耘科技',
|
||||||
|
providerUrl: server.operationalUrls[0].url,
|
||||||
|
logoUrl: server.logoUrl || '',
|
||||||
|
tags: server.tags ?? (server.chineseName ? [server.chineseName] : [])
|
||||||
|
}
|
||||||
|
|
||||||
|
addedServers.push(mcpServer)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error processing LanYun server:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: t('settings.mcp.sync.success', { count: addedServers.length }),
|
||||||
|
addedServers
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TokenLanyun sync error:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: t('settings.mcp.sync.error'),
|
||||||
|
addedServers: [],
|
||||||
|
errorDetails: String(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
|
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
|
||||||
import { Button, Divider, Input, Modal, Switch } from 'antd'
|
import { Button, Divider, Flex, Input, Modal, Popover, Switch } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
|
|
||||||
TopicNamingModalPopup.hide = onCancel
|
TopicNamingModalPopup.hide = onCancel
|
||||||
|
|
||||||
|
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('settings.models.topic_naming_model_setting_title')}
|
title={t('settings.models.topic_naming_model_setting_title')}
|
||||||
@@ -53,7 +56,12 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
<Flex align="center" style={{ marginBottom: 10, gap: 5 }}>
|
||||||
|
<div>{t('settings.models.topic_naming_prompt')}</div>
|
||||||
|
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
|
||||||
|
<QuestionCircleOutlined size={14} style={{ color: 'var(--color-text-2)' }} />
|
||||||
|
</Popover>
|
||||||
|
</Flex>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
value={topicNamingPrompt || t('prompts.title')}
|
value={topicNamingPrompt || t('prompts.title')}
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ const SettingsActionsListHeader = memo(({ customItemsCount, maxCustomItems, onRe
|
|||||||
? t('selection.settings.actions.add_tooltip.disabled', { max: maxCustomItems })
|
? t('selection.settings.actions.add_tooltip.disabled', { max: maxCustomItems })
|
||||||
: t('selection.settings.actions.add_tooltip.enabled')
|
: t('selection.settings.actions.add_tooltip.enabled')
|
||||||
}>
|
}>
|
||||||
<Button type="primary" icon={<Plus size={16} />} onClick={onAdd} disabled={isCustomItemLimitReached} />
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
onClick={onAdd}
|
||||||
|
disabled={isCustomItemLimitReached}
|
||||||
|
style={{ paddingInline: '8px' }}>
|
||||||
|
{t('selection.settings.actions.custom')}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 111,
|
version: 112,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,12 +127,32 @@ export const INITIAL_PROVIDERS: Provider[] = [
|
|||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'o3',
|
id: '302ai',
|
||||||
name: 'O3',
|
name: '302.AI',
|
||||||
type: 'openai',
|
type: 'openai',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'https://api.o3.fan',
|
apiHost: 'https://api.302.ai',
|
||||||
models: SYSTEM_MODELS.o3,
|
models: SYSTEM_MODELS['302ai'],
|
||||||
|
isSystem: true,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cephalon',
|
||||||
|
name: 'Cephalon',
|
||||||
|
type: 'openai',
|
||||||
|
apiKey: '',
|
||||||
|
apiHost: 'https://cephalon.cloud/user-center/v1/model',
|
||||||
|
models: SYSTEM_MODELS.cephalon,
|
||||||
|
isSystem: true,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lanyun',
|
||||||
|
name: 'LANYUN',
|
||||||
|
type: 'openai',
|
||||||
|
apiKey: '',
|
||||||
|
apiHost: 'https://maas-api.lanyun.net',
|
||||||
|
models: SYSTEM_MODELS.lanyun,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1554,6 +1554,19 @@ const migrateConfig = {
|
|||||||
// add selection_assistant_toggle and selection_assistant_select_text shortcuts after mini_window
|
// add selection_assistant_toggle and selection_assistant_select_text shortcuts after mini_window
|
||||||
addShortcuts(state, ['selection_assistant_toggle', 'selection_assistant_select_text'], 'mini_window')
|
addShortcuts(state, ['selection_assistant_toggle', 'selection_assistant_select_text'], 'mini_window')
|
||||||
|
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'112': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
addProvider(state, 'cephalon')
|
||||||
|
addProvider(state, '302ai')
|
||||||
|
addProvider(state, 'lanyun')
|
||||||
|
state.llm.providers = moveProvider(state.llm.providers, 'cephalon', 13)
|
||||||
|
state.llm.providers = moveProvider(state.llm.providers, '302ai', 14)
|
||||||
|
state.llm.providers = moveProvider(state.llm.providers, 'lanyun', 15)
|
||||||
return state
|
return state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
return state
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export interface ChatState {
|
|||||||
isMultiSelectMode: boolean
|
isMultiSelectMode: boolean
|
||||||
selectedMessageIds: string[]
|
selectedMessageIds: string[]
|
||||||
activeTopic: Topic | null
|
activeTopic: Topic | null
|
||||||
|
/** topic ids that are currently being renamed */
|
||||||
|
renamingTopics: string[]
|
||||||
|
/** topic ids that are newly renamed */
|
||||||
|
newlyRenamedTopics: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
@@ -65,7 +69,9 @@ const initialState: RuntimeState = {
|
|||||||
chat: {
|
chat: {
|
||||||
isMultiSelectMode: false,
|
isMultiSelectMode: false,
|
||||||
selectedMessageIds: [],
|
selectedMessageIds: [],
|
||||||
activeTopic: null
|
activeTopic: null,
|
||||||
|
renamingTopics: [],
|
||||||
|
newlyRenamedTopics: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +124,12 @@ const runtimeSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
||||||
state.chat.activeTopic = action.payload
|
state.chat.activeTopic = action.payload
|
||||||
|
},
|
||||||
|
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
||||||
|
state.chat.renamingTopics = action.payload
|
||||||
|
},
|
||||||
|
setNewlyRenamedTopics: (state, action: PayloadAction<string[]>) => {
|
||||||
|
state.chat.newlyRenamedTopics = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -137,7 +149,9 @@ export const {
|
|||||||
// Chat related actions
|
// Chat related actions
|
||||||
toggleMultiSelectMode,
|
toggleMultiSelectMode,
|
||||||
setSelectedMessageIds,
|
setSelectedMessageIds,
|
||||||
setActiveTopic
|
setActiveTopic,
|
||||||
|
setRenamingTopics,
|
||||||
|
setNewlyRenamedTopics
|
||||||
} = runtimeSlice.actions
|
} = runtimeSlice.actions
|
||||||
|
|
||||||
export default runtimeSlice.reducer
|
export default runtimeSlice.reducer
|
||||||
|
|||||||
@@ -204,6 +204,16 @@ export const buildSystemPrompt = async (userSystemPrompt: string, tools?: MCPToo
|
|||||||
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model')
|
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userSystemPrompt.includes('{{username}}')) {
|
||||||
|
try {
|
||||||
|
const username = store.getState().settings.userName || 'Unknown Username'
|
||||||
|
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, username)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get username:', error)
|
||||||
|
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tools && tools.length > 0) {
|
if (tools && tools.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user