feat: add model-aware DSH image input

DeepSeek Harness previously treated every selected model as text-only. It now carries the model profile's image capability through Main, the Utility Host, ACP, and Pi-AI, rejects unsupported images before model invocation, and validates supported JPEG/PNG data in a bounded process-local attachment store.

The Composer keeps universal controls on its first row, places OpenCode and Continue controls on a dedicated responsive row, and anchors context compaction without shifting the footer. Runtime supervision ownership for future Subagents and Jobs is documented for the right sidebar.

Release note: DeepSeek Harness 现可按所选模型连接的能力安全接收 JPEG/PNG 图片;Composer 同时将 Runtime 专属选择器移到独立一行,并固定上下文压缩入口。
This commit is contained in:
mesalogo
2026-08-16 23:30:54 +08:00
parent 18263a6b23
commit b751c70d75
30 changed files with 1589 additions and 187 deletions
@@ -45,6 +45,8 @@ function setup(
promptTimeoutMs?: number
maxEventCharacters?: number
maxRequestOutputCharacters?: number
supportsImageInput?: boolean
advertisedImageInput?: boolean
} = {}
) {
const exit = deferred<{
@@ -95,7 +97,13 @@ function setup(
if (method === 'initialize') {
return {
protocolVersion: 1,
agentCapabilities: {}
agentCapabilities: {
promptCapabilities: {
image:
options.advertisedImageInput ??
(options.supportsImageInput === true)
}
}
}
}
if (method === 'session/new') {
@@ -212,6 +220,7 @@ function setup(
defaultWorkspace: 'C:\\workspace',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
supportsImageInput: options.supportsImageInput,
launch,
loadAcpSdk: async () => sdk,
initializationTimeoutMs: 100,
@@ -417,6 +426,86 @@ describe('DeepSeekHarnessRuntime', () => {
).toBeInstanceOf(RequestError)
})
it('rejects images before launch when the selected model is text-only', async () => {
const harness = setup()
await expect(
collect(
harness.runtime.run(
{
...request('text-only-image'),
images: [
{
name: 'reference.png',
mediaType: 'image/png',
data: 'aW1hZ2U='
}
]
},
new AbortController().signal
)
)
).rejects.toThrow('未启用图像输入')
expect(harness.launch).not.toHaveBeenCalled()
})
it('forwards inline images when the selected model supports them', async () => {
const harness = setup({ supportsImageInput: true })
const running = collect(
harness.runtime.run(
{
...request('vision'),
images: [
{
name: 'reference.png',
mediaType: 'image/png',
data: 'aW1hZ2U='
}
]
},
new AbortController().signal
)
)
await vi.waitFor(() =>
expect(harness.promptGates).toHaveLength(1)
)
expect(harness.launch).toHaveBeenCalledWith(
expect.objectContaining({ supportsImageInput: true })
)
expect(
harness.requests.find(
(entry) => entry.method === 'session/prompt'
)?.params
).toMatchObject({
prompt: [
{ type: 'text', text: 'hello' },
{
type: 'image',
mimeType: 'image/png',
data: 'aW1hZ2U='
}
]
})
harness.promptGates[0]!.resolve({ stopReason: 'end_turn' })
await expect(running).resolves.toContainEqual(
expect.objectContaining({ type: 'done' })
)
})
it('fails closed when Host image capability disagrees with the model', async () => {
const harness = setup({
supportsImageInput: true,
advertisedImageInput: false
})
await expect(harness.runtime.getStatus()).resolves.toMatchObject({
available: false,
detail: expect.stringContaining('图片能力')
})
expect(harness.child.terminate).toHaveBeenCalled()
})
it('uses ACP stdio, maps conversations to sessions, and streams text', async () => {
const harness = setup()
const first = collect(
@@ -456,6 +545,7 @@ describe('DeepSeekHarnessRuntime', () => {
signal: expect.any(AbortSignal),
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-test',
supportsImageInput: false,
credentialRefs: [],
skillPackages: [],
extensionPackages: []