mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(frontend): support HTTP plugin pages and consolidate shared helpers
Reuse secure UUID generation for plugin frame channels and sessions on HTTP. Consolidate utility modules and shorten the plugin views store name. Declare route roles and default entries in metadata instead of guard mappings. Keep source integration build output under node_modules/.vite. Validated frontend lint, production and source builds, browser plugin flows, and 20 route guard scenarios with simulated sessions and plugin resources. Fixes #288 Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
# Frontend
|
||||
/frontend/node_modules/
|
||||
/frontend/dist/
|
||||
/frontend/.vite/
|
||||
/frontend/.pnpm-store/
|
||||
/frontend/.pnpm-cache/
|
||||
*.tsbuildinfo
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ pnpm --dir frontend build:source
|
||||
pnpm --dir modules/plugins/examples/workbench/frontend build:source
|
||||
```
|
||||
|
||||
`build:source` 用各项目的 `tsconfig.source.json` 检查源码类型,再构建到各自 `.vite/source-dist`,不改写正式依赖、锁文件或 `dist`。正常 `dev`、`build`、宿主发行 CI 和 Docker 仍消费锁定的 UI 包,无需初始化子模块。CI 另有源码联调检查,显式检出子模块、核对 UI 提交锁定并验证这两个命令。
|
||||
`build:source` 用各项目的 `tsconfig.source.json` 检查源码类型,管理端构建到 `frontend/node_modules/.vite/source-dist`,示例构建到其前端项目的 `.vite/source-dist`,不改写正式依赖、锁文件或 `dist`。正常 `dev`、`build`、宿主发行 CI 和 Docker 仍消费锁定的 UI 包,无需初始化子模块。CI 另有源码联调检查,显式检出子模块、核对 UI 提交锁定并验证这两个命令。
|
||||
|
||||
### 本地 SDK 验证
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@ import { BaseButton, BaseIconButton, BaseModal, BaseScrollbar, BaseSegmented, Ba
|
||||
|
||||
import { Apple, Copy, Monitor, Upload } from '@lucide/vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
import { buildCodexCcSwitchImportDeeplink } from '@/utils/ccswitchImport'
|
||||
import {
|
||||
buildCodexConfigFiles,
|
||||
CODEX_WEBSOCKET_ENABLED_BY_DEFAULT,
|
||||
} from '@/utils/codexConfig'
|
||||
import { buildCodexCcSwitchImportDeeplink, buildCodexConfigFiles, CODEX_WEBSOCKET_ENABLED_BY_DEFAULT } from '@/utils/client'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useUiClock } from '@/composables/useUiClock'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/date'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
value: string | null
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { InputField } from './schema'
|
||||
import { BaseForm, BaseFormItem, BaseIconButton, BaseInput, BasePopover, BaseSelect, BaseTextarea } from '@codex-proxy/ui'
|
||||
import { Braces, Info, List } from '@lucide/vue'
|
||||
import { computed, nextTick, shallowRef, useTemplateRef } from 'vue'
|
||||
import { jsonObjectError, parseJsonObject } from '@/utils/jsonObject'
|
||||
import { jsonObjectError, parseJsonObject } from '@/utils/data'
|
||||
import { inputFields } from './schema'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isRecord } from '@/utils/object'
|
||||
import { isRecord } from '@/utils/data'
|
||||
|
||||
export interface InputField {
|
||||
key: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isRecord } from '@/utils/object'
|
||||
import { isRecord } from '@/utils/data'
|
||||
|
||||
interface ChartTooltipTheme {
|
||||
surface: string
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
import { BaseButton, BaseSegmented } from '@codex-proxy/ui'
|
||||
import { computed, onMounted, shallowRef, watch } from 'vue'
|
||||
import { getClientProfileOptions, previewClientProfile } from '@/api/modules/client-profiles'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import ClientProfilePresetFields from './ClientProfilePresetFields.vue'
|
||||
import ClientProfilePreviewPanel from './ClientProfilePreviewPanel.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ClientProfilePreview } from '@/api/modules/client-profiles'
|
||||
import { BaseSkeleton } from '@codex-proxy/ui'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
defineProps<{
|
||||
preview?: Pick<ClientProfilePreview, 'userAgent' | 'versionSource' | 'checkedAt' | 'error'>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { XaiClientProfilePreview, XaiClientProfileSelection } from '@/api/m
|
||||
import { BaseButton, BaseFormItem, BaseInput, BaseSegmented, BaseSelect } from '@codex-proxy/ui'
|
||||
import { computed, onMounted, shallowRef, watch } from 'vue'
|
||||
import { getXaiClientProfileOptions, previewXaiClientProfile } from '@/api/modules/client-profiles'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import ClientProfilePreviewPanel from './ClientProfilePreviewPanel.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{ active?: boolean, disabled?: boolean, allowInherit?: boolean }>(), {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { toast } from '@codex-proxy/ui'
|
||||
|
||||
import { shallowRef } from 'vue'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/operation'
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
interface AsyncActionRunOptions {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from '@codex-proxy/ui'
|
||||
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
interface CopyTextOptions {
|
||||
successText: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { onScopeDispose, shallowRef } from 'vue'
|
||||
|
||||
import { ApiError } from '@/api/request'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
export function useRequestState(onError?: (error: unknown) => void) {
|
||||
const loading = shallowRef(false)
|
||||
|
||||
@@ -31,7 +31,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useId,
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AppBrandMark from '@/components/AppBrandMark.vue'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import { usePluginManagementViewsStore } from '@/stores/modules/plugin-management-views'
|
||||
import { usePluginViewsStore } from '@/stores/modules/plugin-views'
|
||||
import { useSystemUpdateStore } from '@/stores/modules/system-update'
|
||||
import { useThemeStore } from '@/stores/modules/theme'
|
||||
import { pluginPageLocation, shortPluginInstanceId } from '@/views/plugins/utils/navigation'
|
||||
@@ -56,7 +56,7 @@ const emit = defineEmits<{
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const pluginViewsStore = usePluginManagementViewsStore()
|
||||
const pluginViewsStore = usePluginViewsStore()
|
||||
const systemUpdateStore = useSystemUpdateStore()
|
||||
const themeStore = useThemeStore()
|
||||
const { pageViews: pluginPageViews } = storeToRefs(pluginViewsStore)
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import { normalizeSystemVersion, useSystemUpdateStore } from '@/stores/modules/system-update'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { formatTime } from '@/utils/date'
|
||||
import { formatTime } from '@/utils/format'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import {
|
||||
resolveSystemUpdateLogClasses,
|
||||
resolveSystemUpdatePresentation,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthSession } from '@/api'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
@@ -8,17 +9,32 @@ export const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
function resolveRoleEntry(role: AuthSession['role']) {
|
||||
const entry = router.getRoutes().find(route => route.meta.role === role && route.meta.defaultEntry)
|
||||
if (!entry)
|
||||
throw new Error(`未配置 ${role} 身份的默认入口`)
|
||||
return { path: entry.path }
|
||||
}
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 登录页不依赖会话恢复;只使用当前已知身份决定是否跳转。
|
||||
if (to.name === 'login') {
|
||||
if (authStore.isAuthenticated)
|
||||
return { name: authStore.isAdmin ? 'dashboard' : 'key-usage' }
|
||||
if (to.meta.guestOnly) {
|
||||
if (authStore.session)
|
||||
return resolveRoleEntry(authStore.session.role)
|
||||
return
|
||||
}
|
||||
|
||||
const login = { name: 'login', query: { redirect: to.fullPath } }
|
||||
const requiredRole = to.meta.role
|
||||
if (!requiredRole)
|
||||
return
|
||||
|
||||
const login = {
|
||||
name: 'login',
|
||||
query: { redirect: to.fullPath },
|
||||
state: { loginMode: requiredRole },
|
||||
}
|
||||
|
||||
if (!authStore.sessionChecked) {
|
||||
try {
|
||||
@@ -30,12 +46,10 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!authStore.isAuthenticated)
|
||||
return { ...login, state: { loginMode: to.name === 'key-usage' ? 'key' : 'admin' } }
|
||||
if (!authStore.session)
|
||||
return login
|
||||
|
||||
// 两种身份各自进入独立页面,Key 不挂载会请求管理接口的布局。
|
||||
if (!authStore.isAdmin && to.name !== 'key-usage')
|
||||
return { name: 'key-usage' }
|
||||
if (authStore.isAdmin && to.name === 'key-usage')
|
||||
return { name: 'dashboard' }
|
||||
if (authStore.session.role !== requiredRole)
|
||||
return resolveRoleEntry(authStore.session.role)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { AuthSession } from '@/api'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
role?: AuthSession['role']
|
||||
defaultEntry?: boolean
|
||||
guestOnly?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
meta: { guestOnly: true },
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/key-usage',
|
||||
name: 'key-usage',
|
||||
meta: { role: 'key', defaultEntry: true },
|
||||
component: () => import('@/views/key-usage/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
meta: { role: 'admin', defaultEntry: true },
|
||||
component: () => import('@/layout/index.vue'),
|
||||
children: [
|
||||
{
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ import { computed, shallowRef, watch } from 'vue'
|
||||
import { getPluginManagementViews } from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
export const usePluginManagementViewsStore = defineStore('plugin-management-views', () => {
|
||||
export const usePluginViewsStore = defineStore('plugin-views', () => {
|
||||
const authStore = useAuthStore()
|
||||
const views = shallowRef<PluginManagementView[]>([])
|
||||
const loading = shallowRef(false)
|
||||
@@ -88,4 +88,4 @@ export const usePluginManagementViewsStore = defineStore('plugin-management-view
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(usePluginManagementViewsStore, import.meta.hot))
|
||||
import.meta.hot.accept(acceptHMRUpdate(usePluginViewsStore, import.meta.hot))
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@/api'
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
const maxUpdateLogs = 200
|
||||
const updateEventReadyTimeoutMs = 3_000
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import {
|
||||
buildCodexConfigFiles,
|
||||
CODEX_DEFAULT_MODEL,
|
||||
CODEX_WEBSOCKET_ENABLED_BY_DEFAULT,
|
||||
} from './codexConfig.ts'
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
|
||||
export const CODEX_DEFAULT_MODEL = 'gpt-5.6-terra'
|
||||
export const CODEX_WEBSOCKET_ENABLED_BY_DEFAULT = false
|
||||
|
||||
export interface CodexConfigInput {
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
websocketEnabled?: boolean
|
||||
}
|
||||
|
||||
export function buildCodexConfigFiles(input: CodexConfigInput) {
|
||||
const baseUrl = input.baseUrl.replace(/\/+$/, '')
|
||||
const websocketEnabled = input.websocketEnabled ?? CODEX_WEBSOCKET_ENABLED_BY_DEFAULT
|
||||
// 保留 auth.json 载荷,兼容仍依赖该字段的 CCSwitch 导入器。
|
||||
const auth = { OPENAI_API_KEY: input.apiKey }
|
||||
const configToml = `model_provider = "OpenAI"
|
||||
model = "${CODEX_DEFAULT_MODEL}"
|
||||
review_model = "${CODEX_DEFAULT_MODEL}"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
|
||||
[model_providers.OpenAI]
|
||||
name = "OpenAI"
|
||||
base_url = ${JSON.stringify(baseUrl)}
|
||||
wire_api = "responses"
|
||||
supports_websockets = ${websocketEnabled}
|
||||
requires_openai_auth = false
|
||||
# 代理密钥仅用于网关鉴权,真实账号登录状态由服务端管理。
|
||||
experimental_bearer_token = ${JSON.stringify(input.apiKey)}
|
||||
|
||||
[model_providers.OpenAI.http_headers]
|
||||
# 声明服务端托管认证,让官方客户端启用原生生图;该标记不是密钥。
|
||||
X-OpenAI-Actor-Authorization = "proxy-managed"
|
||||
|
||||
[features]
|
||||
image_generation = true
|
||||
goals = true`
|
||||
|
||||
return {
|
||||
auth,
|
||||
authJson: JSON.stringify(auth, null, 2),
|
||||
baseUrl,
|
||||
configToml,
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodexCcSwitchImportInput {
|
||||
apiKey: string
|
||||
@@ -80,3 +121,16 @@ function encodeBase64(value: string) {
|
||||
binary += String.fromCharCode(byte)
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
export function resolveServiceRootUrl() {
|
||||
const normalizedApiBase = API_BASE_URL.trim().replace(/\/+$/, '')
|
||||
if (/^https?:\/\//i.test(normalizedApiBase))
|
||||
return normalizedApiBase
|
||||
if (typeof window === 'undefined')
|
||||
return normalizedApiBase
|
||||
|
||||
const origin = window.location.origin.replace(/\/+$/, '')
|
||||
if (!normalizedApiBase)
|
||||
return origin
|
||||
return `${origin}${normalizedApiBase.startsWith('/') ? normalizedApiBase : `/${normalizedApiBase}`}`
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
export const CODEX_DEFAULT_MODEL = 'gpt-5.6-terra'
|
||||
export const CODEX_WEBSOCKET_ENABLED_BY_DEFAULT = false
|
||||
|
||||
export interface CodexConfigInput {
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
websocketEnabled?: boolean
|
||||
}
|
||||
|
||||
export function buildCodexConfigFiles(input: CodexConfigInput) {
|
||||
const baseUrl = input.baseUrl.replace(/\/+$/, '')
|
||||
const websocketEnabled = input.websocketEnabled ?? CODEX_WEBSOCKET_ENABLED_BY_DEFAULT
|
||||
// 保留 auth.json 载荷,兼容仍依赖该字段的 CCSwitch 导入器。
|
||||
const auth = { OPENAI_API_KEY: input.apiKey }
|
||||
const configToml = `model_provider = "OpenAI"
|
||||
model = "${CODEX_DEFAULT_MODEL}"
|
||||
review_model = "${CODEX_DEFAULT_MODEL}"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
|
||||
[model_providers.OpenAI]
|
||||
name = "OpenAI"
|
||||
base_url = ${JSON.stringify(baseUrl)}
|
||||
wire_api = "responses"
|
||||
supports_websockets = ${websocketEnabled}
|
||||
requires_openai_auth = false
|
||||
# 代理密钥仅用于网关鉴权,真实账号登录状态由服务端管理。
|
||||
experimental_bearer_token = ${JSON.stringify(input.apiKey)}
|
||||
|
||||
[model_providers.OpenAI.http_headers]
|
||||
# 声明服务端托管认证,让官方客户端启用原生生图;该标记不是密钥。
|
||||
X-OpenAI-Actor-Authorization = "proxy-managed"
|
||||
|
||||
[features]
|
||||
image_generation = true
|
||||
goals = true`
|
||||
|
||||
return {
|
||||
auth,
|
||||
authJson: JSON.stringify(auth, null, 2),
|
||||
baseUrl,
|
||||
configToml,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { RequestLocation } from '@/api'
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function parseJsonObject(text: string, maximumBytes?: number): Record<string, unknown> {
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(text)
|
||||
}
|
||||
catch {
|
||||
throw new Error('请输入有效的 JSON 对象')
|
||||
}
|
||||
if (!isRecord(value))
|
||||
throw new Error('输入必须是 JSON 对象')
|
||||
if (maximumBytes !== undefined && new TextEncoder().encode(JSON.stringify(value)).byteLength > maximumBytes)
|
||||
throw new Error(`输入不能超过 ${maximumBytes / 1024} KiB`)
|
||||
return value
|
||||
}
|
||||
|
||||
export function jsonObjectError(text: string, maximumBytes?: number) {
|
||||
try {
|
||||
parseJsonObject(text, maximumBytes)
|
||||
return undefined
|
||||
}
|
||||
catch (error) {
|
||||
return (error as Error).message
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeRequestLocation(location: RequestLocation): RequestLocation {
|
||||
return {
|
||||
country: location.country.trim().toUpperCase(),
|
||||
region: location.region.trim(),
|
||||
city: location.city.trim(),
|
||||
timezone: location.timezone.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function requestLocationError(location: RequestLocation): string {
|
||||
if (!/^[A-Z]{2}$/.test(location.country) || !location.region || !location.city || !location.timezone)
|
||||
return '请填写两位国家代码、地区、城市和 IANA 时区'
|
||||
return ''
|
||||
}
|
||||
@@ -53,3 +53,43 @@ export function formatRelativeTime(
|
||||
function normalizedDate(value: ConfigType) {
|
||||
return dayjs(typeof value === 'string' ? value.replace(' ', 'T') : value)
|
||||
}
|
||||
|
||||
const integerFormatter = new Intl.NumberFormat('zh-CN')
|
||||
|
||||
const localizedCompactFormatter = new Intl.NumberFormat('zh-CN', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
const metricUnits = [
|
||||
['P', 1_000_000_000_000_000],
|
||||
['T', 1_000_000_000_000],
|
||||
['B', 1_000_000_000],
|
||||
['M', 1_000_000],
|
||||
['K', 1_000],
|
||||
] as const
|
||||
|
||||
export function formatInteger(value: number) {
|
||||
return integerFormatter.format(value)
|
||||
}
|
||||
|
||||
export function formatLocalizedCompactNumber(value: number) {
|
||||
return localizedCompactFormatter.format(Number.isFinite(value) ? value : 0)
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value: number) {
|
||||
const normalized = Math.max(0, Math.round(value))
|
||||
if (normalized < 1_000)
|
||||
return formatInteger(normalized)
|
||||
|
||||
for (const [unit, threshold] of metricUnits) {
|
||||
if (normalized < threshold)
|
||||
continue
|
||||
|
||||
const scaled = normalized / threshold
|
||||
const rounded = scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)
|
||||
return `${rounded.replace(/\.?0+$/, '')}${unit}`
|
||||
}
|
||||
|
||||
return formatInteger(normalized)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { isRecord } from '@/utils/object'
|
||||
|
||||
export function parseJsonObject(text: string, maximumBytes?: number): Record<string, unknown> {
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(text)
|
||||
}
|
||||
catch {
|
||||
throw new Error('请输入有效的 JSON 对象')
|
||||
}
|
||||
if (!isRecord(value))
|
||||
throw new Error('输入必须是 JSON 对象')
|
||||
if (maximumBytes !== undefined && new TextEncoder().encode(JSON.stringify(value)).byteLength > maximumBytes)
|
||||
throw new Error(`输入不能超过 ${maximumBytes / 1024} KiB`)
|
||||
return value
|
||||
}
|
||||
|
||||
export function jsonObjectError(text: string, maximumBytes?: number) {
|
||||
try {
|
||||
parseJsonObject(text, maximumBytes)
|
||||
return undefined
|
||||
}
|
||||
catch (error) {
|
||||
return (error as Error).message
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
const integerFormatter = new Intl.NumberFormat('zh-CN')
|
||||
|
||||
const localizedCompactFormatter = new Intl.NumberFormat('zh-CN', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
const metricUnits = [
|
||||
['P', 1_000_000_000_000_000],
|
||||
['T', 1_000_000_000_000],
|
||||
['B', 1_000_000_000],
|
||||
['M', 1_000_000],
|
||||
['K', 1_000],
|
||||
] as const
|
||||
|
||||
export function formatInteger(value: number) {
|
||||
return integerFormatter.format(value)
|
||||
}
|
||||
|
||||
export function formatLocalizedCompactNumber(value: number) {
|
||||
return localizedCompactFormatter.format(Number.isFinite(value) ? value : 0)
|
||||
}
|
||||
|
||||
export function formatCompactNumber(value: number) {
|
||||
const normalized = Math.max(0, Math.round(value))
|
||||
if (normalized < 1_000)
|
||||
return formatInteger(normalized)
|
||||
|
||||
for (const [unit, threshold] of metricUnits) {
|
||||
if (normalized < threshold)
|
||||
continue
|
||||
|
||||
const scaled = normalized / threshold
|
||||
const rounded = scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)
|
||||
return `${rounded.replace(/\.?0+$/, '')}${unit}`
|
||||
}
|
||||
|
||||
return formatInteger(normalized)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -27,3 +27,12 @@ export async function withMinimumDuration<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRequestId() {
|
||||
// 普通 HTTP 管理端没有 randomUUID;getRandomValues 仍可生成密码学安全的 UUIDv4。
|
||||
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80
|
||||
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { RequestLocation } from '@/api'
|
||||
|
||||
export function normalizeRequestLocation(location: RequestLocation): RequestLocation {
|
||||
return {
|
||||
country: location.country.trim().toUpperCase(),
|
||||
region: location.region.trim(),
|
||||
city: location.city.trim(),
|
||||
timezone: location.timezone.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function requestLocationError(location: RequestLocation): string {
|
||||
if (!/^[A-Z]{2}$/.test(location.country) || !location.region || !location.city || !location.timezone)
|
||||
return '请填写两位国家代码、地区、城市和 IANA 时区'
|
||||
return ''
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
|
||||
export function resolveServiceRootUrl() {
|
||||
const normalizedApiBase = API_BASE_URL.trim().replace(/\/+$/, '')
|
||||
if (/^https?:\/\//i.test(normalizedApiBase))
|
||||
return normalizedApiBase
|
||||
if (typeof window === 'undefined')
|
||||
return normalizedApiBase
|
||||
|
||||
const origin = window.location.origin.replace(/\/+$/, '')
|
||||
if (!normalizedApiBase)
|
||||
return origin
|
||||
return `${origin}${normalizedApiBase.startsWith('/') ? normalizedApiBase : `/${normalizedApiBase}`}`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export function generateRequestId() {
|
||||
// 普通 HTTP 管理端没有 randomUUID;getRandomValues 仍可生成密码学安全的 UUIDv4。
|
||||
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80
|
||||
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { BaseCard, BaseMotionIcon } from '@codex-proxy/ui'
|
||||
|
||||
import { AlertTriangle, Gauge, ShieldCheck, Users } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { formatInteger } from '@/utils/number'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
summary: Awaited<ReturnType<typeof getAccounts>>['summary']
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AccountProfileActivityInsights, AccountProfileInvocation } from '@
|
||||
import { Package } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatInteger } from '@/utils/number'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
insights: AccountProfileActivityInsights
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { AccountProfileStatisticsResponse } from '@/api'
|
||||
import { BaseSkeleton } from '@codex-proxy/ui'
|
||||
import { computed } from 'vue'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/number'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
profile: AccountProfileStatisticsResponse | null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountSubscription } from '@/api'
|
||||
import { computed } from 'vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
subscription: AccountSubscription
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Component } from 'vue'
|
||||
import type { AccountErrorReason, AccountStatus } from '@/api'
|
||||
import { AlertTriangle, CircleCheck, Gauge, Power, Timer } from '@lucide/vue'
|
||||
|
||||
import { formatDateTime, parseTimestamp } from '@/utils/date'
|
||||
import { formatDateTime, parseTimestamp } from '@/utils/format'
|
||||
import { errorReasonLabels, statusLabels, statusTones } from '../../constants'
|
||||
|
||||
export type AccountStatusDisplayMode = AccountStatus | 'refresh_backoff'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AccountQuotaWindow } from '../../constants'
|
||||
import { clamp } from 'es-toolkit'
|
||||
import { formatInteger } from '@/utils/number'
|
||||
import { isRecord } from '@/utils/object'
|
||||
import { isRecord } from '@/utils/data'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
export type AccountUsageWindowVariant = 'compact' | 'detail' | 'metric'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AccountImportSettings, AccountOAuthStartResponse } from '@/api'
|
||||
import { computed, onScopeDispose, shallowRef } from 'vue'
|
||||
import { completeAccountOAuth, startAccountOAuth } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
type Status = 'idle' | 'starting' | 'waiting' | 'paused' | 'expired' | 'completing'
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getAccountModels, refreshAccountModels } from '@/api'
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { formatDateTime, formatTime } from '@/utils/date'
|
||||
import { formatDateTime, formatTime } from '@/utils/format'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/operation'
|
||||
|
||||
interface ConnectionTestRun {
|
||||
accountId: string
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AccountImportTask, AccountImportTaskDetail } from '@/api'
|
||||
import { computed, onMounted, onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { getAccountImportTask, getAccountImportTasks, stopAccountImportTask } from '@/api'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
export function useAccountImportTasks(options: { reload: () => Promise<unknown> }) {
|
||||
const open = shallowRef(false)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/operation'
|
||||
import { isSupportedProvider } from '@/utils/providers'
|
||||
|
||||
import { useAccountOnboarding } from './useAccountOnboarding'
|
||||
|
||||
@@ -3,9 +3,8 @@ import { toast } from '@codex-proxy/ui'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { createAccountImportTask, importAccounts } from '@/api'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage, generateRequestId } from '@/utils/operation'
|
||||
import { formatProviderLabel, isSupportedProvider } from '@/utils/providers'
|
||||
import { generateRequestId } from '@/utils/uuid'
|
||||
import { accountCreateProvider, accountCreateSourceKey, accountImportSettings, accountProxyError, emptyAccountCreateForm } from '../components/AccountCreateModal/model'
|
||||
import { accountImportModes } from '../components/AccountCreateModal/presenter'
|
||||
import { accountImportDocuments, MAX_ACCOUNT_IMPORT_COUNT, mixedImportDocuments } from '../utils/accountImport'
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
getAccountResetCredits,
|
||||
} from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { generateRequestId } from '@/utils/uuid'
|
||||
import { errorMessage, generateRequestId } from '@/utils/operation'
|
||||
|
||||
interface PendingResetCreditOperation {
|
||||
accountId: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isRecord } from '@/utils/object'
|
||||
import { isRecord } from '@/utils/data'
|
||||
import { formatProviderLabel, isSupportedProvider } from '@/utils/providers'
|
||||
|
||||
export const MAX_ACCOUNT_IMPORT_COUNT = 200
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AccountProfileDailyUsage } from '@/api'
|
||||
import { formatCompactNumber } from '@/utils/number'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
|
||||
export type ProfileActivityMode = 'daily' | 'weekly' | 'cumulative'
|
||||
export type ProfileActivityLevel = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseCard, BaseEmpty } from '@codex-proxy/ui'
|
||||
|
||||
import { CircleCheck, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { formatCompactNumber } from '@/utils/number'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
import AccountUsageWindow from '@/views/accounts/components/AccountUsageWindow/index.vue'
|
||||
import { metricToneIconClasses, metricToneValueClasses } from '../constants'
|
||||
import DashboardAccountIdentity from './DashboardAccountIdentity.vue'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BaseCard, BaseEmpty, BaseSegmented } from '@codex-proxy/ui'
|
||||
|
||||
import { Box, CheckCircle2, Monitor, RefreshCw, ShieldCheck, Terminal, TriangleAlert } from '@lucide/vue'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { formatProviderLabel, isSupportedProvider, providerIcon } from '@/utils/providers'
|
||||
|
||||
interface WireProfile {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useIntervalFn } from '@vueuse/core'
|
||||
import { computed, onMounted, onScopeDispose, shallowRef } from 'vue'
|
||||
|
||||
import { getDashboardSummary, getDashboardTrend } from '@/api'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/number'
|
||||
import { formatCompactNumber, formatDateTime, formatInteger } from '@/utils/format'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/operation'
|
||||
|
||||
export function useDashboard() {
|
||||
const activeTrendKind = shallowRef(normalizeDashboardTrendKind('usage'))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MetricTone } from './composables/useDashboard'
|
||||
import { formatInteger } from '@/utils/number'
|
||||
import { formatInteger } from '@/utils/format'
|
||||
|
||||
// tone 到 Tailwind 类的唯一映射;仪表盘各卡片共用。
|
||||
export const metricToneIconClasses: Record<MetricTone, string> = {
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import { DEFAULT_ACCOUNT_GROUP_COLOR } from '../constants'
|
||||
|
||||
export interface AccountGroupFormValue {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { KeyUsageMetrics } from '@/api/modules/key-usage'
|
||||
import { BaseCard, BaseMotionIcon } from '@codex-proxy/ui'
|
||||
import { Gauge, Zap } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/number'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/format'
|
||||
import AnimatedMetricValue from '@/views/dashboard/components/AnimatedMetricValue.vue'
|
||||
import { money } from '../utils/format'
|
||||
import { keyUsageTokenMetrics, keyUsageTokenValue } from '../utils/metrics'
|
||||
|
||||
@@ -6,7 +6,7 @@ import { computed } from 'vue'
|
||||
import BaseChart from '@/components/charts/BaseChart.vue'
|
||||
import { chartTooltipStyle } from '@/components/charts/tooltip'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/number'
|
||||
import { formatCompactNumber, formatInteger } from '@/utils/format'
|
||||
import { keyUsageTime, money } from '../utils/format'
|
||||
import { keyUsageTokenMetrics, keyUsageTokenValue } from '../utils/metrics'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { computed, onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { getKeyUsageConfig } from '@/api/modules/key-usage'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
import { resolveServiceRootUrl } from '@/utils/serviceUrl'
|
||||
import { resolveServiceRootUrl } from '@/utils/client'
|
||||
|
||||
export function useKeyConfig() {
|
||||
const showConfig = shallowRef(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
export const KEY_USAGE_TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ApiKey } from '@/api'
|
||||
import { BasePopover } from '@codex-proxy/ui'
|
||||
import { computed } from 'vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{ apiKey: ApiKey }>()
|
||||
const windows = computed(() => [
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { Ref, ShallowRef } from 'vue'
|
||||
import type { getApiKeys } from '@/api'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
|
||||
import { buildCodexCcSwitchImportDeeplink } from '@/utils/ccswitchImport'
|
||||
import { resolveServiceRootUrl } from '@/utils/serviceUrl'
|
||||
import { buildCodexCcSwitchImportDeeplink, resolveServiceRootUrl } from '@/utils/client'
|
||||
|
||||
// “使用密钥”弹窗展示明文时,在列表行上补挂 reveal 得到的完整 key。
|
||||
type ApiKeyRow = Awaited<ReturnType<typeof getApiKeys>>['items'][number] & { key?: string }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { watchDebounced } from '@vueuse/core'
|
||||
import { computed, onMounted, shallowRef } from 'vue'
|
||||
import { getApiKeys } from '@/api'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
export function useApiKeysQuery() {
|
||||
const searchQuery = shallowRef('')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseEmpty, BaseFormItem, BaseInput, BaseTag } from '@codex-proxy/ui'
|
||||
|
||||
import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import SchemaForm from '@/components/SchemaForm/index.vue'
|
||||
import { jsonObjectError, parseJsonObject } from '@/utils/jsonObject'
|
||||
import { jsonObjectError, parseJsonObject } from '@/utils/data'
|
||||
import PluginHelpPopover from './PluginHelpPopover.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -12,7 +12,7 @@ import { BaseButton, BaseFormItem, BaseIconButton, BaseInput, BaseSegmented, Bas
|
||||
import { Plus, RefreshCw, Trash2 } from '@lucide/vue'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { getApiKeys } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import PluginHelpPopover from './PluginHelpPopover.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { useThemeStore } from '@/stores/modules/theme'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage, generateRequestId } from '@/utils/operation'
|
||||
import {
|
||||
assemblePluginManagementPage,
|
||||
MAXIMUM_MANAGEMENT_BODY_BYTES,
|
||||
@@ -131,8 +131,8 @@ async function loadPage() {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
const controller = new AbortController()
|
||||
const channel = crypto.randomUUID()
|
||||
const sessionId = crypto.randomUUID()
|
||||
const channel = generateRequestId()
|
||||
const sessionId = generateRequestId()
|
||||
try {
|
||||
await nextTick()
|
||||
if (sequence !== loadSequence)
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Blocks, CircleAlert, RefreshCw } from '@lucide/vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, shallowRef } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePluginManagementViewsStore } from '@/stores/modules/plugin-management-views'
|
||||
import { usePluginViewsStore } from '@/stores/modules/plugin-views'
|
||||
import { pluginPageLocation } from '../utils/navigation'
|
||||
import PluginManagementFrame from './PluginManagementFrame.vue'
|
||||
import PluginPageLoading from './PluginPageLoading.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const directory = usePluginManagementViewsStore()
|
||||
const directory = usePluginViewsStore()
|
||||
const { pageViews, loading, loaded, loadError } = storeToRefs(directory)
|
||||
const frameReload = shallowRef(0)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BaseButton, BaseEmpty, BaseSelect, toast } from '@codex-proxy/ui'
|
||||
import { CircleAlert, LoaderCircle } from '@lucide/vue'
|
||||
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { getAccountGroups, getApiKeys } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
const props = withDefaults(defineProps<{ kind: 'keys' | 'groups', disabled?: boolean, maxCollapseTags?: number }>(), { maxCollapseTags: 1 })
|
||||
const selected = defineModel<string[]>({ required: true })
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
updatePluginInstance,
|
||||
} from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { usePluginManagementViewsStore } from '@/stores/modules/plugin-management-views'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { usePluginViewsStore } from '@/stores/modules/plugin-views'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import { configurationStatus, currentPluginInstance, groupInstalledPlugins } from '../utils/catalog'
|
||||
import { usePluginInstallation } from './usePluginInstallation'
|
||||
import { usePluginUninstall } from './usePluginUninstall'
|
||||
@@ -44,7 +44,7 @@ export function usePluginManagement() {
|
||||
const instances = shallowRef<PluginInstance[]>([])
|
||||
const sources = shallowRef<PluginUpdateSourceBinding[]>([])
|
||||
const credentials = shallowRef<PluginSourceCredential[]>([])
|
||||
const extensionDirectory = usePluginManagementViewsStore()
|
||||
const extensionDirectory = usePluginViewsStore()
|
||||
const { views: extensions } = storeToRefs(extensionDirectory)
|
||||
const loading = shallowRef(false)
|
||||
const catalog = computed(() => groupInstalledPlugins(artifacts.value, instances.value, sources.value))
|
||||
|
||||
@@ -7,8 +7,8 @@ import { computed, onMounted, reactive, ref, shallowRef, watch } from 'vue'
|
||||
import { createProxy, deleteProxy, getProxies, probeProxy, testProxy, updateProxy } from '@/api'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { normalizeRequestLocation, requestLocationError } from '@/utils/request-location'
|
||||
import { normalizeRequestLocation, requestLocationError } from '@/utils/data'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import ProxyAccountsModal from './components/ProxyAccountsModal.vue'
|
||||
import ProxyFormModal from './components/ProxyFormModal.vue'
|
||||
import { effectiveProxyLocation } from './utils/location'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BackupRecord } from '@/api'
|
||||
|
||||
import { BaseButton, BaseCard, BaseConfirmModal, BaseIconButton, BaseTable, BaseTablePagination, defineTableColumns } from '@codex-proxy/ui'
|
||||
import { Download, Play, RefreshCw, Trash2 } from '@lucide/vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
import BackupStatusBadge from './BackupStatusBadge.vue'
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import type { ClientDownloadPackage, CodexDesktopWindowsDownloads } from '@/api'
|
||||
import { BaseButton, BaseEmpty, BaseIconButton, BaseSkeleton } from '@codex-proxy/ui'
|
||||
|
||||
import { ArrowDownToLine, PackageOpen, RefreshCw } from '@lucide/vue'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
import ClientInstallCommandList from './ClientInstallCommandList.vue'
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import type { CodexDesktopWindowsDownloads } from '@/api'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
import { getCodexDesktopWindowsDownloads } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
export function useClientDownloads() {
|
||||
const open = shallowRef(false)
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
const ACTIVE_STATUSES: BackupStatus[] = ['queued', 'dumping', 'uploading']
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
updateBackupSchedule,
|
||||
updateBackupStorage,
|
||||
} from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
/** 存储与计划配置共用的加载/保存/测试 composable。 */
|
||||
export function useBackupSettings() {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { computed, reactive, ref, shallowRef } from 'vue'
|
||||
import { getSettings, updateSettings } from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { normalizeRequestLocation, requestLocationError } from '@/utils/request-location'
|
||||
import { normalizeRequestLocation, requestLocationError } from '@/utils/data'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
type RotationStrategy = (typeof rotationOptions)[number]['value']
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { toast } from '@codex-proxy/ui'
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { getPricing, previewPricingSync, syncPricing, updatePricing } from '@/api'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
import { pricingProviders, pricingRows } from './model'
|
||||
|
||||
export function usePricing() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
zeroInactiveValues,
|
||||
} from '@/components/charts/timeSeriesGap'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/number'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/format'
|
||||
|
||||
import {
|
||||
tooltipIndex,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseCard, BaseEmpty, BaseSegmented, BaseTable, defineTableColumns } fro
|
||||
|
||||
import { CornerDownRight } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/number'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/format'
|
||||
|
||||
import { formatDuration, formatPercent, formatUsd } from '../utils/format'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { use } from 'echarts/core'
|
||||
import { computed } from 'vue'
|
||||
import BaseChart from '@/components/charts/BaseChart.vue'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/number'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/format'
|
||||
|
||||
import {
|
||||
tooltipIndex,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { computed, shallowRef } from 'vue'
|
||||
import BaseChart from '@/components/charts/BaseChart.vue'
|
||||
import { requestActivityByBucket, zeroInactiveValues } from '@/components/charts/timeSeriesGap'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/number'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber } from '@/utils/format'
|
||||
|
||||
import {
|
||||
tooltipIndex,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { watchDebounced } from '@vueuse/core'
|
||||
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { getOpsErrors } from '@/api'
|
||||
import { useStablePagedQuery } from '@/composables/useStablePagedQuery'
|
||||
import { withMinimumDuration } from '@/utils/async'
|
||||
import { withMinimumDuration } from '@/utils/operation'
|
||||
|
||||
interface UseOpsErrorsTableOptions {
|
||||
timeRangeParams: Readonly<Ref<UsageTimeRangeParams>>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UsageRecordDetail } from '@/api'
|
||||
import { shallowRef, watch } from 'vue'
|
||||
import { getUsageRecordDetail } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { errorMessage } from '@/utils/operation'
|
||||
|
||||
/** Requests can be switched while a lookup is pending; stale results never replace the active trace. */
|
||||
export function useRequestDiagnostics(requestId: () => string) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getUsageRecords,
|
||||
getUsageRecordSummary,
|
||||
} from '@/api'
|
||||
import { withMinimumDuration } from '@/utils/async'
|
||||
import { withMinimumDuration } from '@/utils/operation'
|
||||
|
||||
interface UseUsageRecordsTableOptions {
|
||||
timeRangeParams: Readonly<Ref<UsageTimeRangeParams>>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatLocalizedCompactNumber } from '@/utils/number'
|
||||
import { formatLocalizedCompactNumber } from '@/utils/format'
|
||||
import { formatProviderLabel } from '@/utils/providers'
|
||||
|
||||
const percentFormatter = new Intl.NumberFormat('zh-CN', {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
UsageTokenDetails,
|
||||
} from '@/api'
|
||||
|
||||
import { isRecord } from '@/utils/object'
|
||||
import { isRecord } from '@/utils/data'
|
||||
import { formatDuration } from './format'
|
||||
|
||||
// Usage 记录的规范化 view model:组件只消费这个形状。
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: sourceUi ? '.vite/source-dist' : 'dist',
|
||||
outDir: sourceUi ? 'node_modules/.vite/source-dist' : 'dist',
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 600,
|
||||
rolldownOptions: {
|
||||
|
||||
Reference in New Issue
Block a user