代码提交

This commit is contained in:
董宏凯 committed 2026-07-28 19:17:40 +08:00
1 parent 151ecceb85
commit 816c46d447
6 files changed
+140 -26

No files matched your search

@@ -130,10 +130,86 @@ interface ServerSession {
nodes?: ServerNode[];
}
const BASE = () => `${getBackendBaseURL()}/api/position-roundtable`;
const PRIMARY_API_PATH = "/api/position-roundtable";
const COMPAT_API_PATH = "/api/multi-agent/position-roundtable";
const BASE = () => `${getBackendBaseURL()}${PRIMARY_API_PATH}`;
/**
* Some intranet gateways only forward the long-established `/api/multi-agent`
* namespace. The position workspace was added later under its own top-level
* prefix, so a frontend-only release can work locally but receive FastAPI's
* route-level `{"detail":"Not Found"}` after deployment.
*
* Retry only that exact "route is absent" response against the compatibility
* namespace. A domain 404 such as "Position roundtable session not found"
* remains a real 404 and is never hidden by this routing fallback.
*/
let resolvedApiPath: typeof PRIMARY_API_PATH | typeof COMPAT_API_PATH | null = null;
function replaceApiPath(url: string, apiPath: string): string {
return url.replace(PRIMARY_API_PATH, apiPath);
}
async function isMissingRoute(response: Response): Promise<boolean> {
if (response.status !== 404) return false;
const raw = await response.clone().text().catch(() => "");
try {
const parsed = JSON.parse(raw) as { detail?: unknown };
return parsed?.detail === "Not Found";
} catch {
const normalized = raw.trim();
return (
normalized === "Not Found" ||
/<title>\s*404 Not Found\s*<\/title>/i.test(normalized)
);
}
}
async function positionApiFetch(
input: string,
init?: RequestInit,
): Promise<Response> {
if (resolvedApiPath === COMPAT_API_PATH) {
const compatibilityResponse = await apiFetch(
replaceApiPath(input, COMPAT_API_PATH),
init,
);
if (!(await isMissingRoute(compatibilityResponse))) {
return compatibilityResponse;
}
// A gateway may be reloaded while the SPA remains open. Re-probe the
// canonical route instead of pinning the tab to a now-stale route choice.
resolvedApiPath = null;
}
const primaryResponse = await apiFetch(input, init);
if (!(await isMissingRoute(primaryResponse))) {
resolvedApiPath = PRIMARY_API_PATH;
return primaryResponse;
}
const compatibilityResponse = await apiFetch(
replaceApiPath(input, COMPAT_API_PATH),
init,
);
if (!(await isMissingRoute(compatibilityResponse))) {
resolvedApiPath = COMPAT_API_PATH;
return compatibilityResponse;
}
// Preserve the canonical response/error when neither route exists. This
// keeps diagnostics stable and identifies an outdated backend release.
return primaryResponse;
}
async function readJson<T>(response: Response, label: string): Promise<T> {
if (!response.ok) {
if (await isMissingRoute(response)) {
throw new Error(
`${label} failed (${response.status}): 岗位会商会话接口尚未部署或未被网关转发;` +
`已尝试 ${PRIMARY_API_PATH} 与 ${COMPAT_API_PATH}`,
);
}
const detail = await response.text().catch(() => "");
throw new Error(`${label} failed (${response.status}): ${detail}`);
}
@@ -219,7 +295,7 @@ function toSession(session: ServerSession): PositionRoundtableSession {
export async function listPositionSessions(externalTaskId?: string): Promise<PositionRoundtableSession[]> {
const params = externalTaskId ? `?external_task_id=${encodeURIComponent(externalTaskId)}` : "";
const result = await readJson<ServerSession[]>(
await apiFetch(`${BASE()}/sessions${params}`),
await positionApiFetch(`${BASE()}/sessions${params}`),
"list position sessions",
);
return result.map(toSession);
@@ -230,7 +306,7 @@ export async function createPositionSession(input: {
taskSnapshot: Record<string, unknown>;
}): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions`, {
await positionApiFetch(`${BASE()}/sessions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -244,8 +320,8 @@ export async function createPositionSession(input: {
}
export async function getPositionSession(sessionId: string): Promise<PositionRoundtableSession> {
const response = await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}`);
if (response.status === 404) {
const response = await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}`);
if (response.status === 404 && !(await isMissingRoute(response))) {
// Consume the body so the connection can be reused, then expose a typed
// error that the page can resolve by selecting another history record.
await response.text().catch(() => "");
@@ -260,7 +336,7 @@ export async function updatePositionTaskSnapshot(
taskSnapshot: Record<string, unknown>,
): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/task`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/task`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ task_snapshot: taskSnapshot }),
@@ -279,7 +355,7 @@ export async function savePositionIntent(
},
): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/intent`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/intent`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -299,7 +375,7 @@ export async function activatePositionSession(
): Promise<PositionRoundtableSession> {
// 启动即冻结链条快照并创建节点;后续页面不得用当前编辑中的链条替换它。
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/activate`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/activate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chain_id: chainId }),
@@ -311,7 +387,7 @@ export async function activatePositionSession(
export async function archivePositionSession(sessionId: string): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/archive`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/archive`, {
method: "POST",
}),
"archive position session",
@@ -320,12 +396,12 @@ export async function archivePositionSession(sessionId: string): Promise<Positio
}
export async function deletePositionSession(sessionId: string): Promise<void> {
const response = await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}`, {
const response = await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}`, {
method: "DELETE",
});
// DELETE is deliberately idempotent from the UI's perspective. A stale
// history row that is already gone still needs to be removed locally.
if (response.status === 404) {
if (response.status === 404 && !(await isMissingRoute(response))) {
await response.text().catch(() => "");
return;
}
@@ -338,7 +414,7 @@ export async function bindPositionNodeThread(
threadId: string,
): Promise<PositionRoundtableNode> {
const result = await readJson<ServerNode>(
await apiFetch(
await positionApiFetch(
`${BASE()}/sessions/${encodeURIComponent(sessionId)}/nodes/${encodeURIComponent(nodeKey)}/thread`,
{
method: "POST",
@@ -360,7 +436,7 @@ export async function savePositionNodeConversation(
},
): Promise<PositionRoundtableNode> {
const result = await readJson<ServerNode>(
await apiFetch(
await positionApiFetch(
`${BASE()}/sessions/${encodeURIComponent(sessionId)}/nodes/${encodeURIComponent(nodeKey)}/state`,
{
method: "PUT",
@@ -386,7 +462,7 @@ export async function savePositionBuiltinConversation(
): Promise<PositionRoundtableSession> {
const path = kind === "summary" ? "summary/state" : "action-plan/state";
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -416,7 +492,7 @@ export async function seedPositionDemoSession(
payload: PositionDemoSeedPayload,
): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/demo/seed`, {
await positionApiFetch(`${BASE()}/demo/seed`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -437,7 +513,7 @@ export async function getPositionNodeContext(
nodeKey: string,
): Promise<Record<string, unknown>> {
return readJson<Record<string, unknown>>(
await apiFetch(
await positionApiFetch(
`${BASE()}/sessions/${encodeURIComponent(sessionId)}/nodes/${encodeURIComponent(nodeKey)}/context`,
{ method: "POST" },
),
@@ -456,7 +532,7 @@ export async function completePositionNodeTurn(
): Promise<PositionRoundtableNode> {
// 后端依据 artifactManifest 判断是否有 Markdown 交付物,并在需要时解锁下游。
const result = await readJson<ServerNode>(
await apiFetch(
await positionApiFetch(
`${BASE()}/sessions/${encodeURIComponent(sessionId)}/nodes/${encodeURIComponent(nodeKey)}/complete-turn`,
{
method: "POST",
@@ -479,7 +555,7 @@ export async function rejectPositionNode(
input: { reason?: string; title?: string },
): Promise<PositionRoundtableSession> {
const result = await readJson<ServerSession>(
await apiFetch(
await positionApiFetch(
`${BASE()}/sessions/${encodeURIComponent(sessionId)}/nodes/${encodeURIComponent(nodeKey)}/reject`,
{
method: "POST",
@@ -533,7 +609,7 @@ async function prepareBuiltinRun(
// prepare 阶段固定本轮的上游版本与隐藏提示词,避免总结/行动规划引用漂移数据。
const path = kind === "summary" ? "summary/prepare" : "action-plan/prepare";
const result = await readJson<ServerBuiltinRunPreparation>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
@@ -557,7 +633,7 @@ async function completeBuiltinRun(
// complete 阶段会校验 sourceRevisions;上游已变化时不能把旧结果覆盖为最新交付。
const path = kind === "summary" ? "summary/complete" : "action-plan/complete";
const result = await readJson<ServerSession>(
await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
await positionApiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({