From d9c8bf200a6bf7d6f4575458f9c63e2fd55807fb Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 00:23:57 +0800 Subject: [PATCH 01/12] chore: fix import openapi tooltip ui error --- apps/frontend/src/components/workbench/WorkbenchSidebar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/workbench/WorkbenchSidebar.vue b/apps/frontend/src/components/workbench/WorkbenchSidebar.vue index d398fe2..68479d0 100644 --- a/apps/frontend/src/components/workbench/WorkbenchSidebar.vue +++ b/apps/frontend/src/components/workbench/WorkbenchSidebar.vue @@ -175,7 +175,7 @@ function getEnvColor(envName: string): string { - + 导入 OpenAPI From e3fd58b08bcef18a6d821076f7c927847feaa566 Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 00:35:31 +0800 Subject: [PATCH 02/12] chore: add omx git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 118069a..46fc556 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ docs/ # NestJS uploads/ +.omx/ From 9cfa6419278a8bca1c0c27415b05be7a7d3b4870 Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 01:54:23 +0800 Subject: [PATCH 03/12] omx(team): auto-checkpoint worker-1 [1] --- .../src/stores/useWorkbenchResourceStore.ts | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 apps/frontend/src/stores/useWorkbenchResourceStore.ts diff --git a/apps/frontend/src/stores/useWorkbenchResourceStore.ts b/apps/frontend/src/stores/useWorkbenchResourceStore.ts new file mode 100644 index 0000000..7fcc1d6 --- /dev/null +++ b/apps/frontend/src/stores/useWorkbenchResourceStore.ts @@ -0,0 +1,220 @@ +import type { ApiDetail } from '@/types/api' +import type { ApiVersionBrief, ApiVersionComparison, ApiVersionDetail } from '@/types/version' +import { defineStore } from 'pinia' +import { apiApi } from '@/api/api' +import { versionApi } from '@/api/version' + +interface GetResourceOptions { + force?: boolean +} + +interface ResourceBucket { + cache: Map + inflight: Map> + versions: Map +} + +function createResourceBucket(): ResourceBucket { + return { + cache: new Map(), + inflight: new Map>(), + versions: new Map(), + } +} + +function bumpResourceVersion(bucket: ResourceBucket, key: string) { + const nextVersion = (bucket.versions.get(key) ?? 0) + 1 + bucket.versions.set(key, nextVersion) + return nextVersion +} + +function invalidateResource(bucket: ResourceBucket, key: string) { + bucket.cache.delete(key) + bucket.inflight.delete(key) + bumpResourceVersion(bucket, key) +} + +function resetResource(bucket: ResourceBucket) { + bucket.cache.clear() + bucket.inflight.clear() + bucket.versions.clear() +} + +export const useWorkbenchResourceStore = defineStore('workbenchResource', () => { + const apiDetailCache = createResourceBucket() + const versionListCache = createResourceBucket() + const versionDetailCache = createResourceBucket() + const versionComparisonCache = createResourceBucket() + + function apiDetailKey(projectId: string, apiId: string) { + return `api-detail:${projectId}:${apiId}` + } + + function versionListKey(projectId: string, apiId: string) { + return `version-list:${projectId}:${apiId}` + } + + function versionDetailKey(projectId: string, apiId: string, versionId: string) { + return `version-detail:${projectId}:${apiId}:${versionId}` + } + + function versionComparisonKey(projectId: string, apiId: string, fromVersionId: string, toVersionId: string) { + return `version-compare:${projectId}:${apiId}:${fromVersionId}:${toVersionId}` + } + + async function getResource( + bucket: ResourceBucket, + key: string, + fetcher: () => Promise, + options: GetResourceOptions = {}, + ): Promise { + if (!options.force && bucket.cache.has(key)) { + return bucket.cache.get(key)! + } + + const inflight = bucket.inflight.get(key) + if (!options.force && inflight) { + return inflight + } + + const requestVersion = options.force + ? bumpResourceVersion(bucket, key) + : bucket.versions.get(key) ?? 0 + + const request = fetcher() + .then((data) => { + if ((bucket.versions.get(key) ?? 0) === requestVersion) { + bucket.cache.set(key, data) + } + return data + }) + .finally(() => { + if (bucket.inflight.get(key) === request) { + bucket.inflight.delete(key) + } + }) + + if (!options.force) { + bucket.inflight.set(key, request) + } + + return request + } + + function getApiDetail(projectId: string, apiId: string, options?: GetResourceOptions) { + return getResource( + apiDetailCache, + apiDetailKey(projectId, apiId), + () => apiApi.getApiDetail(projectId, apiId), + options, + ) + } + + function getVersionList(projectId: string, apiId: string, options?: GetResourceOptions) { + return getResource( + versionListCache, + versionListKey(projectId, apiId), + async () => { + const response = await versionApi.getVersionList(projectId, apiId) + return response.versions + }, + options, + ) + } + + function getVersionDetail(projectId: string, apiId: string, versionId: string, options?: GetResourceOptions) { + return getResource( + versionDetailCache, + versionDetailKey(projectId, apiId, versionId), + () => versionApi.getVersionDetail(projectId, apiId, versionId), + options, + ) + } + + function getVersionComparison( + projectId: string, + apiId: string, + fromVersionId: string, + toVersionId: string, + options?: GetResourceOptions, + ) { + return getResource( + versionComparisonCache, + versionComparisonKey(projectId, apiId, fromVersionId, toVersionId), + () => versionApi.compareVersions(projectId, apiId, fromVersionId, toVersionId), + options, + ) + } + + function invalidateApiDetail(projectId: string, apiId: string) { + invalidateResource(apiDetailCache, apiDetailKey(projectId, apiId)) + } + + function invalidateVersionList(projectId: string, apiId: string) { + invalidateResource(versionListCache, versionListKey(projectId, apiId)) + } + + function invalidateVersionDetail(projectId: string, apiId: string, versionId: string) { + invalidateResource(versionDetailCache, versionDetailKey(projectId, apiId, versionId)) + } + + function invalidateVersionDetailsByApi(projectId: string, apiId: string) { + const prefix = `version-detail:${projectId}:${apiId}:` + for (const key of Array.from(versionDetailCache.cache.keys())) { + if (key.startsWith(prefix)) { + invalidateResource(versionDetailCache, key) + } + } + for (const key of Array.from(versionDetailCache.inflight.keys())) { + if (key.startsWith(prefix)) { + invalidateResource(versionDetailCache, key) + } + } + } + + function invalidateVersionComparisonsByApi(projectId: string, apiId: string) { + const prefix = `version-compare:${projectId}:${apiId}:` + for (const key of Array.from(versionComparisonCache.cache.keys())) { + if (key.startsWith(prefix)) { + invalidateResource(versionComparisonCache, key) + } + } + for (const key of Array.from(versionComparisonCache.inflight.keys())) { + if (key.startsWith(prefix)) { + invalidateResource(versionComparisonCache, key) + } + } + } + + function invalidateApi(projectId: string, apiId: string) { + invalidateApiDetail(projectId, apiId) + invalidateVersionList(projectId, apiId) + invalidateVersionDetailsByApi(projectId, apiId) + invalidateVersionComparisonsByApi(projectId, apiId) + } + + function reset() { + resetResource(apiDetailCache) + resetResource(versionListCache) + resetResource(versionDetailCache) + resetResource(versionComparisonCache) + } + + return { + apiDetailKey, + versionListKey, + versionDetailKey, + versionComparisonKey, + getApiDetail, + getVersionList, + getVersionDetail, + getVersionComparison, + invalidateApiDetail, + invalidateVersionList, + invalidateVersionDetail, + invalidateVersionDetailsByApi, + invalidateVersionComparisonsByApi, + invalidateApi, + reset, + } +}) From 56ebbdba3a22a244a8d541288f312337f97ae46c Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 01:55:08 +0800 Subject: [PATCH 04/12] omx(team): auto-checkpoint worker-1 [1] --- .../src/stores/useWorkbenchResourceStore.ts | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/apps/frontend/src/stores/useWorkbenchResourceStore.ts b/apps/frontend/src/stores/useWorkbenchResourceStore.ts index 7fcc1d6..9678f9b 100644 --- a/apps/frontend/src/stores/useWorkbenchResourceStore.ts +++ b/apps/frontend/src/stores/useWorkbenchResourceStore.ts @@ -40,6 +40,19 @@ function resetResource(bucket: ResourceBucket) { bucket.versions.clear() } +function invalidateResourcesByPrefix(bucket: ResourceBucket, prefix: string) { + const keys = new Set([ + ...bucket.cache.keys(), + ...bucket.inflight.keys(), + ]) + + for (const key of keys) { + if (key.startsWith(prefix)) { + invalidateResource(bucket, key) + } + } +} + export const useWorkbenchResourceStore = defineStore('workbenchResource', () => { const apiDetailCache = createResourceBucket() const versionListCache = createResourceBucket() @@ -81,7 +94,8 @@ export const useWorkbenchResourceStore = defineStore('workbenchResource', () => ? bumpResourceVersion(bucket, key) : bucket.versions.get(key) ?? 0 - const request = fetcher() + let request: Promise + request = fetcher() .then((data) => { if ((bucket.versions.get(key) ?? 0) === requestVersion) { bucket.cache.set(key, data) @@ -159,31 +173,11 @@ export const useWorkbenchResourceStore = defineStore('workbenchResource', () => } function invalidateVersionDetailsByApi(projectId: string, apiId: string) { - const prefix = `version-detail:${projectId}:${apiId}:` - for (const key of Array.from(versionDetailCache.cache.keys())) { - if (key.startsWith(prefix)) { - invalidateResource(versionDetailCache, key) - } - } - for (const key of Array.from(versionDetailCache.inflight.keys())) { - if (key.startsWith(prefix)) { - invalidateResource(versionDetailCache, key) - } - } + invalidateResourcesByPrefix(versionDetailCache, `version-detail:${projectId}:${apiId}:`) } function invalidateVersionComparisonsByApi(projectId: string, apiId: string) { - const prefix = `version-compare:${projectId}:${apiId}:` - for (const key of Array.from(versionComparisonCache.cache.keys())) { - if (key.startsWith(prefix)) { - invalidateResource(versionComparisonCache, key) - } - } - for (const key of Array.from(versionComparisonCache.inflight.keys())) { - if (key.startsWith(prefix)) { - invalidateResource(versionComparisonCache, key) - } - } + invalidateResourcesByPrefix(versionComparisonCache, `version-compare:${projectId}:${apiId}:`) } function invalidateApi(projectId: string, apiId: string) { From 8c6cc3fffee3d10dc3779d8823411ff75b9adf2d Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 01:56:22 +0800 Subject: [PATCH 05/12] omx(team): auto-checkpoint worker-1 [1] --- apps/frontend/src/stores/useWorkbenchResourceStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/frontend/src/stores/useWorkbenchResourceStore.ts b/apps/frontend/src/stores/useWorkbenchResourceStore.ts index 9678f9b..f711d9d 100644 --- a/apps/frontend/src/stores/useWorkbenchResourceStore.ts +++ b/apps/frontend/src/stores/useWorkbenchResourceStore.ts @@ -94,8 +94,7 @@ export const useWorkbenchResourceStore = defineStore('workbenchResource', () => ? bumpResourceVersion(bucket, key) : bucket.versions.get(key) ?? 0 - let request: Promise - request = fetcher() + const request = fetcher() .then((data) => { if ((bucket.versions.get(key) ?? 0) === requestVersion) { bucket.cache.set(key, data) From 3b311a7ae44f4b4a6dc714c0d15e6f34b6f3a191 Mon Sep 17 00:00:00 2001 From: non_hana Date: Sun, 19 Apr 2026 01:57:18 +0800 Subject: [PATCH 06/12] omx(team): auto-checkpoint worker-2 [2] --- .../workbench/api-editor/ApiEditor.vue | 37 +++++++++----- .../workbench/version/VersionCompareSheet.vue | 50 ++++++++++--------- .../workbench/version/VersionDetailSheet.vue | 44 ++++++++-------- .../workbench/version/VersionHistory.vue | 44 +++++++++++----- apps/frontend/src/layouts/WorkbenchLayout.vue | 8 ++- 5 files changed, 112 insertions(+), 71 deletions(-) diff --git a/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue b/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue index 60f79fc..2cbcaa5 100644 --- a/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue +++ b/apps/frontend/src/components/workbench/api-editor/ApiEditor.vue @@ -4,7 +4,7 @@ import type { ApiDetail } from '@/types/api' import { useRouteParams, useRouteQuery } from '@vueuse/router' import { AlertCircle, FileText, GitBranch, Loader2, Pencil, Play, Settings2 } from 'lucide-vue-next' import { computed, ref, watch } from 'vue' -import { apiApi } from '@/api/api' +import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import ApiRunnerView from '../api-runner/ApiRunnerView.vue' @@ -34,36 +34,49 @@ watch(activeTab, (newV, oldV) => { } }) +const resourceStore = useWorkbenchResourceStore() + const apiDetail = ref(null) const isLoading = ref(false) const loadError = ref(null) const isLoaded = computed(() => apiDetail.value !== null) -async function fetchApiDetail() { +function getCurrentApiRequestKey() { + return `${projectId.value}:${apiId.value}` +} + +async function fetchApiDetail(options: { force?: boolean } = {}) { + const currentProjectId = projectId.value + const currentApiId = apiId.value + if (!currentProjectId || !currentApiId) + return + + const requestKey = `${currentProjectId}:${currentApiId}` isLoading.value = true loadError.value = null + try { - apiDetail.value = await apiApi.getApiDetail(projectId.value, apiId.value) + const detail = await resourceStore.getApiDetail(currentProjectId, currentApiId, options) + if (getCurrentApiRequestKey() !== requestKey) + return + apiDetail.value = detail } catch (error) { + if (getCurrentApiRequestKey() !== requestKey) + return console.error('获取 API 详情失败:', error) loadError.value = `获取 API 详情失败: ${error}` } finally { - isLoading.value = false + if (getCurrentApiRequestKey() === requestKey) { + isLoading.value = false + } } } async function refreshApiDetail() { - loadError.value = null - try { - apiDetail.value = await apiApi.getApiDetail(projectId.value, apiId.value) - } - catch (error) { - console.error('获取 API 详情失败:', error) - loadError.value = `获取 API 详情失败: ${error}` - } + await fetchApiDetail({ force: true }) } // apiId 和 projectId 变化,刷新 API 详情 diff --git a/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue b/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue index 2c22b5b..f566dae 100644 --- a/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue +++ b/apps/frontend/src/components/workbench/version/VersionCompareSheet.vue @@ -10,7 +10,7 @@ import { X, } from 'lucide-vue-next' import { computed, ref, toRaw, watch } from 'vue' -import { versionApi } from '@/api/version' +import { useWorkbenchResourceStore } from '@/stores/useWorkbenchResourceStore' import CodeBlock from '@/components/common/CodeBlock.vue' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' @@ -48,6 +48,7 @@ interface ComparisonResult { } const isOpen = defineModel('open', { required: true }) +const resourceStore = useWorkbenchResourceStore() const comparison = ref(null) const isLoading = ref(false) @@ -124,47 +125,50 @@ function isComplexValue(value: unknown): boolean { return typeof value === 'object' && value !== null } -async function fetchComparison() { - if (!props.fromVersionId || !props.toVersionId) - return +function getComparisonRequestKey( + projectId = props.projectId, + apiId = props.apiId, + fromVersionId = props.fromVersionId, + toVersionId = props.toVersionId, +) { + return `${projectId}:${apiId}:${fromVersionId ?? ''}:${toVersionId ?? ''}` +} +async function fetchComparison(projectId: string, apiId: string, fromVersionId: string, toVersionId: string) { + const requestKey = getComparisonRequestKey(projectId, apiId, fromVersionId, toVersionId) isLoading.value = true loadError.value = null try { - comparison.value = await versionApi.compareVersions( - props.projectId, - props.apiId, - props.fromVersionId, - props.toVersionId, - ) + const result = await resourceStore.getVersionComparison(projectId, apiId, fromVersionId, toVersionId) + if (!isOpen.value || getComparisonRequestKey() !== requestKey) + return + comparison.value = result } catch (error) { + if (!isOpen.value || getComparisonRequestKey() !== requestKey) + return loadError.value = `获取比较数据失败: ${error}` console.error('Failed to fetch comparison:', error) } finally { - isLoading.value = false + if (!isOpen.value || getComparisonRequestKey() === requestKey) { + isLoading.value = false + } } } watch( - () => [props.fromVersionId, props.toVersionId], - ([from, to]) => { - if (from && to && isOpen.value) { - fetchComparison() - } - else { + () => [isOpen.value, props.projectId, props.apiId, props.fromVersionId, props.toVersionId] as const, + ([open, projectId, apiId, from, to]) => { + if (!open || !from || !to) { comparison.value = null + return } + fetchComparison(projectId, apiId, from, to) }, + { immediate: true }, ) - -watch(isOpen, (open) => { - if (open && props.fromVersionId && props.toVersionId) { - fetchComparison() - } -})