Compare commits
19 Commits
feat/reset
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
984eb7ac5f | ||
|
|
0ae9c12cb1 | ||
|
|
92d558feff | ||
|
|
ac8d28687d | ||
|
|
9f00f00546 | ||
|
|
bd94d23343 | ||
|
|
5f1c14e2c0 | ||
|
|
6ec0985ae5 | ||
|
|
f47673a153 | ||
|
|
5545921b8b | ||
|
|
70d8a8ac28 | ||
|
|
345b7cf231 | ||
|
|
71d924854e | ||
|
|
2a51ec628e | ||
|
|
9b8402da0c | ||
|
|
cdc12d5092 | ||
|
|
e5967fd874 | ||
|
|
e2f1d80697 | ||
|
|
28bc89ac7c |
2
.github/workflows/auto-i18n.yml
vendored
2
.github/workflows/auto-i18n.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
4
.github/workflows/github-issue-tracker.yml
vendored
4
.github/workflows/github-issue-tracker.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
4
.github/workflows/issue-management.yml
vendored
4
.github/workflows/issue-management.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: none
|
||||
steps:
|
||||
- name: Close needs-more-info issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: 'needs-more-info'
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
|
||||
- name: Close inactive issues
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
|
||||
4
.github/workflows/nightly-build.yml
vendored
4
.github/workflows/nightly-build.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }}
|
||||
path: renamed-artifacts/*
|
||||
|
||||
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@ releaseInfo:
|
||||
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
|
||||
- Translation: Enhanced translation script with concurrency and validation
|
||||
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
|
||||
- QR Code Generation: Optimized performance for phone LAN export
|
||||
- Enterprise Settings: Added enterprise section in About settings
|
||||
- Assistant/Agent Popup: Enhanced UI for adding assistants and agents
|
||||
|
||||
Claude Code Tool Improvements:
|
||||
- GlobTool: Now counts lines instead of files in output for better clarity
|
||||
@@ -188,6 +191,9 @@ releaseInfo:
|
||||
- Fixed reranker API error response capture
|
||||
- Fixed right-click paste file content into inputbar
|
||||
- Fixed minimax-m2 support in aiCore
|
||||
- Fixed Azure embedding issues
|
||||
- Fixed agent edit modal loading race condition
|
||||
- Fixed debounced save cancellation on file path update
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.3 新特性
|
||||
@@ -218,6 +224,9 @@ releaseInfo:
|
||||
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
|
||||
- 翻译:增强翻译脚本的并发和验证功能
|
||||
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
|
||||
- 二维码生成:优化手机局域网导出性能
|
||||
- 企业设置:在关于设置中添加企业部分
|
||||
- 助手/Agent 弹窗:增强添加助手和 Agent 的界面
|
||||
|
||||
Claude Code 工具改进:
|
||||
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
|
||||
@@ -245,4 +254,7 @@ releaseInfo:
|
||||
- 修复 reranker API 错误响应捕获
|
||||
- 修复右键粘贴文件内容到输入栏
|
||||
- 修复 aiCore 中的 minimax-m2 支持
|
||||
- 修复 Azure embedding 问题
|
||||
- 修复 Agent 编辑模态框加载竞态条件
|
||||
- 修复文件路径更新时防抖保存取消问题
|
||||
<!--LANG:END-->
|
||||
|
||||
@@ -54,6 +54,8 @@ export enum IpcChannel {
|
||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
||||
Webview_SearchHotkey = 'webview:search-hotkey',
|
||||
Webview_PrintToPDF = 'webview:print-to-pdf',
|
||||
Webview_SaveAsHTML = 'webview:save-as-html',
|
||||
|
||||
// Open
|
||||
Open_Path = 'open:path',
|
||||
|
||||
@@ -31,3 +31,16 @@ export type WebviewKeyEvent = {
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
}
|
||||
|
||||
export interface WebSocketStatusResponse {
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}
|
||||
|
||||
export interface WebSocketCandidatesResponse {
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
@@ -809,6 +809,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
webview.session.setSpellCheckerEnabled(isEnable)
|
||||
})
|
||||
|
||||
// Webview print and save handlers
|
||||
ipcMain.handle(IpcChannel.Webview_PrintToPDF, async (_, webviewId: number) => {
|
||||
const { printWebviewToPDF } = await import('./services/WebviewService')
|
||||
return await printWebviewToPDF(webviewId)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Webview_SaveAsHTML, async (_, webviewId: number) => {
|
||||
const { saveWebviewAsHTML } = await import('./services/WebviewService')
|
||||
return await saveWebviewAsHTML(webviewId)
|
||||
})
|
||||
|
||||
// store sync
|
||||
storeSyncService.registerIpcHandler()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
|
||||
import * as fs from 'fs'
|
||||
import { networkInterfaces } from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -202,12 +203,7 @@ class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
public getStatus = async (): Promise<{
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}> => {
|
||||
public getStatus = async (): Promise<WebSocketStatusResponse> => {
|
||||
return {
|
||||
isRunning: this.isStarted,
|
||||
port: this.isStarted ? this.port : undefined,
|
||||
@@ -216,13 +212,7 @@ class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
public getAllCandidates = async (): Promise<
|
||||
Array<{
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}>
|
||||
> => {
|
||||
public getAllCandidates = async (): Promise<WebSocketCandidatesResponse[]> => {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { app, session, shell, webContents } from 'electron'
|
||||
import { app, dialog, session, shell, webContents } from 'electron'
|
||||
import { promises as fs } from 'fs'
|
||||
|
||||
/**
|
||||
* init the useragent of the webview session
|
||||
@@ -53,11 +54,17 @@ const attachKeyboardHandler = (contents: Electron.WebContents) => {
|
||||
return
|
||||
}
|
||||
|
||||
const isFindShortcut = (input.control || input.meta) && key === 'f'
|
||||
const isEscape = key === 'escape'
|
||||
const isEnter = key === 'enter'
|
||||
// Helper to check if this is a shortcut we handle
|
||||
const isHandledShortcut = (k: string) => {
|
||||
const isFindShortcut = (input.control || input.meta) && k === 'f'
|
||||
const isPrintShortcut = (input.control || input.meta) && k === 'p'
|
||||
const isSaveShortcut = (input.control || input.meta) && k === 's'
|
||||
const isEscape = k === 'escape'
|
||||
const isEnter = k === 'enter'
|
||||
return isFindShortcut || isPrintShortcut || isSaveShortcut || isEscape || isEnter
|
||||
}
|
||||
|
||||
if (!isFindShortcut && !isEscape && !isEnter) {
|
||||
if (!isHandledShortcut(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,11 +73,20 @@ const attachKeyboardHandler = (contents: Electron.WebContents) => {
|
||||
return
|
||||
}
|
||||
|
||||
const isFindShortcut = (input.control || input.meta) && key === 'f'
|
||||
const isPrintShortcut = (input.control || input.meta) && key === 'p'
|
||||
const isSaveShortcut = (input.control || input.meta) && key === 's'
|
||||
|
||||
// Always prevent Cmd/Ctrl+F to override the guest page's native find dialog
|
||||
if (isFindShortcut) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Prevent default print/save dialogs and handle them with custom logic
|
||||
if (isPrintShortcut || isSaveShortcut) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Send the hotkey event to the renderer
|
||||
// The renderer will decide whether to preventDefault for Escape and Enter
|
||||
// based on whether the search bar is visible
|
||||
@@ -100,3 +116,129 @@ export function initWebviewHotkeys() {
|
||||
attachKeyboardHandler(contents)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Print webview content to PDF
|
||||
* @param webviewId The webview webContents id
|
||||
* @returns Path to saved PDF file or null if user cancelled
|
||||
*/
|
||||
export async function printWebviewToPDF(webviewId: number): Promise<string | null> {
|
||||
const webview = webContents.fromId(webviewId)
|
||||
if (!webview) {
|
||||
throw new Error('Webview not found')
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the page title for default filename
|
||||
const pageTitle = await webview.executeJavaScript('document.title || "webpage"').catch(() => 'webpage')
|
||||
// Sanitize filename by removing invalid characters
|
||||
const sanitizedTitle = pageTitle.replace(/[<>:"/\\|?*]/g, '-').substring(0, 100)
|
||||
const defaultFilename = sanitizedTitle ? `${sanitizedTitle}.pdf` : `webpage-${Date.now()}.pdf`
|
||||
|
||||
// Show save dialog
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
title: 'Save as PDF',
|
||||
defaultPath: defaultFilename,
|
||||
filters: [{ name: 'PDF Files', extensions: ['pdf'] }]
|
||||
})
|
||||
|
||||
if (canceled || !filePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Generate PDF with settings to capture full page
|
||||
const pdfData = await webview.printToPDF({
|
||||
marginsType: 0,
|
||||
printBackground: true,
|
||||
printSelectionOnly: false,
|
||||
landscape: false,
|
||||
pageSize: 'A4',
|
||||
preferCSSPageSize: true
|
||||
})
|
||||
|
||||
// Save PDF to file
|
||||
await fs.writeFile(filePath, pdfData)
|
||||
|
||||
return filePath
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to print to PDF: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save webview content as HTML
|
||||
* @param webviewId The webview webContents id
|
||||
* @returns Path to saved HTML file or null if user cancelled
|
||||
*/
|
||||
export async function saveWebviewAsHTML(webviewId: number): Promise<string | null> {
|
||||
const webview = webContents.fromId(webviewId)
|
||||
if (!webview) {
|
||||
throw new Error('Webview not found')
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the page title for default filename
|
||||
const pageTitle = await webview.executeJavaScript('document.title || "webpage"').catch(() => 'webpage')
|
||||
// Sanitize filename by removing invalid characters
|
||||
const sanitizedTitle = pageTitle.replace(/[<>:"/\\|?*]/g, '-').substring(0, 100)
|
||||
const defaultFilename = sanitizedTitle ? `${sanitizedTitle}.html` : `webpage-${Date.now()}.html`
|
||||
|
||||
// Show save dialog
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
title: 'Save as HTML',
|
||||
defaultPath: defaultFilename,
|
||||
filters: [
|
||||
{ name: 'HTML Files', extensions: ['html', 'htm'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
})
|
||||
|
||||
if (canceled || !filePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the HTML content with safe error handling
|
||||
const html = await webview.executeJavaScript(`
|
||||
(() => {
|
||||
try {
|
||||
// Build complete DOCTYPE string if present
|
||||
let doctype = '';
|
||||
if (document.doctype) {
|
||||
const dt = document.doctype;
|
||||
doctype = '<!DOCTYPE ' + (dt.name || 'html');
|
||||
|
||||
// Add PUBLIC identifier if publicId is present
|
||||
if (dt.publicId) {
|
||||
// Escape single quotes in publicId
|
||||
const escapedPublicId = String(dt.publicId).replace(/'/g, "\\'");
|
||||
doctype += " PUBLIC '" + escapedPublicId + "'";
|
||||
|
||||
// Add systemId if present (required when publicId is present)
|
||||
if (dt.systemId) {
|
||||
const escapedSystemId = String(dt.systemId).replace(/'/g, "\\'");
|
||||
doctype += " '" + escapedSystemId + "'";
|
||||
}
|
||||
} else if (dt.systemId) {
|
||||
// SYSTEM identifier (without PUBLIC)
|
||||
const escapedSystemId = String(dt.systemId).replace(/'/g, "\\'");
|
||||
doctype += " SYSTEM '" + escapedSystemId + "'";
|
||||
}
|
||||
|
||||
doctype += '>';
|
||||
}
|
||||
return doctype + (document.documentElement?.outerHTML || '');
|
||||
} catch (error) {
|
||||
// Fallback: just return the HTML without DOCTYPE if there's an error
|
||||
return document.documentElement?.outerHTML || '';
|
||||
}
|
||||
})()
|
||||
`)
|
||||
|
||||
// Save HTML to file
|
||||
await fs.writeFile(filePath, html, 'utf-8')
|
||||
|
||||
return filePath
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save as HTML: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +406,8 @@ const api = {
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
||||
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable),
|
||||
printToPDF: (webviewId: number) => ipcRenderer.invoke(IpcChannel.Webview_PrintToPDF, webviewId),
|
||||
saveAsHTML: (webviewId: number) => ipcRenderer.invoke(IpcChannel.Webview_SaveAsHTML, webviewId),
|
||||
onFindShortcut: (callback: (payload: WebviewKeyEvent) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, payload: WebviewKeyEvent) => {
|
||||
callback(payload)
|
||||
|
||||
@@ -192,7 +192,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: 0
|
||||
thinking_budget: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,8 +327,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: -1,
|
||||
includeThoughts: true
|
||||
thinking_budget: -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,8 +338,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: budgetTokens,
|
||||
includeThoughts: true
|
||||
thinking_budget: budgetTokens,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,7 +670,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
|
||||
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinkingBudget}`
|
||||
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
|
||||
}
|
||||
// FIXME: poe 不支持多个text part,上传文本文件的时候用的不是file part而是text part,因此会出问题
|
||||
// 临时解决方案是强制poe用string content,但是其实poe部分支持array
|
||||
|
||||
@@ -52,7 +52,7 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
}
|
||||
}
|
||||
],
|
||||
fallbackRule: (provider: Provider) => provider
|
||||
fallbackRule: (provider: Provider) => extraProviderConfig(provider)
|
||||
}
|
||||
|
||||
export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES)
|
||||
|
||||
@@ -98,7 +98,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: 0
|
||||
thinking_budget: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,8 +259,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: -1,
|
||||
includeThoughts: true
|
||||
thinking_budget: -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,8 +270,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
extra_body: {
|
||||
google: {
|
||||
thinking_config: {
|
||||
thinkingBudget: budgetTokens,
|
||||
includeThoughts: true
|
||||
thinking_budget: budgetTokens ?? -1,
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,8 +431,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
if (reasoningEffort === undefined) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
includeThoughts: false,
|
||||
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {})
|
||||
include_thoughts: false,
|
||||
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,7 +442,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
if (effortRatio > 1) {
|
||||
return {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,8 +452,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
||||
|
||||
return {
|
||||
thinkingConfig: {
|
||||
...(budget > 0 ? { thinkingBudget: budget } : {}),
|
||||
includeThoughts: true
|
||||
...(budget > 0 ? { thinking_budget: budget } : {}),
|
||||
include_thoughts: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,51 @@ const WebviewContainer = memo(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appid, url])
|
||||
|
||||
// Setup keyboard shortcuts handler for print and save
|
||||
useEffect(() => {
|
||||
if (!webviewRef.current) return
|
||||
|
||||
const unsubscribe = window.api?.webview?.onFindShortcut?.(async (payload) => {
|
||||
// Get webviewId when event is triggered
|
||||
const webviewId = webviewRef.current?.getWebContentsId()
|
||||
|
||||
// Only handle events for this webview
|
||||
if (!webviewId || payload.webviewId !== webviewId) return
|
||||
|
||||
const key = payload.key?.toLowerCase()
|
||||
const isModifier = payload.control || payload.meta
|
||||
|
||||
if (!isModifier || !key) return
|
||||
|
||||
try {
|
||||
if (key === 'p') {
|
||||
// Print to PDF
|
||||
logger.info(`Printing webview ${appid} to PDF`)
|
||||
const filePath = await window.api.webview.printToPDF(webviewId)
|
||||
if (filePath) {
|
||||
window.toast?.success?.(`PDF saved to: ${filePath}`)
|
||||
logger.info(`PDF saved to: ${filePath}`)
|
||||
}
|
||||
} else if (key === 's') {
|
||||
// Save as HTML
|
||||
logger.info(`Saving webview ${appid} as HTML`)
|
||||
const filePath = await window.api.webview.saveAsHTML(webviewId)
|
||||
if (filePath) {
|
||||
window.toast?.success?.(`HTML saved to: ${filePath}`)
|
||||
logger.info(`HTML saved to: ${filePath}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to handle shortcut for webview ${appid}:`, error as Error)
|
||||
window.toast?.error?.(`Failed: ${(error as Error).message}`)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [appid])
|
||||
|
||||
// Update webview settings when they change
|
||||
useEffect(() => {
|
||||
if (!webviewRef.current) return
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@herou
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import type { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -38,12 +40,12 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
<QRCodeSVG
|
||||
marginSize={2}
|
||||
value={qrCodeValue}
|
||||
level="Q"
|
||||
size={160}
|
||||
level="H"
|
||||
size={200}
|
||||
imageSettings={{
|
||||
src: '/src/assets/images/logo.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
src: AppLogo,
|
||||
width: 60,
|
||||
height: 60,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
@@ -198,17 +200,28 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const { port, ip } = await window.api.webSocket.status()
|
||||
|
||||
if (ip && port) {
|
||||
const candidates = await window.api.webSocket.getAllCandidates()
|
||||
const connectionInfo = {
|
||||
type: 'cherry-studio-app',
|
||||
candidates,
|
||||
selectedHost: ip,
|
||||
port,
|
||||
timestamp: Date.now()
|
||||
const candidatesData = await window.api.webSocket.getAllCandidates()
|
||||
|
||||
const optimizeConnectionInfo = () => {
|
||||
const ipToNumber = (ip: string) => {
|
||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0)
|
||||
}
|
||||
|
||||
const compressedData = [
|
||||
'CSA',
|
||||
ipToNumber(ip),
|
||||
candidatesData.map((candidate: WebSocketCandidatesResponse) => ipToNumber(candidate.host)),
|
||||
port, // 端口号
|
||||
Date.now() % 86400000
|
||||
]
|
||||
|
||||
return compressedData
|
||||
}
|
||||
setQrCodeValue(JSON.stringify(connectionInfo))
|
||||
|
||||
const compressedData = optimizeConnectionInfo()
|
||||
const qrCodeValue = JSON.stringify(compressedData)
|
||||
setQrCodeValue(qrCodeValue)
|
||||
setConnectionPhase('waiting_qr_scan')
|
||||
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
|
||||
} else {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
|
||||
setConnectionPhase('error')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Bewältigen Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
|
||||
"error": {
|
||||
"failed": "Agent hinzufügen fehlgeschlagen",
|
||||
"invalid_agent": "Ungültiger Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Tägliche Gespräche und schnelle Fragen & Antworten",
|
||||
"title": "Assistent hinzufügen"
|
||||
},
|
||||
"option": {
|
||||
"title": "Typ auswählen"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Neues Thema erstellen"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Ein KI-Assistent für Kreative",
|
||||
"downloading": "Update wird heruntergeladen...",
|
||||
"enterprise": {
|
||||
"title": "Unternehmen"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Feedback"
|
||||
},
|
||||
"label": "Über uns",
|
||||
"license": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Lizenz"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Anzeigen",
|
||||
"title": "Changelog"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
|
||||
"error": {
|
||||
"failed": "Αποτυχία προσθήκης πράκτορα",
|
||||
"invalid_agent": "Μη έγκυρος Agent"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
|
||||
"title": "Προσθήκη βοηθού"
|
||||
},
|
||||
"option": {
|
||||
"title": "Επιλέξτε Τύπο"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Δημιουργία νέου θέματος"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
|
||||
"downloading": "Λήψη ενημερώσεων...",
|
||||
"enterprise": {
|
||||
"title": "Επιχείρηση"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Σχόλια και Παρατηρήσεις",
|
||||
"title": "Αποστολή σχολίων"
|
||||
},
|
||||
"label": "Περί μας",
|
||||
"license": {
|
||||
"button": "Προβολή",
|
||||
"title": "Licenses"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Προβολή",
|
||||
"title": "Ημερολόγιο Ενημερώσεων"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Maneja tareas complejas con varias herramientas",
|
||||
"error": {
|
||||
"failed": "Error al añadir agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
|
||||
"title": "Agregar asistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Seleccionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Crear nuevo tema"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Una asistente de IA creada para los creadores",
|
||||
"downloading": "Descargando actualización...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Enviar feedback",
|
||||
"title": "Enviar comentarios"
|
||||
},
|
||||
"label": "Acerca de nosotros",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licencia"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de cambios"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Gérez des tâches complexes avec divers outils",
|
||||
"error": {
|
||||
"failed": "Échec de l'ajout de l'agent",
|
||||
"invalid_agent": "Agent invalide"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversations quotidiennes et Q&R rapides",
|
||||
"title": "Ajouter un assistant"
|
||||
},
|
||||
"option": {
|
||||
"title": "Sélectionner le type"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Nouveau sujet"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Un assistant IA conçu pour les créateurs",
|
||||
"downloading": "Téléchargement de la mise à jour en cours...",
|
||||
"enterprise": {
|
||||
"title": "Entreprise"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Faire un retour",
|
||||
"title": "Retour d'information"
|
||||
},
|
||||
"label": "À propos de nous",
|
||||
"license": {
|
||||
"button": "Afficher",
|
||||
"title": "Licence"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Afficher",
|
||||
"title": "Journal des mises à jour"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "さまざまなツールを使って複雑なタスクを処理する",
|
||||
"error": {
|
||||
"failed": "エージェントの追加に失敗しました",
|
||||
"invalid_agent": "無効なエージェント"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "日常会話と簡単なQ&A",
|
||||
"title": "アシスタントを追加"
|
||||
},
|
||||
"option": {
|
||||
"title": "種類を選択"
|
||||
},
|
||||
"topic": {
|
||||
"title": "新しいトピック"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "クリエイターのための強力なAIアシスタント",
|
||||
"downloading": "ダウンロード中...",
|
||||
"enterprise": {
|
||||
"title": "エンタープライズ"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "フィードバック",
|
||||
"title": "フィードバック"
|
||||
},
|
||||
"label": "について",
|
||||
"license": {
|
||||
"button": "ライセンス",
|
||||
"title": "ライセンス"
|
||||
},
|
||||
"releases": {
|
||||
"button": "リリース",
|
||||
"title": "リリースノート"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Lide com tarefas complexas usando várias ferramentas",
|
||||
"error": {
|
||||
"failed": "Falha ao adicionar agente",
|
||||
"invalid_agent": "Agent inválido"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Conversas diárias e perguntas e respostas rápidas",
|
||||
"title": "Adicionar assistente"
|
||||
},
|
||||
"option": {
|
||||
"title": "Selecionar Tipo"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Novo Tópico"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Um assistente de IA criado para criadores",
|
||||
"downloading": "Baixando atualizações...",
|
||||
"enterprise": {
|
||||
"title": "Empresa"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Feedback",
|
||||
"title": "Enviar feedback"
|
||||
},
|
||||
"label": "Sobre Nós",
|
||||
"license": {
|
||||
"button": "Ver",
|
||||
"title": "Licença"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Ver",
|
||||
"title": "Registro de alterações"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"agent": {
|
||||
"add": {
|
||||
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
|
||||
"error": {
|
||||
"failed": "Не удалось добавить агента",
|
||||
"invalid_agent": "Недействительный агент"
|
||||
@@ -547,8 +548,12 @@
|
||||
"chat": {
|
||||
"add": {
|
||||
"assistant": {
|
||||
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
|
||||
"title": "Добавить ассистента"
|
||||
},
|
||||
"option": {
|
||||
"title": "Выберите тип"
|
||||
},
|
||||
"topic": {
|
||||
"title": "Новый топик"
|
||||
}
|
||||
@@ -2923,15 +2928,14 @@
|
||||
},
|
||||
"description": "Мощный AI-ассистент для созидания",
|
||||
"downloading": "Загрузка...",
|
||||
"enterprise": {
|
||||
"title": "Предприятие"
|
||||
},
|
||||
"feedback": {
|
||||
"button": "Обратная связь",
|
||||
"title": "Обратная связь"
|
||||
},
|
||||
"label": "О программе и обратная связь",
|
||||
"license": {
|
||||
"button": "Лицензия",
|
||||
"title": "Лицензия"
|
||||
},
|
||||
"releases": {
|
||||
"button": "Релизы",
|
||||
"title": "Заметки о релизах"
|
||||
@@ -3043,7 +3047,7 @@
|
||||
"confirm": {
|
||||
"button": "Выберите файл резервной копии"
|
||||
},
|
||||
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
|
||||
"content": "Экспорт части данных, включая историю чатов и настройки. Обратите внимание, процесс резервного копирования может занять некоторое время, благодарим за ваше терпение.",
|
||||
"lan": {
|
||||
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
|
||||
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {
|
||||
GoogleGenAI,
|
||||
Model as GeminiModel,
|
||||
SendMessageParameters,
|
||||
ThinkingConfig,
|
||||
Tool
|
||||
} from '@google/genai'
|
||||
|
||||
@@ -92,7 +91,10 @@ export type ReasoningEffortOptionalParams = {
|
||||
}
|
||||
extra_body?: {
|
||||
google?: {
|
||||
thinking_config: ThinkingConfig
|
||||
thinking_config: {
|
||||
thinking_budget: number
|
||||
include_thoughts?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add any other potential reasoning-related keys here if they exist
|
||||
|
||||
Reference in New Issue
Block a user