feat: add DeepSeek Harness runtime
This commit is contained in:
@@ -19,13 +19,14 @@ const controlCharacterFreeString = (maximumLength: number) =>
|
||||
export const runtimeTargetSchema = z.enum([
|
||||
'model',
|
||||
'opencode',
|
||||
'continue'
|
||||
'continue',
|
||||
'deepseek-harness'
|
||||
])
|
||||
export type RuntimeTarget = z.infer<typeof runtimeTargetSchema>
|
||||
|
||||
export const capabilityAssignmentsSchema = z
|
||||
.array(runtimeTargetSchema)
|
||||
.max(3)
|
||||
.max(4)
|
||||
.refine(
|
||||
(assignments) => new Set(assignments).size === assignments.length,
|
||||
'Runtime 分配不能重复'
|
||||
|
||||
+54
-3
@@ -234,7 +234,8 @@ export const runtimeProviderSchema = z.enum([
|
||||
'auto',
|
||||
'model',
|
||||
'opencode',
|
||||
'continue'
|
||||
'continue',
|
||||
'deepseek-harness'
|
||||
])
|
||||
|
||||
export const toolApprovalPolicySchema = z.enum([
|
||||
@@ -391,6 +392,26 @@ export const runtimeModelSourceSchema = z.discriminatedUnion('kind', [
|
||||
.strict()
|
||||
])
|
||||
|
||||
export function isDeepSeekHarnessModelProfile(
|
||||
profile: Pick<
|
||||
ModelConnectionSettings,
|
||||
'baseUrl' | 'protocol' | 'authentication'
|
||||
>
|
||||
): boolean {
|
||||
if (
|
||||
profile.protocol !== 'openai-chat-completions' ||
|
||||
profile.authentication !== 'api-key'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return new URL(profile.baseUrl).hostname.toLowerCase() ===
|
||||
'api.deepseek.com'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeSettingsInputSchema = z
|
||||
.object({
|
||||
provider: runtimeProviderSchema,
|
||||
@@ -440,6 +461,8 @@ export const runtimeSettingsInputSchema = z
|
||||
defaultModelProfileId: modelProfileIdSchema.optional(),
|
||||
opencodeModelSource: runtimeModelSourceSchema.optional(),
|
||||
continueModelSource: runtimeModelSourceSchema.optional(),
|
||||
deepseekHarnessModelSource: runtimeModelSourceSchema
|
||||
.default({ kind: 'platform' }),
|
||||
toolApproval: toolApprovalPolicySchema
|
||||
}).strict()
|
||||
.superRefine((settings, context) => {
|
||||
@@ -505,7 +528,8 @@ export const runtimeSettingsInputSchema = z
|
||||
}
|
||||
for (const [key, source] of [
|
||||
['opencodeModelSource', settings.opencodeModelSource],
|
||||
['continueModelSource', settings.continueModelSource]
|
||||
['continueModelSource', settings.continueModelSource],
|
||||
['deepseekHarnessModelSource', settings.deepseekHarnessModelSource]
|
||||
] as const) {
|
||||
if (source?.kind === 'profile' && !ids.has(source.profileId)) {
|
||||
context.addIssue({
|
||||
@@ -551,6 +575,24 @@ export const runtimeSettingsInputSchema = z
|
||||
'Continue 独立模型连接仅支持文本对话协议,不支持图像生成协议'
|
||||
})
|
||||
}
|
||||
const deepseekHarnessSource = settings.deepseekHarnessModelSource
|
||||
const deepseekHarnessProfile =
|
||||
deepseekHarnessSource?.kind === 'profile'
|
||||
? settings.modelProfiles.find(
|
||||
(profile) => profile.id === deepseekHarnessSource.profileId
|
||||
)
|
||||
: undefined
|
||||
if (
|
||||
deepseekHarnessProfile &&
|
||||
!isDeepSeekHarnessModelProfile(deepseekHarnessProfile)
|
||||
) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
path: ['deepseekHarnessModelSource'],
|
||||
message:
|
||||
'DeepSeek Harness 独立模型连接仅支持 api.deepseek.com 的 OpenAI Chat Completions 协议'
|
||||
})
|
||||
}
|
||||
}
|
||||
if (
|
||||
settings.opencodeBaseUrl &&
|
||||
@@ -615,6 +657,7 @@ export type ConfiguredRuntimeSettings = {
|
||||
workspacePath: string
|
||||
opencodeModelSource: RuntimeModelSource
|
||||
continueModelSource: RuntimeModelSource
|
||||
deepseekHarnessModelSource?: RuntimeModelSource
|
||||
}
|
||||
|
||||
export type RuntimeSettings = {
|
||||
@@ -659,6 +702,7 @@ export type RuntimeSettings = {
|
||||
defaultModelProfileId: string
|
||||
opencodeModelSource: RuntimeModelSource
|
||||
continueModelSource: RuntimeModelSource
|
||||
deepseekHarnessModelSource?: RuntimeModelSource
|
||||
secureStorageAvailable: boolean
|
||||
toolApproval: RuntimeSettingsInput['toolApproval']
|
||||
configured?: ConfiguredRuntimeSettings
|
||||
@@ -716,7 +760,12 @@ export type WindowCaptureOption = {
|
||||
}
|
||||
|
||||
export type AgentRuntimeStatus = {
|
||||
id: 'setup' | 'model' | 'opencode' | 'continue'
|
||||
id:
|
||||
| 'setup'
|
||||
| 'model'
|
||||
| 'opencode'
|
||||
| 'continue'
|
||||
| 'deepseek-harness'
|
||||
label: string
|
||||
available: boolean
|
||||
detail: string
|
||||
@@ -729,6 +778,7 @@ export type RuntimeBinaryDetection =
|
||||
available: true
|
||||
path: string
|
||||
version?: string
|
||||
source?: 'bundled' | 'configured' | 'automatic'
|
||||
detail: string
|
||||
}
|
||||
| {
|
||||
@@ -741,6 +791,7 @@ export type RuntimeBinaryDetection =
|
||||
export type AgentRuntimeDetection = {
|
||||
opencode: RuntimeBinaryDetection
|
||||
continue: RuntimeBinaryDetection
|
||||
deepseekHarness: RuntimeBinaryDetection
|
||||
}
|
||||
|
||||
export const approvalDecisionSchema = z.enum([
|
||||
|
||||
@@ -23,6 +23,12 @@ export const agentRuntimeSelectionSchema = z.discriminatedUnion(
|
||||
provider: z.literal('continue'),
|
||||
profileId: runtimeSelectionProfileIdSchema.optional()
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
provider: z.literal('deepseek-harness'),
|
||||
profileId: runtimeSelectionProfileIdSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
]
|
||||
)
|
||||
@@ -34,6 +40,7 @@ export type AgentRuntimeSelection = z.infer<
|
||||
export type RuntimeSelectionRepairSettings = {
|
||||
modelProfiles: ReadonlyArray<{
|
||||
id: string
|
||||
baseUrl?: string
|
||||
protocol?: string
|
||||
authentication?: 'api-key' | 'none'
|
||||
apiKeyConfigured?: boolean
|
||||
@@ -45,6 +52,9 @@ export type RuntimeSelectionRepairSettings = {
|
||||
continueModelSource:
|
||||
| { kind: 'platform' }
|
||||
| { kind: 'profile'; profileId: string }
|
||||
deepseekHarnessModelSource?:
|
||||
| { kind: 'platform' }
|
||||
| { kind: 'profile'; profileId: string }
|
||||
}
|
||||
|
||||
type ChannelModelProfile = RuntimeSelectionRepairSettings['modelProfiles'][number]
|
||||
@@ -61,6 +71,25 @@ export function isChannelModelProfileUsable(
|
||||
)
|
||||
}
|
||||
|
||||
function isDeepSeekHarnessRepairProfileUsable(
|
||||
profile: ChannelModelProfile
|
||||
): boolean {
|
||||
if (
|
||||
profile.protocol !== 'openai-chat-completions' ||
|
||||
profile.authentication !== 'api-key' ||
|
||||
profile.apiKeyConfigured === false ||
|
||||
!profile.baseUrl
|
||||
) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return new URL(profile.baseUrl).hostname.toLowerCase() ===
|
||||
'api.deepseek.com'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function repairChannelRuntimeSelection(
|
||||
selection: AgentRuntimeSelection,
|
||||
settings: RuntimeSelectionRepairSettings
|
||||
@@ -86,6 +115,21 @@ export function repairChannelRuntimeSelection(
|
||||
) {
|
||||
return { provider: selection.provider }
|
||||
}
|
||||
if (selection.provider === 'deepseek-harness') {
|
||||
const repaired = repairAgentRuntimeSelection(selection, settings)
|
||||
if (
|
||||
repaired.provider === 'deepseek-harness' &&
|
||||
repaired.profileId
|
||||
) {
|
||||
const profile = settings.modelProfiles.find(
|
||||
(candidate) => candidate.id === repaired.profileId
|
||||
)
|
||||
if (profile && isDeepSeekHarnessRepairProfileUsable(profile)) {
|
||||
return repaired
|
||||
}
|
||||
}
|
||||
return { provider: 'deepseek-harness' }
|
||||
}
|
||||
const repaired = repairAgentRuntimeSelection(selection, settings)
|
||||
if (repaired.provider !== 'model') {
|
||||
return repaired
|
||||
@@ -120,7 +164,9 @@ export function repairAgentRuntimeSelection(
|
||||
const source =
|
||||
selection.provider === 'opencode'
|
||||
? settings.opencodeModelSource
|
||||
: settings.continueModelSource
|
||||
: selection.provider === 'continue'
|
||||
? settings.continueModelSource
|
||||
: settings.deepseekHarnessModelSource ?? { kind: 'platform' }
|
||||
return {
|
||||
provider: selection.provider,
|
||||
...(source.kind === 'profile'
|
||||
|
||||
Reference in New Issue
Block a user