Merge pull request #1328 from pikasTech/fix/1327-session-tab-status
fix: 同步 workbench session 标签状态
This commit is contained in:
@@ -1035,7 +1035,8 @@ class AccessController {
|
||||
const conversations = await this.repairVisibleConversationsIfNeeded(
|
||||
auth.actor,
|
||||
conversationsFromAgentSessions(sessions),
|
||||
projectId
|
||||
projectId,
|
||||
{ deadlineMs: 1500 }
|
||||
);
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
@@ -1479,11 +1480,24 @@ class AccessController {
|
||||
}
|
||||
}
|
||||
|
||||
async repairVisibleConversationsIfNeeded(actor, conversations = [], projectId = "") {
|
||||
async repairVisibleConversationsIfNeeded(actor, conversations = [], projectId = "", options = {}) {
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) return [];
|
||||
const deadlineMs = positiveInteger(options.deadlineMs, 0);
|
||||
const deadline = deadlineMs > 0 ? Date.now() + deadlineMs : 0;
|
||||
const repaired = [];
|
||||
for (const conversation of conversations) {
|
||||
repaired.push(await this.repairVisibleConversationIfNeeded(actor, conversation, projectId) ?? conversation);
|
||||
if (!terminalConversationRepairTraceId(conversation)) {
|
||||
repaired.push(conversation);
|
||||
continue;
|
||||
}
|
||||
const remainingMs = deadline > 0 ? deadline - Date.now() : 0;
|
||||
if (deadline > 0 && remainingMs <= 0) {
|
||||
repaired.push(conversation);
|
||||
continue;
|
||||
}
|
||||
const repair = this.repairVisibleConversationIfNeeded(actor, conversation, projectId);
|
||||
const next = deadline > 0 ? await resolveBeforeDeadline(repair, remainingMs) : await repair;
|
||||
repaired.push(next ?? conversation);
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
@@ -2867,6 +2881,19 @@ function shouldUseSecureSessionCookie(request, env = process.env) {
|
||||
return false;
|
||||
}
|
||||
function errorPayload(code, message, status) { return { ok: false, status, error: { code, message } }; }
|
||||
function positiveInteger(value, fallback = 0) { const number = Number(value); return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback; }
|
||||
async function resolveBeforeDeadline(promise, timeoutMs) {
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let timer = null;
|
||||
const boundedTimeoutMs = Math.max(1, positiveInteger(timeoutMs, 1));
|
||||
const guarded = Promise.resolve(promise).catch(() => null);
|
||||
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(null), boundedTimeoutMs); });
|
||||
try {
|
||||
return await Promise.race([guarded, timeout]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
function accessWriteErrorPayload(result = {}) {
|
||||
const error = result.error ?? {};
|
||||
const code = textOr(error.code, "openfga_write_failed");
|
||||
|
||||
@@ -107,11 +107,12 @@ export function mergeSelectedConversation(conversations: ConversationRecord[], w
|
||||
return next;
|
||||
}
|
||||
|
||||
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string): WorkspaceRecord | null {
|
||||
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string, sessionStatus?: string | null): WorkspaceRecord | null {
|
||||
if (!workspace || !traceId) return workspace;
|
||||
const activeTraceId = activeTraceIdFromWorkspace(workspace);
|
||||
if (activeTraceId !== traceId) return workspace;
|
||||
const nested = workspace.workspace ?? {};
|
||||
const nextSessionStatus = normalizeSessionStatus(sessionStatus) ?? (isActiveStatus(nested.sessionStatus) ? "failed" : normalizeSessionStatus(nested.sessionStatus));
|
||||
return {
|
||||
...workspace,
|
||||
activeTraceId: null,
|
||||
@@ -120,7 +121,7 @@ export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | nul
|
||||
activeTraceId: null,
|
||||
staleActiveTraceId: traceId,
|
||||
staleActiveTraceReason: reason,
|
||||
sessionStatus: isActiveStatus(nested.sessionStatus) ? "failed" : nested.sessionStatus,
|
||||
sessionStatus: nextSessionStatus,
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: "cloud-web-vue-active-trace-repair",
|
||||
valuesRedacted: true,
|
||||
@@ -161,14 +162,33 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveConversationSessionStatus(conversation: ConversationRecord): string {
|
||||
export function resolveConversationSessionStatus(conversation: ConversationRecord, extraStatuses: unknown[] = []): string {
|
||||
const latestAgent = latestAgentMessage(conversation.messages);
|
||||
const latestMessage = conversation.messages?.at(-1);
|
||||
return firstNonEmptyString(
|
||||
conversation.status,
|
||||
const stored = normalizeSessionStatus(conversation.status);
|
||||
const terminalEvidence = firstNonActiveStatus(
|
||||
...extraStatuses,
|
||||
conversation.session?.status,
|
||||
conversation.snapshot?.sessionStatus,
|
||||
conversation.snapshot?.status,
|
||||
latestAgent?.status,
|
||||
latestAgent?.runnerTrace?.status,
|
||||
latestAgent?.runnerTrace?.traceStatus,
|
||||
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "terminalStatus"),
|
||||
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "commandState"),
|
||||
latestMessage?.status
|
||||
);
|
||||
if (stored && isActiveStatus(stored) && terminalEvidence) return terminalEvidence;
|
||||
return stored ?? firstNormalizedSessionStatus(
|
||||
...extraStatuses,
|
||||
conversation.session?.status,
|
||||
conversation.snapshot?.sessionStatus,
|
||||
conversation.snapshot?.status,
|
||||
latestAgent?.status,
|
||||
latestAgent?.runnerTrace?.status,
|
||||
latestAgent?.runnerTrace?.traceStatus,
|
||||
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "terminalStatus"),
|
||||
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "commandState"),
|
||||
latestMessage?.status
|
||||
) ?? "source";
|
||||
}
|
||||
@@ -268,7 +288,7 @@ function selectedConversationStub(workspace: WorkspaceRecord | null, selectedCon
|
||||
const lastTraceId = firstNonEmptyString(workspace.workspace?.lastTraceId, workspace.activeTraceId, workspace.workspace?.activeTraceId, [...messages].reverse().find((message) => message.traceId)?.traceId) ?? null;
|
||||
const lastUserMessageAt = latestUserMessageAtFromMessages(messages);
|
||||
const latestAgent = latestAgentMessage(messages);
|
||||
return {
|
||||
const stub: ConversationRecord = {
|
||||
conversationId: selectedConversationId,
|
||||
projectId: firstNonEmptyString(workspace.projectId, workspace.workspace?.projectId) ?? null,
|
||||
sessionId,
|
||||
@@ -280,6 +300,7 @@ function selectedConversationStub(workspace: WorkspaceRecord | null, selectedCon
|
||||
messageCount: messages.length,
|
||||
messages
|
||||
};
|
||||
return { ...stub, status: resolveConversationSessionStatus(stub) };
|
||||
}
|
||||
|
||||
function mergeConversationRecords(existing: ConversationRecord, selected: ConversationRecord): ConversationRecord {
|
||||
@@ -289,7 +310,13 @@ function mergeConversationRecords(existing: ConversationRecord, selected: Conver
|
||||
const supplementalMessages = supplemental.messages ?? [];
|
||||
const messages = supplementalMessages.length > 0 ? supplementalMessages : primaryMessages;
|
||||
const lastUserMessageAt = firstNonEmptyString(primary.lastUserMessageAt, primary.snapshot?.lastUserMessageAt, latestUserMessageAtFromMessages(primaryMessages), supplemental.lastUserMessageAt, supplemental.snapshot?.lastUserMessageAt, latestUserMessageAtFromMessages(supplementalMessages)) ?? null;
|
||||
const status = firstNonEmptyString(primary.status, primary.snapshot?.sessionStatus, supplemental.status, supplemental.snapshot?.sessionStatus, statusFromMessages(messages)) ?? null;
|
||||
const status = resolveConversationSessionStatus({ ...primary, messages }, [
|
||||
supplemental.status,
|
||||
supplemental.session?.status,
|
||||
supplemental.snapshot?.sessionStatus,
|
||||
supplemental.snapshot?.status,
|
||||
statusFromMessages(messages)
|
||||
]);
|
||||
return {
|
||||
...supplemental,
|
||||
...primary,
|
||||
@@ -335,11 +362,43 @@ function latestConversationMessage(messages: ChatMessage[], activeConversationId
|
||||
}
|
||||
|
||||
function isActiveStatus(value: unknown): boolean {
|
||||
return ["running", "pending", "accepted", "processing", "busy", "creating"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
|
||||
const status = normalizeSessionStatus(value);
|
||||
return ["running", "pending", "accepted", "processing", "busy", "creating"].includes(status ?? "");
|
||||
}
|
||||
|
||||
function isTerminalStatus(value: unknown): boolean {
|
||||
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
|
||||
return ["completed", "failed", "blocked", "timeout", "canceled", "stale", "thread-resume-failed", "interrupted", "expired"].includes(normalizeSessionStatus(value) ?? "");
|
||||
}
|
||||
|
||||
function firstNonActiveStatus(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
const status = normalizeSessionStatus(value);
|
||||
if (status && !isActiveStatus(status) && isTerminalStatus(status)) return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function firstNormalizedSessionStatus(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
const status = normalizeSessionStatus(value);
|
||||
if (status) return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeSessionStatus(value: unknown): string | null {
|
||||
const text = firstNonEmptyString(value);
|
||||
if (!text) return null;
|
||||
const normalized = text.trim().toLowerCase().replace(/_/gu, "-");
|
||||
if (normalized === "cancelled") return "canceled";
|
||||
if (normalized === "in-flight") return "running";
|
||||
if (normalized === "error") return "failed";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function runnerTraceAgentRunStatus(runnerTrace: ChatMessage["runnerTrace"], key: string): unknown {
|
||||
const agentRun = runnerTrace && typeof runnerTrace === "object" ? (runnerTrace as Record<string, unknown>).agentRun : null;
|
||||
return agentRun && typeof agentRun === "object" ? (agentRun as Record<string, unknown>)[key] : null;
|
||||
}
|
||||
|
||||
function profileOptionFromUnknown(item: unknown): ProviderProfileOption | null {
|
||||
|
||||
@@ -248,9 +248,10 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
async function cancelAgentMessage(message: ChatMessage): Promise<void> {
|
||||
const traceId = message.traceId ?? message.runnerTrace?.traceId;
|
||||
if (!traceId) return;
|
||||
await api.agent.cancelAgentMessage({ traceId, projectId: activeProjectId.value, sessionId: message.sessionId ?? selectedSessionId.value, threadId: message.threadId ?? selectedThreadId.value, conversationId: message.conversationId ?? activeConversationId.value });
|
||||
const response = await api.agent.cancelAgentMessage({ traceId, projectId: activeProjectId.value, sessionId: message.sessionId ?? selectedSessionId.value, threadId: message.threadId ?? selectedThreadId.value, conversationId: message.conversationId ?? activeConversationId.value });
|
||||
const canceledStatus = workspaceSessionStatusFromChatStatus(firstNonEmptyString((response.data as Record<string, unknown> | null)?.status, "canceled"));
|
||||
markMessage(traceId, { status: "canceled", text: "用户已取消该 turn。" });
|
||||
void clearActiveTrace(traceId, "cancel-agent-message");
|
||||
void clearActiveTrace(traceId, "cancel-agent-message", canceledStatus);
|
||||
if (message.status === "running") chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
}
|
||||
@@ -360,7 +361,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
reattachTrace(traceId);
|
||||
return;
|
||||
}
|
||||
await clearActiveTrace(traceId, "reattach-terminal-result");
|
||||
await clearActiveTrace(traceId, "reattach-terminal-result", workspaceSessionStatusFromChatStatus(statusFromResult(result.data.status)));
|
||||
}
|
||||
|
||||
function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void {
|
||||
@@ -371,17 +372,18 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
|
||||
function completeTrace(traceId: string, result: AgentChatResultResponse): void {
|
||||
const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
|
||||
const terminalStatus = result.status === "completed" ? "completed" : statusFromResult(result.status);
|
||||
messages.value = messages.value.map((message) => {
|
||||
if (message.traceId !== traceId || message.role !== "agent") return message;
|
||||
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
|
||||
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
|
||||
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
|
||||
return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
|
||||
return { ...message, status: terminalStatus, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
|
||||
});
|
||||
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: statusFromResult(result.status), updatedAt: resultActivityUpdatedAt(result) });
|
||||
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: terminalStatus, updatedAt: resultActivityUpdatedAt(result) });
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void clearActiveTrace(traceId, "trace-terminal");
|
||||
void clearActiveTrace(traceId, "trace-terminal", workspaceSessionStatusFromChatStatus(terminalStatus));
|
||||
}
|
||||
|
||||
async function hydrateTerminalMessageDiagnostics(): Promise<void> {
|
||||
@@ -411,7 +413,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
markMessage(traceId, { status: "failed", text: message });
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void clearActiveTrace(traceId, "trace-infrastructure-error");
|
||||
void clearActiveTrace(traceId, "trace-infrastructure-error", "failed");
|
||||
}
|
||||
|
||||
function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
|
||||
@@ -426,9 +428,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return workspace.value;
|
||||
}
|
||||
|
||||
async function clearActiveTrace(traceId: string, reason: string): Promise<void> {
|
||||
async function clearActiveTrace(traceId: string, reason: string, sessionStatus?: string | null): Promise<void> {
|
||||
const requestEpoch = workspaceSelectionEpoch.value;
|
||||
const current = workspaceWithClearedActiveTrace(workspace.value, traceId, reason);
|
||||
const current = workspaceWithClearedActiveTrace(workspace.value, traceId, reason, sessionStatus);
|
||||
if (current === workspace.value) return;
|
||||
workspace.value = current;
|
||||
if (!current?.workspaceId) return;
|
||||
@@ -716,6 +718,16 @@ function statusFromResult(status: string | undefined): ChatMessage["status"] {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
function workspaceSessionStatusFromChatStatus(status: unknown): string | null {
|
||||
const normalized = normalizedStatusText(status);
|
||||
if (normalized === "completed") return "idle";
|
||||
if (normalized === "canceled" || normalized === "cancelled") return "canceled";
|
||||
if (normalized === "blocked") return "blocked";
|
||||
if (normalized === "timeout") return "timeout";
|
||||
if (normalized === "failed" || normalized === "error") return "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
function isTraceActiveStatus(status: unknown): boolean {
|
||||
return ["accepted", "pending", "processing", "running", "busy", "creating"].includes(String(status ?? "").trim().toLowerCase().replace(/_/gu, "-"));
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export interface ConversationRecord {
|
||||
name?: string | null;
|
||||
snapshot?: Record<string, unknown> | null;
|
||||
userPreview?: string | null;
|
||||
session?: { sessionId?: string | null; threadId?: string | null };
|
||||
session?: { sessionId?: string | null; threadId?: string | null; status?: string | null };
|
||||
messages?: ChatMessage[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user