refactor(office): render 的图片输入检查改为 office 工具私有实现,不再改动 tool-fs 与 dsh-tools
This commit is contained in:
1 parent
78da437976
commit
44f644ef85
15 files changed
+82
-147
No files matched your search
@@ -32,9 +32,6 @@
|
||||
/deepseek-ai-dsh-tools/use-this-package/enforce-policy-on-calls:
|
||||
en: cbfe8317565ac62d
|
||||
zh: 1e71c66941e50881
|
||||
/deepseek-ai-dsh-tools/use-this-package/check-image-input:
|
||||
en: f31cd4927bcffd46
|
||||
zh: b39caf5cefb85417
|
||||
/deepseek-ai-dsh-tools/use-this-package/host-presentation-descriptors:
|
||||
en: 258e16bb367229e5
|
||||
zh: 7c1eb91062b4e4ad
|
||||
@@ -45,8 +42,8 @@
|
||||
en: 516a6195a21e742b
|
||||
zh: 33c7b99b42c5acac
|
||||
/deepseek-ai-dsh-tools/understand-the-implementation/source-map:
|
||||
en: 4adef0ebd3e125e4
|
||||
zh: af307d853c22f12d
|
||||
en: 2dee0a37e0c1fa38
|
||||
zh: 0963cba32685612e
|
||||
/deepseek-ai-dsh-tools/understand-the-implementation/execution-and-cancellation:
|
||||
en: dc44a88997777988
|
||||
zh: 56454aeace1c8b77
|
||||
|
||||
@@ -93,10 +93,6 @@ Agent-step concurrency uses the [agent-loop configuration](../agent-loop/README.
|
||||
|
||||
A tool’s `projectContent` installs execution-prepared content before post-execute policies. Policies may still replace or block it; `finalizeContent` remains the final content transform after those policies.
|
||||
|
||||
### Check image input
|
||||
|
||||
`assertImageInputRoute(ctx, exec, refusals)` lets a tool whose result shows images to the model refuse the call before any file or attachment work. It reads the calling Session's latest request-header route, then the agent options, and requires `ctx.llm.resolveModelInfo()` to declare `image` input; an unknown capability, a missing agent, provider, model, or `llm` service is refused. The tool passes its own `unresolved` message and `unsupported(model)` builder, so each refusal names that tool's operation and argument.
|
||||
|
||||
### Host presentation descriptors
|
||||
|
||||
A tool can retain pure `presentCall()` and `presentResult()` methods for Host-local consumers. The built-in Web Client does not consume those values. It selects a renderer through `tool.call.toolview` and derives card props from raw call arguments, result content, failure state, and persisted metadata. The [Client-derived presentation decision](../../../.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md) owns this transport split.
|
||||
@@ -126,7 +122,6 @@ The registry holds typed `ToolDefinition`s in scoped layers and projects them on
|
||||
- [`src/schema.ts`](src/schema.ts) — Typed tool definitions and model-facing schema projection.
|
||||
- [`src/json-schema.ts`](src/json-schema.ts) — Supported JSON Schema validation.
|
||||
- [`src/presentation.ts`](src/presentation.ts) — Tool-card render intents.
|
||||
- [`src/image-route.ts`](src/image-route.ts) — Image-input check for the calling model route.
|
||||
|
||||
### Execution and cancellation
|
||||
|
||||
|
||||
@@ -93,10 +93,6 @@ Agent 步骤的并发上限使用 [agent-loop 配置](../agent-loop/README.zh.md
|
||||
|
||||
工具的 `projectContent` 在执行后策略之前安装执行期间准备的图文内容。策略仍可替换或阻止这些内容;`finalizeContent` 保留为策略之后的最终内容处理。
|
||||
|
||||
### 检查图片输入
|
||||
|
||||
`assertImageInputRoute(ctx, exec, refusals)` 供结果会向模型展示图片的工具使用,在读取文件或写入附件前拒绝调用。它先读取调用方 Session 最新的请求头路由,再读取 agent 选项,并要求 `ctx.llm.resolveModelInfo()` 声明 `image` 输入;能力未知,或缺少 agent、provider、模型或 `llm` 服务时都会拒绝。工具传入自己的 `unresolved` 文案和 `unsupported(model)` 构造函数,因此每条拒绝信息都写明该工具的操作和参数。
|
||||
|
||||
### Host 展示描述
|
||||
|
||||
工具可以为 Host 本地消费方保留纯函数 `presentCall()` 与 `presentResult()` 方法。内置 Web Client 不消费这些值,而是通过 `tool.call.toolview` 选择 renderer,并从原始调用参数、结果内容、失败状态与持久 metadata 派生 card props。[Client 派生展示决策](../../../.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md)负责该 transport 拆分。
|
||||
@@ -126,7 +122,6 @@ Agent 步骤的并发上限使用 [agent-loop 配置](../agent-loop/README.zh.md
|
||||
- [`src/schema.ts`](src/schema.ts) — 类型化工具定义与面向模型的 schema 投影。
|
||||
- [`src/json-schema.ts`](src/json-schema.ts) — 支持的 JSON Schema 校验.
|
||||
- [`src/presentation.ts`](src/presentation.ts) — 工具卡片呈现意图.
|
||||
- [`src/image-route.ts`](src/image-route.ts) — 调用方模型路由的图片输入检查.
|
||||
|
||||
### 执行与取消
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/** Image-input capability check for the model route that called a tool. @module */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolExecution } from './contracts.ts'
|
||||
|
||||
/** Refusal messages a tool supplies for its own operation and argument. */
|
||||
export interface ImageRouteRefusals {
|
||||
/** Message when the provider, model, or `llm` service is unavailable. */
|
||||
readonly unresolved: string
|
||||
/**
|
||||
* Builds the message when the resolved model does not declare image input.
|
||||
* @param model - Resolved model id.
|
||||
* @returns Refusal message naming that model.
|
||||
*/
|
||||
readonly unsupported: (model: string) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Require the calling model route to declare image input. The route is the Session's latest request header
|
||||
* configuration, falling back to the agent options; a model whose capability is unknown is refused.
|
||||
* @param ctx - Plugin context that reads the optional `llm` service.
|
||||
* @param exec - Tool execution supplying the calling agent and cancellation.
|
||||
* @param refusals - Tool-specific refusal messages.
|
||||
* @returns Resolves when the route accepts images; rejects with an `Error` carrying one refusal message, or with
|
||||
* the model-information lookup failure.
|
||||
*/
|
||||
export async function assertImageInputRoute(ctx: Context, exec: Pick<ToolExecution, 'agent' | 'signal'>, refusals: ImageRouteRefusals): Promise<void> {
|
||||
const routed = exec.agent?.session.requestHeader()?.config
|
||||
const provider = routed?.provider ?? exec.agent?.options.provider
|
||||
const model = routed?.model ?? exec.agent?.options.model
|
||||
const llm = ctx.get('llm')
|
||||
if (provider === undefined || model === undefined || llm === undefined) throw new Error(refusals.unresolved)
|
||||
const info = await llm.resolveModelInfo(provider, model, exec.signal)
|
||||
if (info.inputModalities?.includes('image') !== true) throw new Error(refusals.unsupported(model))
|
||||
}
|
||||
@@ -409,5 +409,4 @@ export * from './executor.ts'
|
||||
export * from './schema.ts'
|
||||
export * from './json-schema.ts'
|
||||
export * from './presentation.ts'
|
||||
export * from './image-route.ts'
|
||||
export { ToolsService as ToolRuntime }
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { assertImageInputRoute } from '@deepseek-ai/dsh-tools'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
|
||||
const MODELS: readonly LlmModelInfo[] = [
|
||||
{ provider: 'visual', id: 'vision-model', name: 'Vision', inputModalities: ['text', 'image'] },
|
||||
{ provider: 'visual', id: 'text-model', name: 'Text', inputModalities: ['text'] },
|
||||
{ provider: 'visual', id: 'legacy-model', name: 'Legacy' },
|
||||
]
|
||||
|
||||
/** Catalog-only adapter; `stream` is unreachable. */
|
||||
class CatalogAdapter extends LlmAdapter {
|
||||
override listModels(): Promise<readonly LlmModelInfo[]> { return Promise.resolve(MODELS) }
|
||||
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
|
||||
const found = MODELS.find(candidate => candidate.id === model)
|
||||
return Promise.resolve({
|
||||
provider, id: model, name: model,
|
||||
...found?.inputModalities === undefined ? {} : { inputModalities: [...found.inputModalities] },
|
||||
})
|
||||
}
|
||||
override stream(_options: GenerateOptions): AsyncIterable<StreamChunk> { throw new Error('never streams') }
|
||||
}
|
||||
|
||||
const refusals = { unresolved: 'route unknown', unsupported: (model: string) => `${model} cannot see images` }
|
||||
const signal = new AbortController().signal
|
||||
|
||||
async function setup(llm = true): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
onTestFinished(async () => { await ctx.fiber.dispose() })
|
||||
if (llm) {
|
||||
await ctx.plugin(LlmRuntime)
|
||||
ctx.llm.registerAdapter(['visual'], new CatalogAdapter())
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
function agent(header: { provider: string; model: string } | undefined, options: { provider?: string; model?: string } = {}): Agent {
|
||||
// The check reads only the Session request header and the agent options.
|
||||
return { options, session: { requestHeader: () => header === undefined ? undefined : { config: header } } } as never
|
||||
}
|
||||
|
||||
describe('assertImageInputRoute', () => {
|
||||
it('accepts a request-header route that declares image input', async () => {
|
||||
const ctx = await setup()
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent({ provider: 'visual', model: 'vision-model' }) }, refusals)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('falls back to the agent options when the Session has no request header', async () => {
|
||||
const ctx = await setup()
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent(undefined, { provider: 'visual', model: 'vision-model' }) }, refusals)).resolves.toBeUndefined()
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent(undefined, { provider: 'visual', model: 'text-model' }) }, refusals))
|
||||
.rejects.toThrow('text-model cannot see images')
|
||||
})
|
||||
|
||||
it('refuses a model without declared image input, including unknown capability', async () => {
|
||||
const ctx = await setup()
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent({ provider: 'visual', model: 'text-model' }) }, refusals))
|
||||
.rejects.toThrow('text-model cannot see images')
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent({ provider: 'visual', model: 'legacy-model' }) }, refusals))
|
||||
.rejects.toThrow('legacy-model cannot see images')
|
||||
})
|
||||
|
||||
it('refuses an unresolved route: no agent, no provider or model, or no llm service', async () => {
|
||||
const ctx = await setup()
|
||||
await expect(assertImageInputRoute(ctx, { signal }, refusals)).rejects.toThrow('route unknown')
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent(undefined, { model: 'vision-model' }) }, refusals)).rejects.toThrow('route unknown')
|
||||
await expect(assertImageInputRoute(ctx, { signal, agent: agent(undefined, { provider: 'visual' }) }, refusals)).rejects.toThrow('route unknown')
|
||||
const bare = await setup(false)
|
||||
await expect(assertImageInputRoute(bare, { signal, agent: agent({ provider: 'visual', model: 'vision-model' }) }, refusals)).rejects.toThrow('route unknown')
|
||||
})
|
||||
})
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-tool-office/use-this-package:
|
||||
en: a5780ccfbefe5a17
|
||||
zh: 2e0ffa6eb96d82cb
|
||||
en: 0d4e3ab4fefd478c
|
||||
zh: a6926f482c0c7576
|
||||
/deepseek-ai-dsh-tool-office/understand-the-implementation:
|
||||
en: b90e60b4bf9a200b
|
||||
zh: fbc977cf7c31d169
|
||||
|
||||
@@ -41,7 +41,7 @@ The [Web bundle](../../bundle/web-app/README.md) and the [SDK bundle](../../bund
|
||||
| `maxImageDimension` | `8192` | Maximum width and height of each image |
|
||||
| `maxImageBytes` | `33554432` | Maximum PNG bytes across all images of one render |
|
||||
|
||||
A render applies the lower of each field and the attachment store's corresponding image limit (`maxImagesPerMessage`, `maxImagePixels`, `maxImageDimension`, `maxMessageImageBytes`), and refuses any single image above `maxImageBytes` of the store. A selection beyond a bound fails instead of being truncated. Rendering requires an attachment store and a calling model route that declares image input, checked by `dsh-tools` `assertImageInputRoute`; other routes receive a refusal before any rendering work starts.
|
||||
A render applies the lower of each field and the attachment store's corresponding image limit (`maxImagesPerMessage`, `maxImagePixels`, `maxImageDimension`, `maxMessageImageBytes`), and refuses any single image above `maxImageBytes` of the store. A selection beyond a bound fails instead of being truncated. Rendering requires an attachment store and a calling model route that declares image input. The tool reads that route from the latest model request of the Session, or from the agent options before the first request, and refuses a model whose input modalities omit images or are not declared before any rendering work starts.
|
||||
|
||||
Inputs resolve against the Session working directory and use the filesystem backend's read authority, as `read` and Office previews do, so files outside the workspace are readable. The input extension selects the document family, and `format` selects the output of `convert` and `recalculate`. Their result names a file in the Office kit's private [output store](../office-kit/README.md#use-this-package) under the Harness home; the model copies it with the shell, so the shell's sandbox and approvals govern where it lands. When the filesystem backend cannot read Host files, such as an SSH workspace, `convert` and `recalculate` fail before converting; `render` still works.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ kind: "package-reference"
|
||||
| `maxImageDimension` | `8192` | 每张图片的最大宽度和高度 |
|
||||
| `maxImageBytes` | `33554432` | 一次渲染全部图片的最大 PNG 字节数 |
|
||||
|
||||
渲染对每个字段取其与附件存储对应图片上限(`maxImagesPerMessage`、`maxImagePixels`、`maxImageDimension`、`maxMessageImageBytes`)中的较小值,并拒绝超过存储 `maxImageBytes` 的单张图片。超出上限的选择直接失败,不会被截断。渲染要求挂载附件存储,且调用方模型路由声明支持图片输入(由 `dsh-tools` 的 `assertImageInputRoute` 检查);其他路由在开始任何渲染工作之前就会被拒绝。
|
||||
渲染对每个字段取其与附件存储对应图片上限(`maxImagesPerMessage`、`maxImagePixels`、`maxImageDimension`、`maxMessageImageBytes`)中的较小值,并拒绝超过存储 `maxImageBytes` 的单张图片。超出上限的选择直接失败,不会被截断。渲染要求挂载附件存储,且调用方模型路由声明支持图片输入。工具从会话最近一次模型请求读取该路由,首次请求之前则读取 agent 选项;模型的输入模态不含图片或未声明时,在开始任何渲染工作之前就会被拒绝。
|
||||
|
||||
输入相对 Session 工作目录解析,并沿用文件系统后端的读取权限,与 `read` 和 Office 预览一致,因此可以读取工作区外的文件。输入扩展名决定文档类别,`format` 决定 `convert` 和 `recalculate` 的输出格式。其结果给出 Office 套件私有[输出存储](../office-kit/README.zh.md#use-this-package)中位于 Harness 主目录下的一个文件;模型用 shell 复制它,因此文件落在哪里由 shell 的沙箱和审批决定。文件系统后端无法读取 Host 文件时(例如 SSH 工作区),`convert` 和 `recalculate` 在转换前即失败;`render` 不受影响。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { FsInfo, FsTarget } from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import { OfficeKitError, OfficeSourceKey, type OfficeKit, type OfficeKitSource } from '@deepseek-ai/dsh-office-kit'
|
||||
import { assertImageInputRoute, defineTool, type GenericCallView, type ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { defineTool, type GenericCallView, type ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-working-directory'
|
||||
import { checkArguments, checkOutputFormat, extensionOf, inputFamily, OUTPUT_FORMATS, type OfficeArgs, type OfficeFamily } from './formats.ts'
|
||||
import { renderFileContent, renderImagesContent, type OfficeFileValue, type OfficeRenderValue } from './content.ts'
|
||||
@@ -227,16 +227,29 @@ function officeFailure(verb: string, path: string, error: unknown): unknown {
|
||||
return new Error(`cannot ${verb} "${path}": ${error.message}${hint}`, { cause: error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Require the calling model route to declare image input. The route is the Session's latest request header
|
||||
* configuration, falling back to the agent options; a model whose capability is unknown is refused.
|
||||
*/
|
||||
async function assertRenderRoute(ctx: Context, exec: ToolExecution, displayPath: string): Promise<void> {
|
||||
const llm = ctx.get('llm')
|
||||
const header = exec.agent?.session.requestHeader()?.config
|
||||
const model = header?.model ?? exec.agent?.options.model
|
||||
const provider = header?.provider ?? exec.agent?.options.provider
|
||||
if (llm === undefined || provider === undefined || model === undefined) {
|
||||
throw new Error(`cannot render "${displayPath}": the current model route could not be resolved`)
|
||||
}
|
||||
const { inputModalities = [] } = await llm.resolveModelInfo(provider, model, exec.signal)
|
||||
if (!inputModalities.includes('image')) throw new Error(`cannot render "${displayPath}": model "${model}" does not accept images`)
|
||||
}
|
||||
|
||||
async function render(
|
||||
ctx: Context, kit: OfficeKit, config: Config, exec: ToolExecution, args: OfficeArgs, target: FsTarget, source: OfficeKitSource,
|
||||
): Promise<OfficeRenderValue> {
|
||||
const path = ctx.fs.processPath(target)
|
||||
const attachments = ctx.get('attachments')
|
||||
if (attachments === undefined) throw new Error(`cannot render "${target.displayPath}": no attachment service is mounted`)
|
||||
await assertImageInputRoute(ctx, exec, {
|
||||
unresolved: `cannot render "${target.displayPath}": the current model route could not be resolved`,
|
||||
unsupported: model => `cannot render "${target.displayPath}": model "${model}" does not accept images`,
|
||||
})
|
||||
await assertRenderRoute(ctx, exec, target.displayPath)
|
||||
const limits = attachments.imageLimits
|
||||
let result
|
||||
try {
|
||||
|
||||
@@ -35,10 +35,16 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR
|
||||
const generation = OfficeKitGeneration('test')
|
||||
const signal = new AbortController().signal
|
||||
|
||||
/** Declared input modalities by model id; `legacy` declares none. */
|
||||
const MODALITIES: Partial<Record<string, readonly ('text' | 'image')[]>> = { vision: ['text', 'image'], text: ['text'] }
|
||||
|
||||
class CatalogAdapter extends LlmAdapter {
|
||||
override listModels(): Promise<readonly LlmModelInfo[]> { return Promise.resolve([]) }
|
||||
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
|
||||
return Promise.resolve({ provider, id: model, name: model, ...model === 'vision' ? { inputModalities: ['text', 'image'] } : {} })
|
||||
const inputModalities = MODALITIES[model]
|
||||
return Promise.resolve({
|
||||
provider, id: model, name: model, ...inputModalities === undefined ? {} : { inputModalities: [...inputModalities] },
|
||||
})
|
||||
}
|
||||
/* v8 ignore next 3 -- office tests never stream */
|
||||
override stream(_options: GenerateOptions): AsyncIterable<StreamChunk> { throw new Error('unused') }
|
||||
@@ -120,11 +126,11 @@ async function setup(options: SetupOptions = {}) {
|
||||
}
|
||||
|
||||
let sessions = 0
|
||||
function agentOn(model: string | undefined): object {
|
||||
function agentOn(model: string | undefined, options: { provider?: string; model?: string } = {}): object {
|
||||
const id = SessionId(`office-session-${++sessions}`)
|
||||
const session = Session.create(id, [], { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd: dir, isSeeded: false })
|
||||
vi.spyOn(session, 'requestHeader').mockReturnValue(model === undefined ? undefined : { config: { provider: 'visual', model } })
|
||||
return { options: {}, session }
|
||||
return { options, session }
|
||||
}
|
||||
|
||||
let calls = 0
|
||||
@@ -209,6 +215,7 @@ describe('render', () => {
|
||||
|
||||
it.each([
|
||||
['a text-only model', { model: 'text' }, 'model "text" does not accept images'],
|
||||
['a model without declared input modalities', { model: 'legacy' }, 'model "legacy" does not accept images'],
|
||||
['an unresolved route', { model: undefined }, 'the current model route could not be resolved'],
|
||||
['no attachment service', { attachments: false }, 'no attachment service is mounted'],
|
||||
['no model runtime', { llm: false }, 'the current model route could not be resolved'],
|
||||
@@ -221,6 +228,25 @@ describe('render', () => {
|
||||
expect(kit.renderImages).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the agent route when the Session has no request header', async () => {
|
||||
const { ctx, kit } = await setup()
|
||||
expect((await office(ctx, { operation: 'render', input: 'deck.pptx' }, agentOn(undefined, { provider: 'visual', model: 'vision' }))).isError).toBe(false)
|
||||
expect(text(await office(ctx, { operation: 'render', input: 'deck.pptx' }, agentOn(undefined, { provider: 'visual', model: 'text' }))))
|
||||
.toBe(`Error: cannot render "${join(dir, 'deck.pptx')}": model "text" does not accept images`)
|
||||
expect(kit.renderImages).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['no agent', null],
|
||||
['an agent route without a provider', { model: 'vision' }],
|
||||
['an agent route without a model', { provider: 'visual' }],
|
||||
] as const)('refuses %s as an unresolved route', async (_label, route) => {
|
||||
const { ctx, kit } = await setup()
|
||||
const result = await office(ctx, { operation: 'render', input: 'deck.pptx' }, route === null ? null : agentOn(undefined, route))
|
||||
expect(text(result)).toBe(`Error: cannot render "${join(dir, 'deck.pptx')}": the current model route could not be resolved`)
|
||||
expect(kit.renderImages).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses an image above the per-image attachment byte limit', async () => {
|
||||
const { ctx, kit } = await setup()
|
||||
const limit = ctx.get('attachments')!.imageLimits.maxImageBytes
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
en: 1f4d7213aa3d0d60
|
||||
zh: aa6573f96c8b5220
|
||||
/deepseek-ai-dsh-tool-fs/understand-the-implementation/source-map:
|
||||
en: 05a5498628bbd17a
|
||||
zh: 7a769fb2ea51e370
|
||||
en: 459b773aafc6c163
|
||||
zh: ccd729aded7d98ba
|
||||
/deepseek-ai-dsh-tool-fs/understand-the-implementation/per-tool-flow:
|
||||
en: 35fbee1ef9811806
|
||||
zh: 17c725e2b742c073
|
||||
|
||||
@@ -105,7 +105,7 @@ The tools are the executor; policy is an event gate. The tools inject no policy
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | Plugin entry: `Config`, tool composition, `read_image` attachments gate |
|
||||
| [`src/read.ts`](src/read.ts) | `read` executor: one stat, streaming decision, window build, observation |
|
||||
| [`src/read-image.ts`](src/read-image.ts) | `read_image` executor: route gate through `dsh-tools` `assertImageInputRoute`, media-type gates, bounded bytes, attachment save |
|
||||
| [`src/read-image.ts`](src/read-image.ts) | `read_image` executor: route and media-type gates, bounded bytes, attachment save |
|
||||
| [`src/write.ts`](src/write.ts) | `write` executor: intent waterfall, atomic write, observation |
|
||||
| [`src/apply-patch.ts`](src/apply-patch.ts), [`apply-patch-output.ts`](src/apply-patch-output.ts) | `apply_patch` registration, strict commit values and compact diagnostics |
|
||||
| [`src/apply-patch-presentation.ts`](src/apply-patch-presentation.ts) | Pure call previews and known result diffs |
|
||||
|
||||
@@ -105,7 +105,7 @@ kind: "package-reference"
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | 插件入口:`Config`、工具组合、`read_image` 附件门禁 |
|
||||
| [`src/read.ts`](src/read.ts) | `read` 执行器:一次 stat、流式决策、窗口构建、观察 |
|
||||
| [`src/read-image.ts`](src/read-image.ts) | `read_image` 执行器:通过 `dsh-tools` 的 `assertImageInputRoute` 检查路由、媒体类型门禁、有界字节、附件保存 |
|
||||
| [`src/read-image.ts`](src/read-image.ts) | `read_image` 执行器:路由与媒体类型门禁、有界字节、附件保存 |
|
||||
| [`src/write.ts`](src/write.ts) | `write` 执行器:意图 waterfall、原子写入、观察 |
|
||||
| [`src/apply-patch.ts`](src/apply-patch.ts)、[`apply-patch-output.ts`](src/apply-patch-output.ts) | `apply_patch` 注册、严格提交值与精简诊断 |
|
||||
| [`src/apply-patch-presentation.ts`](src/apply-patch-presentation.ts) | 纯调用预览与已知结果 diff |
|
||||
|
||||
@@ -16,8 +16,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { assertImageInputRoute, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-fs'
|
||||
import { resolveRegularReadTarget } from './read-target.ts'
|
||||
|
||||
@@ -109,6 +109,28 @@ export function imageMediaTypeForPath(filePath: string): ImageMediaType | undefi
|
||||
return IMAGE_EXTENSIONS[extname(filePath).toLowerCase()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce the strict image-capability gate for the calling route. Resolves the
|
||||
* session's latest routed provider/model (request header config, then agent
|
||||
* options) and requires the exact resolved route to declare `image` input explicitly.
|
||||
* @param ctx - the plugin context used to resolve the optional `llm` service.
|
||||
* @param exec - the tool-execution context supplying the calling agent.
|
||||
* @param requestedPath - the raw, not-yet-resolved path rendered in refusal messages.
|
||||
*/
|
||||
export async function assertImageCapableRoute(ctx: Context, exec: ToolExecution, requestedPath: string): Promise<void> {
|
||||
const routed = exec.agent?.session.requestHeader()?.config
|
||||
const provider = routed?.provider ?? exec.agent?.options.provider
|
||||
const model = routed?.model ?? exec.agent?.options.model
|
||||
const llm = ctx.get('llm')
|
||||
if (provider === undefined || model === undefined || llm === undefined) {
|
||||
throw new Error(`cannot read "${requestedPath}" as an image: the current model route could not be resolved`)
|
||||
}
|
||||
const active = await llm.resolveModelInfo(provider, model, exec.signal)
|
||||
if (active.inputModalities === undefined || !active.inputModalities.includes('image')) {
|
||||
throw new Error(`cannot read "${requestedPath}" as an image: model "${model}" does not declare image input; switch to an image-capable model to read images`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Refuse a media type outside the deployment's accepted set, naming the offending path. */
|
||||
function assertDeploymentAccepts(attachments: AttachmentStore, mediaType: ImageMediaType, displayPath: string): void {
|
||||
if (!attachments.imageLimits.mediaTypes.includes(mediaType)) {
|
||||
@@ -232,10 +254,7 @@ export function applyReadImageTool(ctx: Context): void {
|
||||
throw new Error(`cannot read "${args.file_path}" as an image: no attachment service is mounted`)
|
||||
}
|
||||
if (declared !== undefined) assertDeploymentAccepts(attachments, declared, args.file_path)
|
||||
await assertImageInputRoute(ctx, exec, {
|
||||
unresolved: `cannot read "${args.file_path}" as an image: the current model route could not be resolved`,
|
||||
unsupported: model => `cannot read "${args.file_path}" as an image: model "${model}" does not declare image input; switch to an image-capable model to read images`,
|
||||
})
|
||||
await assertImageCapableRoute(ctx, exec, args.file_path)
|
||||
|
||||
const { target, info } = await resolveRegularReadTarget(ctx, exec, args.file_path)
|
||||
|
||||
|
||||
Reference in new issue
Block a user