feat: add computer control and managed browser
This commit is contained in:
@@ -2,6 +2,16 @@ import { z } from 'zod'
|
||||
|
||||
export const assistantIdSchema = z.string().uuid()
|
||||
export const workModeSchema = z.enum(['ask', 'plan', 'execute'])
|
||||
export const interactiveWorkModes = ['ask', 'execute'] as const
|
||||
|
||||
export type WorkMode = z.infer<typeof workModeSchema>
|
||||
export type InteractiveWorkMode = (typeof interactiveWorkModes)[number]
|
||||
|
||||
export function normalizeInteractiveWorkMode(
|
||||
workMode: WorkMode | undefined
|
||||
): InteractiveWorkMode {
|
||||
return workMode === 'execute' ? 'execute' : 'ask'
|
||||
}
|
||||
|
||||
export const projectCreateSchema = z
|
||||
.object({
|
||||
@@ -14,7 +24,6 @@ export const projectCreateSchema = z
|
||||
|
||||
export const projectUpdateSchema = projectCreateSchema
|
||||
|
||||
export type WorkMode = z.infer<typeof workModeSchema>
|
||||
export type ProjectCreateInput = z.infer<typeof projectCreateSchema>
|
||||
|
||||
export const conversationSnapshotSchema = z
|
||||
@@ -44,6 +53,7 @@ export const conversationSnapshotSchema = z
|
||||
'running',
|
||||
'completed',
|
||||
'failed',
|
||||
'recoverable',
|
||||
'cancelled',
|
||||
'interrupted'
|
||||
]),
|
||||
@@ -433,6 +443,7 @@ export const expertCreateSchema = z
|
||||
.strict()
|
||||
|
||||
export type ExpertCreateInput = z.infer<typeof expertCreateSchema>
|
||||
export type ExpertUpdateInput = ExpertCreateInput
|
||||
|
||||
export type AssistantExpert = ExpertCreateInput & {
|
||||
id: string
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
export type BuiltinModelToolSummary = {
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
access: 'read' | 'write'
|
||||
}
|
||||
|
||||
export const builtinModelTools = [
|
||||
{
|
||||
name: 'workspace_read_text',
|
||||
displayName: '读取工作区文本',
|
||||
description: '读取当前工作区内不超过 256KB 的 UTF-8 文本文件。',
|
||||
access: 'read'
|
||||
},
|
||||
{
|
||||
name: 'workspace_list_directory',
|
||||
displayName: '列出工作区目录',
|
||||
description: '列出当前工作区内目录的直属内容,最多返回 200 项。',
|
||||
access: 'read'
|
||||
},
|
||||
{
|
||||
name: 'workspace_write_text',
|
||||
displayName: '写入工作区文本',
|
||||
description:
|
||||
'在当前工作区内新建或覆盖不超过 512KB 的 UTF-8 文本文件,父目录必须已存在。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_navigate',
|
||||
displayName: '浏览器导航',
|
||||
description: '在隔离浏览器中打开公开的 HTTP(S) 页面。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_snapshot',
|
||||
displayName: '读取浏览器快照',
|
||||
description: '读取当前页面的有界可访问性快照;可编辑值会被隐藏。',
|
||||
access: 'read'
|
||||
},
|
||||
{
|
||||
name: 'browser_click',
|
||||
displayName: '点击浏览器元素',
|
||||
description: '点击最近一次浏览器快照中的可见且未受保护元素。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_type',
|
||||
displayName: '输入浏览器文本',
|
||||
description: '向可编辑且未受保护的页面元素输入文本;不支持上传文件。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_select',
|
||||
displayName: '选择浏览器选项',
|
||||
description: '在最近一次快照标识的原生选择控件中选择值。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_back',
|
||||
displayName: '浏览器返回',
|
||||
description: '在隔离浏览器的历史记录中返回上一页。',
|
||||
access: 'write'
|
||||
},
|
||||
{
|
||||
name: 'browser_screenshot',
|
||||
displayName: '截取浏览器页面',
|
||||
description: '截取当前可见页面区域的有界 PNG 图片。',
|
||||
access: 'read'
|
||||
}
|
||||
] as const satisfies readonly BuiltinModelToolSummary[]
|
||||
@@ -80,6 +80,130 @@ export const skillSummarySchema = z
|
||||
.strict()
|
||||
export type SkillSummary = z.infer<typeof skillSummarySchema>
|
||||
|
||||
export const computerCapabilityIdSchema = z.enum([
|
||||
'host-browser-control',
|
||||
'linux-desktop-control'
|
||||
])
|
||||
export type ComputerCapabilityId = z.infer<
|
||||
typeof computerCapabilityIdSchema
|
||||
>
|
||||
|
||||
export const browserProfileIdSchema = z.string().uuid()
|
||||
export const browserProfileNameSchema = controlCharacterFreeString(80)
|
||||
|
||||
export const browserProfileCreateInputSchema = z
|
||||
.object({
|
||||
name: browserProfileNameSchema
|
||||
})
|
||||
.strict()
|
||||
export type BrowserProfileCreateInput = z.infer<
|
||||
typeof browserProfileCreateInputSchema
|
||||
>
|
||||
|
||||
export const browserProfileRenameInputSchema = z
|
||||
.object({
|
||||
profileId: browserProfileIdSchema,
|
||||
name: browserProfileNameSchema
|
||||
})
|
||||
.strict()
|
||||
export type BrowserProfileRenameInput = z.infer<
|
||||
typeof browserProfileRenameInputSchema
|
||||
>
|
||||
|
||||
export const browserProfileSelectionInputSchema = z
|
||||
.object({
|
||||
profileId: browserProfileIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const browserProfileSummarySchema = z
|
||||
.object({
|
||||
id: browserProfileIdSchema,
|
||||
name: browserProfileNameSchema,
|
||||
mode: z.literal('managed-isolated')
|
||||
})
|
||||
.strict()
|
||||
export type BrowserProfileSummary = z.infer<
|
||||
typeof browserProfileSummarySchema
|
||||
>
|
||||
|
||||
export const browserProfilesSummarySchema = z
|
||||
.object({
|
||||
profiles: z.array(browserProfileSummarySchema).max(32),
|
||||
defaultProfileId: browserProfileIdSchema.nullable()
|
||||
})
|
||||
.strict()
|
||||
export type BrowserProfilesSummary = z.infer<
|
||||
typeof browserProfilesSummarySchema
|
||||
>
|
||||
|
||||
export const computerCapabilityToggleInputSchema = z
|
||||
.object({
|
||||
capabilityId: computerCapabilityIdSchema,
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const computerCapabilityConfigInputSchema = z
|
||||
.object({
|
||||
capabilityId: computerCapabilityIdSchema,
|
||||
browserProfileId: browserProfileIdSchema.nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const computerCapabilityConfigSummarySchema = z
|
||||
.object({
|
||||
id: computerCapabilityIdSchema,
|
||||
name: z.string().min(1).max(80),
|
||||
description: z.string().min(1).max(500),
|
||||
enabled: z.boolean(),
|
||||
supported: z.boolean(),
|
||||
browserProfileId: browserProfileIdSchema.nullable(),
|
||||
riskSummary: z.string().min(1).max(500)
|
||||
})
|
||||
.strict()
|
||||
export type ComputerCapabilityConfigSummary = z.infer<
|
||||
typeof computerCapabilityConfigSummarySchema
|
||||
>
|
||||
|
||||
export const capabilityDiagnosticStatusSchema = z.enum([
|
||||
'available',
|
||||
'degraded',
|
||||
'unavailable',
|
||||
'disabled'
|
||||
])
|
||||
export type CapabilityDiagnosticStatus = z.infer<
|
||||
typeof capabilityDiagnosticStatusSchema
|
||||
>
|
||||
|
||||
export const capabilityDiagnosticCheckStatusSchema =
|
||||
capabilityDiagnosticStatusSchema.exclude(['disabled'])
|
||||
|
||||
export const capabilityDiagnosticCheckSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.regex(/^[a-z][a-z0-9-]*$/u),
|
||||
status: capabilityDiagnosticCheckStatusSchema,
|
||||
summary: z.string().min(1).max(240),
|
||||
remedy: z.string().min(1).max(400).optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const capabilityDiagnosticReportSchema = z
|
||||
.object({
|
||||
capabilityId: computerCapabilityIdSchema,
|
||||
status: capabilityDiagnosticStatusSchema,
|
||||
checkedAt: z.string().datetime(),
|
||||
checks: z.array(capabilityDiagnosticCheckSchema).max(16)
|
||||
})
|
||||
.strict()
|
||||
export type CapabilityDiagnosticReport = z.infer<
|
||||
typeof capabilityDiagnosticReportSchema
|
||||
>
|
||||
|
||||
export const mcpTransportSchema = z.enum(['stdio', 'http', 'sse'])
|
||||
export type McpTransport = z.infer<typeof mcpTransportSchema>
|
||||
|
||||
@@ -185,7 +309,12 @@ export type McpServerSummary = z.infer<typeof mcpServerSummarySchema>
|
||||
export const capabilitySnapshotSchema = z
|
||||
.object({
|
||||
skills: z.array(skillSummarySchema).max(256),
|
||||
mcpServers: z.array(mcpServerSummarySchema).max(64)
|
||||
mcpServers: z.array(mcpServerSummarySchema).max(64),
|
||||
computerCapabilities: z
|
||||
.array(computerCapabilityConfigSummarySchema)
|
||||
.max(2)
|
||||
.optional(),
|
||||
browserProfiles: browserProfilesSummarySchema.optional()
|
||||
})
|
||||
.strict()
|
||||
export type CapabilitySnapshot = z.infer<typeof capabilitySnapshotSchema>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
computerControlActionSchema,
|
||||
computerControlApprovalResultSchema,
|
||||
computerControlObservationSchema,
|
||||
computerControlRuntimeCommandSchema
|
||||
} from './computer-control-contracts'
|
||||
|
||||
const id = 'opaque_identifier_123456'
|
||||
|
||||
describe('computer control contracts', () => {
|
||||
it('accepts only the bounded semantic action vocabulary', () => {
|
||||
expect(
|
||||
computerControlActionSchema.parse({
|
||||
kind: 'replace_text',
|
||||
elementRef: id,
|
||||
text: 'hello'
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'replace_text',
|
||||
elementRef: id,
|
||||
text: 'hello'
|
||||
})
|
||||
|
||||
for (const action of [
|
||||
{ kind: 'click_at', x: 10, y: 20 },
|
||||
{ kind: 'key_chord', keys: ['CTRL', 'A'] },
|
||||
{ kind: 'launch_process', command: 'cmd.exe' },
|
||||
{ kind: 'clipboard_write', text: 'secret' },
|
||||
{ kind: 'open_file_picker', path: 'C:\\private.txt' }
|
||||
]) {
|
||||
expect(computerControlActionSchema.safeParse(action).success).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects unbounded and unknown command fields', () => {
|
||||
expect(
|
||||
computerControlRuntimeCommandSchema.safeParse({
|
||||
kind: 'act',
|
||||
commandId: id,
|
||||
leaseId: id,
|
||||
observationId: id,
|
||||
revision: 1,
|
||||
action: {
|
||||
kind: 'replace_text',
|
||||
elementRef: id,
|
||||
text: 'x'.repeat(4_097)
|
||||
}
|
||||
}).success
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
computerControlRuntimeCommandSchema.safeParse({
|
||||
kind: 'observe',
|
||||
commandId: id,
|
||||
leaseId: id,
|
||||
coordinates: [10, 20]
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('bounds observations and excludes element values', () => {
|
||||
const element = {
|
||||
ref: id,
|
||||
role: 'textbox',
|
||||
name: 'Search',
|
||||
enabled: true,
|
||||
focused: false,
|
||||
risk: 'input',
|
||||
blocked: false
|
||||
}
|
||||
expect(
|
||||
computerControlObservationSchema.safeParse({
|
||||
observationId: id,
|
||||
leaseId: id,
|
||||
revision: 1,
|
||||
capturedAt: 1,
|
||||
windowTitle: 'Window',
|
||||
elements: Array.from({ length: 201 }, () => element)
|
||||
}).success
|
||||
).toBe(false)
|
||||
expect(
|
||||
computerControlObservationSchema.safeParse({
|
||||
observationId: id,
|
||||
leaseId: id,
|
||||
revision: 1,
|
||||
capturedAt: 1,
|
||||
windowTitle: 'Window',
|
||||
elements: [{ ...element, value: 'password' }]
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('allows approvals only once and never as a broad grant', () => {
|
||||
expect(
|
||||
computerControlApprovalResultSchema.parse({
|
||||
approvalId: id,
|
||||
decision: 'approve_once'
|
||||
}).decision
|
||||
).toBe('approve_once')
|
||||
expect(
|
||||
computerControlApprovalResultSchema.safeParse({
|
||||
approvalId: id,
|
||||
decision: 'approve_session'
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,237 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const boundedText = (maximumLength: number) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(maximumLength)
|
||||
.refine(
|
||||
(value) =>
|
||||
[...value].every((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return code > 31 && code !== 127
|
||||
}),
|
||||
'值包含控制字符'
|
||||
)
|
||||
|
||||
const opaqueIdSchema = z
|
||||
.string()
|
||||
.min(16)
|
||||
.max(160)
|
||||
.regex(/^[A-Za-z0-9_-]+$/)
|
||||
|
||||
export const computerControlRiskSchema = z.enum([
|
||||
'observe',
|
||||
'navigate',
|
||||
'input',
|
||||
'commit',
|
||||
'forbidden'
|
||||
])
|
||||
export type ComputerControlRisk = z.infer<
|
||||
typeof computerControlRiskSchema
|
||||
>
|
||||
|
||||
export const computerControlElementRoleSchema = z.enum([
|
||||
'button',
|
||||
'link',
|
||||
'textbox',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'combobox',
|
||||
'option',
|
||||
'menuitem',
|
||||
'tab',
|
||||
'listitem',
|
||||
'scrollarea'
|
||||
])
|
||||
export type ComputerControlElementRole = z.infer<
|
||||
typeof computerControlElementRoleSchema
|
||||
>
|
||||
|
||||
export const computerControlElementSchema = z
|
||||
.object({
|
||||
ref: opaqueIdSchema,
|
||||
role: computerControlElementRoleSchema,
|
||||
name: boundedText(256),
|
||||
enabled: z.boolean(),
|
||||
focused: z.boolean(),
|
||||
risk: computerControlRiskSchema,
|
||||
blocked: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
export type ComputerControlElement = z.infer<
|
||||
typeof computerControlElementSchema
|
||||
>
|
||||
|
||||
export const computerControlObservationSchema = z
|
||||
.object({
|
||||
observationId: opaqueIdSchema,
|
||||
leaseId: opaqueIdSchema,
|
||||
revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
||||
capturedAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
||||
windowTitle: z.string().trim().max(256),
|
||||
elements: z.array(computerControlElementSchema).max(200)
|
||||
})
|
||||
.strict()
|
||||
export type ComputerControlObservation = z.infer<
|
||||
typeof computerControlObservationSchema
|
||||
>
|
||||
|
||||
export const computerControlActionSchema = z.discriminatedUnion('kind', [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('activate'),
|
||||
elementRef: opaqueIdSchema
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('replace_text'),
|
||||
elementRef: opaqueIdSchema,
|
||||
text: z.string().max(4_096)
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('select_option'),
|
||||
elementRef: opaqueIdSchema,
|
||||
optionName: boundedText(256)
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('scroll'),
|
||||
elementRef: opaqueIdSchema,
|
||||
direction: z.enum(['up', 'down', 'page_up', 'page_down'])
|
||||
})
|
||||
.strict()
|
||||
])
|
||||
export type ComputerControlAction = z.infer<
|
||||
typeof computerControlActionSchema
|
||||
>
|
||||
|
||||
const commandBase = {
|
||||
commandId: opaqueIdSchema,
|
||||
leaseId: opaqueIdSchema
|
||||
}
|
||||
|
||||
export const computerControlRuntimeCommandSchema = z.discriminatedUnion(
|
||||
'kind',
|
||||
[
|
||||
z
|
||||
.object({
|
||||
...commandBase,
|
||||
kind: z.literal('observe')
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
...commandBase,
|
||||
kind: z.literal('act'),
|
||||
observationId: opaqueIdSchema,
|
||||
revision: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.max(Number.MAX_SAFE_INTEGER),
|
||||
action: computerControlActionSchema
|
||||
})
|
||||
.strict()
|
||||
]
|
||||
)
|
||||
export type ComputerControlRuntimeCommand = z.infer<
|
||||
typeof computerControlRuntimeCommandSchema
|
||||
>
|
||||
|
||||
export const computerControlErrorCodeSchema = z.enum([
|
||||
'invalid_request',
|
||||
'driver_unavailable',
|
||||
'driver_timeout',
|
||||
'lease_not_found',
|
||||
'lease_expired',
|
||||
'lease_mismatch',
|
||||
'observation_not_found',
|
||||
'observation_stale',
|
||||
'observation_consumed',
|
||||
'element_not_found',
|
||||
'window_not_foreground',
|
||||
'element_identity_changed',
|
||||
'focus_failed',
|
||||
'forbidden',
|
||||
'approval_denied',
|
||||
'approval_timeout',
|
||||
'cancelled',
|
||||
'command_id_conflict',
|
||||
'outcome_unknown',
|
||||
'internal_error'
|
||||
])
|
||||
export type ComputerControlErrorCode = z.infer<
|
||||
typeof computerControlErrorCodeSchema
|
||||
>
|
||||
|
||||
export const computerControlErrorSchema = z
|
||||
.object({
|
||||
code: computerControlErrorCodeSchema,
|
||||
message: boundedText(256),
|
||||
retryable: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
export type ComputerControlError = z.infer<
|
||||
typeof computerControlErrorSchema
|
||||
>
|
||||
|
||||
export const computerControlApprovalRequestSchema = z
|
||||
.object({
|
||||
approvalId: opaqueIdSchema,
|
||||
leaseId: opaqueIdSchema,
|
||||
commandId: opaqueIdSchema,
|
||||
risk: z.enum(['input', 'commit']),
|
||||
action: z.enum(['activate', 'replace_text', 'select_option']),
|
||||
targetName: boundedText(256),
|
||||
textLength: z.number().int().nonnegative().max(4_096).optional()
|
||||
})
|
||||
.strict()
|
||||
export type ComputerControlApprovalRequest = z.infer<
|
||||
typeof computerControlApprovalRequestSchema
|
||||
>
|
||||
|
||||
export const computerControlApprovalResultSchema = z
|
||||
.object({
|
||||
approvalId: opaqueIdSchema,
|
||||
decision: z.enum(['approve_once', 'deny'])
|
||||
})
|
||||
.strict()
|
||||
export type ComputerControlApprovalResult = z.infer<
|
||||
typeof computerControlApprovalResultSchema
|
||||
>
|
||||
|
||||
export const computerControlCommandResultSchema = z.discriminatedUnion(
|
||||
'status',
|
||||
[
|
||||
z
|
||||
.object({
|
||||
status: z.literal('observed'),
|
||||
commandId: opaqueIdSchema,
|
||||
observation: computerControlObservationSchema
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
status: z.literal('completed'),
|
||||
commandId: opaqueIdSchema,
|
||||
risk: computerControlRiskSchema.exclude(['forbidden'])
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
status: z.literal('error'),
|
||||
commandId: opaqueIdSchema,
|
||||
error: computerControlErrorSchema
|
||||
})
|
||||
.strict()
|
||||
]
|
||||
)
|
||||
export type ComputerControlCommandResult = z.infer<
|
||||
typeof computerControlCommandResultSchema
|
||||
>
|
||||
+80
-3
@@ -1,7 +1,11 @@
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
BrowserProfileCreateInput,
|
||||
BrowserProfileRenameInput,
|
||||
CapabilityDiagnosticReport,
|
||||
CapabilityAssignments,
|
||||
CapabilitySnapshot,
|
||||
ComputerCapabilityId,
|
||||
McpServerInput,
|
||||
McpServerTestResult
|
||||
} from './capability-contracts'
|
||||
@@ -27,7 +31,8 @@ import {
|
||||
type ScheduleCreateInput,
|
||||
type HeartbeatCreateInput,
|
||||
type HeartbeatUpdateInput,
|
||||
type ExpertCreateInput
|
||||
type ExpertCreateInput,
|
||||
type ExpertUpdateInput
|
||||
} from './assistant-contracts'
|
||||
|
||||
export const workspaceRelativePathSchema = z
|
||||
@@ -61,10 +66,12 @@ export const workspaceFileRequestSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const conversationIdSchema = z.string().min(1).max(128)
|
||||
|
||||
export const agentRequestSchema = z
|
||||
.object({
|
||||
requestId: z.string().uuid(),
|
||||
conversationId: z.string().min(1).max(128),
|
||||
conversationId: conversationIdSchema,
|
||||
projectId: z.string().uuid().optional(),
|
||||
expertId: z.string().uuid().optional(),
|
||||
teamMode: z.boolean().optional(),
|
||||
@@ -550,7 +557,12 @@ export type AgentEvent =
|
||||
type: 'tool'
|
||||
callId: string
|
||||
name: string
|
||||
state: 'pending' | 'running' | 'completed' | 'failed'
|
||||
state:
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'recoverable'
|
||||
summary: string
|
||||
}
|
||||
| {
|
||||
@@ -590,6 +602,39 @@ export type AppInfo = {
|
||||
shortcut: string
|
||||
}
|
||||
|
||||
export const browserLiveStateSchema = z
|
||||
.object({
|
||||
conversationId: conversationIdSchema,
|
||||
status: z.enum([
|
||||
'creating',
|
||||
'loading',
|
||||
'ready',
|
||||
'acting',
|
||||
'failed',
|
||||
'stopped'
|
||||
]),
|
||||
url: z.string().max(2_048).optional(),
|
||||
frameDataUrl: z
|
||||
.string()
|
||||
.max(7_000_000)
|
||||
.refine(
|
||||
(value) => value.startsWith('data:image/png;base64,'),
|
||||
'浏览器画面格式无效'
|
||||
)
|
||||
.optional(),
|
||||
error: z.string().min(1).max(240).optional(),
|
||||
updatedAt: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type BrowserLiveState = z.infer<typeof browserLiveStateSchema>
|
||||
|
||||
export const browserStopRequestSchema = z
|
||||
.object({
|
||||
conversationId: conversationIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const knowledgeIdSchema = z.string().uuid()
|
||||
export const knowledgeCreateSchema = z
|
||||
.object({
|
||||
@@ -749,6 +794,10 @@ export type DesktopApi = {
|
||||
) => Promise<void>
|
||||
onEvent: (listener: (event: AgentEvent) => void) => () => void
|
||||
}
|
||||
browser: {
|
||||
stop: (conversationId: string) => Promise<void>
|
||||
onState: (listener: (state: BrowserLiveState) => void) => () => void
|
||||
}
|
||||
settings: {
|
||||
getRuntime: () => Promise<RuntimeSettings>
|
||||
updateRuntime: (input: RuntimeSettingsInput) => Promise<RuntimeSettings>
|
||||
@@ -836,6 +885,11 @@ export type DesktopApi = {
|
||||
experts: {
|
||||
list: () => Promise<AssistantExpert[]>
|
||||
create: (input: ExpertCreateInput) => Promise<AssistantExpert>
|
||||
update: (
|
||||
expertId: string,
|
||||
input: ExpertUpdateInput
|
||||
) => Promise<AssistantExpert>
|
||||
remove: (expertId: string) => Promise<void>
|
||||
}
|
||||
capabilities: {
|
||||
getSnapshot: () => Promise<CapabilitySnapshot>
|
||||
@@ -855,6 +909,29 @@ export type DesktopApi = {
|
||||
) => Promise<CapabilitySnapshot>
|
||||
removeMcpServer: (serverId: string) => Promise<CapabilitySnapshot>
|
||||
testMcpServer: (serverId: string) => Promise<McpServerTestResult>
|
||||
setComputerCapabilityEnabled?: (
|
||||
capabilityId: ComputerCapabilityId,
|
||||
enabled: boolean
|
||||
) => Promise<CapabilitySnapshot>
|
||||
setComputerCapabilityBrowserProfile?: (
|
||||
capabilityId: ComputerCapabilityId,
|
||||
browserProfileId: string | null
|
||||
) => Promise<CapabilitySnapshot>
|
||||
diagnoseComputerCapability?: (
|
||||
capabilityId: ComputerCapabilityId
|
||||
) => Promise<CapabilityDiagnosticReport>
|
||||
createBrowserProfile?: (
|
||||
input: BrowserProfileCreateInput
|
||||
) => Promise<CapabilitySnapshot>
|
||||
renameBrowserProfile?: (
|
||||
input: BrowserProfileRenameInput
|
||||
) => Promise<CapabilitySnapshot>
|
||||
setDefaultBrowserProfile?: (
|
||||
profileId: string
|
||||
) => Promise<CapabilitySnapshot>
|
||||
removeBrowserProfile?: (
|
||||
profileId: string
|
||||
) => Promise<CapabilitySnapshot>
|
||||
}
|
||||
context: {
|
||||
selectFiles: () => Promise<ContextAttachment[]>
|
||||
|
||||
@@ -15,6 +15,8 @@ export const ipcChannels = {
|
||||
agentCancel: 'agent:cancel',
|
||||
agentApprovalRespond: 'agent:approval:respond',
|
||||
agentEvent: 'agent:event',
|
||||
browserStop: 'browser:stop',
|
||||
browserState: 'browser:state',
|
||||
runtimeSettingsGet: 'settings:runtime:get',
|
||||
runtimeSettingsUpdate: 'settings:runtime:update',
|
||||
runtimeSettingsSelectWorkspace: 'settings:runtime:select-workspace',
|
||||
@@ -54,6 +56,8 @@ export const ipcChannels = {
|
||||
heartbeatsHistory: 'heartbeats:history',
|
||||
expertsList: 'experts:list',
|
||||
expertsCreate: 'experts:create',
|
||||
expertsUpdate: 'experts:update',
|
||||
expertsRemove: 'experts:remove',
|
||||
capabilitiesSnapshot: 'capabilities:snapshot',
|
||||
capabilitiesImportSkill: 'capabilities:skill:import',
|
||||
capabilitiesRemoveSkill: 'capabilities:skill:remove',
|
||||
@@ -62,6 +66,13 @@ export const ipcChannels = {
|
||||
capabilitiesSaveMcp: 'capabilities:mcp:save',
|
||||
capabilitiesRemoveMcp: 'capabilities:mcp:remove',
|
||||
capabilitiesTestMcp: 'capabilities:mcp:test',
|
||||
capabilitiesToggleComputer: 'capabilities:computer:toggle',
|
||||
capabilitiesConfigureComputer: 'capabilities:computer:configure',
|
||||
capabilitiesDiagnoseComputer: 'capabilities:computer:diagnose',
|
||||
capabilitiesCreateBrowserProfile: 'capabilities:browser-profile:create',
|
||||
capabilitiesRenameBrowserProfile: 'capabilities:browser-profile:rename',
|
||||
capabilitiesDefaultBrowserProfile: 'capabilities:browser-profile:default',
|
||||
capabilitiesRemoveBrowserProfile: 'capabilities:browser-profile:remove',
|
||||
contextSelectFiles: 'context:select-files',
|
||||
contextCaptureScreen: 'context:capture-screen',
|
||||
contextCaptureWindow: 'context:capture-window',
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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',
|
||||
'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'
|
||||
})
|
||||
expect(
|
||||
modelProfilePresets.find((preset) => preset.id === 'openai')
|
||||
).toMatchObject({
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
protocol: 'openai-responses',
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,148 +0,0 @@
|
||||
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 Responses API',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
modelName: 'gpt-4.1',
|
||||
protocol: 'openai-responses',
|
||||
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