mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 04:00:09 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c554d748 | |||
| 4e4933d861 | |||
| a5bb31b844 |
@@ -2,6 +2,23 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Version 2.0.0-next.70](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.69...v2.0.0-next.70)
|
||||
|
||||
<sup>Released on **2025-11-17**</sup>
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 2.0.0-next.69](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.68...v2.0.0-next.69)
|
||||
|
||||
<sup>Released on **2025-11-17**</sup>
|
||||
|
||||
@@ -330,6 +330,11 @@
|
||||
"screenshot": "Screenshot",
|
||||
"settings": "Export Settings",
|
||||
"text": "Text",
|
||||
"widthMode": {
|
||||
"label": "Width Mode",
|
||||
"narrow": "Narrow",
|
||||
"wide": "Wide"
|
||||
},
|
||||
"withBackground": "Include Background Image",
|
||||
"withFooter": "Include Footer",
|
||||
"withPluginInfo": "Include Plugin Information",
|
||||
|
||||
@@ -330,6 +330,11 @@
|
||||
"screenshot": "截图",
|
||||
"settings": "导出设置",
|
||||
"text": "文本",
|
||||
"widthMode": {
|
||||
"label": "宽度模式",
|
||||
"narrow": "窄屏模式",
|
||||
"wide": "宽屏模式"
|
||||
},
|
||||
"withBackground": "包含背景图片",
|
||||
"withFooter": "包含页脚",
|
||||
"withPluginInfo": "包含插件信息",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/lobehub",
|
||||
"version": "2.0.0-next.69",
|
||||
"version": "2.0.0-next.70",
|
||||
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -191,6 +191,7 @@
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"@virtuoso.dev/masonry": "^1.3.5",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@zumer/snapdom": "^1.9.14",
|
||||
"ahooks": "^3.9.6",
|
||||
"antd": "^5.28.1",
|
||||
"antd-style": "^3.7.1",
|
||||
@@ -228,7 +229,6 @@
|
||||
"marked": "^16.4.2",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"model-bank": "workspace:*",
|
||||
"modern-screenshot": "^4.6.6",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "^16.0.3",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
|
||||
@@ -17,16 +17,16 @@ export interface LobeAgentChatConfig {
|
||||
enableMaxTokens?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启流式输出
|
||||
* Whether to enable streaming output
|
||||
*/
|
||||
enableStreaming?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启推理
|
||||
* Whether to enable reasoning
|
||||
*/
|
||||
enableReasoning?: boolean;
|
||||
/**
|
||||
* 自定义推理强度
|
||||
* Custom reasoning effort level
|
||||
*/
|
||||
enableReasoningEffort?: boolean;
|
||||
reasoningBudgetToken?: number;
|
||||
@@ -34,25 +34,25 @@ export interface LobeAgentChatConfig {
|
||||
gpt5ReasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
|
||||
gpt5_1ReasoningEffort?: 'none' | 'low' | 'medium' | 'high';
|
||||
/**
|
||||
* 输出文本详细程度控制
|
||||
* Output text verbosity control
|
||||
*/
|
||||
textVerbosity?: 'low' | 'medium' | 'high';
|
||||
thinking?: 'disabled' | 'auto' | 'enabled';
|
||||
thinkingBudget?: number;
|
||||
/**
|
||||
* 禁用上下文缓存
|
||||
* Disable context caching
|
||||
*/
|
||||
disableContextCaching?: boolean;
|
||||
/**
|
||||
* 历史消息条数
|
||||
* Number of historical messages
|
||||
*/
|
||||
historyCount?: number;
|
||||
/**
|
||||
* 开启历史记录条数
|
||||
* Enable historical message count
|
||||
*/
|
||||
enableHistoryCount?: boolean;
|
||||
/**
|
||||
* 历史消息长度压缩阈值
|
||||
* Enable history message compression threshold
|
||||
*/
|
||||
enableCompressHistory?: boolean;
|
||||
|
||||
|
||||
@@ -56,12 +56,12 @@ export interface LobeDocument {
|
||||
source: string;
|
||||
|
||||
/**
|
||||
* 文档来源类型
|
||||
* Document source type
|
||||
*/
|
||||
sourceType: DocumentSourceType;
|
||||
|
||||
/**
|
||||
* 文档标题 (如果可用)。
|
||||
* Document title (if available)
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
@@ -168,12 +168,12 @@ export enum DocumentSourceType {
|
||||
API = 'api',
|
||||
|
||||
/**
|
||||
* 编辑器创建的文档
|
||||
* Document created in editor
|
||||
*/
|
||||
EDITOR = 'editor',
|
||||
|
||||
/**
|
||||
* 本地或上传的文件
|
||||
* Local or uploaded file
|
||||
*/
|
||||
FILE = 'file',
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
import type { ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
|
||||
export const ChatErrorType = {
|
||||
// ******* 业务错误语义 ******* //
|
||||
// ******* Business Error Semantics ******* //
|
||||
|
||||
InvalidAccessCode: 'InvalidAccessCode', // is in valid password
|
||||
InvalidClerkUser: 'InvalidClerkUser', // is not Clerk User
|
||||
FreePlanLimit: 'FreePlanLimit', // is not Clerk User
|
||||
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // 订阅用户超限
|
||||
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // 订阅 key 不匹配
|
||||
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // Subscription user limit exceeded
|
||||
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // Subscription key mismatch
|
||||
|
||||
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // 主持人决策失败
|
||||
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // Supervisor decision failed
|
||||
|
||||
InvalidUserKey: 'InvalidUserKey', // is not valid User key
|
||||
CreateMessageError: 'CreateMessageError',
|
||||
@@ -18,20 +18,20 @@ export const ChatErrorType = {
|
||||
* @deprecated
|
||||
*/
|
||||
NoOpenAIAPIKey: 'NoOpenAIAPIKey',
|
||||
OllamaServiceUnavailable: 'OllamaServiceUnavailable', // 未启动/检测到 Ollama 服务
|
||||
OllamaServiceUnavailable: 'OllamaServiceUnavailable', // Ollama service not started/detected
|
||||
PluginFailToTransformArguments: 'PluginFailToTransformArguments',
|
||||
UnknownChatFetchError: 'UnknownChatFetchError',
|
||||
SystemTimeNotMatchError: 'SystemTimeNotMatchError',
|
||||
|
||||
// ******* 客户端错误 ******* //
|
||||
// ******* Client Errors ******* //
|
||||
BadRequest: 400,
|
||||
Unauthorized: 401,
|
||||
Forbidden: 403,
|
||||
ContentNotFound: 404, // 没找到接口
|
||||
MethodNotAllowed: 405, // 不支持
|
||||
ContentNotFound: 404, // Endpoint not found
|
||||
MethodNotAllowed: 405, // Method not supported
|
||||
TooManyRequests: 429,
|
||||
|
||||
// ******* 服务端错误 ******* //InvalidPluginArgumentsTransform
|
||||
// ******* Server Errors ******* //InvalidPluginArgumentsTransform
|
||||
InternalServerError: 500,
|
||||
BadGateway: 502,
|
||||
ServiceUnavailable: 503,
|
||||
|
||||
@@ -84,7 +84,7 @@ export const HotkeyGroupEnum = {
|
||||
export const HotkeyScopeEnum = {
|
||||
Chat: 'chat',
|
||||
Files: 'files',
|
||||
// 默认全局注册的快捷键 scope
|
||||
// Default globally registered hotkey scope
|
||||
// https://react-hotkeys-hook.vercel.app/docs/documentation/hotkeys-provider
|
||||
Global: 'global',
|
||||
|
||||
@@ -96,13 +96,13 @@ export type HotkeyGroupId = (typeof HotkeyGroupEnum)[keyof typeof HotkeyGroupEnu
|
||||
export type HotkeyScopeId = (typeof HotkeyScopeEnum)[keyof typeof HotkeyScopeEnum];
|
||||
|
||||
export interface HotkeyItem {
|
||||
// 快捷键分组用于展示
|
||||
// Hotkey grouping for display purposes
|
||||
group: HotkeyGroupId;
|
||||
id: HotkeyId;
|
||||
keys: string;
|
||||
// 是否为不可编辑的快捷键
|
||||
// Whether the hotkey is non-editable
|
||||
nonEditable?: boolean;
|
||||
// 快捷键作用域
|
||||
// Hotkey scope
|
||||
scopes?: HotkeyScopeId[];
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export interface DesktopHotkeyItem {
|
||||
id: DesktopHotkeyId;
|
||||
|
||||
keys: string;
|
||||
// 是否为不可编辑的快捷键
|
||||
// Whether the hotkey is non-editable
|
||||
nonEditable?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ export interface ImportMessage {
|
||||
createdAt: number;
|
||||
error?: ChatMessageError;
|
||||
|
||||
// 扩展字段
|
||||
// Extended fields
|
||||
extra?: {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
// 翻译
|
||||
// Translation
|
||||
translate?: ChatTranslate | false | null;
|
||||
// TTS
|
||||
tts?: ChatTTS;
|
||||
|
||||
@@ -109,8 +109,8 @@ export interface MessageMetadata extends ModelUsage, ModelPerformance {
|
||||
activeBranchIndex?: number;
|
||||
activeColumn?: boolean;
|
||||
/**
|
||||
* 消息折叠状态
|
||||
* true: 折叠, false/undefined: 展开
|
||||
* Message collapse state
|
||||
* true: collapsed, false/undefined: expanded
|
||||
*/
|
||||
collapsed?: boolean;
|
||||
compare?: boolean;
|
||||
|
||||
@@ -112,7 +112,7 @@ export const ChatToolPayloadSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* 聊天消息错误对象
|
||||
* Chat message error object
|
||||
*/
|
||||
export interface ChatMessagePluginError {
|
||||
body?: any;
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
export const LobeMetaDataSchema = z.object({
|
||||
/**
|
||||
* 角色头像
|
||||
* Character avatar
|
||||
*/
|
||||
avatar: z.string().optional(),
|
||||
/**
|
||||
* 背景色
|
||||
* Background color
|
||||
*/
|
||||
backgroundColor: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
@@ -17,7 +17,7 @@ export const LobeMetaDataSchema = z.object({
|
||||
|
||||
tags: z.array(z.string()).optional(),
|
||||
/**
|
||||
* 名称
|
||||
* Name
|
||||
*/
|
||||
title: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BaseDataModel } from '../meta';
|
||||
|
||||
// 类型定义
|
||||
// Type definitions
|
||||
export type TimeGroupId =
|
||||
| 'today'
|
||||
| 'yesterday'
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface CrawlErrorResult {
|
||||
}
|
||||
|
||||
export interface FilterOptions {
|
||||
// 是否启用Readability
|
||||
// Whether to enable Readability
|
||||
enableReadability?: boolean;
|
||||
|
||||
pureText?: boolean;
|
||||
@@ -34,12 +34,12 @@ export type CrawlImpl<Params = object> = (
|
||||
) => Promise<CrawlSuccessResult | undefined>;
|
||||
|
||||
export interface CrawlUrlRule {
|
||||
// 内容过滤配置(可选)
|
||||
// Content filtering configuration (optional)
|
||||
filterOptions?: FilterOptions;
|
||||
impls?: CrawlImplType[];
|
||||
// URL匹配模式,仅支持正则表达式
|
||||
// URL matching pattern, only supports regular expressions
|
||||
urlPattern: string;
|
||||
// URL转换模板(可选),如果提供则进行URL转换
|
||||
// URL transformation template (optional), performs URL conversion if provided
|
||||
urlTransform?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { copyImageToClipboard, sanitizeSVGContent } from '@lobechat/utils/client';
|
||||
import { Button, Dropdown, Tooltip } from '@lobehub/ui';
|
||||
import { snapdom } from '@zumer/snapdom';
|
||||
import { App, Space } from 'antd';
|
||||
import { css, cx } from 'antd-style';
|
||||
import { CopyIcon, DownloadIcon } from 'lucide-react';
|
||||
import { domToPng } from 'modern-screenshot';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
@@ -41,12 +41,29 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
|
||||
const sanitizedContent = useMemo(() => sanitizeSVGContent(content), [content]);
|
||||
|
||||
const generatePng = async () => {
|
||||
return domToPng(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
const blob = await snapdom.toBlob(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
|
||||
scale: 2,
|
||||
type: 'png',
|
||||
});
|
||||
|
||||
if (!blob) {
|
||||
throw new Error('Failed to generate PNG blob');
|
||||
}
|
||||
|
||||
// Convert blob to data URL
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('FileReader result is not a string'));
|
||||
}
|
||||
});
|
||||
reader.addEventListener('error', () =>
|
||||
reject(reader.error || new Error('Failed to read blob as data URL')),
|
||||
);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useStyles } from './style';
|
||||
import { FieldType } from './type';
|
||||
|
||||
const Preview = memo<FieldType & { title?: string }>(
|
||||
({ title, withSystemRole, withBackground, withFooter }) => {
|
||||
({ title, withSystemRole, withBackground, withFooter, widthMode }) => {
|
||||
const [model, plugins, systemRole] = useAgentStore((s) => [
|
||||
agentSelectors.currentAgentModel(s),
|
||||
agentSelectors.displayableAgentPlugins(s),
|
||||
@@ -34,7 +34,7 @@ const Preview = memo<FieldType & { title?: string }>(
|
||||
|
||||
const { t } = useTranslation('chat');
|
||||
const { styles } = useStyles(withBackground);
|
||||
const { styles: containerStyles } = useContainerStyles();
|
||||
const { styles: containerStyles } = useContainerStyles(widthMode);
|
||||
|
||||
const displayTitle = isInbox ? t('inbox.title') : title;
|
||||
const displayDesc = isInbox ? t('inbox.desc') : description;
|
||||
|
||||
@@ -14,10 +14,11 @@ import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
|
||||
import { useStyles } from '../style';
|
||||
import Preview from './Preview';
|
||||
import { FieldType } from './type';
|
||||
import { FieldType, WidthMode } from './type';
|
||||
|
||||
const DEFAULT_FIELD_VALUE: FieldType = {
|
||||
imageType: ImageType.JPG,
|
||||
widthMode: WidthMode.Wide,
|
||||
withBackground: true,
|
||||
withFooter: true,
|
||||
withPluginInfo: false,
|
||||
@@ -34,7 +35,20 @@ const ShareImage = memo<{ mobile?: boolean }>(() => {
|
||||
title: currentAgentTitle,
|
||||
});
|
||||
const { loading: copyLoading, onCopy } = useImgToClipboard();
|
||||
|
||||
const widthModeOptions = [
|
||||
{ label: t('shareModal.widthMode.wide'), value: WidthMode.Wide },
|
||||
{ label: t('shareModal.widthMode.narrow'), value: WidthMode.Narrow },
|
||||
];
|
||||
|
||||
const settings: FormItemProps[] = [
|
||||
{
|
||||
children: <Segmented options={widthModeOptions} />,
|
||||
label: t('shareModal.widthMode.label'),
|
||||
layout: 'horizontal',
|
||||
minWidth: undefined,
|
||||
name: 'widthMode',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.withSystemRole'),
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ImageType } from '@/hooks/useScreenshot';
|
||||
|
||||
export enum WidthMode {
|
||||
Narrow = 'narrow',
|
||||
Wide = 'wide'
|
||||
}
|
||||
|
||||
export type FieldType = {
|
||||
imageType: ImageType;
|
||||
widthMode: WidthMode;
|
||||
withBackground: boolean;
|
||||
withFooter: boolean;
|
||||
withPluginInfo: boolean;
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useContainerStyles = createStyles(({ css, token, stylish, cx, responsive }) => ({
|
||||
preview: cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
overflow: hidden scroll;
|
||||
import { WidthMode } from './ShareImage/type';
|
||||
|
||||
width: 100%;
|
||||
max-height: 70dvh;
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
export const useContainerStyles = createStyles(
|
||||
({ css, token, stylish, cx, responsive }, widthMode?: WidthMode) => {
|
||||
const isNarrow = widthMode === WidthMode.Narrow;
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
return {
|
||||
preview: cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
overflow: hidden scroll;
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.react-pdf__Document *,
|
||||
.react-pdf__Page * {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
width: 100%;
|
||||
max-width: ${isNarrow ? '480px' : 'none'};
|
||||
max-height: 70dvh;
|
||||
margin: ${isNarrow ? '0 auto' : '0'};
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
background: ${token.colorBgLayout};
|
||||
|
||||
${responsive.mobile} {
|
||||
max-height: 40dvh;
|
||||
}
|
||||
`,
|
||||
),
|
||||
}));
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.react-pdf__Document *,
|
||||
.react-pdf__Page * {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
${responsive.mobile} {
|
||||
max-height: 40dvh;
|
||||
}
|
||||
`,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const useStyles = createStyles(({ responsive, token, css }) => ({
|
||||
body: css`
|
||||
|
||||
+44
-28
@@ -1,6 +1,6 @@
|
||||
import type { SegmentedProps } from '@lobehub/ui';
|
||||
import { snapdom } from '@zumer/snapdom';
|
||||
import dayjs from 'dayjs';
|
||||
import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
@@ -40,26 +40,6 @@ export const getImageUrl = async ({
|
||||
imageType: ImageType;
|
||||
width?: number;
|
||||
}) => {
|
||||
let screenshotFn: any;
|
||||
switch (imageType) {
|
||||
case ImageType.JPG: {
|
||||
screenshotFn = domToJpeg;
|
||||
break;
|
||||
}
|
||||
case ImageType.PNG: {
|
||||
screenshotFn = domToPng;
|
||||
break;
|
||||
}
|
||||
case ImageType.SVG: {
|
||||
screenshotFn = domToSvg;
|
||||
break;
|
||||
}
|
||||
case ImageType.WEBP: {
|
||||
screenshotFn = domToWebp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
||||
let copy: HTMLDivElement = dom;
|
||||
|
||||
@@ -69,18 +49,54 @@ export const getImageUrl = async ({
|
||||
document.body.append(copy);
|
||||
}
|
||||
|
||||
const dataUrl = await screenshotFn(width ? copy : dom, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
const baseOptions = {
|
||||
scale: 2,
|
||||
width,
|
||||
});
|
||||
};
|
||||
|
||||
let blob: Blob;
|
||||
|
||||
if (imageType === ImageType.SVG) {
|
||||
// For SVG, we need to use the full snapdom API to get the raw SVG string
|
||||
const result = await snapdom(width ? copy : dom, baseOptions);
|
||||
const svgString = result.toRaw();
|
||||
blob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
} else {
|
||||
// For raster formats, use toBlob directly with type option
|
||||
const blobType = (imageType === ImageType.JPG ? 'jpg' : imageType) as 'png' | 'jpg' | 'webp';
|
||||
const blobResult = await snapdom.toBlob(width ? copy : dom, {
|
||||
type: blobType,
|
||||
useProxy: 'https://proxy.corsfix.com/?',
|
||||
});
|
||||
|
||||
if (!blobResult) {
|
||||
throw new Error('Failed to generate blob from snapdom');
|
||||
}
|
||||
|
||||
blob = blobResult;
|
||||
}
|
||||
|
||||
if (width && copy) copy?.remove();
|
||||
|
||||
return dataUrl;
|
||||
if (!blob) {
|
||||
throw new Error('Blob is undefined');
|
||||
}
|
||||
|
||||
// Convert blob to data URL using FileReader
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('FileReader result is not a string'));
|
||||
}
|
||||
});
|
||||
reader.addEventListener('error', () =>
|
||||
reject(reader.error || new Error('Failed to read blob as data URL')),
|
||||
);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
export const useScreenshot = ({
|
||||
|
||||
@@ -361,6 +361,11 @@ export default {
|
||||
screenshot: '截图',
|
||||
settings: '导出设置',
|
||||
text: '文本',
|
||||
widthMode: {
|
||||
label: '宽度模式',
|
||||
narrow: '窄屏模式',
|
||||
wide: '宽屏模式',
|
||||
},
|
||||
withBackground: '包含背景图片',
|
||||
withFooter: '包含页脚',
|
||||
withPluginInfo: '包含插件信息',
|
||||
|
||||
Reference in New Issue
Block a user