From 92d35a3d3dbd278e363c0e6a723d49273d3f5531 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 19 May 2026 20:06:56 +0800 Subject: [PATCH] fix(workspace): keep workspace ids as strings so a switch survives reload --- mateclaw-ui/src/api/index.ts | 14 +++++++------- .../src/components/ChangePasswordDialog.vue | 2 +- .../src/components/chat/ConversationSidebar.vue | 2 +- .../components/workspace/WorkspaceSwitcher.vue | 2 +- mateclaw-ui/src/stores/useWorkspaceStore.ts | 17 +++++++++++------ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index aa8609e7..483de0c6 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -87,7 +87,7 @@ export const authApi = { http.post('/auth/login', data), listUsers: () => http.get('/auth/users'), createUser: (data: any) => http.post('/auth/users', data), - changePassword: (id: number, oldPassword: string, newPassword: string) => + changePassword: (id: string | number, oldPassword: string, newPassword: string) => http.put(`/auth/users/${id}/password`, null, { params: { oldPassword, newPassword } }), } @@ -953,7 +953,7 @@ export const hotCacheApi = { export interface WorkflowSummary { id: number - workspaceId: number + workspaceId: string | number name: string description?: string enabled: boolean @@ -985,7 +985,7 @@ export interface WorkflowRun { id: number workflowId: number revisionId: number - workspaceId: number + workspaceId: string | number state: string triggeredBy?: string initialInputRef?: string @@ -1048,7 +1048,7 @@ export interface ResumeResponse { } export const workflowApi = { - list: (workspaceId: number) => + list: (workspaceId: string | number) => http.get('/workflows', { params: { workspaceId } }), get: (id: number) => http.get(`/workflows/${id}`), create: (data: Partial) => @@ -1119,7 +1119,7 @@ export interface WorkflowDraftTemplate { export interface TriggerSummary { id: number - workspaceId: number + workspaceId: string | number name?: string patternType: string patternJson: string @@ -1149,7 +1149,7 @@ export interface TriggerSummary { } export const triggerApi = { - list: (workspaceId: number) => + list: (workspaceId: string | number) => http.get('/triggers', { params: { workspaceId } }), get: (id: number) => http.get(`/triggers/${id}`), create: (data: Partial) => @@ -1158,7 +1158,7 @@ export const triggerApi = { http.put(`/triggers/${id}`, data), delete: (id: number) => http.delete(`/triggers/${id}`), ingestEvent: (envelope: { - workspaceId: number + workspaceId: string | number patternType: string eventId?: string senderId?: string diff --git a/mateclaw-ui/src/components/ChangePasswordDialog.vue b/mateclaw-ui/src/components/ChangePasswordDialog.vue index ffc9caa1..24432e27 100644 --- a/mateclaw-ui/src/components/ChangePasswordDialog.vue +++ b/mateclaw-ui/src/components/ChangePasswordDialog.vue @@ -107,7 +107,7 @@ async function handleSubmit() { } submitting.value = true try { - const userId = Number(localStorage.getItem('userId') || '1') + const userId = localStorage.getItem('userId') || '1' await authApi.changePassword(userId, form.oldPassword, form.newPassword) mcToast.success(t('auth.passwordChanged')) close() diff --git a/mateclaw-ui/src/components/chat/ConversationSidebar.vue b/mateclaw-ui/src/components/chat/ConversationSidebar.vue index 50ab2a7d..c548583b 100644 --- a/mateclaw-ui/src/components/chat/ConversationSidebar.vue +++ b/mateclaw-ui/src/components/chat/ConversationSidebar.vue @@ -397,7 +397,7 @@ function hasUnread(conv: Conversation): boolean { if (!Number.isFinite(lastActive)) return false let viewed = 0 try { - viewed = Number(localStorage.getItem(VIEWED_KEY_PREFIX + conv.conversationId) || '0') + viewed = Number(localStorage.getItem(VIEWED_KEY_PREFIX + conv.conversationId) || '0') // snowflake-precision-ok: stored value is a last-viewed epoch-ms timestamp, not an id } catch { // Treat as never-viewed when storage is unavailable. } diff --git a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue index abd99581..c5c752ea 100644 --- a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue +++ b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue @@ -127,7 +127,7 @@ onMounted(() => { store.fetchWorkspaces() }) -function onSelect(id: number) { +function onSelect(id: string) { open.value = false if (id !== currentWorkspaceId.value) { store.switchWorkspace(id) diff --git a/mateclaw-ui/src/stores/useWorkspaceStore.ts b/mateclaw-ui/src/stores/useWorkspaceStore.ts index e85faae8..5dbf501b 100644 --- a/mateclaw-ui/src/stores/useWorkspaceStore.ts +++ b/mateclaw-ui/src/stores/useWorkspaceStore.ts @@ -5,12 +5,14 @@ import type { Capability, WorkspaceRole } from '@/composables/capabilities' import { ROLE_LEVEL } from '@/composables/capabilities' export interface Workspace { - id: number + // Backend-issued Snowflake id — kept as a string for its whole lifecycle so a + // Number() round-trip never truncates the 19-digit value (see CLAUDE.md). + id: string name: string slug: string description?: string basePath?: string - ownerId?: number + ownerId?: string settingsJson?: string createTime?: string updateTime?: string @@ -24,8 +26,11 @@ export interface Workspace { export const useWorkspaceStore = defineStore('workspace', () => { const workspaces = ref([]) - const currentWorkspaceId = ref( - Number(localStorage.getItem('mc-workspace-id')) || null + // Stored as a string: the workspace id is a 19-digit Snowflake, so a Number() + // coercion here would corrupt every non-default workspace id and the reloaded + // value would match no workspace, silently snapping back to Default. + const currentWorkspaceId = ref( + localStorage.getItem('mc-workspace-id') || null ) const loading = ref(false) @@ -102,9 +107,9 @@ export const useWorkspaceStore = defineStore('workspace', () => { } } - async function switchWorkspace(id: number) { + async function switchWorkspace(id: string) { currentWorkspaceId.value = id - localStorage.setItem('mc-workspace-id', String(id)) + localStorage.setItem('mc-workspace-id', id) accessLoaded.value = false currentCapabilities.value = new Set() await refreshAccess()