github-actions[bot] commited on
Commit
580dda3
·
1 Parent(s): e2f5438

Sync from GitHub Viciy2023/Qwen2API-A@6b6dad4f42bf8a6c60bfb5e517749b0b07ecbe7f

Browse files
Files changed (2) hide show
  1. .gitignore +2 -0
  2. public/src/views/dashboard.vue +366 -2
.gitignore CHANGED
@@ -10,3 +10,5 @@ pkg_dist/*
10
  pkg_dist/
11
  .idea
12
  /public/dist
 
 
 
10
  pkg_dist/
11
  .idea
12
  /public/dist
13
+ 构建日志.MD
14
+ 运行日志.MD
public/src/views/dashboard.vue CHANGED
@@ -46,8 +46,12 @@
46
  class="action-button font-bold border border-yellow-200 bg-yellow-50 text-yellow-900 px-4 py-2 rounded-xl shadow-sm hover:bg-yellow-100 hover:border-yellow-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
47
  导出账号
48
  </button>
 
 
 
 
49
  <router-link to="/settings"
50
- class="action-button col-span-2 sm:col-span-1 font-bold border border-blue-200 bg-blue-50 text-blue-900 px-4 py-2 rounded-xl shadow-sm hover:bg-blue-100 hover:border-blue-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
51
  系统设置
52
  </router-link>
53
  </div>
@@ -309,6 +313,128 @@
309
  <span>{{ toast.message }}</span>
310
  </div>
311
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  </div>
313
  </template>
314
 
@@ -342,6 +468,11 @@ const showDeleteAllConfirm = ref(false)
342
  const isRefreshingAll = ref(false)
343
  const isForceRefreshingAll = ref(false)
344
  const refreshingTokens = ref([])
 
 
 
 
 
345
 
346
  // Toast 通知
347
  const toast = ref({
@@ -350,6 +481,201 @@ const toast = ref({
350
  type: 'success'
351
  })
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  const isSelected = (email) => {
354
  return selectedTokens.value.includes(email)
355
  }
@@ -678,6 +1004,44 @@ const exportAccounts = async () => {
678
  }
679
  }
680
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  onMounted(() => {
682
  getTokens()
683
  })
@@ -1022,4 +1386,4 @@ onMounted(() => {
1022
  min-height: 44px;
1023
  }
1024
  }
1025
- </style>
 
46
  class="action-button font-bold border border-yellow-200 bg-yellow-50 text-yellow-900 px-4 py-2 rounded-xl shadow-sm hover:bg-yellow-100 hover:border-yellow-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
47
  导出账号
48
  </button>
49
+ <button @click="openModelsPanel"
50
+ class="action-button font-bold border border-violet-200 bg-violet-50 text-violet-900 px-4 py-2 rounded-xl shadow-sm hover:bg-violet-100 hover:border-violet-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
51
+ 可用模型
52
+ </button>
53
  <router-link to="/settings"
54
+ class="action-button col-span-2 sm:col-span-1 font-bold border border-blue-200 bg-blue-50 text-blue-900 px-4 py-2 rounded-xl shadow-sm hover:bg-blue-100 hover:border-blue-400 transition-all duration-300 transform hover:-translate-y-1 active:translate-y-0 text-center">
55
  系统设置
56
  </router-link>
57
  </div>
 
313
  <span>{{ toast.message }}</span>
314
  </div>
315
  </div>
316
+
317
+ <!-- 可用模型面板 -->
318
+ <div v-if="showModelsPanel"
319
+ class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
320
+ @click.self="showModelsPanel = false">
321
+ <div class="relative bg-white/90 backdrop-blur-lg rounded-2xl p-6 w-11/12 max-w-5xl max-h-[85vh] overflow-hidden transform transition-all duration-300 scale-100 opacity-100">
322
+ <div class="flex items-center justify-between mb-4">
323
+ <div>
324
+ <h2 class="text-2xl font-bold text-slate-800">可用模型</h2>
325
+ <p class="text-sm text-slate-500 mt-1">展示当前实例可直接调用的全部模型</p>
326
+ </div>
327
+ <button @click="showModelsPanel = false"
328
+ class="px-3 py-2 rounded-xl bg-slate-100 text-slate-600 hover:bg-slate-200 transition-all duration-300">
329
+ 关闭
330
+ </button>
331
+ </div>
332
+
333
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
334
+ <div class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
335
+ <span>共 {{ availableModels.length }} 个模型</span>
336
+ <span class="rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600">基础 {{ modelGroups.base.length }}</span>
337
+ <span class="rounded-full bg-amber-100 px-3 py-1 text-xs text-amber-700">Thinking {{ modelGroups.thinking.length }}</span>
338
+ <span class="rounded-full bg-cyan-100 px-3 py-1 text-xs text-cyan-700">Search {{ modelGroups.search.length }}</span>
339
+ <span class="rounded-full bg-rose-100 px-3 py-1 text-xs text-rose-700">Image {{ modelGroups.image.length }}</span>
340
+ <span class="rounded-full bg-indigo-100 px-3 py-1 text-xs text-indigo-700">Video {{ modelGroups.video.length }}</span>
341
+ <span class="rounded-full bg-emerald-100 px-3 py-1 text-xs text-emerald-700">Image Edit {{ modelGroups.imageEdit.length }}</span>
342
+ </div>
343
+ <div class="flex w-full sm:w-auto gap-2">
344
+ <div class="relative w-full sm:w-72">
345
+ <input v-model="modelKeyword"
346
+ type="text"
347
+ placeholder="搜索模型 ID"
348
+ class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 shadow-sm focus:border-violet-400 focus:ring-violet-400 transition-all duration-300">
349
+ </div>
350
+ <button @click="refreshModels"
351
+ :disabled="isLoadingModels"
352
+ :class="[
353
+ 'rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-300 border',
354
+ isLoadingModels
355
+ ? 'bg-violet-200 text-violet-600 border-violet-200 cursor-not-allowed'
356
+ : 'bg-violet-600 text-white border-violet-600 hover:bg-violet-700'
357
+ ]">
358
+ {{ isLoadingModels ? '刷新中...' : '刷新模型列表' }}
359
+ </button>
360
+ </div>
361
+ </div>
362
+
363
+ <div v-if="isLoadingModels" class="py-12 text-center text-slate-500">
364
+ 正在加载模型列表...
365
+ </div>
366
+
367
+ <div v-else-if="modelsError" class="rounded-2xl border border-red-200 bg-red-50 px-4 py-8 text-center text-red-600">
368
+ <div class="text-lg font-semibold">模型列表加载失败</div>
369
+ <div class="mt-2 text-sm whitespace-pre-line">{{ modelsError }}</div>
370
+ <button @click="refreshModels"
371
+ class="mt-4 rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 transition-all duration-300">
372
+ 重新加载
373
+ </button>
374
+ </div>
375
+
376
+ <div v-else class="max-h-[60vh] overflow-y-auto pr-2">
377
+ <div class="space-y-5">
378
+ <div v-for="group in groupedModelSections"
379
+ :key="group.key"
380
+ v-show="group.models.length"
381
+ class="rounded-2xl border border-slate-200 bg-white/70 p-4 shadow-sm">
382
+ <div class="mb-3 flex items-center justify-between gap-3">
383
+ <div>
384
+ <h3 class="text-lg font-semibold text-slate-800">{{ group.title }}</h3>
385
+ <p class="text-xs text-slate-500 mt-1">{{ group.description }}</p>
386
+ </div>
387
+ <span :class="group.badgeClass" class="rounded-full px-3 py-1 text-xs font-semibold">
388
+ {{ group.models.length }} 个
389
+ </span>
390
+ </div>
391
+
392
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
393
+ <div v-for="model in group.models"
394
+ :key="model.id"
395
+ class="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm hover:shadow-md transition-all duration-300">
396
+ <div class="flex items-start justify-between gap-3">
397
+ <div class="min-w-0 flex-1">
398
+ <div class="font-semibold text-slate-800 break-all">{{ model.id }}</div>
399
+ <div class="mt-2 text-xs text-slate-500 break-all">模型名称:{{ getModelDisplayName(model) }}</div>
400
+ <div class="mt-2 flex flex-wrap gap-2">
401
+ <span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">{{ model.owned_by || 'unknown' }}</span>
402
+ <span v-if="modelTagSummary(model).length"
403
+ class="rounded-full bg-violet-100 px-2 py-1 text-xs text-violet-700">
404
+ {{ modelTagSummary(model).join(' / ') }}
405
+ </span>
406
+ <span class="rounded-full bg-emerald-100 px-2 py-1 text-xs text-emerald-700">
407
+ 推荐:{{ getModelUseCase(model) }}
408
+ </span>
409
+ </div>
410
+ </div>
411
+ </div>
412
+
413
+ <div class="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
414
+ <button @click="copyToClipboard(model.id)"
415
+ class="rounded-lg bg-violet-100 px-2 py-2 text-sm text-violet-700 hover:bg-violet-200 transition-all duration-300">
416
+ 复制 ID
417
+ </button>
418
+ <button @click="copyToClipboard(getModelDisplayName(model))"
419
+ class="rounded-lg bg-sky-100 px-2 py-2 text-sm text-sky-700 hover:bg-sky-200 transition-all duration-300">
420
+ 复制名字
421
+ </button>
422
+ <button @click="copyModelRequestExample(model)"
423
+ class="col-span-2 sm:col-span-1 rounded-lg bg-amber-100 px-2 py-2 text-sm text-amber-700 hover:bg-amber-200 transition-all duration-300">
424
+ 复制示例
425
+ </button>
426
+ </div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+
432
+ <div v-if="!filteredModels.length" class="py-12 text-center text-slate-500">
433
+ 没有匹配的模型
434
+ </div>
435
+ </div>
436
+ </div>
437
+ </div>
438
  </div>
439
  </template>
440
 
 
468
  const isRefreshingAll = ref(false)
469
  const isForceRefreshingAll = ref(false)
470
  const refreshingTokens = ref([])
471
+ const showModelsPanel = ref(false)
472
+ const availableModels = ref([])
473
+ const isLoadingModels = ref(false)
474
+ const modelsError = ref('')
475
+ const modelKeyword = ref('')
476
 
477
  // Toast 通知
478
  const toast = ref({
 
481
  type: 'success'
482
  })
483
 
484
+ const filteredModels = computed(() => {
485
+ const keyword = modelKeyword.value.trim().toLowerCase()
486
+ if (!keyword) {
487
+ return availableModels.value
488
+ }
489
+
490
+ return availableModels.value.filter(model =>
491
+ model.id.toLowerCase().includes(keyword)
492
+ )
493
+ })
494
+
495
+ const createModelGroupMap = (models) => {
496
+ const groups = {
497
+ base: [],
498
+ thinking: [],
499
+ search: [],
500
+ image: [],
501
+ video: [],
502
+ imageEdit: []
503
+ }
504
+
505
+ models.forEach(model => {
506
+ const id = model.id.toLowerCase()
507
+
508
+ if (id.includes('image-edit')) {
509
+ groups.imageEdit.push(model)
510
+ } else if (id.includes('video')) {
511
+ groups.video.push(model)
512
+ } else if (id.includes('image')) {
513
+ groups.image.push(model)
514
+ } else if (id.includes('thinking') && id.includes('search')) {
515
+ groups.thinking.push(model)
516
+ groups.search.push(model)
517
+ } else if (id.includes('thinking')) {
518
+ groups.thinking.push(model)
519
+ } else if (id.includes('search')) {
520
+ groups.search.push(model)
521
+ } else {
522
+ groups.base.push(model)
523
+ }
524
+ })
525
+
526
+ return groups
527
+ }
528
+
529
+ const modelGroups = computed(() => createModelGroupMap(filteredModels.value))
530
+
531
+ const groupedModelSections = computed(() => [
532
+ {
533
+ key: 'base',
534
+ title: '基础模型',
535
+ description: '适合普通对话、通用推理和默认场景',
536
+ badgeClass: 'bg-slate-100 text-slate-700',
537
+ models: modelGroups.value.base
538
+ },
539
+ {
540
+ key: 'thinking',
541
+ title: 'Thinking 模型',
542
+ description: '带推理输出能力,适合复杂思考场景',
543
+ badgeClass: 'bg-amber-100 text-amber-700',
544
+ models: modelGroups.value.thinking
545
+ },
546
+ {
547
+ key: 'search',
548
+ title: 'Search 模型',
549
+ description: '带搜索能力,适合联网或检索场景',
550
+ badgeClass: 'bg-cyan-100 text-cyan-700',
551
+ models: modelGroups.value.search
552
+ },
553
+ {
554
+ key: 'image',
555
+ title: 'Image 模型',
556
+ description: '适合图片理解或生图相关能力',
557
+ badgeClass: 'bg-rose-100 text-rose-700',
558
+ models: modelGroups.value.image
559
+ },
560
+ {
561
+ key: 'video',
562
+ title: 'Video 模型',
563
+ description: '适合视频相关能力或多模态视频处理',
564
+ badgeClass: 'bg-indigo-100 text-indigo-700',
565
+ models: modelGroups.value.video
566
+ },
567
+ {
568
+ key: 'imageEdit',
569
+ title: 'Image Edit 模型',
570
+ description: '适合图像编辑与改图类能力',
571
+ badgeClass: 'bg-emerald-100 text-emerald-700',
572
+ models: modelGroups.value.imageEdit
573
+ }
574
+ ])
575
+
576
+ const modelTagSummary = (model) => {
577
+ const id = model.id.toLowerCase()
578
+ const tags = []
579
+
580
+ if (id.includes('thinking')) tags.push('thinking')
581
+ if (id.includes('search')) tags.push('search')
582
+ if (id.includes('image-edit')) tags.push('image-edit')
583
+ else if (id.includes('image')) tags.push('image')
584
+ if (id.includes('video')) tags.push('video')
585
+
586
+ return tags
587
+ }
588
+
589
+ const getModelDisplayName = (model) => {
590
+ return model.id
591
+ }
592
+
593
+ const getModelUseCase = (model) => {
594
+ const id = model.id.toLowerCase()
595
+
596
+ if (id.includes('coder')) {
597
+ return '代码生成/编程'
598
+ }
599
+
600
+ if (id.includes('vl') || id.includes('omni')) {
601
+ return '多模态理解'
602
+ }
603
+
604
+ if (id.includes('image-edit')) {
605
+ return '图片编辑'
606
+ }
607
+
608
+ if (id.includes('image')) {
609
+ return '图像生成/识图'
610
+ }
611
+
612
+ if (id.includes('video')) {
613
+ return '视频理解'
614
+ }
615
+
616
+ if (id.includes('thinking')) {
617
+ return '复杂推理'
618
+ }
619
+
620
+ if (id.includes('search')) {
621
+ return '联网搜索'
622
+ }
623
+
624
+ if (id.includes('flash')) {
625
+ return '快速响应'
626
+ }
627
+
628
+ return '通用对话'
629
+ }
630
+
631
+ const copyModelRequestExample = async (model) => {
632
+ const example = {
633
+ model: model.id,
634
+ messages: [
635
+ {
636
+ role: 'user',
637
+ content: '你好,请介绍一下这个模型的适用场景。'
638
+ }
639
+ ],
640
+ stream: false
641
+ }
642
+
643
+ try {
644
+ await navigator.clipboard.writeText(JSON.stringify(example, null, 2))
645
+ showToast(`已复制 ${model.id} 的请求示例`)
646
+ } catch (error) {
647
+ console.error('复制模型请求示例失败:', error)
648
+ showToast('复制请求示例失败', 'error')
649
+ }
650
+ }
651
+
652
+ const getModelsErrorMessage = (error) => {
653
+ if (error.response) {
654
+ const status = error.response.status
655
+ const backendMessage = error.response.data?.error || error.response.data?.message
656
+
657
+ if (status === 401 || status === 403) {
658
+ return '当前 API Key 没有访问模型列表的权限,请检查管理员密钥或重新登录。'
659
+ }
660
+
661
+ if (status === 404) {
662
+ return '模型列表接口不存在,请确认当前服务版本是否已正确部署。'
663
+ }
664
+
665
+ if (status >= 500) {
666
+ return `服务端返回 ${status} 错误。${backendMessage ? `\n后端信息:${backendMessage}` : ''}`
667
+ }
668
+
669
+ return `请求失败,状态码 ${status}。${backendMessage ? `\n后端信息:${backendMessage}` : ''}`
670
+ }
671
+
672
+ if (error.request) {
673
+ return '请求已发出,但没有收到服务响应。请检查当前 Space 是否正在重启或网络是否可用。'
674
+ }
675
+
676
+ return `请求构造失败:${error.message}`
677
+ }
678
+
679
  const isSelected = (email) => {
680
  return selectedTokens.value.includes(email)
681
  }
 
1004
  }
1005
  }
1006
 
1007
+ const getAvailableModels = async () => {
1008
+ isLoadingModels.value = true
1009
+ modelsError.value = ''
1010
+
1011
+ try {
1012
+ const response = await axios.get('/v1/models', {
1013
+ headers: {
1014
+ 'Authorization': `Bearer ${localStorage.getItem('apiKey') || ''}`
1015
+ }
1016
+ })
1017
+
1018
+ const models = Array.isArray(response.data?.data) ? response.data.data : []
1019
+ availableModels.value = [...models].sort((a, b) => a.id.localeCompare(b.id))
1020
+ } catch (error) {
1021
+ console.error('获取模型列表失败:', error)
1022
+ modelsError.value = getModelsErrorMessage(error)
1023
+ } finally {
1024
+ isLoadingModels.value = false
1025
+ }
1026
+ }
1027
+
1028
+ const refreshModels = async () => {
1029
+ await getAvailableModels()
1030
+
1031
+ if (!modelsError.value) {
1032
+ showToast('模型列表已刷新')
1033
+ }
1034
+ }
1035
+
1036
+ const openModelsPanel = async () => {
1037
+ showModelsPanel.value = true
1038
+ modelKeyword.value = ''
1039
+
1040
+ if (!availableModels.value.length) {
1041
+ await getAvailableModels()
1042
+ }
1043
+ }
1044
+
1045
  onMounted(() => {
1046
  getTokens()
1047
  })
 
1386
  min-height: 44px;
1387
  }
1388
  }
1389
+ </style>