File size: 7,854 Bytes
4283ada
462f5f6
 
 
b7e2b8e
395e9dc
1f4cfcb
 
 
 
 
395e9dc
1f4cfcb
395e9dc
 
 
 
 
 
 
1f4cfcb
 
395e9dc
 
 
b7e2b8e
 
 
 
 
a0dfc48
 
d971f9f
 
a0dfc48
 
395e9dc
d971f9f
395e9dc
1f4cfcb
 
395e9dc
 
 
 
 
 
 
4283ada
 
2204456
 
 
 
1f4cfcb
 
 
4283ada
2204456
4283ada
2204456
 
1f4cfcb
 
a6cca4c
2204456
 
1f4cfcb
 
95df048
2204456
1f4cfcb
a08d7b4
2204456
 
a6cca4c
2204456
a0dfc48
2204456
 
 
1f4cfcb
2204456
a0dfc48
395e9dc
 
4283ada
 
 
 
1f4cfcb
b7e2b8e
1f4cfcb
462f5f6
a0dfc48
4283ada
a0dfc48
a6cca4c
 
 
a0dfc48
a08d7b4
95df048
395e9dc
 
4283ada
462f5f6
b7e2b8e
 
 
 
 
a0dfc48
b7e2b8e
1f4cfcb
 
 
 
 
 
 
 
 
 
 
 
 
 
395e9dc
 
 
4283ada
b7e2b8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4283ada
1f4cfcb
4283ada
 
395e9dc
 
 
 
 
 
1f4cfcb
395e9dc
 
 
 
 
4283ada
1f4cfcb
4283ada
 
40b21ae
1f4cfcb
4283ada
 
1f4cfcb
 
4283ada
 
462f5f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395e9dc
 
 
 
 
 
 
 
b7e2b8e
462f5f6
 
 
 
 
b7e2b8e
 
 
 
4283ada
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
<template>
  <!-- 初始配置页 -->
  <SetupPage v-if="needSetup" @configured="onSetupDone" />

  <!-- 登录页 -->
  <div v-else-if="!authenticated" class="min-h-screen flex items-center justify-center px-4">
    <div class="glass rounded-lg p-8 w-full max-w-sm">
      <div>
        <div class="text-[10px] uppercase tracking-[0.3em] text-ink-400 mb-1">Account Operations</div>
        <h1 class="text-2xl font-extrabold text-ink-950 mb-1 tracking-tight">AutoTeam</h1>
        <p class="text-sm text-ink-500 mb-6">输入管理 API Key 进入控制台</p>
        <div v-if="authError"
          class="mb-4 px-3 py-2.5 rounded-lg text-sm bg-rose-50 text-rose-700 border border-rose-200">
          {{ authError }}
        </div>
        <input
          v-model.trim="inputKey"
          type="password"
          placeholder="API Key"
          @keyup.enter="doLogin"
          class="w-full px-3.5 py-2.5 bg-surface border border-hairline rounded-lg text-sm text-ink-950
                 font-mono placeholder:text-ink-400 focus-ring mb-4 transition" />
        <AtButton variant="primary" class="w-full" :loading="authLoading" :disabled="!inputKey" @click="doLogin">
          {{ authLoading ? '验证中…' : '进入控制台' }}
        </AtButton>
      </div>
    </div>
  </div>

  <!-- 主面板 -->
  <div v-else class="flex min-h-screen">
    <!-- 侧边栏 -->
    <Sidebar :active="currentPage" :loading="loading" :auth-required="authRequired"
      @navigate="currentPage = $event" @refresh="refresh" @logout="doLogout" />

    <!-- 主内容区 -->
    <div class="flex-1 p-4 md:p-6 overflow-y-auto pb-20 md:pb-6 max-w-screen-2xl mx-auto w-full">
      <!-- 任务执行中提示 -->
      <div v-if="busyTask"
        class="flex items-center gap-2.5 text-sm text-amber-800 mb-4 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 w-fit animate-rise">
        <span class="animate-spin inline-block w-3.5 h-3.5 border-2 border-amber-700 border-t-transparent rounded-full"></span>
        <span class="font-medium">
          {{ busyTask.command === 'admin-login'
            ? '管理员登录中...'
            : busyTask.command === 'main-codex-sync'
              ? '主号 Codex 同步中...'
              : `${busyTask.command} 执行中...` }}
        </span>
      </div>

      <!-- 页面内容 — round-12 F1 加 Vue Transition page 过渡 -->
      <Transition name="page" mode="out-in">
        <Dashboard v-if="currentPage === 'dashboard'" key="dashboard"
          :status="status" :loading="loading" :running-task="busyTask" :admin-status="adminStatus"
          :register-failures="registerFailures" :register-failures-loading="registerFailuresLoading"
          :register-failures-unavailable="registerFailuresUnavailable"
          :master-health="masterHealth" @refresh="onActionRefresh" @reload-master-health="reloadMasterHealth" />

        <TeamMembers v-else-if="currentPage === 'team'" key="team" />

        <PoolPage v-else-if="currentPage === 'pool'" key="pool"
          :running-task="busyTask" :admin-status="adminStatus" :master-health="masterHealth" :status="status"
          :rotate-stream="rotateStream"
          @task-started="onTaskStarted" @refresh="onActionRefresh" @reload-master-health="reloadMasterHealth" />

        <SyncPage v-else-if="currentPage === 'sync'" key="sync"
          :running-task="busyTask" :admin-status="adminStatus"
          :rotate-stream="rotateStream"
          @task-started="onTaskStarted" @refresh="onActionRefresh" />

        <OAuthPage v-else-if="currentPage === 'oauth'" key="oauth"
          :manual-account-status="manualAccountStatus" @refresh="onActionRefresh" @progress="onAdminProgress" />

        <TaskHistoryPage v-else-if="currentPage === 'tasks'" key="tasks"
          :tasks="tasks" />

        <LogViewer v-else-if="currentPage === 'logs'" key="logs" />

        <Settings v-else-if="currentPage === 'settings'" key="settings"
          :admin-status="adminStatus" :codex-status="codexStatus"
          :master-health="masterHealth" :status="status"
          @refresh="onActionRefresh" @admin-progress="onAdminProgress" @reload-master-health="reloadMasterHealth" />
      </Transition>
    </div>

    <ToastHost />
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { api, setApiKey, clearApiKey } from './api.js'
import { useAppState } from './composables/useAppState.js'
import SetupPage from './components/SetupPage.vue'
import Sidebar from './components/Sidebar.vue'
import Dashboard from './components/Dashboard.vue'
import TeamMembers from './components/TeamMembers.vue'
import PoolPage from './components/PoolPage.vue'
import SyncPage from './components/SyncPage.vue'
import TaskHistoryPage from './components/TaskHistoryPage.vue'
import LogViewer from './components/LogViewer.vue'
import OAuthPage from './components/OAuthPage.vue'
import Settings from './components/Settings.vue'
import ToastHost from './components/ToastHost.vue'
import AtButton from './components/AtButton.vue'

