Browse Source

feat: 产物与工作区迁移

master
Senor-Liu 1 month ago
parent
commit
04a5c1f192
  1. 2
      web/client/src/sections/superAgent/actions/agentResources.js
  2. 1
      web/client/src/sections/superAgent/container/index.jsx
  3. 178
      web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js
  4. 16
      web/client/src/sections/superAgent/utils/agentApi.js
  5. 9
      web/tests/agentApiAdapter.test.mjs

2
web/client/src/sections/superAgent/actions/agentResources.js

@ -170,6 +170,7 @@ export const getArtifactChapter = (artifactId, version, outlineNodeId) =>
method: "GET",
path: superAgentServiceApiTable.artifactChapter,
params: { artifactId, version, outlineNodeId },
returnMeta: true,
fallbackMessage: "获取章节失败",
});
@ -180,6 +181,7 @@ export const updateArtifactChapter = (artifactId, version, outlineNodeId, body,
params: { artifactId, version, outlineNodeId },
body,
headers: etag ? { "If-Match": etag } : {},
returnMeta: true,
fallbackMessage: "保存章节失败",
});

1
web/client/src/sections/superAgent/container/index.jsx

@ -510,6 +510,7 @@ const SuperAgent = () => {
workspaceOpen,
setWorkspaceOpen,
activeSessionId,
activeConversationId,
currentUserId,
taskStage,
actions,

178
web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js

@ -18,12 +18,13 @@
import { useEffect, useRef, useState } from "react";
import { message } from "antd";
import {
getSuperAgentSessionSectionDirect,
getSuperAgentSessionSectionsDirect,
updateSuperAgentSessionSectionDirect,
} from "../actions/api";
getArtifact,
getArtifactChapter,
listArtifactChapters,
listArtifacts,
updateArtifactChapter,
} from "../actions/agentResources";
import { defaultCatalog, defaultScoreItems, TASK_STAGE } from "../constants";
import { htmlToMarkdown } from "../../bookWriter/utils/htmlToMarkdown";
import { exportToWord } from "../../aiTenderDocument/utils/exportToWord";
import {
buildBodyExportContent,
@ -202,6 +203,7 @@ const useSuperAgentWorkspace = ({
workspaceOpen,
setWorkspaceOpen,
activeSessionId,
activeConversationId,
currentUserId,
taskStage,
actions,
@ -225,6 +227,7 @@ const useSuperAgentWorkspace = ({
const sectionDetailLoadedIdsRef = useRef(new Set());
const [workspaceSectionsLoading, setWorkspaceSectionsLoading] = useState(false);
const activeSessionIdRef = useRef("");
const artifactContextRef = useRef({ artifactId: "", version: 0 });
activeSessionIdRef.current = String(activeSessionId || "");
const isCurrentSession = (sessionId) =>
@ -350,14 +353,15 @@ const useSuperAgentWorkspace = ({
}) => {
const sectionId = getSectionId(section) || `section_${index + 1}`;
try {
const artifactId = section.artifactId || section.artifact_id || artifactContextRef.current.artifactId;
const artifactVersion = Number(
section.artifactVersion || section.artifact_version || artifactContextRef.current.version
);
if (!artifactId || !artifactVersion) return section;
const requestKey = `${sessionId}_${sectionId}_${currentUserId}`;
let sectionRequest = sectionDetailRequestMap.get(requestKey);
if (forceFresh || !sectionRequest) {
sectionRequest = getSuperAgentSessionSectionDirect({
sessionId,
sectionId,
userId: currentUserId,
}).finally(() => {
sectionRequest = getArtifactChapter(artifactId, artifactVersion, sectionId).finally(() => {
sectionDetailRequestMap.delete(requestKey);
});
if (!forceFresh) {
@ -366,11 +370,15 @@ const useSuperAgentWorkspace = ({
}
const sectionResult = await sectionRequest;
const sectionPayload = sectionResult?.data || sectionResult || {};
const sectionData = sectionPayload.section || sectionPayload;
const sectionData = sectionPayload.chapter || sectionPayload;
return {
...section,
...sectionData,
id: sectionId,
artifactId,
artifactVersion,
chapterVersion: Number(sectionData.version || section.chapterVersion || 0),
etag: sectionResult?.headers?.get?.("ETag") || section.etag || "",
title:
sectionData.title ||
section.title ||
@ -399,13 +407,44 @@ const useSuperAgentWorkspace = ({
forceFresh = false,
loadDetails = true,
}) => {
const sectionsResult = await getSuperAgentSessionSectionsDirect({
sessionId,
userId: currentUserId,
includeContent: 0,
});
const sectionsData = sectionsResult?.data || sectionsResult;
const sectionList = getSessionSectionList(sectionsData);
const resourceConversationId = String(
activeConversationId || workspaceData?.conversationId || ""
);
if (!resourceConversationId) return fallbackSections;
const artifactPage = await listArtifacts(resourceConversationId, { limit: 100 });
const artifactItems = Array.isArray(artifactPage?.items) ? artifactPage.items : [];
const selectedArtifact = artifactItems
.filter((item) => item?.id && Number(item.version) > 0)
.sort((left, right) =>
String(right.updated_at || right.updatedAt || "").localeCompare(
String(left.updated_at || left.updatedAt || "")
)
)[0];
if (!selectedArtifact) return fallbackSections;
const artifactDetail = await getArtifact(
String(selectedArtifact.id),
Number(selectedArtifact.version)
);
artifactContextRef.current = {
artifactId: String(selectedArtifact.id),
version: Number(selectedArtifact.version),
title: artifactDetail?.title || selectedArtifact.title || "",
};
const chapterResponse = await listArtifactChapters(
artifactContextRef.current.artifactId,
artifactContextRef.current.version
);
const sectionList = (chapterResponse?.items || []).map((chapter, index) => ({
...chapter,
id: chapter.outline_node_id,
section_id: chapter.outline_node_id,
title: chapter.title || `正文${index + 1}`,
content: chapter.content || "",
artifactId: String(chapter.artifact_id || artifactContextRef.current.artifactId),
artifactVersion: Number(chapter.artifact_version || artifactContextRef.current.version),
chapterVersion: Number(chapter.version || 0),
etag: `"${Number(chapter.version || 1)}"`,
}));
const sourceSections = sectionList.length
? mergeSectionContentList(fallbackSections, sectionList)
: fallbackSections;
@ -1035,6 +1074,40 @@ const useSuperAgentWorkspace = ({
sectionBaselineContentRef.current.clear();
};
const refreshSectionAfterConflict = async (sessionId, section) => {
try {
const refreshedSection = await loadSessionSectionDetail({
sessionId,
section,
forceFresh: true,
});
const sectionId = getSectionId(section, "");
dirtySectionIdsRef.current.delete(sectionId);
setWorkspaceBodyList((previousList) =>
mergeSectionContentList(previousList, [refreshedSection])
);
setWorkspaceData((previousData) => {
if (!previousData || previousData.sessionId !== sessionId) return previousData;
const nextSections = mergeSectionContentList(previousData.sections, [refreshedSection]);
return {
...previousData,
sections: nextSections,
payload: {
...(previousData.payload || {}),
sections: nextSections,
chapters: nextSections,
},
};
});
} catch (refreshError) {
superAgentWarn("workspace.conflict", "refresh_failed", {
sessionId,
sectionId: getSectionId(section, ""),
error: refreshError?.message,
});
}
};
useEffect(() => {
if (!activeSessionId) return;
closeWorkspace();
@ -1164,18 +1237,28 @@ const useSuperAgentWorkspace = ({
const savedIds = new Set();
for (const item of sectionsToPersist) {
const sectionId = getSectionId(item, "");
const content = htmlToMarkdown(item.content || "");
const content = String(item.content || "");
try {
if (!sectionId) throw new Error("章节缺少 section_id");
await updateSuperAgentSessionSectionDirect({
sessionId,
const artifactId = item.artifactId || item.artifact_id || artifactContextRef.current.artifactId;
const artifactVersion = Number(
item.artifactVersion || item.artifact_version || artifactContextRef.current.version
);
const etag = item.etag || `"${Number(item.chapterVersion || item.version || 1)}"`;
if (!artifactId || !artifactVersion) throw new Error("章节缺少 artifact 版本信息");
const savedResult = await updateArtifactChapter(
artifactId,
artifactVersion,
sectionId,
userId: currentUserId,
content,
status: "edited",
title: item.title || item.name || "",
order: getSectionOrder(item, savedCount + 1),
});
{
title: item.title || item.name || undefined,
content,
},
etag
);
const savedData = savedResult?.data || {};
item.chapterVersion = Number(savedData.version || item.chapterVersion || 0);
item.etag = savedResult?.headers?.get?.("ETag") || `"${item.chapterVersion}"`;
savedCount += 1;
savedIds.add(sectionId);
rememberSavedSectionContent(sessionId, item);
@ -1186,6 +1269,10 @@ const useSuperAgentWorkspace = ({
);
} catch (error) {
failedCount += 1;
if (Number(error?.status) === 412) {
await refreshSectionAfterConflict(sessionId, item);
message.warning("章节已被智能体更新,请刷新后重新编辑");
}
superAgentWarn("workspace.save", "sections.persist.item_failed", {
traceId,
sessionId,
@ -1456,15 +1543,25 @@ const useSuperAgentWorkspace = ({
}
try {
await updateSuperAgentSessionSectionDirect({
sessionId,
const artifactId = artifactContextRef.current.artifactId;
const artifactVersion = artifactContextRef.current.version;
const currentSection = (workspaceData?.sections || []).find(
(item) => getSectionId(item, "") === sectionId
);
const chapterId = currentSection?.artifactId || currentSection?.artifact_id || artifactId;
const chapterVersion = Number(
currentSection?.artifactVersion || currentSection?.artifact_version || artifactVersion
);
const etag = currentSection?.etag || `"${Number(currentSection?.chapterVersion || currentSection?.version || 1)}"`;
if (!chapterId || !chapterVersion) throw new Error("章节缺少 artifact 版本信息");
const savedResult = await updateArtifactChapter(
chapterId,
chapterVersion,
sectionId,
userId: currentUserId,
content: htmlToMarkdown(content || ""),
status: "edited",
title: title || "",
order,
});
{ title: title || undefined, content: String(content || "") },
etag
);
const savedData = savedResult?.data || {};
rememberSavedSectionContent(sessionId, {
id: sectionId,
content,
@ -1473,6 +1570,10 @@ const useSuperAgentWorkspace = ({
id: sectionId,
section_id: sectionId,
sectionId,
artifactId: chapterId,
artifactVersion: chapterVersion,
chapterVersion: Number(savedData.version || currentSection?.chapterVersion || 0),
etag: savedResult?.headers?.get?.("ETag") || `"${Number(savedData.version || currentSection?.chapterVersion || 1)}"`,
content,
status: "edited",
title: title || "",
@ -1557,7 +1658,12 @@ const useSuperAgentWorkspace = ({
sectionId,
error,
});
message.error(error?.message || "章节保存失败,请稍后重试");
if (Number(error?.status) === 412) {
await refreshSectionAfterConflict(sessionId, { id: sectionId });
message.warning("章节已被智能体更新,请刷新后重新编辑");
} else {
message.error(error?.message || "章节保存失败,请稍后重试");
}
return false;
}
};

16
web/client/src/sections/superAgent/utils/agentApi.js

@ -147,13 +147,19 @@ export const buildSuperAgentHeaders = async (extraHeaders = {}) => {
};
};
const parseAgentResponse = async (response, fallbackMessage) => {
if (response.status === 204) return null;
const parseAgentResponse = async (response, fallbackMessage, returnMeta = false) => {
if (response.status === 204) {
return returnMeta
? { data: null, headers: response.headers, status: response.status }
: null;
}
const data = await readJsonSafe(response);
if (!response.ok) {
throw parseAgentErrorPayload(data, response.status, fallbackMessage);
}
return data;
return returnMeta
? { data, headers: response.headers, status: response.status }
: data;
};
export const requestAgentJson = async ({
@ -168,6 +174,7 @@ export const requestAgentJson = async ({
idempotencyKey,
retryOnUnauthorized = true,
service = superAgentBusinessService,
returnMeta = false,
} = {}) => {
const resolvedPath = buildSuperAgentPath(path, params);
const search = new URLSearchParams();
@ -213,10 +220,11 @@ export const requestAgentJson = async ({
idempotencyKey: key,
retryOnUnauthorized: false,
service,
returnMeta,
});
}
return parseAgentResponse(response, fallbackMessage);
return parseAgentResponse(response, fallbackMessage, returnMeta);
};
export const withAgentAuthorization = async (init = {}) => {

9
web/tests/agentApiAdapter.test.mjs

@ -91,6 +91,7 @@ test("adapter source exchanges login token for agent jwt and omits identity head
const config = read("config.cjs");
const chatHook = read("client", "src", "sections", "superAgent", "hooks", "useSuperAgentChat.js");
const uploadUtils = read("client", "src", "sections", "superAgent", "utils", "attachmentUpload.js");
const workspace = read("client", "src", "sections", "superAgent", "hooks", "useSuperAgentWorkspace.js");
assert.doesNotMatch(table, /\/api\/v1\/tasks/);
assert.match(agentApi, /ACCESS_TOKEN_PATH = "\/_api\/super-agent\/access-token"/);
assert.match(agentApi, /Authorization: `Bearer \$\{token\}`/);
@ -114,6 +115,14 @@ test("adapter source exchanges login token for agent jwt and omits identity head
assert.match(agentApi, /getSuperAgentFrameworkApiUrl/);
assert.match(resources, /completeAttachmentUpload/);
assert.match(resources, /listAttachments/);
assert.match(resources, /getArtifactChapter/);
assert.match(resources, /returnMeta: true/);
assert.match(workspace, /listArtifacts\(resourceConversationId/);
assert.match(workspace, /listArtifactChapters/);
assert.match(workspace, /updateArtifactChapter/);
assert.match(resources, /If-Match/);
assert.doesNotMatch(workspace, /getSuperAgentSessionSectionsDirect/);
assert.doesNotMatch(workspace, /updateSuperAgentSessionSectionDirect/);
assert.match(chatHook, /createAttachmentUploadIntent\(conversationId/);
assert.match(chatHook, /completeAttachmentUpload\(uploadIntent\.attachmentId\)/);
assert.match(chatHook, /listAttachments\(conversationId\)/);

Loading…
Cancel
Save