feat: expand secure assistant workflows

Harden runtime execution and add local knowledge, Smart Heartbeat, usage visibility, responsive product surfaces, and cross-platform packaging support.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-08-02 10:04:59 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 6ef1795b81
commit b3fdf96962
82 changed files with 17608 additions and 825 deletions
+239 -2
View File
@@ -37,12 +37,15 @@ export const conversationSnapshotSchema = z
.array(
z
.object({
callId: z.string().max(256).optional(),
name: z.string().max(200),
state: z.enum([
'pending',
'running',
'completed',
'failed'
'failed',
'cancelled',
'interrupted'
]),
summary: z.string().max(2_000)
})
@@ -50,7 +53,34 @@ export const conversationSnapshotSchema = z
)
.max(100)
.optional(),
sources: z.array(z.string().max(8_192)).max(100).optional()
sources: z.array(z.string().max(8_192)).max(100).optional(),
sourceReferences: z
.array(
z
.object({
libraryId: assistantIdSchema,
libraryName: z.string().max(200),
documentId: assistantIdSchema,
documentName: z.string().max(500),
sourceName: z.string().max(500),
sourceLocation: z.string().max(4_096).optional(),
locator: z.string().max(1_000).optional(),
snippet: z.string().max(16_000),
rank: z.number().finite(),
retrievalChannels: z
.array(z.enum(['fts', 'vector', 'graph']))
.max(3)
.optional(),
evidenceIds: z
.array(assistantIdSchema)
.max(100)
.optional()
})
.strict()
)
.max(20)
.optional(),
artifactIds: z.array(assistantIdSchema).max(8).optional()
})
.strict()
)
@@ -106,6 +136,47 @@ export type AssistantTask = {
error?: string
}
export type ModelUsageCallInput = {
requestId: string
callId: string
runtime: string
provider: string
model: string
input: number
output: number
cacheRead: number
cacheWrite: number
}
export type TokenUsageRecord = {
requestId: string
projectId?: string
projectName?: string
conversationId?: string
conversationTitle?: string
runtime: string
provider: string
model: string
callCount: number
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens: number
}
export type TokenUsageSummary = {
totals: {
callCount: number
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens: number
}
records: TokenUsageRecord[]
}
export type AssistantArtifact = {
id: string
projectId?: string
@@ -160,6 +231,172 @@ export type AssistantSchedule = ScheduleCreateInput & {
updatedAt: string
}
export const heartbeatRecurrenceSchema = z.discriminatedUnion('type', [
z
.object({
type: z.literal('daily'),
localTime: z
.string()
.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/)
})
.strict(),
z
.object({
type: z.literal('weekly'),
localTime: z
.string()
.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/),
weekday: z.number().int().min(0).max(6)
})
.strict()
])
export const heartbeatCreateSchema = z
.object({
projectId: assistantIdSchema.optional(),
name: z.string().trim().min(1).max(120),
timezone: z.string().trim().min(1).max(100),
recurrence: heartbeatRecurrenceSchema,
enabled: z.boolean(),
lookbackHours: z.number().int().min(1).max(24 * 30),
retentionDays: z.number().int().min(1).max(365)
})
.strict()
export const heartbeatUpdateSchema = heartbeatCreateSchema
export const heartbeatListSchema = z
.object({
projectId: assistantIdSchema.optional()
})
.strict()
export const heartbeatHistorySchema = z
.object({
configId: assistantIdSchema.optional(),
limit: z.number().int().min(1).max(200).default(50)
})
.strict()
export const heartbeatIdSchema = z
.object({
id: assistantIdSchema
})
.strict()
export const heartbeatUpdateRequestSchema = z
.object({
id: assistantIdSchema,
config: heartbeatUpdateSchema
})
.strict()
export const heartbeatPauseSchema = z
.object({
id: assistantIdSchema,
paused: z.boolean()
})
.strict()
export const heartbeatRunNowSchema = z
.object({
id: assistantIdSchema,
idempotencyKey: z.string().trim().min(1).max(200)
})
.strict()
export const heartbeatSummaryOutputSchema = z
.object({
summary: z.string().trim().min(1).max(12_000),
highlights: z.array(z.string().trim().min(1).max(1_000)).max(20),
proposedMemories: z
.array(
z
.object({
scope: z.enum(['global', 'project']),
type: z.enum([
'preference',
'fact',
'summary',
'procedure'
]),
content: z.string().trim().min(1).max(8_000),
confidence: z.number().min(0).max(1),
salience: z.number().min(0).max(1)
})
.strict()
)
.max(10),
followUpTasks: z
.array(
z
.object({
title: z.string().trim().min(1).max(200),
instructions: z.string().trim().min(1).max(8_000)
})
.strict()
)
.max(10)
})
.strict()
export type HeartbeatRecurrence = z.infer<
typeof heartbeatRecurrenceSchema
>
export type HeartbeatCreateInput = z.infer<
typeof heartbeatCreateSchema
>
export type HeartbeatUpdateInput = z.infer<
typeof heartbeatUpdateSchema
>
export type HeartbeatSummaryOutput = z.infer<
typeof heartbeatSummaryOutputSchema
>
export type HeartbeatRunStatus =
| 'claimed'
| 'completed'
| 'failed'
| 'skipped'
export type AssistantHeartbeatConfig = HeartbeatCreateInput & {
id: string
nextRunAt: string
lastRunAt?: string
lastStatus?: HeartbeatRunStatus
createdAt: string
updatedAt: string
}
export type AssistantHeartbeatRun = {
id: string
configId: string
trigger: 'scheduled' | 'manual'
scheduledFor: string
status: HeartbeatRunStatus
attemptCount: number
nextAttemptAt?: string
startedAt?: string
completedAt?: string
error?: string
entryId?: string
createdAt: string
updatedAt: string
}
export type AssistantHeartbeatEntry = {
id: string
configId: string
runId: string
scheduledFor: string
summary: string
highlights: string[]
artifactId?: string
proposedMemoryIds: string[]
followUpTaskIds: string[]
createdAt: string
}
export const expertCreateSchema = z
.object({
name: z.string().trim().min(1).max(80),
+184 -4
View File
@@ -11,13 +11,19 @@ import {
type AssistantArtifact,
type AssistantMemory,
type AssistantSchedule,
type AssistantHeartbeatConfig,
type AssistantHeartbeatEntry,
type AssistantHeartbeatRun,
type AssistantExpert,
type AssistantTask,
type TokenUsageSummary,
type ConversationSnapshot,
type WorkspaceChanges,
type ProjectCreateInput,
type MemoryCreateInput,
type ScheduleCreateInput,
type HeartbeatCreateInput,
type HeartbeatUpdateInput,
type ExpertCreateInput
} from './assistant-contracts'
@@ -76,6 +82,17 @@ export const toolApprovalPolicySchema = z.enum([
])
export const continueModeSchema = z.enum(['chat', 'agent'])
export const runtimeSandboxModeSchema = z.enum(['off', 'auto', 'strict'])
export const modelProtocolSchema = z.enum([
'anthropic-messages',
'openai-chat-completions',
'openai-images-generations'
])
export const modelAuthenticationSchema = z.enum(['api-key', 'none'])
export type ModelProtocol = z.infer<typeof modelProtocolSchema>
export type ModelAuthentication = z.infer<
typeof modelAuthenticationSchema
>
export const defaultModelProfileId =
'00000000-0000-4000-8000-000000000001'
@@ -83,6 +100,8 @@ export const defaultRuntimeSettings = {
provider: 'auto',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
opencodeBaseUrl: '',
opencodeEmbedded: false,
opencodeBinaryPath: '',
@@ -90,6 +109,10 @@ export const defaultRuntimeSettings = {
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat',
runtimeSandboxMode: 'auto',
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl: 'http://127.0.0.1:11434',
knowledgeEmbeddingModel: 'nomic-embed-text',
workspacePath: '',
toolApproval: 'always'
} as const
@@ -153,6 +176,8 @@ const modelProfileInputSchema = z
.min(1)
.max(128)
.regex(/^[\w./:-]+$/, '模型名称包含不支持的字符'),
protocol: modelProtocolSchema,
authentication: modelAuthenticationSchema,
apiKey: modelApiKeyUpdateSchema
})
.strict()
@@ -177,6 +202,8 @@ export const runtimeSettingsInputSchema = z
.min(1)
.max(128)
.regex(/^[\w./:-]+$/, '模型名称包含不支持的字符'),
modelProtocol: modelProtocolSchema,
modelAuthentication: modelAuthenticationSchema,
opencodeBaseUrl: z.union([
z.literal(''),
z.string().url().max(2_048)
@@ -187,6 +214,15 @@ export const runtimeSettingsInputSchema = z
continueBinaryPath: runtimePathSchema,
continueConfigPath: runtimePathSchema,
continueMode: continueModeSchema,
runtimeSandboxMode: runtimeSandboxModeSchema,
knowledgeEmbeddingEnabled: z.boolean(),
knowledgeEmbeddingBaseUrl: z.string().url().max(2_048),
knowledgeEmbeddingModel: z
.string()
.trim()
.min(1)
.max(256)
.regex(/^[\w./:-]+$/, '向量模型名称包含不支持的字符'),
workspacePath: z.string().trim().min(1).max(4_096),
apiKey: modelApiKeyUpdateSchema,
modelProfiles: z.array(modelProfileInputSchema).min(1).max(20).optional(),
@@ -196,28 +232,58 @@ export const runtimeSettingsInputSchema = z
toolApproval: toolApprovalPolicySchema
}).strict()
.superRefine((settings, context) => {
if (
!settings.modelProfiles &&
settings.modelAuthentication === 'none' &&
settings.apiKey.action === 'replace'
) {
context.addIssue({
code: 'custom',
path: ['apiKey'],
message: '无认证模型连接不得配置 API Key'
})
}
const endpoints = settings.modelProfiles?.map((profile, index) => ({
path: ['modelProfiles', index, 'baseUrl'] as (string | number)[],
value: profile.baseUrl
})) ?? [{ path: ['modelBaseUrl'], value: settings.modelBaseUrl }]
for (const endpoint of endpoints) {
const url = new URL(endpoint.value)
const hostname = url.hostname.toLowerCase()
const loopback =
hostname === 'localhost' ||
hostname === '::1' ||
hostname === '[::1]' ||
/^127(?:\.\d{1,3}){3}$/u.test(hostname)
if (
url.protocol !== 'https:' ||
(url.protocol !== 'https:' &&
!(url.protocol === 'http:' && loopback)) ||
url.username ||
url.password ||
url.search ||
url.hash ||
(url.pathname !== '/' && url.pathname !== '')
url.hash
) {
context.addIssue({
code: 'custom',
path: endpoint.path,
message: '模型服务地址必须是无凭据和路径的 HTTPS origin'
message:
'模型服务地址必须使用 HTTPS;仅本机回环地址可使用 HTTP,且不得包含凭据、查询参数或片段'
})
}
}
if (settings.modelProfiles) {
for (const [index, profile] of settings.modelProfiles.entries()) {
if (
profile.authentication === 'none' &&
profile.apiKey.action === 'replace'
) {
context.addIssue({
code: 'custom',
path: ['modelProfiles', index, 'apiKey'],
message: '无认证模型连接不得配置 API Key'
})
}
}
const ids = new Set(settings.modelProfiles.map((profile) => profile.id))
const names = new Set(
settings.modelProfiles.map((profile) => profile.name.toLowerCase())
@@ -253,6 +319,39 @@ export const runtimeSettingsInputSchema = z
})
}
}
const opencodeSource = settings.opencodeModelSource
const opencodeProfile =
opencodeSource?.kind === 'profile'
? settings.modelProfiles.find(
(profile) => profile.id === opencodeSource.profileId
)
: undefined
if (
opencodeProfile &&
(opencodeProfile.protocol !== 'anthropic-messages' ||
opencodeProfile.authentication !== 'api-key')
) {
context.addIssue({
code: 'custom',
path: ['opencodeModelSource'],
message:
'OpenCode 独立模型连接仅支持需要 API Key 的 Anthropic Messages 协议'
})
}
const continueSource = settings.continueModelSource
const continueProfile =
continueSource?.kind === 'profile'
? settings.modelProfiles.find(
(profile) => profile.id === continueSource.profileId
)
: undefined
if (continueProfile?.protocol === 'openai-images-generations') {
context.addIssue({
code: 'custom',
path: ['continueModelSource'],
message: 'Continue 不支持图像生成模型连接'
})
}
}
if (settings.opencodeBaseUrl) {
const opencodeUrl = new URL(settings.opencodeBaseUrl)
@@ -271,6 +370,38 @@ export const runtimeSettingsInputSchema = z
})
}
}
const embeddingUrl = new URL(settings.knowledgeEmbeddingBaseUrl)
const embeddingHost = embeddingUrl.hostname.toLowerCase()
const privateIpv4 =
/^10(?:\.\d{1,3}){3}$/u.test(embeddingHost) ||
/^192\.168(?:\.\d{1,3}){2}$/u.test(embeddingHost) ||
/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/u.test(
embeddingHost
)
const loopback =
embeddingHost === 'localhost' ||
embeddingHost === '::1' ||
embeddingHost === '[::1]' ||
/^127(?:\.\d{1,3}){3}$/u.test(embeddingHost)
if (
(embeddingUrl.protocol !== 'https:' &&
!(
embeddingUrl.protocol === 'http:' &&
(loopback || privateIpv4)
)) ||
embeddingUrl.username ||
embeddingUrl.password ||
embeddingUrl.search ||
embeddingUrl.hash ||
(embeddingUrl.pathname !== '/' && embeddingUrl.pathname !== '')
) {
context.addIssue({
code: 'custom',
path: ['knowledgeEmbeddingBaseUrl'],
message:
'Ollama 向量地址必须使用 HTTPS,或使用本机/私有网络 HTTP origin,且不得包含凭据、路径、查询参数或片段'
})
}
})
export type RuntimeSettingsInput = z.infer<typeof runtimeSettingsInputSchema>
@@ -282,6 +413,8 @@ export type ModelConnectionSettings = {
name: string
baseUrl: string
modelName: string
protocol: ModelProtocol
authentication: ModelAuthentication
apiKeyConfigured: boolean
credentialSource: 'none' | 'encrypted' | 'environment'
}
@@ -290,6 +423,8 @@ export type RuntimeSettings = {
provider: RuntimeSettingsInput['provider']
modelBaseUrl: string
modelName: string
modelProtocol: ModelProtocol
modelAuthentication: ModelAuthentication
opencodeBaseUrl: string
opencodeEmbedded: boolean
opencodeBinaryPath: string
@@ -297,6 +432,10 @@ export type RuntimeSettings = {
continueBinaryPath: string
continueConfigPath: string
continueMode: RuntimeSettingsInput['continueMode']
runtimeSandboxMode: RuntimeSettingsInput['runtimeSandboxMode']
knowledgeEmbeddingEnabled: boolean
knowledgeEmbeddingBaseUrl: string
knowledgeEmbeddingModel: string
workspacePath: string
apiKeyConfigured: boolean
credentialSource: 'none' | 'encrypted' | 'environment'
@@ -323,6 +462,8 @@ export type AgentRuntimeStatus = {
label: string
available: boolean
detail: string
capability?: 'chat' | 'image-generation'
supportsToolExecution: boolean
}
export type RuntimeBinaryDetection =
@@ -367,6 +508,7 @@ export type AgentEvent =
| {
requestId: string
type: 'tool'
callId: string
name: string
state: 'pending' | 'running' | 'completed' | 'failed'
summary: string
@@ -381,6 +523,13 @@ export type AgentEvent =
argumentSummary?: string
allowPermanent?: boolean
}
| {
requestId: string
type: 'artifact'
artifactId: string
kind: 'image'
title: string
}
| {
requestId: string
type: 'done'
@@ -389,6 +538,7 @@ export type AgentEvent =
| {
requestId: string
type: 'error'
status: 'failed' | 'cancelled'
message: string
}
@@ -531,6 +681,8 @@ export type KnowledgeSearchReference = {
locator?: string
snippet: string
rank: number
retrievalChannels?: Array<'fts' | 'vector' | 'graph'>
evidenceIds?: string[]
}
export type DesktopApi = {
@@ -538,6 +690,7 @@ export type DesktopApi = {
getInfo: () => Promise<AppInfo>
show: () => Promise<void>
hide: () => Promise<void>
clearLocalData: () => Promise<void>
onNewConversation: (listener: () => void) => () => void
onOpenSettings: (listener: () => void) => () => void
}
@@ -579,9 +732,17 @@ export type DesktopApi = {
}
tasks: {
list: () => Promise<AssistantTask[]>
setStatus: (
taskId: string,
status: Extract<AssistantTask['status'], 'completed' | 'cancelled'>
) => Promise<void>
}
usage: {
getTokenSummary: () => Promise<TokenUsageSummary>
}
artifacts: {
list: (projectId?: string) => Promise<AssistantArtifact[]>
get: (artifactId: string) => Promise<AssistantArtifact>
importFiles: (projectId?: string) => Promise<AssistantArtifact[]>
}
memory: {
@@ -600,6 +761,25 @@ export type DesktopApi = {
remove: (scheduleId: string) => Promise<void>
runNow: (scheduleId: string) => Promise<void>
}
heartbeats: {
list: (projectId?: string) => Promise<AssistantHeartbeatConfig[]>
create: (
input: HeartbeatCreateInput
) => Promise<AssistantHeartbeatConfig>
update: (
heartbeatId: string,
input: HeartbeatUpdateInput
) => Promise<AssistantHeartbeatConfig>
setPaused: (heartbeatId: string, paused: boolean) => Promise<void>
remove: (heartbeatId: string) => Promise<void>
runNow: (heartbeatId: string) => Promise<AssistantHeartbeatRun>
history: (
heartbeatId?: string
) => Promise<{
runs: AssistantHeartbeatRun[]
entries: AssistantHeartbeatEntry[]
}>
}
experts: {
list: () => Promise<AssistantExpert[]>
create: (input: ExpertCreateInput) => Promise<AssistantExpert>
+11
View File
@@ -2,6 +2,7 @@ export const ipcChannels = {
appInfo: 'app:get-info',
appShow: 'app:show',
appHide: 'app:hide',
appClearLocalData: 'app:clear-local-data',
conversationNew: 'conversation:new',
settingsOpen: 'settings:open',
agentStatus: 'agent:get-status',
@@ -23,7 +24,10 @@ export const ipcChannels = {
conversationsReplace: 'conversations:replace',
workspaceChangesGet: 'workspace:changes:get',
tasksList: 'tasks:list',
tasksSetStatus: 'tasks:set-status',
tokenUsageSummary: 'usage:token-summary',
artifactsList: 'artifacts:list',
artifactsGet: 'artifacts:get',
artifactsImportFiles: 'artifacts:import-files',
memoryList: 'memory:list',
memoryCreate: 'memory:create',
@@ -34,6 +38,13 @@ export const ipcChannels = {
schedulesSetEnabled: 'schedules:set-enabled',
schedulesRemove: 'schedules:remove',
schedulesRunNow: 'schedules:run-now',
heartbeatsList: 'heartbeats:list',
heartbeatsCreate: 'heartbeats:create',
heartbeatsUpdate: 'heartbeats:update',
heartbeatsSetPaused: 'heartbeats:set-paused',
heartbeatsRemove: 'heartbeats:remove',
heartbeatsRunNow: 'heartbeats:run-now',
heartbeatsHistory: 'heartbeats:history',
expertsList: 'experts:list',
expertsCreate: 'experts:create',
capabilitiesSnapshot: 'capabilities:snapshot',
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { modelProfilePresets } from './model-presets'
describe('modelProfilePresets', () => {
it('includes domestic, local, and generic protocol presets', () => {
expect(modelProfilePresets.map((preset) => preset.id)).toEqual(
expect.arrayContaining([
'bigtoken-gpt-image-2',
'deepseek',
'qwen',
'glm',
'kimi',
'minimax',
'siliconflow',
'volcengine-ark',
'hunyuan-deployment',
'huawei-deployment',
'ollama',
'openai-compatible',
'anthropic-compatible'
])
)
expect(
modelProfilePresets.find((preset) => preset.id === 'ollama')
).toMatchObject({
baseUrl: 'http://127.0.0.1:11434/v1',
protocol: 'openai-chat-completions',
authentication: 'none'
})
expect(
modelProfilePresets.find(
(preset) => preset.id === 'bigtoken-gpt-image-2'
)
).toMatchObject({
baseUrl: 'https://bigtoken.ai/v1',
modelName: 'gpt-image-2',
protocol: 'openai-images-generations',
authentication: 'api-key'
})
})
it('does not invent universal Hunyuan or Huawei endpoints', () => {
for (const id of ['hunyuan-deployment', 'huawei-deployment']) {
expect(
modelProfilePresets.find((preset) => preset.id === id)
).toMatchObject({
baseUrl: '',
requiresDeploymentUrl: true
})
}
})
})
+148
View File
@@ -0,0 +1,148 @@
import type {
ModelAuthentication,
ModelProtocol
} from './contracts'
export type ModelProfilePreset = {
id: string
name: string
description: string
baseUrl: string
modelName: string
protocol: ModelProtocol
authentication: ModelAuthentication
requiresDeploymentUrl?: boolean
}
export const modelProfilePresets = [
{
id: 'bigtoken-gpt-image-2',
name: 'BigToken GPT Image 2',
description: 'BigToken 图像生成接口,生成结果直接显示在会话中',
baseUrl: 'https://bigtoken.ai/v1',
modelName: 'gpt-image-2',
protocol: 'openai-images-generations',
authentication: 'api-key'
},
{
id: 'deepseek',
name: 'DeepSeek',
description: 'DeepSeek 官方 OpenAI 兼容接口',
baseUrl: 'https://api.deepseek.com/v1',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'qwen',
name: 'QwenDashScope',
description: '阿里云百炼 DashScope OpenAI 兼容接口',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
modelName: 'qwen-plus',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'glm',
name: 'GLM(智谱)',
description: '智谱 AI OpenAI 兼容接口',
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
modelName: 'glm-4.5',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'kimi',
name: 'Kimi(月之暗面)',
description: 'Moonshot OpenAI 兼容接口',
baseUrl: 'https://api.moonshot.cn/v1',
modelName: 'moonshot-v1-8k',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'minimax',
name: 'MiniMax',
description: 'MiniMax 国内 OpenAI 兼容接口',
baseUrl: 'https://api.minimaxi.com/v1',
modelName: 'MiniMax-M2.1',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'siliconflow',
name: 'SiliconFlow(硅基流动)',
description: 'SiliconFlow OpenAI 兼容接口',
baseUrl: 'https://api.siliconflow.cn/v1',
modelName: 'deepseek-ai/DeepSeek-V3.2',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'volcengine-ark',
name: '火山引擎方舟',
description: '方舟 OpenAI 兼容接口;模型填写推理接入点 ID',
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
modelName: 'ep-your-endpoint-id',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'hunyuan-deployment',
name: '腾讯混元(自定义部署)',
description: '填写部署文档提供的专属 API Root 和模型或部署 ID',
baseUrl: '',
modelName: 'deployment-id',
protocol: 'openai-chat-completions',
authentication: 'api-key',
requiresDeploymentUrl: true
},
{
id: 'huawei-deployment',
name: '华为云模型(自定义部署)',
description: '填写部署所在区域提供的专属 API Root 和部署 ID',
baseUrl: '',
modelName: 'deployment-id',
protocol: 'openai-chat-completions',
authentication: 'api-key',
requiresDeploymentUrl: true
},
{
id: 'ollama',
name: 'Ollama(本机)',
description: '本机 Ollama OpenAI 兼容接口,无需 API Key',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'llama3.2',
protocol: 'openai-chat-completions',
authentication: 'none'
},
{
id: 'openai',
name: 'OpenAI',
description: 'OpenAI Chat Completions 接口',
baseUrl: 'https://api.openai.com/v1',
modelName: 'gpt-4.1',
protocol: 'openai-chat-completions',
authentication: 'api-key'
},
{
id: 'openai-compatible',
name: 'OpenAI 兼容(自定义)',
description: '填写服务商提供的 API Root 和模型名称',
baseUrl: '',
modelName: 'model-name',
protocol: 'openai-chat-completions',
authentication: 'api-key',
requiresDeploymentUrl: true
},
{
id: 'anthropic-compatible',
name: 'Anthropic Messages 兼容(自定义)',
description: '填写服务商提供的 API Root 和模型名称',
baseUrl: '',
modelName: 'model-name',
protocol: 'anthropic-messages',
authentication: 'api-key',
requiresDeploymentUrl: true
}
] as const satisfies readonly ModelProfilePreset[]