const needSetup = ref(false)
const authenticated = ref(false)
const authRequired = ref(false)
const authLoading = ref(false)
const authError = ref('')
const inputKey = ref('')
const currentPage = ref('dashboard')

const appState = useAppState(authenticated)
const {
  status,
  adminStatus,
  codexStatus,
  manualAccountStatus,
  registerFailures,
  registerFailuresLoading,
  registerFailuresUnavailable,
  tasks,
  loading,
  busyTask,
  rotateStream,
} = appState
// Round 9 — master-health 提到 App 级,4 个页面共享同一份(避免每页各刷各的)
const masterHealth = ref(null)
const masterHealthLoading = ref(false)

async function checkAuth() {
  try {
    const result = await api.checkAuth()
    authenticated.value = result.authenticated
    authRequired.value = result.auth_required
    return result.authenticated
  } catch (e) {
    if (e.status === 401) {
      authenticated.value = false
      authRequired.value = true
      return false
    }
    authenticated.value = true
    authRequired.value = false
    return true
  }
}

async function doLogin() {
  authError.value = ''
  authLoading.value = true
  try {
    setApiKey(inputKey.value)
    const ok = await checkAuth()
    if (!ok) {
      clearApiKey()
      authError.value = 'API Key 无效'
    } else {
      inputKey.value = ''
      refresh()
    }
  } catch (e) {
    clearApiKey()
    authError.value = e.message
  } finally {
    authLoading.value = false
  }
}

function doLogout() {
  clearApiKey()
  authenticated.value = false
}

async function refresh() {
  await appState.refresh()
}

async function reloadMasterHealth(forceRefresh = false) {
  if (!adminStatus.value?.configured) return
  masterHealthLoading.value = true
  try {
    masterHealth.value = await api.getMasterHealth(!!forceRefresh)
  } catch (e) {
    masterHealth.value = null
  } finally {
    masterHealthLoading.value = false
  }
}

function onTaskStarted() {
  appState.notifyActionStarted()
}

function onAdminProgress() {
  appState.notifyActionStarted()
}

function onActionRefresh() {
  appState.notifyActionStarted()
}

async function checkSetup() {
  try {
    const result = await api.getSetupStatus()
    return result.configured
  } catch {
    return true // 接口不存在说明是旧版本,跳过
  }
}

function onSetupDone() {
  needSetup.value = false
  checkAuth().then(ok => {
    if (ok) {
      refresh()
    }
  })
}

// admin 配置完成后自动拉一次 master-health
watch(
  () => adminStatus.value?.configured,
  (configured) => {
    if (configured && !masterHealth.value) reloadMasterHealth(false)
  }
)

onMounted(async () => {
  const setupOk = await checkSetup()
  if (!setupOk) {
    needSetup.value = true
    return
  }
  const ok = await checkAuth()
  if (ok) {
    refresh()
  }
})
</script>