download_doc_image
Download an image embedded in a document using its image token. Optionally scope permissions with a doc token, and save images larger than 2 MiB to a local path.
Instructions
[User Identity / Official API] Download an image embedded in a docx document so the model can see it. Pass the image_token from get_doc_blocks (block.image.token), and optionally the doc/wiki/URL token to scope the lookup. UAT-first.
Size cap: payloads > 2 MiB MUST pass save_path.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| image_token | Yes | Image token (from get_doc_blocks image block) | |
| doc_token | No | Document ID, wiki node token, or Feishu URL (optional but recommended for permission scoping). | |
| save_path | No | Absolute local path. Required when image bytes > 2 MiB. |
Implementation Reference
- src/tools/diagnostics.js:150-176 (handler)The tool handler for 'download_doc_image' — validates args, resolves doc token, calls official client to download, handles inline vs save-to-disk with 2 MiB cap.
async download_doc_image(args, ctx) { if (!args.image_token) { return text('download_doc_image requires image_token (from get_doc_blocks image block). Optionally pass doc_token (native id / wiki node / Feishu URL).'); } const docToken = args.doc_token ? await ctx.resolveDocId(args.doc_token) : undefined; const r = await ctx.getOfficialClient().downloadDocImage(args.image_token, docToken); const sizeNote = `${r.bytes} bytes (${fmtMB(r.bytes)}, ${r.mimeType})`; const tooBig = inlineTooBig(r.bytes); if (tooBig && !args.save_path) { return text(`Image is ${sizeNote} — exceeds the 2 MiB inline cap. Re-run download_doc_image with save_path=<absolute path>.`); } const saved = maybeSave(args.save_path, r.base64); const saveNote = saved ? (saved.ok ? `\nSaved to: ${saved.path}` : `\nSave to ${saved.path} failed: ${saved.error}`) : ''; const source = docToken ? `docx ${docToken}` : 'drive media'; const ident = r.viaUser ? 'as user' : 'as app'; if (tooBig) { return text(`Image from ${source} downloaded (${ident}, ${sizeNote})${saveNote}\nInline content omitted because the payload exceeds the 2 MiB cap.`); } return { content: [ { type: 'text', text: `Image from ${source} (${ident}, ${sizeNote})${saveNote}` }, { type: 'image', data: r.base64, mimeType: r.mimeType }, ], }; }, - src/tools/diagnostics.js:43-55 (schema)Schema/input definition for 'download_doc_image' — describes the tool and its parameters (image_token required, doc_token optional, save_path optional).
{ name: 'download_doc_image', description: '[User Identity / Official API] Download an image embedded in a docx document so the model can see it. Pass the `image_token` from `get_doc_blocks` (block.image.token), and optionally the doc/wiki/URL token to scope the lookup. UAT-first.\n\n**Size cap:** payloads > 2 MiB MUST pass `save_path`.', inputSchema: { type: 'object', properties: { image_token: { type: 'string', description: 'Image token (from get_doc_blocks image block)' }, doc_token: { type: 'string', description: 'Document ID, wiki node token, or Feishu URL (optional but recommended for permission scoping).' }, save_path: { type: 'string', description: 'Absolute local path. Required when image bytes > 2 MiB.' }, }, required: ['image_token'], }, }, - src/server.js:56-57 (registration)Tool modules are loaded and their schemas/handlers flattened into TOOLS and HANDLERS arrays; diagnostics module (containing download_doc_image) is included via line 41.
const TOOLS = TOOL_MODULES.flatMap((m) => m.schemas); const HANDLERS = Object.fromEntries(TOOL_MODULES.flatMap((m) => Object.entries(m.handlers))); - The underlying Feishu API client method downloadDocImage — calls drive/v1/medias/{token}/download with user identity (UAT) first, falls back to app identity.
async downloadDocImage(imageToken, docToken, docType = 'docx') { if (!imageToken) throw new Error('downloadDocImage: imageToken is required'); // Feishu's drive media download uses `extra` as a JSON-string query param to // identify the enclosing doc context. Most observed forms carry both // `doc_type` and `doc_token`; omitting docType falls back to 'docx' which // is the by-far most common case. Omitting extra entirely is safe for // standalone drive-media tokens that don't live inside a doc. const extra = docToken ? `?extra=${encodeURIComponent(JSON.stringify({ doc_type: docType, doc_token: docToken }))}` : ''; const path = `/open-apis/drive/v1/medias/${encodeURIComponent(imageToken)}/download${extra}`; const url = 'https://open.feishu.cn' + path; // Attempt 1 — user identity (most reliable for user-owned docs). if (this.hasUAT) { try { const uat = await this._getValidUAT(); const res = await fetchWithTimeout(url, { headers: { 'Authorization': `Bearer ${uat}` }, timeoutMs: 60000 }); if (res.ok && !res.headers.get('content-type')?.includes('application/json')) { const buf = Buffer.from(await res.arrayBuffer()); return { base64: buf.toString('base64'), mimeType: res.headers.get('content-type') || 'application/octet-stream', bytes: buf.length, viaUser: true, }; } const errJson = await res.json().catch(() => null); console.error(`[feishu-user-plugin] downloadDocImage as user failed: ${errJson?.code}: ${errJson?.msg || res.statusText}, retrying as app`); } catch (e) { console.error(`[feishu-user-plugin] downloadDocImage as user threw (${e.message}), retrying as app`); } } // Attempt 2 — app identity. Requires the app to have drive access to the doc. const token = await this._getAppToken(); const res = await fetchWithTimeout(url, { headers: { 'Authorization': `Bearer ${token}` }, timeoutMs: 60000 }); if (!res.ok || res.headers.get('content-type')?.includes('application/json')) { const errJson = await res.json().catch(() => null); throw new Error(`downloadDocImage failed: ${errJson?.code}: ${errJson?.msg || res.statusText}. Note: app identity requires drive access to the document; configure UAT for user-owned docs.`); } const buf = Buffer.from(await res.arrayBuffer()); return { base64: buf.toString('base64'), mimeType: res.headers.get('content-type') || 'application/octet-stream', bytes: buf.length, viaUser: false, }; },