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

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

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

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

@ -18,12 +18,13 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { message } from "antd"; import { message } from "antd";
import { import {
getSuperAgentSessionSectionDirect, getArtifact,
getSuperAgentSessionSectionsDirect, getArtifactChapter,
updateSuperAgentSessionSectionDirect, listArtifactChapters,
} from "../actions/api"; listArtifacts,
updateArtifactChapter,
} from "../actions/agentResources";
import { defaultCatalog, defaultScoreItems, TASK_STAGE } from "../constants"; import { defaultCatalog, defaultScoreItems, TASK_STAGE } from "../constants";
import { htmlToMarkdown } from "../../bookWriter/utils/htmlToMarkdown";
import { exportToWord } from "../../aiTenderDocument/utils/exportToWord"; import { exportToWord } from "../../aiTenderDocument/utils/exportToWord";
import { import {
buildBodyExportContent, buildBodyExportContent,
@ -202,6 +203,7 @@ const useSuperAgentWorkspace = ({
workspaceOpen, workspaceOpen,
setWorkspaceOpen, setWorkspaceOpen,
activeSessionId, activeSessionId,
activeConversationId,
currentUserId, currentUserId,
taskStage, taskStage,
actions, actions,
@ -225,6 +227,7 @@ const useSuperAgentWorkspace = ({
const sectionDetailLoadedIdsRef = useRef(new Set()); const sectionDetailLoadedIdsRef = useRef(new Set());
const [workspaceSectionsLoading, setWorkspaceSectionsLoading] = useState(false); const [workspaceSectionsLoading, setWorkspaceSectionsLoading] = useState(false);
const activeSessionIdRef = useRef(""); const activeSessionIdRef = useRef("");
const artifactContextRef = useRef({ artifactId: "", version: 0 });
activeSessionIdRef.current = String(activeSessionId || ""); activeSessionIdRef.current = String(activeSessionId || "");
const isCurrentSession = (sessionId) => const isCurrentSession = (sessionId) =>
@ -350,14 +353,15 @@ const useSuperAgentWorkspace = ({
}) => { }) => {
const sectionId = getSectionId(section) || `section_${index + 1}`; const sectionId = getSectionId(section) || `section_${index + 1}`;
try { 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}`; const requestKey = `${sessionId}_${sectionId}_${currentUserId}`;
let sectionRequest = sectionDetailRequestMap.get(requestKey); let sectionRequest = sectionDetailRequestMap.get(requestKey);
if (forceFresh || !sectionRequest) { if (forceFresh || !sectionRequest) {
sectionRequest = getSuperAgentSessionSectionDirect({ sectionRequest = getArtifactChapter(artifactId, artifactVersion, sectionId).finally(() => {
sessionId,
sectionId,
userId: currentUserId,
}).finally(() => {
sectionDetailRequestMap.delete(requestKey); sectionDetailRequestMap.delete(requestKey);
}); });
if (!forceFresh) { if (!forceFresh) {
@ -366,11 +370,15 @@ const useSuperAgentWorkspace = ({
} }
const sectionResult = await sectionRequest; const sectionResult = await sectionRequest;
const sectionPayload = sectionResult?.data || sectionResult || {}; const sectionPayload = sectionResult?.data || sectionResult || {};
const sectionData = sectionPayload.section || sectionPayload; const sectionData = sectionPayload.chapter || sectionPayload;
return { return {
...section, ...section,
...sectionData, ...sectionData,
id: sectionId, id: sectionId,
artifactId,
artifactVersion,
chapterVersion: Number(sectionData.version || section.chapterVersion || 0),
etag: sectionResult?.headers?.get?.("ETag") || section.etag || "",
title: title:
sectionData.title || sectionData.title ||
section.title || section.title ||
@ -399,13 +407,44 @@ const useSuperAgentWorkspace = ({
forceFresh = false, forceFresh = false,
loadDetails = true, loadDetails = true,
}) => { }) => {
const sectionsResult = await getSuperAgentSessionSectionsDirect({ const resourceConversationId = String(
sessionId, activeConversationId || workspaceData?.conversationId || ""
userId: currentUserId, );
includeContent: 0, if (!resourceConversationId) return fallbackSections;
}); const artifactPage = await listArtifacts(resourceConversationId, { limit: 100 });
const sectionsData = sectionsResult?.data || sectionsResult; const artifactItems = Array.isArray(artifactPage?.items) ? artifactPage.items : [];
const sectionList = getSessionSectionList(sectionsData); 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 const sourceSections = sectionList.length
? mergeSectionContentList(fallbackSections, sectionList) ? mergeSectionContentList(fallbackSections, sectionList)
: fallbackSections; : fallbackSections;
@ -1035,6 +1074,40 @@ const useSuperAgentWorkspace = ({
sectionBaselineContentRef.current.clear(); 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(() => { useEffect(() => {
if (!activeSessionId) return; if (!activeSessionId) return;
closeWorkspace(); closeWorkspace();
@ -1164,18 +1237,28 @@ const useSuperAgentWorkspace = ({
const savedIds = new Set(); const savedIds = new Set();
for (const item of sectionsToPersist) { for (const item of sectionsToPersist) {
const sectionId = getSectionId(item, ""); const sectionId = getSectionId(item, "");
const content = htmlToMarkdown(item.content || ""); const content = String(item.content || "");
try { try {
if (!sectionId) throw new Error("章节缺少 section_id"); if (!sectionId) throw new Error("章节缺少 section_id");
await updateSuperAgentSessionSectionDirect({ const artifactId = item.artifactId || item.artifact_id || artifactContextRef.current.artifactId;
sessionId, 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, sectionId,
userId: currentUserId, {
content, title: item.title || item.name || undefined,
status: "edited", content,
title: item.title || item.name || "", },
order: getSectionOrder(item, savedCount + 1), etag
}); );
const savedData = savedResult?.data || {};
item.chapterVersion = Number(savedData.version || item.chapterVersion || 0);
item.etag = savedResult?.headers?.get?.("ETag") || `"${item.chapterVersion}"`;
savedCount += 1; savedCount += 1;
savedIds.add(sectionId); savedIds.add(sectionId);
rememberSavedSectionContent(sessionId, item); rememberSavedSectionContent(sessionId, item);
@ -1186,6 +1269,10 @@ const useSuperAgentWorkspace = ({
); );
} catch (error) { } catch (error) {
failedCount += 1; failedCount += 1;
if (Number(error?.status) === 412) {
await refreshSectionAfterConflict(sessionId, item);
message.warning("章节已被智能体更新,请刷新后重新编辑");
}
superAgentWarn("workspace.save", "sections.persist.item_failed", { superAgentWarn("workspace.save", "sections.persist.item_failed", {
traceId, traceId,
sessionId, sessionId,
@ -1456,15 +1543,25 @@ const useSuperAgentWorkspace = ({
} }
try { try {
await updateSuperAgentSessionSectionDirect({ const artifactId = artifactContextRef.current.artifactId;
sessionId, 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, sectionId,
userId: currentUserId, { title: title || undefined, content: String(content || "") },
content: htmlToMarkdown(content || ""), etag
status: "edited", );
title: title || "", const savedData = savedResult?.data || {};
order,
});
rememberSavedSectionContent(sessionId, { rememberSavedSectionContent(sessionId, {
id: sectionId, id: sectionId,
content, content,
@ -1473,6 +1570,10 @@ const useSuperAgentWorkspace = ({
id: sectionId, id: sectionId,
section_id: sectionId, section_id: sectionId,
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, content,
status: "edited", status: "edited",
title: title || "", title: title || "",
@ -1557,7 +1658,12 @@ const useSuperAgentWorkspace = ({
sectionId, sectionId,
error, error,
}); });
message.error(error?.message || "章节保存失败,请稍后重试"); if (Number(error?.status) === 412) {
await refreshSectionAfterConflict(sessionId, { id: sectionId });
message.warning("章节已被智能体更新,请刷新后重新编辑");
} else {
message.error(error?.message || "章节保存失败,请稍后重试");
}
return false; 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) => { const parseAgentResponse = async (response, fallbackMessage, returnMeta = false) => {
if (response.status === 204) return null; if (response.status === 204) {
return returnMeta
? { data: null, headers: response.headers, status: response.status }
: null;
}
const data = await readJsonSafe(response); const data = await readJsonSafe(response);
if (!response.ok) { if (!response.ok) {
throw parseAgentErrorPayload(data, response.status, fallbackMessage); throw parseAgentErrorPayload(data, response.status, fallbackMessage);
} }
return data; return returnMeta
? { data, headers: response.headers, status: response.status }
: data;
}; };
export const requestAgentJson = async ({ export const requestAgentJson = async ({
@ -168,6 +174,7 @@ export const requestAgentJson = async ({
idempotencyKey, idempotencyKey,
retryOnUnauthorized = true, retryOnUnauthorized = true,
service = superAgentBusinessService, service = superAgentBusinessService,
returnMeta = false,
} = {}) => { } = {}) => {
const resolvedPath = buildSuperAgentPath(path, params); const resolvedPath = buildSuperAgentPath(path, params);
const search = new URLSearchParams(); const search = new URLSearchParams();
@ -213,10 +220,11 @@ export const requestAgentJson = async ({
idempotencyKey: key, idempotencyKey: key,
retryOnUnauthorized: false, retryOnUnauthorized: false,
service, service,
returnMeta,
}); });
} }
return parseAgentResponse(response, fallbackMessage); return parseAgentResponse(response, fallbackMessage, returnMeta);
}; };
export const withAgentAuthorization = async (init = {}) => { 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 config = read("config.cjs");
const chatHook = read("client", "src", "sections", "superAgent", "hooks", "useSuperAgentChat.js"); const chatHook = read("client", "src", "sections", "superAgent", "hooks", "useSuperAgentChat.js");
const uploadUtils = read("client", "src", "sections", "superAgent", "utils", "attachmentUpload.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.doesNotMatch(table, /\/api\/v1\/tasks/);
assert.match(agentApi, /ACCESS_TOKEN_PATH = "\/_api\/super-agent\/access-token"/); assert.match(agentApi, /ACCESS_TOKEN_PATH = "\/_api\/super-agent\/access-token"/);
assert.match(agentApi, /Authorization: `Bearer \$\{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(agentApi, /getSuperAgentFrameworkApiUrl/);
assert.match(resources, /completeAttachmentUpload/); assert.match(resources, /completeAttachmentUpload/);
assert.match(resources, /listAttachments/); 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, /createAttachmentUploadIntent\(conversationId/);
assert.match(chatHook, /completeAttachmentUpload\(uploadIntent\.attachmentId\)/); assert.match(chatHook, /completeAttachmentUpload\(uploadIntent\.attachmentId\)/);
assert.match(chatHook, /listAttachments\(conversationId\)/); assert.match(chatHook, /listAttachments\(conversationId\)/);

Loading…
Cancel
Save