fix(ui): URL-encode conversationId in path segments

When a webchat visitorId + sessionId pair exceeds the conversation_id
column width, WebChatController#deriveConversationId folds the variable
part into a SHA-256 hash prefixed with `#`:

  webchat:<key8>:#<sha256[0..40]>

That `#` is the URL fragment delimiter. Every URL the admin console
builds by interpolating the conversationId into a path — message list,
status, rename, pin, model, delete, goals/by-conversation, chat/stop,
chat/pending-approvals — gets truncated at the `#` before reaching the
server. Symptom: opening one of these conversations in the console
surfaces as 405 (GET landing on @DeleteMapping("/{conversationId}"))
and 403 (owner check on the truncated id).

Add an `encId` helper (encodeURIComponent) and apply it to every
conversationId path segment. The server's @PathVariable decoder already
handles the percent-encoded form transparently, so this is purely a
client-side fix that recovers every existing hashed-id row in addition
to any future ones.

Issue: #372
This commit is contained in:
倪程伟 2026-06-18 19:53:31 +08:00 committed by matevip
parent 7c36b0d752
commit 4f160b6ffb

View File

@ -9,6 +9,17 @@ import type {
GrantScope,
} from '@/types'
/**
* URL-encode a conversation id before interpolating it into a path. Some ids
* contain characters that the browser interprets as URL structural notably
* `#`, which webchat emits as a hash marker when the visitorId+sessionId pair
* exceeds the conversation_id column width (see WebChatController#deriveConversationId:
* `webchat:<key8>:#<sha256[0..40]>`). Without encoding, everything after the
* `#` is treated as a fragment and never reaches the server, producing 405 on
* `@DeleteMapping` fallbacks and 403 from the owner check.
*/
const encId = (id: string) => encodeURIComponent(id)
// Axios 实例
export const http = axios.create({
baseURL: '/api/v1',
@ -156,9 +167,9 @@ export const chatApi = {
})
},
stop: (conversationId: string) =>
http.post<{ stopped: boolean }>(`/chat/${conversationId}/stop`),
http.post<{ stopped: boolean }>(`/chat/${encId(conversationId)}/stop`),
getPendingApprovals: (conversationId: string) =>
http.get(`/chat/${conversationId}/pending-approvals`),
http.get(`/chat/${encId(conversationId)}/pending-approvals`),
}
// ==================== Conversation ====================
@ -172,17 +183,17 @@ export const conversationApi = {
page: (params: { page?: number; size?: number; keyword?: string }) =>
http.get('/conversations/page', { params }),
listMessages: (conversationId: string, params?: { beforeId?: number; limit?: number }) =>
http.get(`/conversations/${conversationId}/messages`, { params }),
http.get(`/conversations/${encId(conversationId)}/messages`, { params }),
getStatus: (conversationId: string) =>
http.get(`/conversations/${conversationId}/status`),
http.get(`/conversations/${encId(conversationId)}/status`),
delete: (conversationId: string) =>
http.delete(`/conversations/${conversationId}`),
http.delete(`/conversations/${encId(conversationId)}`),
clearMessages: (conversationId: string) =>
http.delete(`/conversations/${conversationId}/messages`),
http.delete(`/conversations/${encId(conversationId)}/messages`),
rename: (conversationId: string, title: string) =>
http.put(`/conversations/${conversationId}/title`, { title }),
http.put(`/conversations/${encId(conversationId)}/title`, { title }),
setPinned: (conversationId: string, pinned: boolean) =>
http.put(`/conversations/${conversationId}/pin`, { pinned }),
http.put(`/conversations/${encId(conversationId)}/pin`, { pinned }),
/**
* Pin this conversation to a specific (provider, model). Closes issue
* #183 lets the admin UI switch model for IM-channel conversations
@ -190,7 +201,7 @@ export const conversationApi = {
* not just for the Web channel. Both params required and non-empty.
*/
setModel: (conversationId: string, modelProvider: string, modelName: string) =>
http.put(`/conversations/${conversationId}/model`, { modelProvider, modelName }),
http.put(`/conversations/${encId(conversationId)}/model`, { modelProvider, modelName }),
batchDelete: (conversationIds: string[]) =>
http.post('/conversations/batch-delete', { conversationIds }),
}
@ -1377,7 +1388,7 @@ export const goalApi = {
}) => http.post<Goal>('/goals', data),
findActive: (conversationId: string) =>
http.get<Goal | null>(`/goals/by-conversation/${conversationId}`),
http.get<Goal | null>(`/goals/by-conversation/${encId(conversationId)}`),
get: (id: string) => http.get<Goal>(`/goals/${id}`),