feat: 知识批量沉淀改为后台任务,避免 nginx 504

- /ingest/threads/batch 新增 background 参数(默认 true),立即返回 {queued:true}
- 抽取循环抽出 _run_batch_ingest,后台路径显式传入 created_by
- 前端知识库页与输入框、线程 hooks/types 配套调整
- config.yaml 上下文压缩测试值

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-16 21:07:17 +08:00
1 parent b380575f16
commit 342fa8265a
12 files changed
+170 -22

No files matched your search

@@ -10,6 +10,7 @@ import {
LightbulbIcon,
PaperclipIcon,
PlusIcon,
ShrinkIcon,
SparklesIcon,
RocketIcon,
XIcon,
@@ -329,6 +330,14 @@ export function InputBox({
setLocalSettings("display", { ragModeEnabled: !ragModeEnabled });
}, [ragModeEnabled, ragModeLocked, setLocalSettings]);
const contextCompressionEnabled =
localSettings.display.contextCompressionEnabled ?? false;
const handleContextCompressionToggle = useCallback(() => {
setLocalSettings("display", {
contextCompressionEnabled: !contextCompressionEnabled,
});
}, [contextCompressionEnabled, setLocalSettings]);
const handleSubmit = useCallback(
async (message: PromptInputMessage) => {
if (status === "streaming") {
@@ -735,6 +744,35 @@ export function InputBox({
<span className="text-xs font-normal">参考文献</span>
</PromptInputButton>
</Tooltip>
<Tooltip
content={
contextCompressionEnabled
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
: "上下文压缩已关闭:点击开启"
}
>
<PromptInputButton
type="button"
aria-pressed={contextCompressionEnabled}
disabled={disabled}
className={cn(
"gap-1.5! rounded-full px-3! shadow-none",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
contextCompressionEnabled
? "border border-violet-300 bg-violet-50 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200"
: compact
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleContextCompressionToggle}
>
{contextCompressionEnabled && (
<CheckIcon className="size-3 text-violet-700 dark:text-violet-200" />
)}
<ShrinkIcon className="size-3" />
<span className="text-xs font-normal">上下文压缩</span>
</PromptInputButton>
</Tooltip>
<PromptInputActionMenu>
<ModeHoverGuide
mode={
+9
View File
@@ -147,6 +147,13 @@ export interface BatchIngestPayload {
include_sources?: boolean;
skip_existing?: boolean;
mode?: "summary" | "full";
/**
* When true (default), the backend runs the per-thread extraction loop in a
* background task and returns immediately (`queued:true`) instead of blocking
* the request until every thread is processed — which otherwise outlives the
* nginx gateway timeout and surfaces as a generic "request failed".
*/
background?: boolean;
/** Optional model override for knowledge extraction (null = configured default). */
model_name?: string | null;
/** Optional extraction-prompt template id (null = scope default). */
@@ -157,6 +164,8 @@ export interface BatchIngestPayload {
export interface BatchIngestResult {
ok: boolean;
/** True when the backend queued the batch as a background task (see payload). */
queued?: boolean;
processed: number;
created: number;
skipped: number;
+7
View File
@@ -7,6 +7,7 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
display: {
streamSpeedMeterEnabled: true,
ragModeEnabled: false,
contextCompressionEnabled: false,
citationCleanupWords: [],
fontScale: 1,
},
@@ -43,6 +44,12 @@ export interface LocalSettings {
* messages render tool results with the legacy generic search UI only.
*/
ragModeEnabled: boolean;
/**
* Per-user switch for context compression (上下文压缩). When enabled, long
* conversations are summarized transiently before the model call to save
* tokens. Default off; toggled from the chat input box (below 参考文献).
*/
contextCompressionEnabled: boolean;
/**
* Admin-maintained words or short phrases removed from reference snippets
* and dialogs, one entry per line in the settings UI.
+4
View File
@@ -664,6 +664,9 @@ export function useThreadStream({
lockThreadRagMode(threadId);
}
const contextCompressionEnabled =
localSettings.display.contextCompressionEnabled ?? false;
const mergedContext = {
...context,
...extraContext,
@@ -715,6 +718,7 @@ export function useThreadStream({
memory_injection_enabled:
mergedContext.memory_injection_enabled ?? true,
rag_mode_enabled: ragModeEnabled,
summarization_enabled: contextCompressionEnabled,
thread_id: threadId,
},
},
+2
View File
@@ -20,6 +20,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
writing_artifact_path?: string;
memory_injection_enabled?: boolean;
rag_mode_enabled?: boolean;
/** Per-request switch for context compression (上下文压缩). Default off. */
summarization_enabled?: boolean;
/** Per-conversation switch for knowledge-base RAG injection (phase 2). */
knowledge_rag_enabled?: boolean;
agent_id?: string;
+12 -1
View File
@@ -176,10 +176,21 @@ function BatchImportDialog({
skip_existing: skipExisting,
status: "approved",
include_sources: true,
// Run in the background so the request returns immediately — a large
// batch otherwise blocks past the gateway timeout and shows "request
// failed". Threads stream into the list as they finish.
background: true,
model_name: model,
template_id: template,
folder,
});
if (result.queued) {
toast.success(
`已转入后台导入 ${result.processed} 个线程,完成后会陆续出现在左侧列表(可稍后刷新查看)`,
);
onOpenChange(false);
return;
}
const base = `批量导入完成:新增 ${result.created},跳过 ${result.skipped},失败 ${result.failed}`;
if (result.llm_fallback && result.llm_fallback > 0) {
// The model failed on some threads — they were saved via rule-based
@@ -549,7 +560,7 @@ export default function KnowledgeBasePage() {
keyword: debouncedKeyword || undefined,
source_type: sourceType === "all" ? undefined : sourceType,
status: status === "all" ? undefined : status,
limit: 500,
limit: 1000,
}),
[debouncedKeyword, sourceType, status],
);