Files
deerflow-code/frontend-web/src/strategy-components/api/enterprise-research.ts
T
吉鑫升 3ca5528640 feat(knowledge): support standalone file ingestion and configurable wiki entry
Use neutral knowledge-service labels and sanitize user-facing task errors.
2026-09-07 17:07:27 +08:00

256 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { userFacingKnowledgeMessage } from "@/lib/knowledge-message";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
const API = () => `${getBackendBaseURL()}/api/enterprise-research`;
/**
* Enterprise research snapshots raw WeKnora evidence chunks. This is
* intentionally separate from LlmWikiSearchResult, whose public search shape
* now represents processed Wiki pages only.
*/
export interface EnterpriseResearchEvidence {
chunk_id: string;
content: string;
knowledge_id: string;
knowledge_base_id: string;
knowledge_base_name: string;
title: string;
filename: string;
score: number;
chunk_index: number;
url: string;
}
export type EnterpriseResearchTaskStatus = "draft" | "collecting" | "ready" | "partial" | "failed";
export interface EnterpriseResearchTask {
id: string;
subject: string;
focus: string;
template_id: string;
knowledge_base_ids: string[];
queries: string[];
selected_source_ids: string[];
sources: EnterpriseResearchEvidence[];
plan_markdown?: string | null;
report_markdown?: string | null;
report_summary?: string | null;
report_model_name?: string | null;
active_report_job_id?: string | null;
status: EnterpriseResearchTaskStatus;
warning?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export type EnterpriseResearchReportJobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
export interface EnterpriseResearchReportJob {
id: string;
task_id: string;
status: EnterpriseResearchReportJobStatus;
phase: string;
model_name?: string | null;
action: "initial" | "rewrite" | "followup";
instruction?: string | null;
style?: string | null;
target_length?: number | null;
original_report?: string | null;
report_markdown: string;
report_summary?: string | null;
error_message?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export type EnterpriseResearchReportEvent =
| { type: "snapshot"; job: EnterpriseResearchReportJob }
| { type: "phase"; phase: string; status: EnterpriseResearchReportJobStatus }
| { type: "report_delta"; delta: string }
| { type: "reasoning_delta"; delta: string }
| { type: "completed" | "failed" | "cancelled"; job: EnterpriseResearchReportJob | null }
| { type: "heartbeat"; status: EnterpriseResearchReportJobStatus; phase: string };
export interface CreateEnterpriseResearchTaskInput {
subject: string;
focus: string;
template_id: string;
knowledge_base_ids: string[];
queries: string[];
}
async function readJson<T>(response: Response, context: string): Promise<T> {
if (!response.ok) {
const payload = await response.json().catch(() => null as unknown);
const detail = payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail: unknown }).detail)
: `${context}失败(${response.status})`;
throw new Error(userFacingKnowledgeMessage(detail));
}
return response.json() as Promise<T>;
}
export async function listEnterpriseResearchTasks(limit = 20): Promise<EnterpriseResearchTask[]> {
const payload = await readJson<{ tasks?: EnterpriseResearchTask[] }>(
await apiFetch(`${API()}/tasks?limit=${encodeURIComponent(limit)}`),
"读取研究任务",
);
return payload.tasks ?? [];
}
export async function createEnterpriseResearchTask(input: CreateEnterpriseResearchTaskInput): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
}),
"创建研究任务",
);
}
export async function collectEnterpriseResearchTask(taskId: string): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/collect`, { method: "POST" }),
"收集研究证据",
);
}
export async function createEnterpriseResearchPlanDraft(taskId: string): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/plan/draft`, { method: "POST" }),
"生成研究计划",
);
}
export async function createEnterpriseResearchAiPlan(
taskId: string,
input: { model_name?: string | null; instruction?: string } = {},
): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/plan/ai`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
}),
"AI 优化研究计划",
);
}
export async function updateEnterpriseResearchPlan(
taskId: string,
input: { plan_markdown: string; selected_source_ids: string[] },
): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/plan`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
}),
"保存研究计划",
);
}
export async function createEnterpriseResearchReportJob(
taskId: string,
input: {
model_name?: string | null;
action?: "initial" | "rewrite" | "followup";
instruction?: string;
style?: "formal" | "analytical" | "concise";
target_length?: number | null;
} = {},
): Promise<EnterpriseResearchReportJob> {
return readJson<EnterpriseResearchReportJob>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/report-jobs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
}),
"创建报告写作任务",
);
}
export async function cancelEnterpriseResearchReportJob(
taskId: string,
jobId: string,
): Promise<EnterpriseResearchReportJob> {
return readJson<EnterpriseResearchReportJob>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/report-jobs/${encodeURIComponent(jobId)}/cancel`, {
method: "POST",
}),
"取消报告任务",
);
}
export async function restoreEnterpriseResearchReportVersion(
taskId: string,
jobId: string,
): Promise<EnterpriseResearchTask> {
return readJson<EnterpriseResearchTask>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/report-jobs/${encodeURIComponent(jobId)}/restore`, {
method: "POST",
}),
"恢复报告版本",
);
}
export async function listEnterpriseResearchReportJobs(taskId: string): Promise<EnterpriseResearchReportJob[]> {
const payload = await readJson<{ jobs?: EnterpriseResearchReportJob[] }>(
await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}/report-jobs`),
"读取报告写作任务",
);
return payload.jobs ?? [];
}
/** Parse the endpoint's standard SSE frames without introducing an EventSource auth gap. */
export async function streamEnterpriseResearchReportJob(
taskId: string,
jobId: string,
handlers: { onEvent: (event: EnterpriseResearchReportEvent) => void; signal?: AbortSignal },
): Promise<void> {
const response = await apiFetch(
`${API()}/tasks/${encodeURIComponent(taskId)}/report-jobs/${encodeURIComponent(jobId)}/stream`,
{ headers: { Accept: "text/event-stream" }, signal: handlers.signal },
);
if (!response.ok || !response.body) {
await readJson(response, "连接报告流");
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let pending = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
pending += decoder.decode(value, { stream: true });
const frames = pending.split("\n\n");
pending = frames.pop() ?? "";
for (const frame of frames) {
const dataLine = frame.split("\n").find((line) => line.startsWith("data: "));
if (!dataLine) continue;
try {
handlers.onEvent(JSON.parse(dataLine.slice(6)) as EnterpriseResearchReportEvent);
} catch {
// One malformed frame must not discard an otherwise healthy stream.
}
}
}
} finally {
reader.releaseLock();
}
}
export async function deleteEnterpriseResearchTask(taskId: string): Promise<void> {
const response = await apiFetch(`${API()}/tasks/${encodeURIComponent(taskId)}`, { method: "DELETE" });
if (!response.ok) {
const payload = await response.json().catch(() => null as unknown);
const detail = payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail: unknown }).detail)
: `删除研究任务失败(${response.status})`;
throw new Error(userFacingKnowledgeMessage(detail));
}
}