fix: remove graph extraction budget and unify image download buttons
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Graph extraction could exhaust its fixed 8192-token budget before returning
structured content. OpenAI requests now omit output-token limits, while
Anthropic uses the model connection limit required by its protocol.
Provider limits, timeout, cancellation and bounded response handling remain.

Use the shared accessible icon button for image downloads in chat and the
image viewer. Add graph request regression coverage and document the limits.

Release note: 知识图谱抽取不再额外限制为 8192 tokens;OpenAI 由模型服务决定输出上限,Anthropic 沿用模型连接配置。聊天和图片预览中的下载按钮统一为带提示的图标按钮。
This commit is contained in:
mesalogo
2026-09-19 21:02:43 +08:00
parent c98eb69cbd
commit b5d8204986
5 changed files with 93 additions and 10 deletions
+7
View File
@@ -31,6 +31,13 @@ FastGPT/RAGFlow 完整 App 与专属配置、真实远程预检索仍待验收
| [2026-09-13 产品验证摘要](./external-knowledge-validation-2026-09-13.json) | 生产服务与本机 HTTP MCP、Dify 桌面、凭据重开、调用计数、最终自动检查状态及远程阻断的脱敏证据 |
| [2026-09-13 UI 与真实生成复验](./external-knowledge-follow-up-2026-09-13.json) | 后续 UI 修正、三家生产 IPC 生成、Dify 完整 App 回答与引用、视觉 fixture 及本轮请求计数 |
## 模型图谱抽取
图谱抽取不设置独立的输出 token 预算。OpenAI Chat Completions 和 Responses
请求省略输出上限参数,由模型服务决定;Anthropic 必填的 `max_tokens` 沿用默认文本
模型连接的最大输出配置,未配置时使用应用共享协议默认值,不再固定为 8192。
服务端仍可能因自身限制返回长度截断;既有超时、取消、响应大小边界和失败诊断保持不变。
## 术语
- **外部实例**:一个由用户配置的 Dify、FastGPT 或 RAGFlow 服务地址及其认证信息。
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts'
import type {
ResolvedRuntimeSettings,
RuntimeSettingsStore
@@ -14,6 +15,8 @@ function store(
modelName: 'intranet-model',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'none',
modelProfiles: [],
defaultModelProfileId: 'default-model',
...overrides
} as ResolvedRuntimeSettings
return {
@@ -29,6 +32,70 @@ function jsonResponse(payload: unknown, status = 200): Response {
}
describe('createModelGraphExtractor', () => {
it.each([
'openai-chat-completions',
'openai-responses'
] as const)('does not impose an output budget for %s', async (protocol) => {
const fetcher = vi.fn<typeof fetch>(async () =>
jsonResponse({
choices: [{ message: { content: '{"entities":[]}' } }],
output_text: '{"entities":[]}'
})
)
const extract = createModelGraphExtractor(
store({
modelProtocol: protocol,
modelProfiles: [{
id: 'default-model',
name: 'Default model',
baseUrl: 'http://localhost:8000',
modelName: 'intranet-model',
protocol,
authentication: 'none',
maximumOutputTokens: 8192
}]
}),
fetcher
)
await expect(extract('extract this')).resolves.toEqual({ entities: [] })
const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body))
expect(body).not.toHaveProperty('max_tokens')
expect(body).not.toHaveProperty('max_completion_tokens')
expect(body).not.toHaveProperty('max_output_tokens')
})
it.each([undefined, 48_000])(
'uses the Anthropic connection protocol limit (%s), not an extraction budget',
async (maximumOutputTokens) => {
const fetcher = vi.fn<typeof fetch>(async () =>
jsonResponse({ content: [{ type: 'text', text: '{"entities":[]}' }] })
)
const profile = {
name: 'Model',
baseUrl: 'http://localhost:8000',
modelName: 'intranet-model',
protocol: 'anthropic-messages' as const,
authentication: 'none' as const
}
const extract = createModelGraphExtractor(
store({
modelProfiles: [
{ ...profile, id: 'other-model', maximumOutputTokens: 1024 },
{ ...profile, id: 'default-model', maximumOutputTokens }
]
}),
fetcher
)
await expect(extract('extract this')).resolves.toEqual({ entities: [] })
const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body))
expect(body.max_tokens).toBe(
maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens
)
}
)
it('uses an unauthenticated Anthropic endpoint with its path and query', async () => {
const fetcher = vi.fn(async () =>
jsonResponse({
+6 -3
View File
@@ -1,4 +1,5 @@
import type { RuntimeSettingsStore } from '../runtime-settings-store'
import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts'
import {
createOpenAIChatCompletionsUrl,
createOpenAIResponsesUrl
@@ -208,7 +209,6 @@ export function createModelGraphExtractor(
protocol === 'openai-responses'
? {
model: settings.modelName,
max_output_tokens: 8192,
stream: false,
instructions: system,
input: userPrompt
@@ -216,14 +216,17 @@ export function createModelGraphExtractor(
: protocol === 'anthropic-messages'
? {
model: settings.modelName,
max_tokens: 8192,
// Anthropic requires this field; reuse the connection's protocol limit.
max_tokens:
settings.modelProfiles.find(
(profile) => profile.id === settings.defaultModelProfileId
)?.maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens,
stream: false,
system,
messages: [{ role: 'user', content: userPrompt }]
}
: {
model: settings.modelName,
max_tokens: 8192,
stream: false,
messages: [
{ role: 'system', content: system },
+5 -3
View File
@@ -11670,12 +11670,14 @@ function App(): React.JSX.Element {
</strong>
<div>
<button
className="secondary-button"
aria-label={t("chat.images.downloadImage")}
className="icon-button attachment-action"
title={t("chat.images.downloadImage")}
data-tooltip={t("chat.images.downloadImage")}
onClick={() => downloadImage(imageViewerItem)}
type="button"
>
<Download size={14} />
{t("chat.images.downloadImage")}
<Download size={16} aria-hidden="true" />
</button>
<button
aria-label={t("chat.images.closeViewer")}
+8 -4
View File
@@ -769,11 +769,13 @@ function ChatMessageRowView({
aria-label={t('chat.images.downloadNamed', {
title: attachment.name
})}
className="icon-button attachment-action"
title={t('chat.images.downloadImage')}
data-tooltip={t('chat.images.downloadImage')}
onClick={() => onDownloadImage(imageItem)}
type="button"
>
<Download size={12} />
{t('chat.images.download')}
<Download size={16} aria-hidden="true" />
</button>
</span>
)}
@@ -911,6 +913,9 @@ function ChatMessageRowView({
aria-label={t('chat.images.downloadNamed', {
title: artifact.title
})}
className="icon-button attachment-action"
title={t('chat.images.downloadImage')}
data-tooltip={t('chat.images.downloadImage')}
onClick={() =>
onDownloadImage({
src: artifact.content!,
@@ -919,8 +924,7 @@ function ChatMessageRowView({
}
type="button"
>
<Download size={12} />
{t('chat.images.download')}
<Download size={16} aria-hidden="true" />
</button>
</div>
</figure>