Merge branch 'main' of github.com:CherryHQ/cherry-studio into wip/data-refactor

This commit is contained in:
fullex 2025-09-03 16:58:15 +08:00
commit 7b633641d1
58 changed files with 998 additions and 316 deletions

3
.gitignore vendored
View File

@ -60,6 +60,9 @@ coverage
.vitest-cache
vitest.config.*.timestamp-*
# TypeScript incremental build
.tsbuildinfo
# playwright
playwright-report
test-results

View File

@ -0,0 +1,30 @@
diff --git a/index.js b/index.js
index dc071739e79876dff88e1be06a9168e294222d13..b9df7525c62bdf777e89e732e1b0c81f84d872f2 100644
--- a/index.js
+++ b/index.js
@@ -380,7 +380,7 @@ if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
}
}
-if (!nativeBinding) {
+if (!nativeBinding && process.platform !== 'linux') {
if (loadErrors.length > 0) {
throw new Error(
`Cannot find native binding. ` +
@@ -392,6 +392,13 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
-module.exports = nativeBinding
-module.exports.OcrAccuracy = nativeBinding.OcrAccuracy
-module.exports.recognize = nativeBinding.recognize
+if (process.platform === 'linux') {
+ module.exports = {OcrAccuracy: {
+ Fast: 0,
+ Accurate: 1
+ }, recognize: () => Promise.resolve({text: '', confidence: 1.0})}
+}else{
+ module.exports = nativeBinding
+ module.exports.OcrAccuracy = nativeBinding.OcrAccuracy
+ module.exports.recognize = nativeBinding.recognize
+}

View File

@ -4,6 +4,8 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import pkg from './package.json' assert { type: 'json' }
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
@ -27,7 +29,7 @@ export default defineConfig({
},
build: {
rollupOptions: {
external: ['@libsql/client', 'bufferutil', 'utf-8-validate'],
external: ['bufferutil', 'utf-8-validate', 'electron', ...Object.keys(pkg.dependencies)],
output: {
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
inlineDynamicImports: true // 内联所有动态导入,这是关键配置

View File

@ -47,7 +47,7 @@
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "tsx scripts/check-i18n.ts",
@ -73,7 +73,7 @@
"dependencies": {
"@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "^1.0.2",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"graceful-fs": "^4.2.11",
"jsdom": "26.1.0",
@ -200,6 +200,7 @@
"cli-progress": "^3.12.0",
"code-inspector-plugin": "^0.20.14",
"color": "^5.0.0",
"concurrently": "^9.2.1",
"country-flag-emoji-polyfill": "0.1.8",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",

View File

@ -2089,7 +2089,7 @@
"Design",
"Education"
],
"prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writting n], 10 being the default value) and to be an accurate and complexe representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n",
"prompt": "I want you to act as a Graphviz DOT generator, an expert to create meaningful diagrams. The diagram should have at least n nodes (I specify n in my input by writing n], 10 being the default value) and to be an accurate and complex representation of the given input. Each node is indexed by a number to reduce the size of the output, should not include any styling, and with layout=neato, overlap=false, node shape=rectangle] as parameters. The code should be valid, bugless and returned on a single line, without any explanation. Provide a clear and organized diagram, the relationships between the nodes have to make sense for an expert of that input. My first diagram is: \"The water cycle 8]\".\n\n",
"description": "Generate meaningful charts."
},
{
@ -2148,7 +2148,7 @@
"Career",
"Business"
],
"prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these heders: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n",
"prompt": "Please acknowledge my following request. Please respond to me as a product manager. I will ask for subject, and you will help me writing a PRD for it with these headers: Subject, Introduction, Problem Statement, Goals and Objectives, User Stories, Technical requirements, Benefits, KPIs, Development Risks, Conclusion. Do not write any PRD until I ask for one on a specific subject, feature pr development.\n\n",
"description": "Help draft the Product Requirements Document."
},
{
@ -2159,7 +2159,7 @@
"Entertainment",
"General"
],
"prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkeness I mentionned. Do not write explanations on replies. My first sentence is \"how are you?",
"prompt": "I want you to act as a drunk person. You will only answer like a very drunk person texting and nothing else. Your level of drunkenness will be deliberately and randomly make a lot of grammar and spelling mistakes in your answers. You will also randomly ignore what I said and say something random with the same level of drunkenness I mentioned. Do not write explanations on replies. My first sentence is \"how are you?",
"description": "Mimic the speech pattern of a drunk person."
},
{
@ -3517,7 +3517,7 @@
"Tools",
"Copywriting"
],
"prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the cooresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n",
"prompt": "I want you to act as a scientific manuscript matcher. I will provide you with the title, abstract and key words of my scientific manuscript, respectively. Your task is analyzing my title, abstract and key words synthetically to find the most related, reputable journals for potential publication of my research based on an analysis of tens of millions of citation connections in database, such as Web of Science, Pubmed, Scopus, ScienceDirect and so on. You only need to provide me with the 15 most suitable journals. Your reply should include the name of journal, the corresponding match score (The full score is ten). I want you to reply in text-based excel sheet and sort by matching scores in reverse order.\nMy title is \"XXX\" My abstract is \"XXX\" My key words are \"XXX\"\n\n",
"description": ""
},
{

View File

@ -7,15 +7,12 @@ const allArm64 = {
'@img/sharp-darwin-arm64': '0.34.3',
'@img/sharp-win32-arm64': '0.34.3',
'@img/sharp-linux-arm64': '0.34.3',
'@img/sharp-linuxmusl-arm64': '0.34.3',
'@img/sharp-libvips-darwin-arm64': '1.2.0',
'@img/sharp-libvips-linux-arm64': '1.2.0',
'@img/sharp-libvips-linuxmusl-arm64': '1.2.0',
'@libsql/darwin-arm64': '0.4.7',
'@libsql/linux-arm64-gnu': '0.4.7',
'@libsql/linux-arm64-musl': '0.4.7',
'@strongtz/win32-arm64-msvc': '0.4.7',
'@napi-rs/system-ocr-darwin-arm64': '1.0.2',
@ -25,16 +22,13 @@ const allArm64 = {
const allX64 = {
'@img/sharp-darwin-x64': '0.34.3',
'@img/sharp-linux-x64': '0.34.3',
'@img/sharp-linuxmusl-x64': '0.34.3',
'@img/sharp-win32-x64': '0.34.3',
'@img/sharp-libvips-darwin-x64': '1.2.0',
'@img/sharp-libvips-linux-x64': '1.2.0',
'@img/sharp-libvips-linuxmusl-x64': '1.2.0',
'@libsql/darwin-x64': '0.4.7',
'@libsql/linux-x64-gnu': '0.4.7',
'@libsql/linux-x64-musl': '0.4.7',
'@libsql/win32-x64-msvc': '0.4.7',
'@napi-rs/system-ocr-darwin-x64': '1.0.2',

View File

@ -32,7 +32,8 @@ class ObsidianVaultService {
)
} else {
// Linux
this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json')
this.obsidianConfigPath = this.resolveLinuxObsidianConfigPath()
logger.debug(`Resolved Obsidian config path (linux): ${this.obsidianConfigPath}`)
}
}
@ -164,6 +165,57 @@ class ObsidianVaultService {
return []
}
}
/**
* Linux Obsidian
* XDG
*/
private resolveLinuxObsidianConfigPath(): string {
const home = app.getPath('home')
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config')
// 常见目录名与文件名大小写差异做兼容
const configDirs = ['obsidian', 'Obsidian']
const fileNames = ['obsidian.json', 'Obsidian.json']
const candidates: string[] = []
// 1) AppImage/DEBXDG 标准路径)
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(xdgConfigHome, dir, file))
}
}
// 2) Snap 安装:
// - 常见:~/snap/obsidian/current/.config/obsidian/obsidian.json
// - 兼容:~/snap/obsidian/common/.config/obsidian/obsidian.json
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(home, 'snap', 'obsidian', 'current', '.config', dir, file))
candidates.push(path.join(home, 'snap', 'obsidian', 'common', '.config', dir, file))
}
}
// 3) Flatpak 安装:~/.var/app/md.obsidian.Obsidian/config/obsidian/obsidian.json
for (const dir of configDirs) {
for (const file of fileNames) {
candidates.push(path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', dir, file))
}
}
const existing = candidates.find((p) => {
try {
return fs.existsSync(p)
} catch {
return false
}
})
if (existing) return existing
return path.join(xdgConfigHome, 'obsidian', 'obsidian.json')
}
}
export default ObsidianVaultService

View File

@ -2,6 +2,7 @@ import { loggerService } from '@logger'
import { isLinux } from '@main/constant'
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
import { systemOcrService } from './builtin/SystemOcrService'
import { tesseractService } from './builtin/TesseractService'
const logger = loggerService.withContext('OcrService')
@ -34,7 +35,4 @@ export const ocrService = new OcrService()
// Register built-in providers
ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(tesseractService))
if (!isLinux) {
const { systemOcrService } = require('./builtin/SystemOcrService')
ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
}
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))

View File

