mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-27 17:30:23 +08:00
feat: 支持API导入
This commit is contained in:
@@ -433,6 +433,27 @@
|
||||
"noToken": "No token generated yet. Please enable the service first",
|
||||
"regenerate": "Regenerate"
|
||||
},
|
||||
"dataSources": {
|
||||
"title": "Data Sources",
|
||||
"desc": "Configure external data source URLs. ChatLab will automatically pull and import chat data at the specified interval.",
|
||||
"empty": "No data sources",
|
||||
"disabled": "Paused",
|
||||
"every": "Every",
|
||||
"minutes": "min",
|
||||
"lastSync": "Last sync",
|
||||
"addBtn": "Add Data Source",
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g., My Chat Server",
|
||||
"url": "Data Source URL",
|
||||
"token": "Access Token (optional)",
|
||||
"tokenPlaceholder": "Bearer Token for the remote API",
|
||||
"interval": "Pull Interval (minutes)",
|
||||
"targetSession": "Target Session ID (optional)",
|
||||
"targetSessionPlaceholder": "Leave empty to create new session each time",
|
||||
"add": "Add"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage Guide",
|
||||
"desc": "After enabling the service, use the following endpoints to query data. All requests require Bearer Token authentication.",
|
||||
|
||||
@@ -433,6 +433,27 @@
|
||||
"noToken": "トークンが未生成です。まずサービスを有効にしてください",
|
||||
"regenerate": "再生成"
|
||||
},
|
||||
"dataSources": {
|
||||
"title": "データソース",
|
||||
"desc": "外部データソースの URL を設定すると、ChatLab が指定間隔で自動的にチャットデータを取得・インポートします。",
|
||||
"empty": "データソースなし",
|
||||
"disabled": "一時停止中",
|
||||
"every": "",
|
||||
"minutes": "分ごと",
|
||||
"lastSync": "最終同期",
|
||||
"addBtn": "データソースを追加",
|
||||
"form": {
|
||||
"name": "名前",
|
||||
"namePlaceholder": "例:マイチャットサーバー",
|
||||
"url": "データソース URL",
|
||||
"token": "アクセストークン(任意)",
|
||||
"tokenPlaceholder": "リモート API の Bearer Token",
|
||||
"interval": "取得間隔(分)",
|
||||
"targetSession": "ターゲットセッション ID(任意)",
|
||||
"targetSessionPlaceholder": "空欄の場合、毎回新規セッションを作成",
|
||||
"add": "追加"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用ガイド",
|
||||
"desc": "サービスを有効にした後、以下のエンドポイントでデータを照会できます。すべてのリクエストに Bearer Token 認証が必要です。",
|
||||
|
||||
@@ -433,6 +433,27 @@
|
||||
"noToken": "尚未生成 Token,请先启用服务",
|
||||
"regenerate": "重新生成"
|
||||
},
|
||||
"dataSources": {
|
||||
"title": "数据源",
|
||||
"desc": "配置外部数据源 URL,ChatLab 将按设定间隔自动拉取并导入聊天数据。",
|
||||
"empty": "暂无数据源",
|
||||
"disabled": "已暂停",
|
||||
"every": "每",
|
||||
"minutes": "分钟",
|
||||
"lastSync": "上次同步",
|
||||
"addBtn": "添加数据源",
|
||||
"form": {
|
||||
"name": "名称",
|
||||
"namePlaceholder": "例如:我的聊天服务器",
|
||||
"url": "数据源 URL",
|
||||
"token": "访问令牌(可选)",
|
||||
"tokenPlaceholder": "远程 API 的 Bearer Token",
|
||||
"interval": "拉取间隔(分钟)",
|
||||
"targetSession": "目标会话 ID(可选)",
|
||||
"targetSessionPlaceholder": "留空则每次新建会话",
|
||||
"add": "添加"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用说明",
|
||||
"desc": "启用服务后,可使用以下端点查询数据。所有请求需携带 Bearer Token 认证。",
|
||||
|
||||
@@ -433,6 +433,27 @@
|
||||
"noToken": "尚未產生 Token,請先啟用服務",
|
||||
"regenerate": "重新產生"
|
||||
},
|
||||
"dataSources": {
|
||||
"title": "資料來源",
|
||||
"desc": "設定外部資料來源 URL,ChatLab 將按設定間隔自動拉取並匯入聊天資料。",
|
||||
"empty": "暫無資料來源",
|
||||
"disabled": "已暫停",
|
||||
"every": "每",
|
||||
"minutes": "分鐘",
|
||||
"lastSync": "上次同步",
|
||||
"addBtn": "新增資料來源",
|
||||
"form": {
|
||||
"name": "名稱",
|
||||
"namePlaceholder": "例如:我的聊天伺服器",
|
||||
"url": "資料來源 URL",
|
||||
"token": "存取權杖(可選)",
|
||||
"tokenPlaceholder": "遠端 API 的 Bearer Token",
|
||||
"interval": "拉取間隔(分鐘)",
|
||||
"targetSession": "目標會話 ID(可選)",
|
||||
"targetSessionPlaceholder": "留空則每次新建會話",
|
||||
"add": "新增"
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用說明",
|
||||
"desc": "啟用服務後,可使用以下端點查詢數據。所有請求需攜帶 Bearer Token 認證。",
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useApiServerStore } from '@/stores/apiServer'
|
||||
import { useApiServerStore, type DataSource } from '@/stores/apiServer'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useApiServerStore()
|
||||
const { config, status, loading, isRunning, hasError, isPortInUse } = storeToRefs(store)
|
||||
const { config, status, loading, isRunning, hasError, isPortInUse, dataSources, pullingId } = storeToRefs(store)
|
||||
|
||||
const tokenVisible = ref(false)
|
||||
const editingPort = ref(false)
|
||||
const portInput = ref(5200)
|
||||
const copied = ref(false)
|
||||
const showAddForm = ref(false)
|
||||
|
||||
const newSource = reactive({
|
||||
name: '',
|
||||
url: '',
|
||||
token: '',
|
||||
intervalMinutes: 60,
|
||||
enabled: true,
|
||||
targetSessionId: '',
|
||||
})
|
||||
|
||||
let unlistenStartupError: (() => void) | null = null
|
||||
let unlistenPullResult: (() => void) | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
await store.refresh()
|
||||
portInput.value = config.value.port
|
||||
unlistenStartupError = store.listenStartupError()
|
||||
unlistenPullResult = store.listenPullResult()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenStartupError?.()
|
||||
unlistenPullResult?.()
|
||||
})
|
||||
|
||||
const maskedToken = computed(() => {
|
||||
@@ -71,14 +84,47 @@ async function copyToken() {
|
||||
await navigator.clipboard.writeText(config.value.token)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
} catch { /* fallback */ }
|
||||
}
|
||||
|
||||
async function handleRegenerateToken() {
|
||||
await store.regenerateToken()
|
||||
}
|
||||
|
||||
// ==================== 数据源管理 ====================
|
||||
|
||||
function resetNewSource() {
|
||||
newSource.name = ''
|
||||
newSource.url = ''
|
||||
newSource.token = ''
|
||||
newSource.intervalMinutes = 60
|
||||
newSource.enabled = true
|
||||
newSource.targetSessionId = ''
|
||||
showAddForm.value = false
|
||||
}
|
||||
|
||||
async function addSource() {
|
||||
if (!newSource.name || !newSource.url) return
|
||||
await store.addDataSource({ ...newSource })
|
||||
resetNewSource()
|
||||
}
|
||||
|
||||
async function toggleSourceEnabled(ds: DataSource) {
|
||||
await store.updateDataSource(ds.id, { enabled: !ds.enabled })
|
||||
}
|
||||
|
||||
async function removeSource(ds: DataSource) {
|
||||
await store.deleteDataSource(ds.id)
|
||||
}
|
||||
|
||||
async function pullNow(ds: DataSource) {
|
||||
await store.triggerPull(ds.id)
|
||||
}
|
||||
|
||||
function formatTime(ts: number): string {
|
||||
if (!ts) return '-'
|
||||
return new Date(ts * 1000).toLocaleString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,8 +147,6 @@ async function handleRegenerateToken() {
|
||||
</div>
|
||||
<USwitch :model-value="config.enabled" :loading="loading" @update:model-value="toggleEnabled" />
|
||||
</div>
|
||||
|
||||
<!-- 运行状态 -->
|
||||
<div v-if="config.enabled" class="mt-3 flex items-center gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
||||
<span class="inline-block h-2 w-2 rounded-full" :class="isRunning ? 'bg-green-500' : hasError ? 'bg-red-500' : 'bg-gray-400'"></span>
|
||||
<span class="text-xs" :class="statusColor">{{ statusText }}</span>
|
||||
@@ -110,8 +154,6 @@ async function handleRegenerateToken() {
|
||||
{{ apiBaseUrl }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 端口占用提示 -->
|
||||
<div v-if="isPortInUse" class="mt-2 rounded-md bg-red-50 p-2 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
|
||||
{{ t('settings.api.service.portInUseHint') }}
|
||||
</div>
|
||||
@@ -127,27 +169,17 @@ async function handleRegenerateToken() {
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.api.port.label') }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.api.port.desc') }}
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ t('settings.api.port.label') }}</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('settings.api.port.desc') }}</p>
|
||||
</div>
|
||||
<div v-if="editingPort" class="flex items-center gap-2">
|
||||
<UInput v-model.number="portInput" type="number" :min="1024" :max="65535" size="sm" class="w-24" />
|
||||
<UButton size="xs" color="primary" :loading="loading" @click="savePort">
|
||||
{{ t('settings.api.port.save') }}
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="cancelPortEdit">
|
||||
{{ t('settings.api.port.cancel') }}
|
||||
</UButton>
|
||||
<UButton size="xs" color="primary" :loading="loading" @click="savePort">{{ t('settings.api.port.save') }}</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="cancelPortEdit">{{ t('settings.api.port.cancel') }}</UButton>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">{{ config.port }}</span>
|
||||
<UButton size="xs" variant="ghost" @click="editingPort = true; portInput = config.port">
|
||||
{{ t('settings.api.port.edit') }}
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="editingPort = true; portInput = config.port">{{ t('settings.api.port.edit') }}</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,34 +192,132 @@ async function handleRegenerateToken() {
|
||||
{{ t('settings.api.token.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.api.token.label') }}
|
||||
</p>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.api.token.desc') }}
|
||||
</p>
|
||||
<div v-if="config.token" class="flex items-center gap-2">
|
||||
<code class="flex-1 rounded bg-gray-100 px-3 py-2 font-mono text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{{ tokenVisible ? config.token : maskedToken }}
|
||||
</code>
|
||||
<UButton size="xs" variant="ghost" @click="tokenVisible = !tokenVisible">
|
||||
<UIcon :name="tokenVisible ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'" class="h-4 w-4" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="copyToken">
|
||||
<UIcon :name="copied ? 'i-heroicons-check' : 'i-heroicons-clipboard'" class="h-4 w-4" />
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">
|
||||
{{ t('settings.api.token.noToken') }}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<UButton size="xs" variant="soft" color="warning" @click="handleRegenerateToken">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1 h-3 w-3" />
|
||||
{{ t('settings.api.token.regenerate') }}
|
||||
</UButton>
|
||||
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ t('settings.api.token.label') }}</p>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">{{ t('settings.api.token.desc') }}</p>
|
||||
<div v-if="config.token" class="flex items-center gap-2">
|
||||
<code class="flex-1 rounded bg-gray-100 px-3 py-2 font-mono text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{{ tokenVisible ? config.token : maskedToken }}
|
||||
</code>
|
||||
<UButton size="xs" variant="ghost" @click="tokenVisible = !tokenVisible">
|
||||
<UIcon :name="tokenVisible ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'" class="h-4 w-4" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="copyToken">
|
||||
<UIcon :name="copied ? 'i-heroicons-check' : 'i-heroicons-clipboard'" class="h-4 w-4" />
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">{{ t('settings.api.token.noToken') }}</div>
|
||||
<div class="mt-3">
|
||||
<UButton size="xs" variant="soft" color="warning" @click="handleRegenerateToken">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1 h-3 w-3" />
|
||||
{{ t('settings.api.token.regenerate') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据源管理 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-cloud-arrow-down" class="h-4 w-4 text-indigo-500" />
|
||||
{{ t('settings.api.dataSources.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.api.dataSources.desc') }}
|
||||
</p>
|
||||
|
||||
<!-- 数据源列表 -->
|
||||
<div v-if="dataSources.length > 0" class="mb-4 space-y-3">
|
||||
<div
|
||||
v-for="ds in dataSources"
|
||||
:key="ds.id"
|
||||
class="rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full"
|
||||
:class="ds.lastStatus === 'success' ? 'bg-green-500' : ds.lastStatus === 'error' ? 'bg-red-500' : 'bg-gray-400'"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ ds.name }}</span>
|
||||
<span v-if="!ds.enabled" class="text-xs text-gray-400">({{ t('settings.api.dataSources.disabled') }})</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
:loading="pullingId === ds.id"
|
||||
@click="pullNow(ds)"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="toggleSourceEnabled(ds)">
|
||||
<UIcon :name="ds.enabled ? 'i-heroicons-pause' : 'i-heroicons-play'" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" color="error" @click="removeSource(ds)">
|
||||
<UIcon name="i-heroicons-trash" class="h-3.5 w-3.5" />
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono">{{ ds.url }}</span>
|
||||
<span class="mx-2">·</span>
|
||||
<span>{{ t('settings.api.dataSources.every') }} {{ ds.intervalMinutes }} {{ t('settings.api.dataSources.minutes') }}</span>
|
||||
</div>
|
||||
<div v-if="ds.lastPullAt" class="mt-1 text-xs text-gray-400">
|
||||
{{ t('settings.api.dataSources.lastSync') }}: {{ formatTime(ds.lastPullAt) }}
|
||||
<span v-if="ds.lastStatus === 'success'" class="text-green-500">
|
||||
(+{{ ds.lastNewMessages }})
|
||||
</span>
|
||||
<span v-if="ds.lastStatus === 'error'" class="text-red-500">
|
||||
{{ ds.lastError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mb-4 text-center text-xs text-gray-400 py-4">
|
||||
{{ t('settings.api.dataSources.empty') }}
|
||||
</div>
|
||||
|
||||
<!-- 添加表单 -->
|
||||
<div v-if="showAddForm" class="rounded-lg border border-blue-200 bg-blue-50/50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{ t('settings.api.dataSources.form.name') }}</label>
|
||||
<UInput v-model="newSource.name" size="sm" :placeholder="t('settings.api.dataSources.form.namePlaceholder')" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{ t('settings.api.dataSources.form.url') }}</label>
|
||||
<UInput v-model="newSource.url" size="sm" placeholder="https://example.com/api/export" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{ t('settings.api.dataSources.form.token') }}</label>
|
||||
<UInput v-model="newSource.token" size="sm" :placeholder="t('settings.api.dataSources.form.tokenPlaceholder')" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{ t('settings.api.dataSources.form.interval') }}</label>
|
||||
<UInput v-model.number="newSource.intervalMinutes" type="number" :min="1" size="sm" class="w-32" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">{{ t('settings.api.dataSources.form.targetSession') }}</label>
|
||||
<UInput v-model="newSource.targetSessionId" size="sm" :placeholder="t('settings.api.dataSources.form.targetSessionPlaceholder')" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<UButton size="xs" color="primary" :disabled="!newSource.name || !newSource.url" @click="addSource">
|
||||
{{ t('settings.api.dataSources.form.add') }}
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="resetNewSource">
|
||||
{{ t('settings.api.port.cancel') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton v-if="!showAddForm" size="xs" variant="soft" @click="showAddForm = true">
|
||||
<UIcon name="i-heroicons-plus" class="mr-1 h-3 w-3" />
|
||||
{{ t('settings.api.dataSources.addBtn') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,9 +328,7 @@ async function handleRegenerateToken() {
|
||||
{{ t('settings.api.usage.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<p class="mb-3 text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.api.usage.desc') }}
|
||||
</p>
|
||||
<p class="mb-3 text-xs text-gray-600 dark:text-gray-400">{{ t('settings.api.usage.desc') }}</p>
|
||||
<div class="space-y-2">
|
||||
<div class="rounded bg-gray-100 p-2 font-mono text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<span class="text-green-600 dark:text-green-400">GET</span> {{ apiBaseUrl }}/status
|
||||
@@ -215,9 +343,7 @@ async function handleRegenerateToken() {
|
||||
<span class="text-blue-600 dark:text-blue-400">POST</span> {{ apiBaseUrl }}/sessions/:id/sql
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.api.usage.authHint') }}
|
||||
</p>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">{{ t('settings.api.usage.authHint') }}</p>
|
||||
<div class="mt-1 rounded bg-gray-100 p-2 font-mono text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
Authorization: Bearer {{ config.token ? maskedToken : 'clb_...' }}
|
||||
</div>
|
||||
|
||||
+94
-1
@@ -19,6 +19,21 @@ export interface ApiServerStatus {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface DataSource {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
token: string
|
||||
intervalMinutes: number
|
||||
enabled: boolean
|
||||
targetSessionId: string
|
||||
lastPullAt: number
|
||||
lastStatus: 'idle' | 'success' | 'error'
|
||||
lastError: string
|
||||
lastNewMessages: number
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export const useApiServerStore = defineStore('apiServer', () => {
|
||||
const config = ref<ApiServerConfig>({
|
||||
enabled: false,
|
||||
@@ -35,6 +50,8 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSources = ref<DataSource[]>([])
|
||||
const pullingId = ref<string | null>(null)
|
||||
|
||||
const isRunning = computed(() => status.value.running)
|
||||
const hasError = computed(() => !!status.value.error)
|
||||
@@ -57,7 +74,7 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([fetchConfig(), fetchStatus()])
|
||||
await Promise.all([fetchConfig(), fetchStatus(), fetchDataSources()])
|
||||
}
|
||||
|
||||
async function setEnabled(enabled: boolean) {
|
||||
@@ -99,10 +116,80 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 数据源管理 ====================
|
||||
|
||||
async function fetchDataSources() {
|
||||
try {
|
||||
dataSources.value = await window.apiServerApi.getDataSources()
|
||||
} catch (err) {
|
||||
console.error('[ApiServerStore] Failed to fetch data sources:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function addDataSource(partial: Omit<DataSource, 'id' | 'createdAt' | 'lastPullAt' | 'lastStatus' | 'lastError' | 'lastNewMessages'>) {
|
||||
try {
|
||||
const ds = await window.apiServerApi.addDataSource(partial)
|
||||
dataSources.value.push(ds)
|
||||
return ds
|
||||
} catch (err) {
|
||||
console.error('[ApiServerStore] Failed to add data source:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDataSource(id: string, updates: Partial<DataSource>) {
|
||||
try {
|
||||
const ds = await window.apiServerApi.updateDataSource(id, updates)
|
||||
if (ds) {
|
||||
const idx = dataSources.value.findIndex((s) => s.id === id)
|
||||
if (idx !== -1) dataSources.value[idx] = ds
|
||||
}
|
||||
return ds
|
||||
} catch (err) {
|
||||
console.error('[ApiServerStore] Failed to update data source:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDataSource(id: string) {
|
||||
try {
|
||||
const ok = await window.apiServerApi.deleteDataSource(id)
|
||||
if (ok) {
|
||||
dataSources.value = dataSources.value.filter((s) => s.id !== id)
|
||||
}
|
||||
return ok
|
||||
} catch (err) {
|
||||
console.error('[ApiServerStore] Failed to delete data source:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerPull(id: string) {
|
||||
pullingId.value = id
|
||||
try {
|
||||
const result = await window.apiServerApi.triggerPull(id)
|
||||
await fetchDataSources()
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('[ApiServerStore] Failed to trigger pull:', err)
|
||||
return { success: false, error: String(err) }
|
||||
} finally {
|
||||
pullingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function listenPullResult() {
|
||||
return window.apiServerApi.onPullResult(() => {
|
||||
fetchDataSources()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
status,
|
||||
loading,
|
||||
dataSources,
|
||||
pullingId,
|
||||
isRunning,
|
||||
hasError,
|
||||
isPortInUse,
|
||||
@@ -113,5 +200,11 @@ export const useApiServerStore = defineStore('apiServer', () => {
|
||||
setPort,
|
||||
regenerateToken,
|
||||
listenStartupError,
|
||||
fetchDataSources,
|
||||
addDataSource,
|
||||
updateDataSource,
|
||||
deleteDataSource,
|
||||
triggerPull,
|
||||
listenPullResult,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user