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:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
6ef1795b81
commit
b3fdf96962
@@ -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
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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: 'Qwen(DashScope)',
|
||||
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[]
|
||||
Reference in New Issue
Block a user