代码提交
This commit is contained in:
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({
|
||||
|
||||
Reference in new issue
Block a user