|
|
|
@@ -29,7 +29,7 @@ import type {
|
|
|
|
|
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
|
|
|
|
import type {
|
|
|
|
|
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
|
|
|
|
|
ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
|
|
|
|
|
ModelProviderGroup, ModelTarget, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
|
|
|
|
|
ToolCallView, ToolEventView, ToolResultView, WorkspaceId, WorkspaceView,
|
|
|
|
|
} from './api.ts'
|
|
|
|
|
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
|
|
|
|
@@ -156,6 +156,35 @@ const OPENAI_REASONING = {
|
|
|
|
|
defaultEffort: 'medium',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Catalog served by `session.models` and `llm.models` alike (fresh copies per call). */
|
|
|
|
|
function fixtureModelGroups(): ModelProviderGroup[] {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek-official',
|
|
|
|
|
name: 'DeepSeek',
|
|
|
|
|
models: [
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek-v4-flash',
|
|
|
|
|
name: 'DeepSeek-V4-Flash',
|
|
|
|
|
description: '快速响应',
|
|
|
|
|
reasoning: DEEPSEEK_REASONING,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek-v4-pro',
|
|
|
|
|
name: 'DeepSeek-V4-Pro',
|
|
|
|
|
description: '复杂任务',
|
|
|
|
|
reasoning: DEEPSEEK_REASONING,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'openai',
|
|
|
|
|
name: 'OpenAI',
|
|
|
|
|
models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sid(id: string): SessionId {
|
|
|
|
|
return id as SessionId
|
|
|
|
|
}
|
|
|
|
@@ -832,8 +861,14 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
const logs = new Map<SessionId, SessionEvent[]>([[sid('fx-alpha'), buildAlphaLog()]])
|
|
|
|
|
const modelTargets = new Map<SessionId, ModelTarget>(sessions.map(session => [
|
|
|
|
|
session.sessionId,
|
|
|
|
|
{ provider: 'deepseek', model: 'deepseek-v4-flash' },
|
|
|
|
|
{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
|
|
|
|
]))
|
|
|
|
|
/** Credential store double: set/unset flip the describe badge, values never read back. */
|
|
|
|
|
const fixtureCredentials = new Map<string, true>([
|
|
|
|
|
// The assembled fixture represents an already-configured shipped
|
|
|
|
|
// DeepSeek route so unrelated GUI journeys do not enter first-run setup.
|
|
|
|
|
['DEEPSEEK_API_KEY', true],
|
|
|
|
|
])
|
|
|
|
|
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 60]])
|
|
|
|
|
let nextSession = 1
|
|
|
|
|
let nextRpc = 1
|
|
|
|
@@ -1158,7 +1193,7 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd,
|
|
|
|
|
}
|
|
|
|
|
sessions.push(created)
|
|
|
|
|
modelTargets.set(created.sessionId, { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
|
|
|
|
modelTargets.set(created.sessionId, { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
|
|
|
|
|
attachedSessions += 1
|
|
|
|
|
const emitSession = (): void => {
|
|
|
|
|
// Mirrors the host: the frame fires at creation, so blank is constantly true.
|
|
|
|
@@ -1199,6 +1234,56 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
const appended = logOf(sessionId).at(-1) as SessionEvent
|
|
|
|
|
return ok(request, { title: normalized, seq: appended.seq })
|
|
|
|
|
},
|
|
|
|
|
fork: (request) => {
|
|
|
|
|
const { sessionId, atSeq } = request.payload
|
|
|
|
|
const source = summaryOf(sessionId)
|
|
|
|
|
if (source === undefined) {
|
|
|
|
|
return err(request, {
|
|
|
|
|
code: 'session-not-found',
|
|
|
|
|
message: `no session ${sessionId}`,
|
|
|
|
|
details: { sessionId },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const log = logs.get(sessionId) ?? []
|
|
|
|
|
const lastSeq = log.at(-1)?.seq ?? -1
|
|
|
|
|
const anchoredBoundary = atSeq === undefined
|
|
|
|
|
? undefined
|
|
|
|
|
: log.find(e => e.type === 'turn/end' && e.seq >= atSeq)
|
|
|
|
|
const boundary = anchoredBoundary
|
|
|
|
|
?? (atSeq === undefined || atSeq > lastSeq
|
|
|
|
|
? log.findLast(e => e.type === 'turn/end')
|
|
|
|
|
: undefined)
|
|
|
|
|
if (boundary === undefined) {
|
|
|
|
|
return err(request, {
|
|
|
|
|
code: 'fork-unavailable',
|
|
|
|
|
message: atSeq !== undefined && atSeq <= lastSeq
|
|
|
|
|
? `session ${sessionId} has not completed the turn containing event ${String(atSeq)}`
|
|
|
|
|
: `session ${sessionId} has no completed turn`,
|
|
|
|
|
details: { sessionId },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
let cut = boundary.seq + 1
|
|
|
|
|
while (cut < log.length && log[cut]?.type !== 'turn/start') cut++
|
|
|
|
|
const child: SessionSummary = {
|
|
|
|
|
sessionId: sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: false,
|
|
|
|
|
parentSessionId: sessionId,
|
|
|
|
|
...source.cwd === undefined ? {} : { cwd: source.cwd },
|
|
|
|
|
}
|
|
|
|
|
logs.set(child.sessionId, log.slice(0, cut))
|
|
|
|
|
sessions.push(child)
|
|
|
|
|
emitHost({
|
|
|
|
|
type: 'host/session-added', sessionId: child.sessionId, blank: false,
|
|
|
|
|
parentSessionId: sessionId,
|
|
|
|
|
...source.cwd === undefined ? {} : { cwd: source.cwd },
|
|
|
|
|
})
|
|
|
|
|
const workspace = workspaces.find(w => w.sessionIds.includes(sessionId))
|
|
|
|
|
if (workspace !== undefined) {
|
|
|
|
|
workspace.sessionIds = [child.sessionId, ...workspace.sessionIds]
|
|
|
|
|
workspace.updatedAt = new Date().toISOString()
|
|
|
|
|
emitHost({ type: 'host/workspace-changed', workspace: { ...workspace } })
|
|
|
|
|
}
|
|
|
|
|
return ok(request, { sessionId: child.sessionId })
|
|
|
|
|
},
|
|
|
|
|
history: async (request) => {
|
|
|
|
|
const log = logs.get(request.payload.sessionId) ?? []
|
|
|
|
|
// Snapshot at request time, deliver after the transit delay (mirrors a real host under latency).
|
|
|
|
@@ -1218,32 +1303,8 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
},
|
|
|
|
|
models: request => ok(request, {
|
|
|
|
|
current: modelTargets.get(request.payload.sessionId)
|
|
|
|
|
?? { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
|
|
|
|
groups: [
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek',
|
|
|
|
|
name: 'DeepSeek',
|
|
|
|
|
models: [
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek-v4-flash',
|
|
|
|
|
name: 'DeepSeek-V4-Flash',
|
|
|
|
|
description: '快速响应',
|
|
|
|
|
reasoning: DEEPSEEK_REASONING,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'deepseek-v4-pro',
|
|
|
|
|
name: 'DeepSeek-V4-Pro',
|
|
|
|
|
description: '复杂任务',
|
|
|
|
|
reasoning: DEEPSEEK_REASONING,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'openai',
|
|
|
|
|
name: 'OpenAI',
|
|
|
|
|
models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
?? { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
|
|
|
|
groups: fixtureModelGroups(),
|
|
|
|
|
failures: [],
|
|
|
|
|
}),
|
|
|
|
|
selectModel: (request) => {
|
|
|
|
@@ -1498,14 +1559,14 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
const spec = PERMISSION_PRESETS[preset]
|
|
|
|
|
if (preset === '') {
|
|
|
|
|
const current = permissionSelectOf(logOf(id)).currentValue
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `Current permission preset: ${current}. Available: ${Object.keys(PERMISSION_PRESETS).join(', ')}.` } })
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `current preset ${current} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
|
|
|
|
|
} else if (spec === undefined) {
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'error', text: `unknown permission preset ${JSON.stringify(preset)} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'error', text: `unknown preset "${preset}" (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } })
|
|
|
|
|
} else {
|
|
|
|
|
if (permissionSelectOf(logOf(id)).currentValue !== preset) append(id, { type: 'permission/preset', data: { preset } })
|
|
|
|
|
append(id, { type: 'sandbox/mode', data: { mode: spec.sandbox } })
|
|
|
|
|
append(id, { type: 'approval/policy', data: { policy: spec.approval } })
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `Permission preset: ${preset}.` } })
|
|
|
|
|
append(id, { type: 'command/done', data: { commandId, kind: 'success', text: `preset ${preset}` } })
|
|
|
|
|
}
|
|
|
|
|
return ok(request, { matched: true as const, commandId })
|
|
|
|
|
}
|
|
|
|
@@ -1689,6 +1750,64 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
settings: {
|
|
|
|
|
// Only the resolved DeepSeek address needed by first-run readiness is
|
|
|
|
|
// represented here. Fixture-backed journeys do not open its Models
|
|
|
|
|
// editor; real schema-driven forms ride the HTTP transport.
|
|
|
|
|
describe: request => ok(request, {
|
|
|
|
|
writable: true,
|
|
|
|
|
namespaces: [{
|
|
|
|
|
ns: 'llm-deepseek',
|
|
|
|
|
schema: {},
|
|
|
|
|
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
|
|
|
|
|
applies: 'live',
|
|
|
|
|
secrets: [{ path: ['apiKey'], set: false }],
|
|
|
|
|
revision: 0,
|
|
|
|
|
}],
|
|
|
|
|
}),
|
|
|
|
|
update: request => err(request, {
|
|
|
|
|
code: 'settings-rejected',
|
|
|
|
|
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
|
|
|
|
details: { ns: request.payload.ns },
|
|
|
|
|
}),
|
|
|
|
|
replace: request => err(request, {
|
|
|
|
|
code: 'settings-rejected',
|
|
|
|
|
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
|
|
|
|
details: { ns: request.payload.ns },
|
|
|
|
|
}),
|
|
|
|
|
mutate: request => err(request, {
|
|
|
|
|
code: 'settings-rejected',
|
|
|
|
|
message: 'fixture: no settings namespaces are registered',
|
|
|
|
|
details: { ns: request.payload.ns },
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
credentials: {
|
|
|
|
|
describe: request => ok(request, {
|
|
|
|
|
credentials: Object.fromEntries(request.payload.refs.map(ref => [ref, {
|
|
|
|
|
configured: fixtureCredentials.has(ref),
|
|
|
|
|
...fixtureCredentials.has(ref) ? { source: 'file' } : {},
|
|
|
|
|
writable: true,
|
|
|
|
|
}])),
|
|
|
|
|
}),
|
|
|
|
|
set: (request) => {
|
|
|
|
|
fixtureCredentials.set(request.payload.ref, true)
|
|
|
|
|
return ok(request, {})
|
|
|
|
|
},
|
|
|
|
|
unset: (request) => {
|
|
|
|
|
fixtureCredentials.delete(request.payload.ref)
|
|
|
|
|
return ok(request, {})
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
llm: {
|
|
|
|
|
providers: request => ok(request, {
|
|
|
|
|
providers: [
|
|
|
|
|
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
|
|
|
|
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true },
|
|
|
|
|
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
models: request => ok(request, { groups: fixtureModelGroups(), failures: [] }),
|
|
|
|
|
},
|
|
|
|
|
respond(message: ClientResponse): Promise<RpcReceipt> {
|
|
|
|
|
// Same routing discipline as the host: rpcId first, then the payload's
|
|
|
|
|
// audit correlation; a settled or unknown id is not-pending.
|
|
|
|
@@ -1758,6 +1877,7 @@ export class FixtureApiClient extends AbstractApiClient {
|
|
|
|
|
case 'session.models': return this.api.sessions.models(request)
|
|
|
|
|
case 'session.selectModel': return this.api.sessions.selectModel(request)
|
|
|
|
|
case 'session.rename': return this.api.sessions.rename(request)
|
|
|
|
|
case 'session.fork': return this.api.sessions.fork(request)
|
|
|
|
|
case 'session.prompt': return this.api.sessions.prompt(request)
|
|
|
|
|
case 'session.updateQueue': return this.api.sessions.updateQueue(request)
|
|
|
|
|
case 'session.cancel': return this.api.sessions.cancel(request)
|
|
|
|
@@ -1781,6 +1901,15 @@ export class FixtureApiClient extends AbstractApiClient {
|
|
|
|
|
case 'goal.resume': return this.api.goals.resume(request)
|
|
|
|
|
case 'goal.complete': return this.api.goals.complete(request)
|
|
|
|
|
case 'goal.clear': return this.api.goals.clear(request)
|
|
|
|
|
case 'settings.describe': return this.api.settings.describe(request)
|
|
|
|
|
case 'settings.update': return this.api.settings.update(request)
|
|
|
|
|
case 'settings.replace': return this.api.settings.replace(request)
|
|
|
|
|
case 'settings.mutate': return this.api.settings.mutate(request)
|
|
|
|
|
case 'credentials.describe': return this.api.credentials.describe(request)
|
|
|
|
|
case 'credentials.set': return this.api.credentials.set(request)
|
|
|
|
|
case 'credentials.unset': return this.api.credentials.unset(request)
|
|
|
|
|
case 'llm.providers': return this.api.llm.providers(request)
|
|
|
|
|
case 'llm.models': return this.api.llm.models(request)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|