fix(admin+server): 酷狗 baseUrl 默认值移除 127.0.0.1 + 新增后台配置入口与推荐快捷地址

问题:容器内 SystemSettings.kugouApiBaseUrl 默认 http://127.0.0.1:3000 指向自己,sidecar 不可达。

修复:1) schema + migration 移除 kugouApiBaseUrl 默认值;kugouService + status API 兜底改空串(不再假设有 localhost),优先级 env(KUGOU_API_BASE) > DB。

2) 新增 POST /api/admin/kugou/settings:ADMIN+/SUPER 即时写 kugouApiBaseUrl 入库+刷新 CacheService。

3) KuGouImporter.client.vue 左栏新增 baseUrl 输入框(回车即存)+ 保存并测试按钮 + 两个推荐快捷按钮(容器宿主 http://172.19.0.1:3000、公网中继 https://bywmusic.bzw-hheb.top/kugou)+ 当前生效地址显式提示。

4) UI/Icon.vue 新增 server / globe / save 三个 SVG 图标与上述入口配套。
This commit is contained in:
2026-09-19 13:40:59 +08:00
parent 6b3a83ebeb
commit 381f3debf1
8 changed files with 143 additions and 6 deletions
+79 -1
View File
@@ -70,7 +70,55 @@
<LoadingState :height="72" /> <LoadingState :height="72" />
</div> </div>
<div class="mt-5 space-y-3"> <div class="mt-5 space-y-4">
<!-- baseUrl 配置 -->
<div class="rounded-xl border border-zinc-200 bg-zinc-50 p-4 space-y-3">
<div class="flex items-center justify-between">
<label class="text-[11px] font-black uppercase tracking-wider text-zinc-600">
KuGouMusicApi 地址
</label>
<span
v-if="baseUrlSaving"
class="text-[11px] text-zinc-500"
>保存中...</span>
<span
v-else-if="baseUrlSavedTick"
class="text-[11px] text-emerald-600 font-bold"
>已保存</span>
</div>
<input
v-model="baseUrlInput"
type="url"
placeholder="例:http://172.19.0.1:3000 或 https://bywmusic.bzw-hheb.top/kugou,留空则走环境变量 KUGOU_API_BASE"
class="w-full h-[42px] px-3 rounded-xl border border-zinc-200 bg-white text-[12px] text-zinc-900 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-400"
@keydown.enter="saveBaseUrl"
/>
<div class="flex flex-wrap gap-2">
<button
class="h-[34px] px-3 rounded-lg text-[11px] font-bold bg-white hover:bg-zinc-100 border border-zinc-200 text-zinc-700 transition-colors inline-flex items-center gap-1"
@click="baseUrlInput = 'http://172.19.0.1:3000'"
>
<Icon name="server" :size="13" /> 容器直连宿主 http://172.19.0.1:3000
</button>
<button
class="h-[34px] px-3 rounded-lg text-[11px] font-bold bg-white hover:bg-zinc-100 border border-zinc-200 text-zinc-700 transition-colors inline-flex items-center gap-1"
@click="baseUrlInput = 'https://bywmusic.bzw-hheb.top/kugou'"
>
<Icon name="globe" :size="13" /> 中继 https://bywmusic.bzw-hheb.top/kugou
</button>
<button
class="h-[34px] px-3 rounded-lg text-[11px] font-bold bg-zinc-900 hover:bg-zinc-800 text-white transition-colors inline-flex items-center gap-1"
@click="saveBaseUrl"
:disabled="baseUrlSaving"
>
<Icon name="save" :size="13" /> 保存并测试
</button>
</div>
<p v-if="status?.service?.baseUrl" class="text-[11px] text-zinc-500 leading-relaxed">
当前生效地址:<code class="px-1.5 py-0.5 rounded-md bg-white border border-zinc-200 text-zinc-700 font-mono">{{ status.service.baseUrl }}</code>
</p>
</div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
class="flex-1 h-[44px] px-3 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors flex items-center justify-center gap-1.5" class="flex-1 h-[44px] px-3 rounded-xl text-[12px] font-bold bg-zinc-100 hover:bg-zinc-200 text-zinc-700 transition-colors flex items-center justify-center gap-1.5"
@@ -379,6 +427,9 @@ const qrContext = ref<any>(null)
const qrHint = ref('') const qrHint = ref('')
const qrState = ref<number>(0) const qrState = ref<number>(0)
const qrPollTimer = ref<any>(null) const qrPollTimer = ref<any>(null)
const baseUrlInput = ref('')
const baseUrlSaving = ref(false)
const baseUrlSavedTick = ref(0)
const totalLoadedSongs = computed( const totalLoadedSongs = computed(
() => Object.values(schedulesByDate.value).reduce((n, arr) => n + arr.length, 0), () => Object.values(schedulesByDate.value).reduce((n, arr) => n + arr.length, 0),
@@ -436,11 +487,38 @@ async function reloadStatus() {
try { try {
const r = await $fetch('/api/admin/kugou/status', { ...auth.getAuthConfig() }) const r = await $fetch('/api/admin/kugou/status', { ...auth.getAuthConfig() })
status.value = r status.value = r
if (r?.service?.baseUrl && !baseUrlInput.value) {
baseUrlInput.value = r.service.baseUrl
}
} catch (e: any) { } catch (e: any) {
toast.show?.(e?.message || '状态刷新失败', 'error') toast.show?.(e?.message || '状态刷新失败', 'error')
} }
} }
async function saveBaseUrl() {
const value = (baseUrlInput.value || '').trim()
if (value && !/^https?:\/\//i.test(value)) {
toast.show?.('请填写合法的 http(s) URL,或留空走环境变量 KUGOU_API_BASE', 'error')
return
}
baseUrlSaving.value = true
baseUrlSavedTick.value = 0
try {
await $fetch('/api/admin/kugou/settings', {
method: 'POST',
...auth.getAuthConfig(),
body: { kugouApiBaseUrl: value || null },
})
toast.show?.('地址已保存', 'success')
baseUrlSavedTick.value = Date.now()
await reloadStatus()
} catch (e: any) {
toast.show?.(e?.message || '保存失败', 'error')
} finally {
baseUrlSaving.value = false
}
}
async function startQrLogin() { async function startQrLogin() {
qrLoading.value = true qrLoading.value = true
qrOpen.value = true qrOpen.value = true
+22
View File
@@ -377,6 +377,28 @@
/> />
</g> </g>
<!-- 服务器图标 -->
<g v-else-if="name === 'server'">
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<line x1="6" x2="6.01" y1="6" y2="6" />
<line x1="6" x2="6.01" y1="18" y2="18" />
</g>
<!-- 地球图标 (globe) -->
<g v-else-if="name === 'globe'">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</g>
<!-- 保存图标 -->
<g v-else-if="name === 'save'">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</g>
<!-- 搜索图标 --> <!-- 搜索图标 -->
<g v-else-if="name === 'search'"> <g v-else-if="name === 'search'">
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
@@ -1,6 +1,6 @@
-- 酷狗导入:管理员账号凭证 + 按日歌单同步记录表 -- 酷狗导入:管理员账号凭证 + 按日歌单同步记录表
ALTER TABLE "SystemSettings" ALTER TABLE "SystemSettings"
ADD COLUMN IF NOT EXISTS "kugouApiBaseUrl" text DEFAULT 'http://127.0.0.1:3000', ADD COLUMN IF NOT EXISTS "kugouApiBaseUrl" text,
ADD COLUMN IF NOT EXISTS "kugouAdminCookie" text, ADD COLUMN IF NOT EXISTS "kugouAdminCookie" text,
ADD COLUMN IF NOT EXISTS "kugouAdminToken" text, ADD COLUMN IF NOT EXISTS "kugouAdminToken" text,
ADD COLUMN IF NOT EXISTS "kugouAdminUser" jsonb, ADD COLUMN IF NOT EXISTS "kugouAdminUser" jsonb,
+1 -1
View File
@@ -354,7 +354,7 @@ export const systemSettings = pgTable('SystemSettings', {
enabledThemes: text('enabledThemes').default('[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]').notNull(), enabledThemes: text('enabledThemes').default('[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]').notNull(),
enabledPlatforms: text('enabledPlatforms'), enabledPlatforms: text('enabledPlatforms'),
platformOrder: text('platformOrder'), platformOrder: text('platformOrder'),
kugouApiBaseUrl: text('kugouApiBaseUrl').default('http://127.0.0.1:3000'), kugouApiBaseUrl: text('kugouApiBaseUrl'),
kugouAdminCookie: text('kugouAdminCookie'), kugouAdminCookie: text('kugouAdminCookie'),
kugouAdminToken: text('kugouAdminToken'), kugouAdminToken: text('kugouAdminToken'),
kugouAdminUser: jsonb('kugouAdminUser').$type<Record<string, any>>(), kugouAdminUser: jsonb('kugouAdminUser').$type<Record<string, any>>(),
+37
View File
@@ -0,0 +1,37 @@
import { eq } from 'drizzle-orm'
import { db } from '~/drizzle/db'
import { systemSettings } from '~/drizzle/schema'
import { CacheService } from '~~/server/services/cacheService'
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) throw createError({ statusCode: 401, message: '未登录' })
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role))
throw createError({ statusCode: 403, message: '仅管理员可修改酷狗配置' })
const body = await readBody(event)
const baseUrl = String(body?.kugouApiBaseUrl || '').trim()
if (baseUrl && !/^https?:\/\//i.test(baseUrl)) {
throw createError({ statusCode: 400, message: 'kugouApiBaseUrl 需要是合法的 http(s) URL,留空表示禁用 sidecar' })
}
const rows = await db.select().from(systemSettings).limit(1)
const patch: Record<string, any> = {
kugouApiBaseUrl: baseUrl || null,
updatedAt: new Date(),
}
if (!rows[0]) {
await db.insert(systemSettings).values(patch)
} else {
await db.update(systemSettings).set(patch).where(eq(systemSettings.id, rows[0].id))
}
try {
const cs = CacheService.getInstance()
await cs.setSystemSettings?.({ ...(rows[0] || {}), ...patch })
} catch {}
return { ok: true, kugouApiBaseUrl: baseUrl || null }
})
+1 -1
View File
@@ -17,7 +17,7 @@ export default defineEventHandler(async (event) => {
const rows = await db.select().from(systemSettings).limit(1) const rows = await db.select().from(systemSettings).limit(1)
const settings = rows[0] as any const settings = rows[0] as any
const usedBase = base || settings?.kugouApiBaseUrl || process.env.KUGOU_API_BASE || 'http://127.0.0.1:3000' const usedBase = base || settings?.kugouApiBaseUrl || process.env.KUGOU_API_BASE || ''
let serviceOk: boolean let serviceOk: boolean
let serviceMessage: string let serviceMessage: string
+1 -1
View File
@@ -23,6 +23,6 @@ export default defineEventHandler(async (event) => {
try { try {
const cs = CacheService.getInstance() const cs = CacheService.getInstance()
if (rows[0]) await cs.setSystemSettings?.({ ...rows[0], ...patch }) if (rows[0]) await cs.setSystemSettings?.({ ...rows[0], ...patch })
} catch (_) {} } catch {}
return { ok: true } return { ok: true }
}) })
+1 -1
View File
@@ -59,7 +59,7 @@ export async function kuGouFetch<T = any>(
options?: KuGouCallOptions, options?: KuGouCallOptions,
): Promise<T> { ): Promise<T> {
const settings = (await readAdminSettings()) as any const settings = (await readAdminSettings()) as any
const base = (options?.forceToken ? null : settings?.kugouApiBaseUrl) || process.env.KUGOU_API_BASE || 'http://127.0.0.1:3000' const base = (options?.forceToken ? null : settings?.kugouApiBaseUrl) || process.env.KUGOU_API_BASE || ''
const headers: Record<string, string> = { const headers: Record<string, string> = {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128 Safari/537.36',