Compare commits

...

3 Commits

Author SHA1 Message Date
suyao
e845d8194a refactor(models.ts): update model definitions and improve seed handling 2025-06-17 13:54:11 +08:00
koinin
68c1a3e1cc Update models.ts, fix doubao-seed-1-6 (#7274)
* Update models.ts, fix doubao-seed-1-6

* fix2 doubao-seed-1-6
2025-06-17 12:59:18 +08:00
George·Dong
8459e53e39 fix(MessageMenubar): add "copy plain text" control (#7261)
* fix(MessageMenubar): add "copy plain text" control

* fix(migrate): add default plain_text export option in v114
2025-06-17 12:43:36 +08:00
11 changed files with 261 additions and 10 deletions

View File

@@ -2863,10 +2863,10 @@ export const findTokenLimit = (modelId: string): { min: number; max: number } |
// Doubao 支持思考模式的模型正则
export const DOUBAO_THINKING_MODEL_REGEX =
/doubao-(?:1(\.|-5)-thinking-vision-pro|1(\.|-)5-thinking-pro-m|seed-1\.6|seed-1\.6-flash)(?:-[\\w-]+)?/i
/doubao-(?:1[.-]5-thinking-vision-pro|1[.-]5-thinking-pro-m|seed-1[.-]6(?:-flash)?)(?:-[\w-]+)?/i
// 支持 auto 的 Doubao 模型
export const DOUBAO_THINKING_AUTO_MODEL_REGEX = /doubao-(?:1-5-thinking-pro-m|seed-1.6)(?:-[\\w-]+)?/i
// 支持 auto 的 Doubao 模型 doubao-seed-1.6-xxx doubao-seed-1-6-xxx doubao-1-5-thinking-pro-m-xxx
export const DOUBAO_THINKING_AUTO_MODEL_REGEX = /doubao-(1-5-thinking-pro-m|seed-1\.6|seed-1-6-[\w-]+)(?:-[\w-]+)*/i
export function isDoubaoThinkingAutoModel(model: Model): boolean {
return DOUBAO_THINKING_AUTO_MODEL_REGEX.test(model.id)

View File

@@ -1104,7 +1104,8 @@
"obsidian": "Export to Obsidian",
"siyuan": "Export to SiYuan Note",
"joplin": "Export to Joplin",
"docx": "Export as Word"
"docx": "Export as Word",
"plain_text": "Copy as Plain Text"
},
"joplin": {
"check": {

View File

@@ -1102,7 +1102,8 @@
"obsidian": "Obsidianにエクスポート",
"siyuan": "思源ノートにエクスポート",
"joplin": "Joplinにエクスポート",
"docx": "Wordとしてエクスポート"
"docx": "Wordとしてエクスポート",
"plain_text": "プレーンテキストとしてコピー"
},
"joplin": {
"check": {

View File

@@ -1101,7 +1101,8 @@
"obsidian": "Экспорт в Obsidian",
"siyuan": "Экспорт в SiYuan Note",
"joplin": "Экспорт в Joplin",
"docx": "Экспорт в Word"
"docx": "Экспорт в Word",
"plain_text": "Копировать как чистый текст"
},
"joplin": {
"check": {

View File

@@ -1104,7 +1104,8 @@
"obsidian": "导出到Obsidian",
"siyuan": "导出到思源笔记",
"joplin": "导出到Joplin",
"docx": "导出为Word"
"docx": "导出为Word",
"plain_text": "复制为纯文本"
},
"joplin": {
"check": {

View File

@@ -1104,7 +1104,8 @@
"obsidian": "匯出到Obsidian",
"siyuan": "匯出到思源筆記",
"joplin": "匯出到Joplin",
"docx": "匯出為Word"
"docx": "匯出為Word",
"plain_text": "複製為純文本"
},
"joplin": {
"check": {

View File

@@ -205,7 +205,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'export',
icon: <Share size={16} color="var(--color-icon)" style={{ marginTop: 3 }} />,
children: [
{
exportMenuOptions.plain_text && {
label: t('chat.topics.copy.plain_text'),
key: 'copy_message_plain_text',
onClick: () => copyMessageAsPlainText(message)

View File

@@ -84,6 +84,16 @@ const ExportMenuOptions: FC = () => {
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
<Switch checked={exportMenuOptions.docx} onChange={(checked) => handleToggleOption('docx', checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.export_menu.plain_text')}</SettingRowTitle>
<Switch
checked={exportMenuOptions.plain_text}
onChange={(checked) => handleToggleOption('plain_text', checked)}
/>
</SettingRow>
</SettingGroup>
)
}

View File

@@ -1596,6 +1596,18 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'114': (state: RootState) => {
try {
if (state.settings && state.settings.exportMenuOptions) {
if (typeof state.settings.exportMenuOptions.plain_text === 'undefined') {
state.settings.exportMenuOptions.plain_text = true
}
}
return state
} catch (error) {
return state
}
}
}

View File

@@ -167,6 +167,7 @@ export interface SettingsState {
obsidian: boolean
siyuan: boolean
docx: boolean
plain_text: boolean
}
// OpenAI
openAI: {
@@ -308,7 +309,8 @@ export const initialState: SettingsState = {
joplin: true,
obsidian: true,
siyuan: true,
docx: true
docx: true,
plain_text: true
},
// OpenAI
openAI: {

View File

@@ -0,0 +1,222 @@
import { z } from 'zod'
export const InputType = z.enum(['text', 'image', 'audio', 'video', 'document'])
export type InputType = z.infer<typeof InputType>
export const OutputType = z.enum(['text', 'image', 'audio', 'video', 'vector'])
export type OutputType = z.infer<typeof OutputType>
export const OutputMode = z.enum(['sync', 'streaming'])
export type OutputMode = z.infer<typeof OutputMode>
export const ModelCapability = z.enum([
'audioGeneration',
'cache',
'codeExecution',
'embedding',
'fineTuning',
'imageGeneration',
'OCR',
'realTime',
'rerank',
'reasoning',
'streaming',
'structuredOutput',
'textGeneration',
'translation',
'transcription',
'toolUse',
'videoGeneration',
'webSearch'
])
export type ModelCapability = z.infer<typeof ModelCapability>
export const ModelSchema = z
.object({
id: z.string(),
modelId: z.string(),
providerId: z.string(),
name: z.string(),
group: z.string(),
description: z.string().optional(),
owned_by: z.string().optional(),
supportedInputs: z.array(InputType),
supportedOutputs: z.array(OutputType),
supportedOutputModes: z.array(OutputMode),
limits: z
.object({
inputTokenLimit: z.number().optional(),
outputTokenLimit: z.number().optional(),
contextWindow: z.number().optional()
})
.optional(),
price: z
.object({
inputTokenPrice: z.number().optional(),
outputTokenPrice: z.number().optional()
})
.optional(),
capabilities: z.array(ModelCapability)
})
.refine(
(data) => {
// 如果模型支持streaming则必须支持streamingOutputMode
if (data.capabilities.includes('streaming') && !data.supportedOutputModes.includes('streaming')) {
return false
}
// 如果模型有OCR能力则必须支持图像输入类型或者文件输入类型
if (
data.capabilities.includes('OCR') &&
!data.supportedInputs.includes('image') &&
!data.supportedInputs.includes('document')
) {
return false
}
// 如果模型有图像生成能力,则必须支持图像输出
if (data.capabilities.includes('imageGeneration') && !data.supportedOutputs.includes('image')) {
return false
}
// 如果有音频生成能力,则必须支持音频输出类型
if (data.capabilities.includes('audioGeneration') && !data.supportedOutputs.includes('audio')) {
return false
}
// 如果有音频识别能力,则必须支持音频输入类型
if (
(data.capabilities.includes('transcription') || data.capabilities.includes('translation')) &&
!data.supportedInputs.includes('audio')
) {
return false
}
// 如果有视频生成能力,则必须支持视频输出类型
if (data.capabilities.includes('videoGeneration') && !data.supportedOutputs.includes('video')) {
return false
}
// 如果模型有embedding能力则必须支持向量输出类型
if (data.capabilities.includes('embedding') && !data.supportedOutputs.includes('vector')) {
return false
}
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, imageGeneration, audioGeneration, videoGeneration webSearch能力则必须支持文字的输入
if (
(data.capabilities.includes('toolUse') ||
data.capabilities.includes('reasoning') ||
data.capabilities.includes('streaming') ||
data.capabilities.includes('cache') ||
data.capabilities.includes('codeExecution') ||
data.capabilities.includes('imageGeneration') ||
data.capabilities.includes('audioGeneration') ||
data.capabilities.includes('videoGeneration') ||
data.capabilities.includes('webSearch')) &&
!data.supportedInputs.includes('text')
) {
return false
}
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, OCR, textGeneration, translation, transcription, webSearch, structuredOutput能力则必须支持文字的输出
if (
(data.capabilities.includes('toolUse') ||
data.capabilities.includes('reasoning') ||
data.capabilities.includes('streaming') ||
data.capabilities.includes('cache') ||
data.capabilities.includes('codeExecution') ||
data.capabilities.includes('OCR') ||
data.capabilities.includes('textGeneration') ||
data.capabilities.includes('translation') ||
data.capabilities.includes('transcription') ||
data.capabilities.includes('webSearch') ||
data.capabilities.includes('structuredOutput')) &&
!data.supportedOutputs.includes('text')
) {
return false
}
return true
},
{
message: 'ModelCard has inconsistent capabilities and supported input/output type'
}
)
export type ModelCard = z.infer<typeof ModelSchema>
export function createModelCard(model: ModelCard): ModelCard {
return ModelSchema.parse(model)
}
export function supportesInputType(model: ModelCard, inputType: InputType) {
return model.supportedInputs.includes(inputType)
}
export function supportesOutputType(model: ModelCard, outputType: OutputType) {
return model.supportedOutputs.includes(outputType)
}
export function supportesOutputMode(model: ModelCard, outputMode: OutputMode) {
return model.supportedOutputModes.includes(outputMode)
}
export function supportesCapability(model: ModelCard, capability: ModelCapability) {
return model.capabilities.includes(capability)
}
export function isVisionModel(model: ModelCard) {
return supportesInputType(model, 'image')
}
export function isImageGenerationModel(model: ModelCard) {
return isVisionModel(model) && supportesCapability(model, 'imageGeneration')
}
export function isAudioModel(model: ModelCard) {
return supportesInputType(model, 'audio')
}
export function isAudioGenerationModel(model: ModelCard) {
return supportesCapability(model, 'audioGeneration')
}
export function isVideoModel(model: ModelCard) {
return supportesInputType(model, 'video')
}
export function isEmbedModel(model: ModelCard) {
return supportesOutputType(model, 'vector') && supportesCapability(model, 'embedding')
}
export function isTextEmbeddingModel(model: ModelCard) {
return isEmbedModel(model) && supportesInputType(model, 'text') && model.supportedInputs.length === 1
}
export function isMultiModalEmbeddingModel(model: ModelCard) {
return isEmbedModel(model) && model.supportedInputs.length > 1
}
export function isRerankModel(model: ModelCard) {
return supportesCapability(model, 'rerank')
}
export function isReasoningModel(model: ModelCard) {
return supportesCapability(model, 'reasoning')
}
export function isToolUseModel(model: ModelCard) {
return supportesCapability(model, 'toolUse')
}
export function isOnlyStreamingModel(model: ModelCard) {
return (
supportesCapability(model, 'streaming') &&
supportesOutputMode(model, 'streaming') &&
model.supportedOutputModes.length === 1
)
}