fix(chat): suppress 403 console spam from polling unpersisted conversations (ISSUE #408)

This commit is contained in:
matevip 2026-06-25 14:36:59 +08:00
parent 2f12c269f4
commit e4e7b4c377
2 changed files with 43 additions and 4 deletions

View File

@ -82,6 +82,16 @@ export const useGoalStore = defineStore('goal', () => {
activeGoalByConv.value[conversationId] = goal
return goal
} catch (e) {
// A conversation that isn't persisted/owned yet — a brand-new empty chat
// before the first message lands, or a thread owned by another user —
// answers 403 (not the owner) / 404 (not found). That just means "no
// active goal yet": record the no-goal state silently. Only genuinely
// unexpected failures (network / 5xx) are worth a console error.
const status = (e as any)?.response?.status ?? (e as any)?.response?.data?.code
if (status === 403 || status === 404) {
activeGoalByConv.value[conversationId] = null
return null
}
console.error('[goal] loadActiveForConversation failed', e)
return null
} finally {

View File

@ -1001,6 +1001,25 @@ const activeCronRuns = ref<ActiveCronRun[]>([])
function isCronConversation(cid: string | null | undefined): boolean {
return !!cid && (cid.startsWith('tasks_') || cid.startsWith('cron_'))
}
// True when a conversation id was minted client-side for a brand-new chat
// the user hasn't sent anything in yet, so the backend has never persisted
// (and therefore can't "own") it. Every per-conversation endpoint called with
// such an id is rejected with 403 "访" the status/messages poller
// and the goal loader both skip the request entirely for these ids instead of
// spamming the console with ownership errors.
//
// Conservative by construction: an id counts as ephemeral only when it is
// absent from BOTH the loaded conversation list AND the local message buffer,
// so a freshly-persisted conversation is never mistaken for one. At worst a
// single stray 403 slips through during the persist race, which the leaf
// callers swallow.
function isEphemeralConversation(cid: string | null | undefined): boolean {
if (!cid) return true
// Server-managed virtual conversations (cron / scheduled tasks) always exist.
if (cid.startsWith('tasks_') || cid.startsWith('cron_')) return false
if (conversations.value.some((c) => c.conversationId === cid)) return false
return !messages.value.some((m: any) => m.conversationId === cid)
}
async function refreshActiveCronRuns(cid: string) {
if (!isCronConversation(cid)) {
activeCronRuns.value = []
@ -1065,8 +1084,13 @@ async function pollActivity() {
} catch {
//
}
// +
if (currentConversationId.value && !isGenerating.value && streamPhase.value !== 'awaiting_approval') {
// +
// ():,
// getStatus / listMessages 403,4s console ( issue #408)
if (currentConversationId.value
&& !isGenerating.value
&& streamPhase.value !== 'awaiting_approval'
&& !isEphemeralConversation(currentConversationId.value)) {
const cid = currentConversationId.value
try {
const statusRes: any = await conversationApi.getStatus(cid)
@ -1157,7 +1181,10 @@ const goalStore = useGoalStore()
const workspaceStoreForGoal = useWorkspaceStore()
const currentWorkspaceId = computed(() => workspaceStoreForGoal.currentWorkspaceId ?? '1')
watch(currentConversationId, async (cid) => {
if (cid) {
// Skip un-persisted conversations: a brand-new empty chat has no goal yet
// and the lookup would only 403 (Not the owner). The ring is hydrated by the
// goal_created SSE event once the first turn lands.
if (cid && !isEphemeralConversation(cid)) {
await goalStore.loadActiveForConversation(cid)
}
}, { immediate: true })
@ -1170,7 +1197,9 @@ watch(currentConversationId, async (cid) => {
// this transition-to-idle refresh is what keeps the goal ring honest after
// every turn against the persisted truth.
watch(isGenerating, async (generating, wasGenerating) => {
if (wasGenerating && !generating && currentConversationId.value) {
if (wasGenerating && !generating
&& currentConversationId.value
&& !isEphemeralConversation(currentConversationId.value)) {
await goalStore.loadActiveForConversation(currentConversationId.value)
}
})