@ -1,5 +1,6 @@
import { isLinux, isWin } from '@main/constant'
import { loadOcrImage } from '@main/utils/ocr'
import { OcrAccuracy, recognize } from '@napi-rs/system-ocr'
import {
ImageFileMetadata,
isImageFileMetadata as isImageFileMetadata,
@ -20,8 +21,6 @@ export class SystemOcrService extends OcrBaseService {
if (isLinux) {
return { text: '' }
}
const { OcrAccuracy, recognize } = require('@napi-rs/system-ocr')
const buffer = await loadOcrImage(file)
const langs = isWin ? options?.langs : undefined
const result = await recognize(buffer, OcrAccuracy.Accurate, langs)

View File

@ -1,8 +1,8 @@
import { ImageFileMetadata } from '@types'
import { readFile } from 'fs/promises'
import sharp from 'sharp'
const preprocessImage = async (buffer: Buffer): Promise<Buffer> => {
const sharp = require('sharp')
return sharp(buffer)
.grayscale() // 转为灰度
.normalize()

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@ -262,12 +262,13 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
) : (
<CodeViewer
className="source-view"
value={children}
language={language}
onHeightChange={handleHeightChange}
expanded={shouldExpand}
wrapped={shouldWrap}
onHeightChange={handleHeightChange}>
{children}
</CodeViewer>
maxHeight={`${MAX_COLLAPSED_CODE_HEIGHT}px`}
/>
),
[children, codeEditorEnabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap]
)

View File

@ -48,8 +48,6 @@ export interface CodeEditorProps {
maxHeight?: string
/** Minimum editor height. */
minHeight?: string
/** Font size that overrides the app setting. */
fontSize?: string
/** Editor options that extend BasicSetupOptions. */
options?: {
/**
@ -70,6 +68,8 @@ export interface CodeEditorProps {
} & BasicSetupOptions
/** Additional extensions for CodeMirror. */
extensions?: Extension[]
/** Font size that overrides the app setting. */
fontSize?: number
/** Style overrides for the editor, passed directly to CodeMirror's style property. */
style?: React.CSSProperties
/** CSS class name appended to the default `code-editor` class. */
@ -108,9 +108,9 @@ const CodeEditor = ({
height,
maxHeight,
minHeight,
fontSize,
options,
extensions,
fontSize: customFontSize,
style,
className,
editable = true,
@ -131,7 +131,7 @@ const CodeEditor = ({
const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap])
// 合并 codeEditor 和 options 的 basicSetupoptions 优先
const customBasicSetup = useMemo(() => {
const basicSetup = useMemo(() => {
return {
lineNumbers: _lineNumbers,
...(codeEditor as BasicSetupOptions),
@ -139,7 +139,7 @@ const CodeEditor = ({
}
}, [codeEditor, _lineNumbers, options])
const customFontSize = useMemo(() => fontSize ?? `${_fontSize - 1}px`, [fontSize, _fontSize])
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
const { activeCmTheme } = useCodeStyle()
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
@ -224,10 +224,10 @@ const CodeEditor = ({
foldKeymap: enableKeymap,
completionKeymap: enableKeymap,
lintKeymap: enableKeymap,
...customBasicSetup // override basicSetup
...basicSetup // override basicSetup
}}
style={{
fontSize: customFontSize,
fontSize,
marginTop: 0,
borderRadius: 'inherit',
...style

View File

@ -1,5 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { MAX_COLLAPSED_CODE_HEIGHT } from '@renderer/config/constant'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
import { uuid } from '@renderer/utils'
@ -11,13 +10,49 @@ import { ThemedToken } from 'shiki/core'
import styled from 'styled-components'
interface CodeViewerProps {
/** Code string value. */
value: string
/**
* Code language string.
* - Case-insensitive.
* - Supports common names: javascript, json, python, etc.
* - Supports shiki aliases: c#/csharp, objective-c++/obj-c++/objc++, etc.
*/
language: string
children: string
expanded?: boolean
wrapped?: boolean
/** Fired when the editor height changes. */
onHeightChange?: (scrollHeight: number) => void
className?: string
/**
* Height of the scroll container.
* Only works when expanded is false.
*/
height?: string | number
/**
* Maximum height of the scroll container.
* Only works when expanded is false.
*/
maxHeight?: string | number
/** Viewer options. */
options?: {
/**
* Whether to show line numbers.
*/
lineNumbers?: boolean
}
/** Font size that overrides the app setting. */
fontSize?: number
/** CSS class name appended to the default `code-viewer` class. */
className?: string
/**
* Whether the editor is expanded.
* If true, the height and maxHeight props are ignored.
* @default true
*/
expanded?: boolean
/**
* Whether the code lines are wrapped.
* @default true
*/
wrapped?: boolean
}
/**
@ -26,20 +61,34 @@ interface CodeViewerProps {
* - 使
* -
*/
const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, className, height }: CodeViewerProps) => {
const [codeShowLineNumbers] = usePreference('chat.code.show_line_numbers')
const [fontSize] = usePreference('chat.message.font_size')
const CodeViewer = ({
value,
language,
height,
maxHeight,
onHeightChange,
options,
fontSize: customFontSize,
className,
expanded = true,
wrapped = true
}: CodeViewerProps) => {
const [_lineNumbers] = usePreference('chat.code.show_line_numbers')
const [_fontSize] = usePreference('chat.message.font_size')
const { getShikiPreProperties, isShikiThemeDark } = useCodeStyle()
const shikiThemeRef = useRef<HTMLDivElement>(null)
const scrollerRef = useRef<HTMLDivElement>(null)
const callerId = useRef(`${Date.now()}-${uuid()}`).current
const rawLines = useMemo(() => (typeof children === 'string' ? children.trimEnd().split('\n') : []), [children])
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
const lineNumbers = useMemo(() => options?.lineNumbers ?? _lineNumbers, [options?.lineNumbers, _lineNumbers])
const rawLines = useMemo(() => (typeof value === 'string' ? value.trimEnd().split('\n') : []), [value])
// 计算行号数字位数
const gutterDigits = useMemo(
() => (codeShowLineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
[codeShowLineNumbers, rawLines.length]
() => (lineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
[lineNumbers, rawLines.length]
)
// 设置 pre 标签属性
@ -69,7 +118,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
const getScrollElement = useCallback(() => scrollerRef.current, [])
const getItemKey = useCallback((index: number) => `${callerId}-${index}`, [callerId])
// `line-height: 1.6` 为全局样式,但是为了避免测量误差在这里取整
const estimateSize = useCallback(() => Math.round((fontSize - 1) * 1.6), [fontSize])
const estimateSize = useCallback(() => Math.round(fontSize * 1.6), [fontSize])
// 创建 virtualizer 实例
const virtualizer = useVirtualizer({
@ -106,20 +155,19 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
}, [rawLines.length, onHeightChange])
return (
<div ref={shikiThemeRef} style={height ? { height } : undefined}>
<div ref={shikiThemeRef} style={expanded ? undefined : { height }}>
<ScrollContainer
ref={scrollerRef}
className="shiki-scroller"
$wrap={wrapped}
$expanded={expanded}
$expand={expanded}
$lineHeight={estimateSize()}
$height={height}
style={
{
'--gutter-width': `${gutterDigits}ch`,
fontSize: `${fontSize - 1}px`,
maxHeight: expanded ? undefined : height ? undefined : MAX_COLLAPSED_CODE_HEIGHT,
height: height,
fontSize,
height: expanded ? undefined : height,
maxHeight: expanded ? undefined : maxHeight,
overflowY: expanded ? 'hidden' : 'auto'
} as React.CSSProperties
}>
@ -143,7 +191,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
<VirtualizedRow
rawLine={rawLines[virtualItem.index]}
tokenLine={tokenLines[virtualItem.index]}
showLineNumbers={codeShowLineNumbers}
showLineNumbers={lineNumbers}
index={virtualItem.index}
/>
</div>
@ -227,9 +275,8 @@ VirtualizedRow.displayName = 'VirtualizedRow'
const ScrollContainer = styled.div<{
$wrap?: boolean
$expanded?: boolean
$expand?: boolean
$lineHeight?: number
$height?: string | number
}>`
display: block;
overflow-x: auto;
@ -245,7 +292,7 @@ const ScrollContainer = styled.div<{
line-height: ${(props) => props.$lineHeight}px;
/* contain 优化 wrap 时滚动性能will-change 优化 unwrap 时滚动性能 */
contain: ${(props) => (props.$wrap ? 'content' : 'none')};
will-change: ${(props) => (!props.$wrap && !props.$expanded ? 'transform' : 'auto')};
will-change: ${(props) => (!props.$wrap && !props.$expand ? 'transform' : 'auto')};
.line-number {
width: var(--gutter-width, 1.2ch);

View File

@ -71,8 +71,9 @@ describe('DraggableList', () => {
})
it('should render nothing when list is empty', () => {
const emptyList: Array<{ id: string; name: string }> = []
render(
<DraggableList list={[]} onUpdate={() => {}}>
<DraggableList list={emptyList} onUpdate={() => {}}>
{(item) => <div data-testid="item">{item.name}</div>}
</DraggableList>
)

View File

@ -33,7 +33,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList, // 列表未过滤
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@ -61,7 +61,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@ -89,7 +89,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@ -110,7 +110,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList: mockOriginalList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)
@ -136,7 +136,7 @@ describe('useDraggableReorder', () => {
originalList: mockOriginalList,
filteredList,
onUpdate,
idKey: 'id'
itemKey: 'id'
})
)

View File

@ -9,7 +9,7 @@ import {
ResponderProvided
} from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { FC, HTMLAttributes } from 'react'
import { HTMLAttributes, Key, useCallback } from 'react'
interface Props<T> {
list: T[]
@ -17,23 +17,25 @@ interface Props<T> {
listStyle?: React.CSSProperties
listProps?: HTMLAttributes<HTMLDivElement>
children: (item: T, index: number) => React.ReactNode
itemKey?: keyof T | ((item: T) => Key)
onUpdate: (list: T[]) => void
onDragStart?: OnDragStartResponder
onDragEnd?: OnDragEndResponder
droppableProps?: Partial<DroppableProps>
}
const DraggableList: FC<Props<any>> = ({
function DraggableList<T>({
children,
list,
style,
listStyle,
listProps,
itemKey,
droppableProps,
onDragStart,
onUpdate,
onDragEnd
}) => {
}: Props<T>) {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided)
if (result.destination) {
@ -46,6 +48,17 @@ const DraggableList: FC<Props<any>> = ({
}
}
const getId = useCallback(
(item: T) => {
if (typeof itemKey === 'function') return itemKey(item)
if (itemKey) return item[itemKey] as Key
if (typeof item === 'string') return item as Key
if (item && typeof item === 'object' && 'id' in item) return item.id as Key
return undefined
},
[itemKey]
)
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<Droppable droppableId="droppable" {...droppableProps}>
@ -53,9 +66,9 @@ const DraggableList: FC<Props<any>> = ({
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
<div {...listProps} className="draggable-list-container">
{list.map((item, index) => {
const id = item.id || item
const draggableId = String(getId(item) ?? index)
return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
<Draggable key={`draggable_${draggableId}`} draggableId={draggableId} index={index}>
{(provided) => (
<div
ref={provided.innerRef}

View File

@ -9,7 +9,7 @@ interface UseDraggableReorderParams<T> {
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
idKey: keyof T | ((item: T) => Key)
itemKey: keyof T | ((item: T) => Key)
}
/**
@ -19,8 +19,16 @@ interface UseDraggableReorderParams<T> {
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns DraggableVirtualList props: { onDragEnd, itemKey }
*/
export function useDraggableReorder<T>({ originalList, filteredList, onUpdate, idKey }: UseDraggableReorderParams<T>) {
const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey])
export function useDraggableReorder<T>({
originalList,
filteredList,
onUpdate,
itemKey
}: UseDraggableReorderParams<T>) {
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)),
[itemKey]
)
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {

View File

@ -208,7 +208,7 @@ const VirtualRow = memo(
const draggableId = String(virtualItem.key)
return (
<Draggable
key={`draggable_${draggableId}_${virtualItem.index}`}
key={`draggable_${draggableId}`}
draggableId={draggableId}
isDragDisabled={disabled}
index={virtualItem.index}>

View File

@ -0,0 +1,145 @@
import CherryLogo from '@renderer/assets/images/banner.png'
import Favicon from '@renderer/components/Icons/FallbackFavicon'
import { useMetaDataParser } from '@renderer/hooks/useMetaDataParser'
import { Skeleton, Typography } from 'antd'
import { useEffect, useMemo } from 'react'
import styled from 'styled-components'
const { Title, Paragraph } = Typography
type Props = {
link: string
show: boolean
}
export const OGCard = ({ link, show }: Props) => {
const openGraph = ['og:title', 'og:description', 'og:image', 'og:imageAlt'] as const
const { metadata, isLoading, parseMetadata } = useMetaDataParser(link, openGraph)
const hasImage = !!metadata['og:image']
const hostname = useMemo(() => {
try {
return new URL(link).hostname
} catch {
return null
}
}, [link])
useEffect(() => {
// use show to lazy loading
if (show && isLoading) {
parseMetadata()
}
}, [parseMetadata, isLoading, show])
if (isLoading) {
return <CardSkeleton />
}
return (
<PreviewContainer hasImage={hasImage}>
{hasImage && (
<PreviewImageContainer>
<PreviewImage src={metadata['og:image']} alt={metadata['og:imageAlt'] || link} />
</PreviewImageContainer>
)}
{!hasImage && (
<PreviewImageContainer>
<PreviewImage src={CherryLogo} alt={'no image'} />
</PreviewImageContainer>
)}
<PreviewContent>
<StyledHyperLink>
{hostname && <Favicon hostname={hostname} alt={link} />}
<Title
style={{
margin: 0,
fontSize: '14px',
lineHeight: '1.2',
color: 'var(--color-text)'
}}>
{metadata['og:title'] || hostname}
</Title>
</StyledHyperLink>
<Paragraph
title={metadata['og:description'] || link}
ellipsis={{ rows: 2 }}
style={{
fontSize: '12px',
lineHeight: '1.2',
color: 'var(--color-text-secondary)'
}}>
{metadata['og:description'] || link}
</Paragraph>
</PreviewContent>
</PreviewContainer>
)
}
const CardSkeleton = () => {
return (
<SkeletonContainer>
<Skeleton.Image style={{ width: '100%', height: 140 }} active />
<Skeleton
paragraph={{
rows: 1,
style: {
margin: '8px 0'
}
}}
active
/>
</SkeletonContainer>
)
}
const StyledHyperLink = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const PreviewContainer = styled.div<{ hasImage?: boolean }>`
display: flex;
flex-direction: column;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
width: 380px;
height: 220px;
overflow: hidden;
`
const PreviewImageContainer = styled.div`
width: 100%;
height: 140px;
min-height: 140px;
overflow: hidden;
`
const PreviewContent = styled.div`
padding: 12px 16px;
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
`
const PreviewImage = styled.img`
width: 100%;
height: 140px;
object-fit: cover;
`
const SkeletonContainer = styled.div`
width: 380px;
height: 220px;
padding: 12px 16px;
display: flex;
flex-direction: column;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
gap: 16px;
`

View File

@ -56,6 +56,7 @@ const MermaidPreview = ({
document.body.removeChild(measureEl)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[diagramId, mermaid, forceRenderKey]
)

View File

@ -8,7 +8,7 @@ interface UseDndReorderParams<T> {
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
idKey: keyof T | ((item: T) => Key)
itemKey: keyof T | ((item: T) => Key)
}
/**
@ -18,8 +18,11 @@ interface UseDndReorderParams<T> {
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns Sortable onSortEnd
*/
export function useDndReorder<T>({ originalList, filteredList, onUpdate, idKey }: UseDndReorderParams<T>) {
const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey])
export function useDndReorder<T>({ originalList, filteredList, onUpdate, itemKey }: UseDndReorderParams<T>) {
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)),
[itemKey]
)
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {

View File

@ -739,6 +739,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
provider: 'gemini',
name: 'Gemini 2.0 Flash',
group: 'Gemini 2.0'
},
{
id: 'gemini-2.5-flash-image-preview',
provider: 'gemini',
name: 'Gemini 2.5 Flash Image',
group: 'Gemini 2.5'
}
],
anthropic: [
@ -1564,6 +1570,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
}
],
openrouter: [
{
id: 'google/gemini-2.5-flash-image-preview',
provider: 'openrouter',
name: 'Google: Gemini 2.5 Flash Image',
group: 'google'
},
{
id: 'google/gemini-2.5-flash-preview',
provider: 'openrouter',
@ -2320,6 +2332,9 @@ export const DEDICATED_IMAGE_MODELS = [
'gpt-image-1'
]
// Models that should auto-enable image generation button when selected
export const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image-preview', ...DEDICATED_IMAGE_MODELS]
export const OPENAI_IMAGE_GENERATION_MODELS = [
'o3',
'gpt-4o',
@ -2340,8 +2355,17 @@ export const GENERATE_IMAGE_MODELS = [
]
export const isDedicatedImageGenerationModel = (model: Model): boolean => {
if (!model) return false
const modelId = getLowerBaseModelName(model.id)
return DEDICATED_IMAGE_MODELS.filter((m) => modelId.includes(m)).length > 0
return DEDICATED_IMAGE_MODELS.some((m) => modelId.includes(m))
}
export const isAutoEnableImageGenerationModel = (model: Model): boolean => {
if (!model) return false
const modelId = getLowerBaseModelName(model.id)
return AUTO_ENABLE_IMAGE_MODELS.some((m) => modelId.includes(m))
}
export function isGenerateImageModel(model: Model): boolean {

View File

@ -0,0 +1,78 @@
import axios from 'axios'
import * as htmlparser2 from 'htmlparser2'
import { useCallback, useEffect, useRef, useState } from 'react'
export function useMetaDataParser<T extends string>(
link: string,
properties: readonly T[],
options?: {
timeout?: number
}
) {
const { timeout = 5000 } = options || {}
const [metadata, setMetadata] = useState<Record<T, string>>({} as Record<T, string>)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const parseMetadata = useCallback(async () => {
if (!link || !isLoading) return
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const controller = new AbortController()
abortControllerRef.current = controller
setIsLoading(true)
setError(null)
try {
const response = await axios.get(link, { timeout, signal: controller.signal })
const htmlContent = response.data
const parsedMetadata = {} as Record<T, string>
const parser = new htmlparser2.Parser({
onopentag(tagName, attributes) {
if (tagName === 'meta') {
const { name: metaName, property: metaProperty, content } = attributes
const metaKey = metaName || metaProperty
if (!metaKey || !properties.includes(metaKey as T)) return
parsedMetadata[metaKey as T] = content
}
}
})
parser.parseComplete(htmlContent)
setMetadata(parsedMetadata)
} catch (err) {
// Don't set error if request was aborted
if (axios.isCancel(err) || (err instanceof Error && err.name === 'AbortError')) {
return
}
setError(err instanceof Error ? err : new Error('Failed to fetch HTML'))
} finally {
setIsLoading(false)
}
}, [isLoading, link, properties, timeout])
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
}, [])
return {
metadata,
isLoading,
error,
parseMetadata
}
}

View File

@ -1552,6 +1552,7 @@
"selected": "Selected tags"
},
"function_calling": "Function Calling",
"invalid_model": "Invalid Model",
"no_matches": "No models available",
"parameter_name": "Parameter Name",
"parameter_type": {

View File

@ -1552,6 +1552,7 @@
"selected": "選択済みのタグ"
},
"function_calling": "関数呼び出し",
"invalid_model": "無効なモデル",
"no_matches": "利用可能なモデルがありません",
"parameter_name": "パラメータ名",
"parameter_type": {

View File

@ -1552,6 +1552,7 @@
"selected": "Выбранные теги"
},
"function_calling": "Вызов функции",
"invalid_model": "Недействительная модель",
"no_matches": "Нет доступных моделей",
"parameter_name": "Имя параметра",
"parameter_type": {

View File

@ -1552,6 +1552,7 @@
"selected": "已选标签"
},
"function_calling": "函数调用",
"invalid_model": "无效模型",
"no_matches": "无可用模型",
"parameter_name": "参数名称",
"parameter_type": {

View File

@ -1552,6 +1552,7 @@
"selected": "已選標籤"
},
"function_calling": "函數調用",
"invalid_model": "無效模型",
"no_matches": "無可用模型",
"parameter_name": "參數名稱",
"parameter_type": {

View File

@ -1550,6 +1550,7 @@
"selected": "Επιλεγμένη ετικέτα"
},
"function_calling": "Ξεχωριστική Κλήση Συναρτήσεων",
"invalid_model": "Μη έγκυρο μοντέλο",
"no_matches": "Δεν υπάρχουν διαθέσιμα μοντέλα",
"parameter_name": "Όνομα παραμέτρου",
"parameter_type": {

View File

@ -1550,6 +1550,7 @@
"selected": "Etiquetas seleccionadas"
},
"function_calling": "Llamada a función",
"invalid_model": "Modelo inválido",
"no_matches": "No hay modelos disponibles",
"parameter_name": "Nombre del parámetro",
"parameter_type": {

View File

@ -1550,6 +1550,7 @@
"selected": "Étiquette sélectionnée"
},
"function_calling": "Appel de fonction",
"invalid_model": "Modèle invalide",
"no_matches": "Aucun modèle disponible",
"parameter_name": "Nom du paramètre",
"parameter_type": {

View File

@ -1550,6 +1550,7 @@
"selected": "Etiqueta selecionada"
},
"function_calling": "Chamada de função",
"invalid_model": "Modelo inválido",
"no_matches": "Nenhum modelo disponível",
"parameter_name": "Nome do parâmetro",
"parameter_type": {

View File

@ -4,7 +4,7 @@ import { loggerService } from '@logger'
import { QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
import TranslateButton from '@renderer/components/TranslateButton'
import {
isDedicatedImageGenerationModel,
isAutoEnableImageGenerationModel,
isGenerateImageModel,
isGenerateImageModels,
isMandatoryWebSearchModel,
@ -783,7 +783,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
if (!isGenerateImageModel(model) && assistant.enableGenerateImage) {
updateAssistant({ ...assistant, enableGenerateImage: false })
}
if (isDedicatedImageGenerationModel(model) && !assistant.enableGenerateImage) {
if (isAutoEnableImageGenerationModel(model) && !assistant.enableGenerateImage) {
updateAssistant({ ...assistant, enableGenerateImage: true })
}
}, [assistant, model, updateAssistant])

View File

@ -1,13 +1,15 @@
import Favicon from '@renderer/components/Icons/FallbackFavicon'
import { OGCard } from '@renderer/components/OGCard'
import { Popover } from 'antd'
import React, { memo, useMemo } from 'react'
import styled from 'styled-components'
import React, { memo, useMemo, useState } from 'react'
interface HyperLinkProps {
children: React.ReactNode
href: string
}
const Hyperlink: React.FC<HyperLinkProps> = ({ children, href }) => {
const [open, setOpen] = useState(false)
const link = useMemo(() => {
try {
return decodeURIComponent(href)
@ -16,32 +18,20 @@ const Hyperlink: React.FC<HyperLinkProps> = ({ children, href }) => {
}
}, [href])
const hostname = useMemo(() => {
try {
return new URL(link).hostname
} catch {
return null
}
}, [link])
if (!href) return children
return (
<Popover
arrow={false}
content={
<StyledHyperLink>
{hostname && <Favicon hostname={hostname} alt={link} />}
<span>{link}</span>
</StyledHyperLink>
}
open={open}
onOpenChange={setOpen}
content={<OGCard link={link} show={open} />}
placement="top"
color="var(--color-background)"
styles={{
body: {
border: '1px solid var(--color-border)',
padding: '12px',
borderRadius: '8px'
padding: 0,
borderRadius: '8px',
overflow: 'hidden'
}
}}>
{children}
@ -49,17 +39,4 @@ const Hyperlink: React.FC<HyperLinkProps> = ({ children, href }) => {
)
}
const StyledHyperLink = styled.div`
color: var(--color-text);
display: flex;
align-items: center;
gap: 8px;
span {
max-width: min(400px, 70vw);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`
export default memo(Hyperlink)

View File

@ -141,7 +141,7 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
} as Partial<Components>
}, [block.id])
if (messageContent.includes('<style>')) {
if (/<style\b[^>]*>/i.test(messageContent)) {
components.style = MarkdownShadowDOMRenderer as any
}

View File

@ -18,17 +18,55 @@ const mocks = vi.hoisted(() => ({
),
Favicon: ({ hostname, alt }: { hostname: string; alt: string }) => (
<img data-testid="favicon" data-hostname={hostname} alt={alt} />
)
),
Typography: {
Title: ({ children }: { children: React.ReactNode }) => <div data-testid="title">{children}</div>,
Text: ({ children }: { children: React.ReactNode }) => <div data-testid="text">{children}</div>
},
Skeleton: () => <div data-testid="skeleton">Loading...</div>,
useMetaDataParser: vi.fn(() => ({
metadata: {},
isLoading: false,
isLoaded: true,
parseMetadata: vi.fn()
}))
}))
vi.mock('antd', () => ({
Popover: mocks.Popover
Popover: mocks.Popover,
Typography: mocks.Typography,
Skeleton: mocks.Skeleton
}))
vi.mock('@renderer/components/Icons/FallbackFavicon', () => ({
__esModule: true,
default: mocks.Favicon
}))
vi.mock('@renderer/hooks/useMetaDataParser', () => ({
useMetaDataParser: mocks.useMetaDataParser
}))
// Mock the OGCard component
vi.mock('@renderer/components/OGCard', () => ({
OGCard: ({ link }: { link: string; show: boolean }) => {
let hostname = ''
try {
hostname = new URL(link).hostname
} catch (e) {
// Ignore invalid URLs
}
return (
<div data-testid="og-card">
{hostname && <mocks.Favicon hostname={hostname} alt={link} />}
<mocks.Typography.Title>{hostname}</mocks.Typography.Title>
<mocks.Typography.Text>{link}</mocks.Typography.Text>
</div>
)
}
}))
describe('Hyperlink', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -69,7 +107,9 @@ describe('Hyperlink', () => {
// Content includes decoded url text and favicon with hostname
expect(screen.getByTestId('favicon')).toHaveAttribute('data-hostname', 'domain.com')
expect(screen.getByTestId('favicon')).toHaveAttribute('alt', 'https://domain.com/a b')
expect(screen.getByTestId('popover-content')).toHaveTextContent('https://domain.com/a b')
// The title should show hostname and text should show the full URL
expect(screen.getByTestId('title')).toHaveTextContent('domain.com')
expect(screen.getByTestId('text')).toHaveTextContent('https://domain.com/a b')
})
it('should not render favicon when URL parsing fails (invalid url)', () => {
@ -81,7 +121,9 @@ describe('Hyperlink', () => {
// decodeURIComponent succeeds => "not/url" is displayed
expect(screen.queryByTestId('favicon')).toBeNull()
expect(screen.getByTestId('popover-content')).toHaveTextContent('not/url')
// Since there's no hostname and no og:title, title shows empty, but text shows the URL
expect(screen.getByTestId('title')).toBeEmptyDOMElement()
expect(screen.getByTestId('text')).toHaveTextContent('not/url')
})
it('should not render favicon for non-http(s) scheme without hostname (mailto:)', () => {
@ -93,6 +135,8 @@ describe('Hyperlink', () => {
// Decoded to mailto:test@example.com, hostname is empty => no favicon
expect(screen.queryByTestId('favicon')).toBeNull()
expect(screen.getByTestId('popover-content')).toHaveTextContent('mailto:test@example.com')
// Since there's no hostname and no og:title, title shows empty, but text shows the decoded URL
expect(screen.getByTestId('title')).toBeEmptyDOMElement()
expect(screen.getByTestId('text')).toHaveTextContent('mailto:test@example.com')
})
})

View File

@ -1,42 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Hyperlink > should match snapshot for normal url 1`] = `
.c0 {
color: var(--color-text);
display: flex;
align-items: center;
gap: 8px;
}
.c0 span {
max-width: min(400px,70vw);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
<div>
<div
data-arrow="false"
data-color="var(--color-background)"
data-placement="top"
data-styles="{"body":{"border":"1px solid var(--color-border)","padding":"12px","borderRadius":"8px"}}"
data-styles="{"body":{"padding":0,"borderRadius":"8px","overflow":"hidden"}}"
data-testid="popover"
>
<div
data-testid="popover-content"
>
<div
class="c0"
data-testid="og-card"
>
<img
alt="https://example.com/path with space"
data-hostname="example.com"
data-testid="favicon"
/>
<span>
<div
data-testid="title"
>
example.com
</div>
<div
data-testid="text"
>
https://example.com/path with space
</span>
</div>
</div>
</div>
<div

View File

@ -156,9 +156,12 @@ const Container = styled.div`
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
&.right {
height: calc(100vh - var(--navbar-height));
}
[navbar-position='left'] & {
background-color: var(--color-background);
}

View File

@ -4,8 +4,9 @@ import { isLocalAi } from '@renderer/config/env'
import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService'
import { useAppSelector } from '@renderer/store'
import { Assistant, Model } from '@renderer/types'
import { Button } from 'antd'
import { Button, Tag } from 'antd'
import { ChevronsUpDown } from 'lucide-react'
import { FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -19,6 +20,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
const { model, updateAssistant } = useAssistant(assistant.id)
const { t } = useTranslation()
const timerRef = useRef<NodeJS.Timeout>(undefined)
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === model?.provider))
const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model)
@ -60,6 +62,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
</ModelName>
</ButtonContent>
<ChevronsUpDown size={14} color="var(--color-icon)" />
{!provider && <Tag color="error">{t('models.invalid_model')}</Tag>}
</DropdownButton>
)
}

View File

@ -1,5 +1,6 @@
import { loggerService } from '@logger'
import Ellipsis from '@renderer/components/Ellipsis'
import { useFiles } from '@renderer/hooks/useFiles'
import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileItem from '@renderer/pages/files/FileItem'
import StatusIcon from '@renderer/pages/knowledge/components/StatusIcon'
@ -48,6 +49,7 @@ const getDisplayTime = (item: KnowledgeItem) => {
const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap, preprocessMap }) => {
const { t } = useTranslation()
const [windowHeight, setWindowHeight] = useState(window.innerHeight)
const { onSelectFile, selecting } = useFiles({ extensions: fileTypes })
const { base, fileItems, addFiles, refreshItem, removeItem, getProcessingStatus } = useKnowledge(
selectedBase.id || ''
@ -71,19 +73,12 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
return null
}
const handleAddFile = () => {
if (disabled) {
const handleAddFile = async () => {
if (disabled || selecting) {
return
}
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.accept = fileTypes.join(',')
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
files && handleDrop(Array.from(files))
}
input.click()
const selectedFiles = await onSelectFile({ multipleSelections: true })
processFiles(selectedFiles)
}
const handleDrop = async (files: File[]) => {
@ -118,8 +113,14 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
}
})
.filter(({ ext }) => fileTypes.includes(ext))
const uploadedFiles = await FileManager.uploadFiles(_files)
logger.debug('uploadedFiles', uploadedFiles)
processFiles(_files)
}
}
const processFiles = async (files: FileMetadata[]) => {
logger.debug('processFiles', files)
if (files.length > 0) {
const uploadedFiles = await FileManager.uploadFiles(files)
addFiles(uploadedFiles)
}
}
@ -150,16 +151,23 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
</ItemHeader>
<ItemFlexColumn>
<Dragger
showUploadList={false}
customRequest={({ file }) => handleDrop([file as File])}
multiple={true}
accept={fileTypes.join(',')}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint">
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
</p>
</Dragger>
<div
onClick={(e) => {
e.stopPropagation()
handleAddFile()
}}>
<Dragger
showUploadList={false}
customRequest={({ file }) => handleDrop([file as File])}
multiple={true}
accept={fileTypes.join(',')}
openFileDialogOnClick={false}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint">
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
</p>
</Dragger>
</div>
{fileItems.length === 0 ? (
<KnowledgeEmptyView />
) : (

View File

@ -2,7 +2,6 @@ import 'emoji-picker-element'
import { CloseCircleFilled } from '@ant-design/icons'
import CodeEditor from '@renderer/components/CodeEditor'
import CodeViewer from '@renderer/components/CodeViewer'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
@ -14,6 +13,7 @@ import { Button, Input, Popover } from 'antd'
import { Edit, HelpCircle, Save } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import { SettingDivider } from '..'
@ -122,7 +122,9 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
<TextAreaContainer>
<RichEditorContainer>
{showPreview ? (
<CodeViewer children={processedPrompt} language="markdown" expanded={true} height="100%" />
<MarkdownContainer>
<ReactMarkdown>{processedPrompt || prompt}</ReactMarkdown>
</MarkdownContainer>
) : (
<CodeEditor
value={prompt}
@ -214,4 +216,10 @@ const RichEditorContainer = styled.div`
}
`
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
height: 100%;
padding: 0.5em;
overflow: auto;
`
export default AssistantPromptSettings

View File

@ -47,12 +47,14 @@ const OcrImageSettings = ({ setProvider }: Props) => {
}))
}, [getOcrProviderName, imageProviders, platformSupport])
const isSystem = imageProvider.id === BuiltinOcrProviderIds.system
return (
<>
<SettingRow>
<SettingRowTitle>{t('settings.tool.ocr.image_provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{!platformSupport && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
{!platformSupport && isSystem && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
<Select
value={imageProvider.id}
style={{ width: '200px' }}

View File

@ -55,7 +55,7 @@ const McpServersList: FC = () => {
originalList: mcpServers,
filteredList: filteredMcpServers,
onUpdate: updateMcpServers,
idKey: 'id'
itemKey: 'id'
})
const scrollRef = useRef<HTMLDivElement>(null)

View File

@ -321,7 +321,7 @@ const ProviderList: FC = () => {
originalList: providers,
filteredList: filteredProviders,
onUpdate: updateProviders,
idKey: 'id'
itemKey: 'id'
})
const handleDragStart = useCallback(() => {

View File

@ -136,8 +136,15 @@ export function getAssistantProvider(assistant: Assistant): Provider {
export function getProviderByModel(model?: Model): Provider {
const providers = store.getState().llm.providers
const providerId = model ? model.provider : getDefaultProvider().id
return providers.find((p) => p.id === providerId) as Provider
const provider = providers.find((p) => p.id === model?.provider)
if (!provider) {
const defaultProvider = providers.find((p) => p.id === getDefaultModel()?.provider)
const cherryinProvider = providers.find((p) => p.id === 'cherryin')
return defaultProvider || cherryinProvider || providers[0]
}
return provider
}
export function getProviderByModelId(modelId?: string) {

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 144,
version: 145,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs']
// migrate
},

View File

@ -2335,8 +2335,21 @@
// logger.error('migrate 144 error', error as Error)
// return state
// }
// },
// '145': (state: RootState) => {
// try {
// if (state.settings) {
// if (state.settings.showMessageOutline === undefined || state.settings.showMessageOutline === null) {
// state.settings.showMessageOutline = false
// }
// }
// return state
// } catch (error) {
// logger.error('migrate 145 error', error as Error)
// return state
// }
// }
// }
// // 注意:添加新迁移时,记得同时更新 persistReducer
// // file://./index.ts

View File

@ -220,7 +220,7 @@ export interface SettingsState {
navbarPosition: 'left' | 'top'
// API Server
apiServer: ApiServerConfig
showMessageOutline?: boolean
showMessageOutline: boolean
// Notes Related
showWorkspace: boolean
}

View File

@ -53,8 +53,8 @@ export const Text: React.FC<React.HTMLAttributes<HTMLSpanElement>> = ({ style, c
)
// VStack 组件
export const VStack: React.FC<{ grap?: number; align?: string; children: React.ReactNode }> = ({
grap = 5,
export const VStack: React.FC<{ gap?: number; align?: string; children: React.ReactNode }> = ({
gap = 5,
align = 'stretch',
children,
...props
@ -64,7 +64,7 @@ export const VStack: React.FC<{ grap?: number; align?: string; children: React.R
display: 'flex',
flexDirection: 'column',
alignItems: align,
gap: `${grap}px`
gap: `${gap}px`
}}
{...props}>
{children}
@ -88,8 +88,8 @@ export const GridItem: React.FC<
)
// HStack 组件
export const HStack: React.FC<{ grap?: number; children: React.ReactNode; style?: React.CSSProperties }> = ({
grap,
export const HStack: React.FC<{ gap?: number; children: React.ReactNode; style?: React.CSSProperties }> = ({
gap,
children,
style,
...props
@ -99,7 +99,7 @@ export const HStack: React.FC<{ grap?: number; children: React.ReactNode; style?
display: 'inline-flex',
flexDirection: 'row',
alignItems: 'center',
gap: grap ? `${grap}px` : '5px',
gap: gap ? `${gap}px` : '5px',
...style
}}
{...props}>

View File

@ -59,7 +59,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({ node, handleClick, treeData, paddin
handleClick(node.id)
}}>
<GridItem colSpan={8} style={{ paddingLeft: `${paddingLeft}px`, textAlign: 'left' }}>
<HStack grap={2}>
<HStack gap={2}>
<IconButton
aria-label="Toggle"
aria-expanded={isOpen ? true : false}

View File

@ -156,7 +156,7 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
<SimpleGrid columns={1} templateColumns="1fr">
<Box padding={0} className="scroll-container">
{showList ? (
<VStack grap={1} align="start">
<VStack gap={1} align="start">
{spans.length === 0 ? (
<Text>Trace信息</Text>
) : (

View File

@ -103,7 +103,7 @@ export const isBuiltinOcrProvider = (p: OcrProvider): p is BuiltinOcrProvider =>
return isBuiltinOcrProviderId(p.id)
}
// Not sure compatiable api endpoint exists. May not support custom ocr provider
// Not sure compatible api endpoint exists. May not support custom ocr provider
export type CustomOcrProvider = OcrProvider & {
id: Exclude<string, BuiltinOcrProviderId>
}

View File

@ -26,7 +26,7 @@ describe('markdownConverter', () => {
it('should convert task list HTML back to Markdown with label', () => {
const html =
'<ul data-type="taskList" class="task-list"><li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox"> abcd</label></li><li data-type="taskItem" class="task-list-item" data-checked="true"><label><input type="checkbox" checked> efgh</lable></li></ul>'
'<ul data-type="taskList" class="task-list"><li data-type="taskItem" class="task-list-item" data-checked="false"><label><input type="checkbox"> abcd</label></li><li data-type="taskItem" class="task-list-item" data-checked="true"><label><input type="checkbox" checked> efgh</label></li></ul>'
const result = htmlToMarkdown(html)
expect(result).toBe('- [ ] abcd\n\n- [x] efgh')
})
@ -361,7 +361,7 @@ describe('markdownConverter', () => {
})
describe('markdown image', () => {
it('should convert markdown iamge to HTML img tag', () => {
it('should convert markdown image to HTML img tag', () => {
const markdown = '![foo](train.jpg)'
const result = markdownToHtml(markdown)
expect(result).toBe('<p><img src="train.jpg" alt="foo" /></p>\n')

View File

@ -182,59 +182,200 @@ export async function captureScrollableIframe(
iframeRef: React.RefObject<HTMLIFrameElement | null>
): Promise<HTMLCanvasElement | undefined> {
const iframe = iframeRef.current
if (!iframe) return Promise.resolve(undefined)
if (!iframe?.contentDocument?.defaultView) return undefined
const doc = iframe.contentDocument
const win = doc?.defaultView
if (!doc || !win) return Promise.resolve(undefined)
const win = iframe.contentWindow!
// 等待两帧渲染稳定
await new Promise<void>((r) => requestAnimationFrame(() => requestAnimationFrame(() => r())))
// 禁用动画以确保捕获静态状态
const disableAnimations = () => {
const style = doc.createElement('style')
style.textContent = `*, *::before, *::after {
animation: none !important;
transition: none !important;
// transform: none !important;
}`
doc.head.appendChild(style)
return style
}
// 触发懒加载资源尽快加载
doc.querySelectorAll('img[loading="lazy"]').forEach((img) => img.setAttribute('loading', 'eager'))
await new Promise((r) => setTimeout(r, 200))
// 内联字体以避免跨域问题
const inlineFonts = async () => {
const fontFaceRegex = /@font-face[\s\S]*?\}/g
const fontUrlRegex = /url\((['"]?)([^)"']+)\1\)/g
const fontExtRegex = /\.(woff2?|ttf|otf)(\?|#|$)/i
const de = doc.documentElement
const b = doc.body
const fetchAsDataUrl = async (url: string): Promise<string> => {
try {
const res = await fetch(url, { mode: 'cors', credentials: 'omit' })
if (!res.ok) return url
const blob = await res.blob()
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = () => resolve(url)
reader.readAsDataURL(blob)
})
} catch {
return url
}
}
// 计算完整尺寸
const totalWidth = Math.max(b.scrollWidth, de.scrollWidth, b.clientWidth, de.clientWidth)
const totalHeight = Math.max(b.scrollHeight, de.scrollHeight, b.clientHeight, de.clientHeight)
const processCss = async (cssText: string, baseUrl: string): Promise<string[]> => {
const fontBlocks: string[] = []
let match: RegExpExecArray | null
logger.verbose('The iframe to be captured has size:', { totalWidth, totalHeight })
while ((match = fontFaceRegex.exec(cssText)) !== null) {
let block = match[0]
const fontUrls: Array<[string, string]> = []
// 按比例缩放以不超过上限
const MAX = 32767
const maxSide = Math.max(totalWidth, totalHeight)
const scale = maxSide > MAX ? MAX / maxSide : 1
const pixelRatio = (win.devicePixelRatio || 1) * scale
let urlMatch: RegExpExecArray | null
fontUrlRegex.lastIndex = 0
while ((urlMatch = fontUrlRegex.exec(block)) !== null) {
const url = urlMatch[2]
if (!url.startsWith('data:') && fontExtRegex.test(url)) {
try {
const absoluteUrl = new URL(url, baseUrl).href
fontUrls.push([urlMatch[0], absoluteUrl])
} catch {
// ignore
}
}
}
const bg = win.getComputedStyle(b).backgroundColor || '#ffffff'
const fg = win.getComputedStyle(b).color || '#000000'
// 并行处理所有字体URL
const dataUrls = await Promise.all(
fontUrls.map(async ([original, url]) => {
const dataUrl = await fetchAsDataUrl(url)
return [original, `url(${dataUrl})`] as const
})
)
dataUrls.forEach(([original, replacement]) => {
block = block.replace(original, replacement)
})
fontBlocks.push(block)
}
return fontBlocks
}
const allFontBlocks: string[] = []
// 处理外部样式表
const externalSheets = doc.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')
await Promise.all(
Array.from(externalSheets).map(async (link) => {
if (!link.href) return
try {
const res = await fetch(link.href, { mode: 'cors', credentials: 'omit' })
if (res.ok) {
const cssText = await res.text()
const blocks = await processCss(cssText, link.href)
allFontBlocks.push(...blocks)
}
} catch {
// ignore
}
})
)
// 处理内联样式
const inlineStyles = doc.querySelectorAll('style')
await Promise.all(
Array.from(inlineStyles).map(async (style) => {
const cssText = style.textContent || ''
const blocks = await processCss(cssText, doc.baseURI)
allFontBlocks.push(...blocks)
})
)
return allFontBlocks.join('\n')
}
const animationStyle = disableAnimations()
let injectedFontStyle: HTMLStyleElement | null = null
const ensureFontStyle = (css: string): HTMLStyleElement => {
const EXISTING = doc.head.querySelector('style[data-cs-inline-fonts="true"]') as HTMLStyleElement | null
if (EXISTING) {
if (css && css.trim()) {
EXISTING.textContent = `${EXISTING.textContent || ''}\n${css}`
}
return EXISTING
}
const style = doc.createElement('style')
style.setAttribute('data-cs-inline-fonts', 'true')
style.textContent = css
doc.head.appendChild(style)
return style
}
try {
const canvas = await htmlToImage.toCanvas(de, {
backgroundColor: bg,
// 等待渲染稳定
await new Promise((r) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => r(null))))
// 强制加载懒加载图片
doc.querySelectorAll('img[loading="lazy"]').forEach((img) => img.setAttribute('loading', 'eager'))
// 获取字体CSS
const fontEmbedCSS = await inlineFonts()
// 将字体 CSS 注入到 iframe 文档中,确保注册到 FontFaceSet
if (fontEmbedCSS && fontEmbedCSS.trim().length > 0) {
injectedFontStyle = ensureFontStyle(fontEmbedCSS)
// 访问一次以避免被标记为未使用
if (injectedFontStyle.parentNode == null) {
doc.head.appendChild(injectedFontStyle)
}
}
// 等待字体就绪,避免序列化时回退到系统字体
await Promise.race([
(doc as any).fonts?.ready ?? Promise.resolve(),
new Promise((resolve) => setTimeout(resolve, 1000))
])
// 计算尺寸
const { documentElement: de, body: b } = doc
const totalWidth = Math.max(b.scrollWidth, de.scrollWidth, b.clientWidth, de.clientWidth)
const totalHeight = Math.max(b.scrollHeight, de.scrollHeight, b.clientHeight, de.clientHeight)
logger.verbose('Capturing iframe:', { totalWidth, totalHeight })
// 限制最大尺寸,按比例缩放
const MAX_SIZE = 32767
const scale = Math.min(1, MAX_SIZE / Math.max(totalWidth, totalHeight))
const pixelRatio = (win.devicePixelRatio || 1) * scale
const styles = win.getComputedStyle(b)
const backgroundColor = styles.backgroundColor || '#ffffff'
const color = styles.color || '#000000'
return await htmlToImage.toCanvas(de, {
fontEmbedCSS,
backgroundColor,
cacheBust: true,
pixelRatio,
skipAutoScale: true,
width: Math.floor(totalWidth),
height: Math.floor(totalHeight),
style: {
backgroundColor: bg,
color: fg,
backgroundColor,
color,
width: `${totalWidth}px`,
height: `${totalHeight}px`,
overflow: 'visible',
display: 'block'
}
})
return canvas
} catch (error) {
logger.error('Error capturing iframe full snapshot:', error as Error)
return Promise.resolve(undefined)
logger.error('Error capturing iframe:', error as Error)
return undefined
} finally {
// 恢复动画
animationStyle.remove()
}
}

View File

@ -12,6 +12,8 @@
"src/renderer/src/services/traceApi.ts" ],
"compilerOptions": {
"composite": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo/tsconfig.node.tsbuildinfo",
"types": [
"electron-vite/node",
"vitest/globals"

View File

@ -12,6 +12,8 @@
],
"compilerOptions": {
"composite": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo/tsconfig.web.tsbuildinfo",
"jsx": "react-jsx",
"baseUrl": ".",
"moduleResolution": "bundler",

291
yarn.lock
View File

@ -4942,7 +4942,7 @@ __metadata:
languageName: node
linkType: hard
"@napi-rs/system-ocr@npm:^1.0.2":
"@napi-rs/system-ocr@npm:1.0.2":
version: 1.0.2
resolution: "@napi-rs/system-ocr@npm:1.0.2"
dependencies:
@ -4963,6 +4963,27 @@ __metadata:
languageName: node
linkType: hard
"@napi-rs/system-ocr@patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch":
version: 1.0.2
resolution: "@napi-rs/system-ocr@patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch::version=1.0.2&hash=407396"
dependencies:
"@napi-rs/system-ocr-darwin-arm64": "npm:1.0.2"
"@napi-rs/system-ocr-darwin-x64": "npm:1.0.2"
"@napi-rs/system-ocr-win32-arm64-msvc": "npm:1.0.2"
"@napi-rs/system-ocr-win32-x64-msvc": "npm:1.0.2"
dependenciesMeta:
"@napi-rs/system-ocr-darwin-arm64":
optional: true
"@napi-rs/system-ocr-darwin-x64":
optional: true
"@napi-rs/system-ocr-win32-arm64-msvc":
optional: true
"@napi-rs/system-ocr-win32-x64-msvc":
optional: true
checksum: 10c0/c1e336b3d506dd771c72b1bddc94e4a9ddeae5292222a7485d66dd0c11eed5d2a37cc7ea338f229e7d4abad276966658bdb4a2b322c9d6a0349dad6e65fcea51
languageName: node
linkType: hard
"@napi-rs/wasm-runtime@npm:^0.2.4":
version: 0.2.12
resolution: "@napi-rs/wasm-runtime@npm:0.2.12"
@ -4974,14 +4995,14 @@ __metadata:
languageName: node
linkType: hard
"@napi-rs/wasm-runtime@npm:^1.0.1":
version: 1.0.1
resolution: "@napi-rs/wasm-runtime@npm:1.0.1"
"@napi-rs/wasm-runtime@npm:^1.0.3":
version: 1.0.3
resolution: "@napi-rs/wasm-runtime@npm:1.0.3"
dependencies:
"@emnapi/core": "npm:^1.4.5"
"@emnapi/runtime": "npm:^1.4.5"
"@tybys/wasm-util": "npm:^0.10.0"
checksum: 10c0/3244105b75637d8d39e76782921fe46e48105bcd390db01a10dc7b596ee99af0f06b7f2b841d7632e756bd3220a5d595b9d426a5453da1ccc895900b894d098f
checksum: 10c0/7918d82477e75931b6e35bb003464382eb93e526362f81a98bf8610407a67b10f4d041931015ad48072c89db547deb7e471dfb91f4ab11ac63a24d8580297f75
languageName: node
linkType: hard
@ -5286,10 +5307,10 @@ __metadata:
languageName: node
linkType: hard
"@oxc-project/runtime@npm:=0.77.3":
version: 0.77.3
resolution: "@oxc-project/runtime@npm:0.77.3"
checksum: 10c0/e2e9d64c9af481c4cad78240f8d5bf252567b026cf857c93bbc43a296b15f2b71cdf99e8890184cc60e26ec9178de4b209ba2729dbe99dab8dc09f8cfa592820
"@oxc-project/runtime@npm:=0.82.3":
version: 0.82.3
resolution: "@oxc-project/runtime@npm:0.82.3"
checksum: 10c0/48fd0577a9bd146da7eefea8e61a7c855f8947ef6233fe7db2921e5c1f07d73459d8fb4d2d9e45f4d522d5bb31af8157c96020860154fdf7223a9cb0957e36c0
languageName: node
linkType: hard
@ -5300,10 +5321,10 @@ __metadata:
languageName: node
linkType: hard
"@oxc-project/types@npm:=0.77.3":
version: 0.77.3
resolution: "@oxc-project/types@npm:0.77.3"
checksum: 10c0/aaccfccd59605a46b605b9c2dd966dc470f593ccb66c2a89c189ccbe90fc768e9bf9abfa82f4302addf9881d372ea9c4e634597ad078cf4f76219ce4d9886119
"@oxc-project/types@npm:=0.82.3":
version: 0.82.3
resolution: "@oxc-project/types@npm:0.82.3"
checksum: 10c0/17dffc91dc3b726be67b7333d251e811bf4badce8ae77269d1626a107cd7cb673674a3fd6e0f127e40951d630281b9a164fee787a1a0cad12e7372a14b89d7cf
languageName: node
linkType: hard
@ -5800,16 +5821,16 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-android-arm64@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-android-arm64@npm:1.0.0-beta.29"
"@rolldown/binding-android-arm64@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-android-arm64@npm:1.0.0-beta.34"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@rolldown/binding-darwin-arm64@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.29"
"@rolldown/binding-darwin-arm64@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.34"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
@ -5821,9 +5842,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-darwin-x64@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-beta.29"
"@rolldown/binding-darwin-x64@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-beta.34"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
@ -5835,9 +5856,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-freebsd-x64@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.29"
"@rolldown/binding-freebsd-x64@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.34"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
@ -5849,9 +5870,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.34"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
@ -5863,9 +5884,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.34"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
@ -5877,9 +5898,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.34"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
@ -5891,16 +5912,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-linux-arm64-ohos@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-arm64-ohos@npm:1.0.0-beta.29"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.29"
"@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.34"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
@ -5912,9 +5926,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.29"
"@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.34"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
@ -5926,11 +5940,18 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.29"
"@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.34"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.34"
dependencies:
"@napi-rs/wasm-runtime": "npm:^1.0.1"
"@napi-rs/wasm-runtime": "npm:^1.0.3"
conditions: cpu=wasm32
languageName: node
linkType: hard
@ -5944,9 +5965,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.29"
"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.34"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
@ -5958,9 +5979,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.29"
"@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.34"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
@ -5972,9 +5993,9 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.29"
"@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.34"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -5986,10 +6007,10 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/pluginutils@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.29"
checksum: 10c0/6b53011bb93c83be617a5511197656991b06a2ffa8eb869af211cbb0aed8cc9a6cf48f0a6d0ec92c0daadb912fd74808a635a6a6477f97ca9effaf5606c77deb
"@rolldown/pluginutils@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.34"
checksum: 10c0/96565287991825ecd90b60607dae908ebfdde233661fc589c98547a75c1fd0282b2e2a7849c3eb0c9941e2fba34667a8d5cdb8d597370815c19c2f29b4c157b4
languageName: node
linkType: hard
@ -9587,7 +9608,7 @@ __metadata:
"@mistralai/mistralai": "npm:^1.7.5"
"@modelcontextprotocol/sdk": "npm:^1.17.0"
"@mozilla/readability": "npm:^0.6.0"
"@napi-rs/system-ocr": "npm:^1.0.2"
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch"
"@notionhq/client": "npm:^2.2.15"
"@opentelemetry/api": "npm:^1.9.0"
"@opentelemetry/core": "npm:2.0.0"
@ -9660,6 +9681,7 @@ __metadata:
cli-progress: "npm:^3.12.0"
code-inspector-plugin: "npm:^0.20.14"
color: "npm:^5.0.0"
concurrently: "npm:^9.2.1"
country-flag-emoji-polyfill: "npm:0.1.8"
dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8"
@ -10908,6 +10930,16 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
ansi-styles: "npm:^4.1.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
languageName: node
linkType: hard
"chalk@npm:^3.0.0":
version: 3.0.0
resolution: "chalk@npm:3.0.0"
@ -10918,16 +10950,6 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
ansi-styles: "npm:^4.1.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
languageName: node
linkType: hard
"chalk@npm:^5.4.1":
version: 5.4.1
resolution: "chalk@npm:5.4.1"
@ -11449,6 +11471,23 @@ __metadata:
languageName: node
linkType: hard
"concurrently@npm:^9.2.1":
version: 9.2.1
resolution: "concurrently@npm:9.2.1"
dependencies:
chalk: "npm:4.1.2"
rxjs: "npm:7.8.2"
shell-quote: "npm:1.8.3"
supports-color: "npm:8.1.1"
tree-kill: "npm:1.2.2"
yargs: "npm:17.7.2"
bin:
conc: dist/bin/concurrently.js
concurrently: dist/bin/concurrently.js
checksum: 10c0/da37f239f82eb7ac24f5ddb56259861e5f1d6da2ade7602b6ea7ad3101b13b5ccec02a77b7001402d1028ff2fdc38eed55644b32853ad5abf30e057002a963aa
languageName: node
linkType: hard
"conf@npm:^10.2.0":
version: 10.2.0
resolution: "conf@npm:10.2.0"
@ -14149,15 +14188,15 @@ __metadata:
languageName: node
linkType: hard
"fdir@npm:^6.4.6":
version: 6.4.6
resolution: "fdir@npm:6.4.6"
"fdir@npm:^6.5.0":
version: 6.5.0
resolution: "fdir@npm:6.5.0"
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
checksum: 10c0/45b559cff889934ebb8bc498351e5acba40750ada7e7d6bde197768d2fa67c149be8ae7f8ff34d03f4e1eb20f2764116e56440aaa2f6689e9a4aa7ef06acafe9
checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f
languageName: node
linkType: hard
@ -19480,6 +19519,13 @@ __metadata:
languageName: node
linkType: hard
"picomatch@npm:^4.0.3":
version: 4.0.3
resolution: "picomatch@npm:4.0.3"
checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2
languageName: node
linkType: hard
"pidtree@npm:^0.6.0":
version: 0.6.0
resolution: "pidtree@npm:0.6.0"
@ -21515,27 +21561,27 @@ __metadata:
languageName: node
linkType: hard
"rolldown@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "rolldown@npm:1.0.0-beta.29"
"rolldown@npm:1.0.0-beta.34":
version: 1.0.0-beta.34
resolution: "rolldown@npm:1.0.0-beta.34"
dependencies:
"@oxc-project/runtime": "npm:=0.77.3"
"@oxc-project/types": "npm:=0.77.3"
"@rolldown/binding-android-arm64": "npm:1.0.0-beta.29"
"@rolldown/binding-darwin-arm64": "npm:1.0.0-beta.29"
"@rolldown/binding-darwin-x64": "npm:1.0.0-beta.29"
"@rolldown/binding-freebsd-x64": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm64-musl": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-arm64-ohos": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-x64-gnu": "npm:1.0.0-beta.29"
"@rolldown/binding-linux-x64-musl": "npm:1.0.0-beta.29"
"@rolldown/binding-wasm32-wasi": "npm:1.0.0-beta.29"
"@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-beta.29"
"@rolldown/binding-win32-ia32-msvc": "npm:1.0.0-beta.29"
"@rolldown/binding-win32-x64-msvc": "npm:1.0.0-beta.29"
"@rolldown/pluginutils": "npm:1.0.0-beta.29"
"@oxc-project/runtime": "npm:=0.82.3"
"@oxc-project/types": "npm:=0.82.3"
"@rolldown/binding-android-arm64": "npm:1.0.0-beta.34"
"@rolldown/binding-darwin-arm64": "npm:1.0.0-beta.34"
"@rolldown/binding-darwin-x64": "npm:1.0.0-beta.34"
"@rolldown/binding-freebsd-x64": "npm:1.0.0-beta.34"
"@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-beta.34"
"@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-beta.34"
"@rolldown/binding-linux-arm64-musl": "npm:1.0.0-beta.34"
"@rolldown/binding-linux-x64-gnu": "npm:1.0.0-beta.34"
"@rolldown/binding-linux-x64-musl": "npm:1.0.0-beta.34"
"@rolldown/binding-openharmony-arm64": "npm:1.0.0-beta.34"
"@rolldown/binding-wasm32-wasi": "npm:1.0.0-beta.34"
"@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-beta.34"
"@rolldown/binding-win32-ia32-msvc": "npm:1.0.0-beta.34"
"@rolldown/binding-win32-x64-msvc": "npm:1.0.0-beta.34"
"@rolldown/pluginutils": "npm:1.0.0-beta.34"
ansis: "npm:^4.0.0"
dependenciesMeta:
"@rolldown/binding-android-arm64":
@ -21552,12 +21598,12 @@ __metadata:
optional: true
"@rolldown/binding-linux-arm64-musl":
optional: true
"@rolldown/binding-linux-arm64-ohos":
optional: true
"@rolldown/binding-linux-x64-gnu":
optional: true
"@rolldown/binding-linux-x64-musl":
optional: true
"@rolldown/binding-openharmony-arm64":
optional: true
"@rolldown/binding-wasm32-wasi":
optional: true
"@rolldown/binding-win32-arm64-msvc":
@ -21568,7 +21614,7 @@ __metadata:
optional: true
bin:
rolldown: bin/cli.mjs
checksum: 10c0/7660c1bc353d6e0be2b046f18110ed4bd66ed64e6d3bde214c5060b22922e9356f5b8c368d7491976b0a2e02202a157d12b005c5aeddb8b4ce25c2f9c7c19e67
checksum: 10c0/3fdaa36b3bfcdd6913973ef8d785a7e7eeb8c181626ac0d0b8a75aecca2ba3d536ff29a3f5c003f692d7c422e022d0357d7d564ab4aa67cf128230ca137473e8
languageName: node
linkType: hard
@ -21700,6 +21746,15 @@ __metadata:
languageName: node
linkType: hard
"rxjs@npm:7.8.2":
version: 7.8.2
resolution: "rxjs@npm:7.8.2"
dependencies:
tslib: "npm:^2.1.0"
checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45
languageName: node
linkType: hard
"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
@ -22032,6 +22087,13 @@ __metadata:
languageName: node
linkType: hard
"shell-quote@npm:1.8.3":
version: 1.8.3
resolution: "shell-quote@npm:1.8.3"
checksum: 10c0/bee87c34e1e986cfb4c30846b8e6327d18874f10b535699866f368ade11ea4ee45433d97bf5eada22c4320c27df79c3a6a7eb1bf3ecfc47f2c997d9e5e2672fd
languageName: node
linkType: hard
"shiki@npm:3.12.0, shiki@npm:^3.12.0":
version: 3.12.0
resolution: "shiki@npm:3.12.0"
@ -22696,6 +22758,15 @@ __metadata:
languageName: node
linkType: hard
"supports-color@npm:8.1.1":
version: 8.1.1
resolution: "supports-color@npm:8.1.1"
dependencies:
has-flag: "npm:^4.0.0"
checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89
languageName: node
linkType: hard
"supports-color@npm:^7.1.0":
version: 7.2.0
resolution: "supports-color@npm:7.2.0"
@ -23131,7 +23202,7 @@ __metadata:
languageName: node
linkType: hard
"tree-kill@npm:^1.2.2":
"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
bin:
@ -23902,15 +23973,15 @@ __metadata:
linkType: hard
"vite@npm:rolldown-vite@latest":
version: 7.0.10
resolution: "rolldown-vite@npm:7.0.10"
version: 7.1.5
resolution: "rolldown-vite@npm:7.1.5"
dependencies:
fdir: "npm:^6.4.6"
fdir: "npm:^6.5.0"
fsevents: "npm:~2.3.3"
lightningcss: "npm:^1.30.1"
picomatch: "npm:^4.0.2"
picomatch: "npm:^4.0.3"
postcss: "npm:^8.5.6"
rolldown: "npm:1.0.0-beta.29"
rolldown: "npm:1.0.0-beta.34"
tinyglobby: "npm:^0.2.14"
peerDependencies:
"@types/node": ^20.19.0 || >=22.12.0
@ -23952,7 +24023,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10c0/9094a52664c475822deee5597d161ab8846062de01040b6cae18d81e5e894c20f201b93229efd51bfcd021b2d171a08852a17acc67dd53a1ac896c800a950eea
checksum: 10c0/55f6648a8700345700382adac4877208eedcfff5757debba74851227dbc50eae3cc7ccea86bcfda689a9855fbbd2c7e7dd020ffc0c01bfb815dbc6bf65991cbd
languageName: node
linkType: hard
@ -24557,7 +24628,7 @@ __metadata:
languageName: node
linkType: hard
"yargs@npm:^17.0.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2":
"yargs@npm:17.7.2, yargs@npm:^17.0.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies: