bhg-helper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/api/app.ts +53 -0
- package/api/index.ts +9 -0
- package/api/lib/logger.ts +65 -0
- package/api/lib/paths.ts +27 -0
- package/api/lib/providers.ts +66 -0
- package/api/lib/repository.ts +153 -0
- package/api/lib/types.ts +43 -0
- package/api/relay/config.ts +76 -0
- package/api/relay/protocol.ts +393 -0
- package/api/relay/server.ts +283 -0
- package/api/routes/backups.ts +73 -0
- package/api/routes/config.ts +197 -0
- package/api/routes/install.ts +158 -0
- package/api/routes/logs.ts +20 -0
- package/api/routes/providers.ts +13 -0
- package/api/routes/relay.ts +106 -0
- package/api/server.ts +40 -0
- package/cli/cli.js +454 -0
- package/dist/assets/index-BjvGHrGe.js +156 -0
- package/dist/assets/index-CQrGCyBr.css +1 -0
- package/dist/favicon.svg +4 -0
- package/dist/index.html +20 -0
- package/index.html +19 -0
- package/nodemon.json +10 -0
- package/package.json +82 -0
- package/postcss.config.js +10 -0
- package/scripts/install.bat +32 -0
- package/scripts/start.bat +46 -0
- package/scripts/start.ps1 +45 -0
- package/src/App.tsx +73 -0
- package/src/assets/react.svg +1 -0
- package/src/components/ConsolePanel.tsx +44 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/ErrorBoundary.tsx +54 -0
- package/src/components/Layout.tsx +17 -0
- package/src/components/Page.tsx +130 -0
- package/src/components/Sidebar.tsx +56 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/index.css +1350 -0
- package/src/lib/api.ts +120 -0
- package/src/lib/store.ts +166 -0
- package/src/lib/types.ts +117 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/ConsolePage.tsx +48 -0
- package/src/pages/Dashboard.tsx +101 -0
- package/src/pages/Install.tsx +195 -0
- package/src/pages/Relay.tsx +409 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +13 -0
- package/tsconfig.json +40 -0
- package/vite.config.ts +28 -0
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// BHG-helper API client
|
|
2
|
+
import type {
|
|
3
|
+
BackupInfo,
|
|
4
|
+
ConfigPayload,
|
|
5
|
+
Credentials,
|
|
6
|
+
BridgeConfig,
|
|
7
|
+
Settings,
|
|
8
|
+
ProviderPreset,
|
|
9
|
+
LogEntry,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
const BASE = '/api'
|
|
13
|
+
|
|
14
|
+
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
15
|
+
const res = await fetch(BASE + path, {
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
...init,
|
|
18
|
+
})
|
|
19
|
+
const json = await res.json().catch(() => ({ ok: false, error: 'invalid JSON' }))
|
|
20
|
+
if (!res.ok || !json.ok) {
|
|
21
|
+
throw new Error(json.error || `HTTP ${res.status}`)
|
|
22
|
+
}
|
|
23
|
+
return json.data as T
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RelayStatus {
|
|
27
|
+
running: boolean
|
|
28
|
+
info: { port: number; running: boolean; baseUrl: string; modelMap: Record<string, string>; deepseekApiKey: string } | null
|
|
29
|
+
config: {
|
|
30
|
+
port: number
|
|
31
|
+
deepseekBaseUrl: string
|
|
32
|
+
spoofProvider: string
|
|
33
|
+
verbose: boolean
|
|
34
|
+
deepseekApiKey: string
|
|
35
|
+
modelMap: Record<string, string>
|
|
36
|
+
configPath?: string
|
|
37
|
+
apiKeyConfigured?: boolean
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const api = {
|
|
42
|
+
getConfig: () => request<ConfigPayload>('/config'),
|
|
43
|
+
|
|
44
|
+
saveBridge: (bridge: BridgeConfig) =>
|
|
45
|
+
request<{ ok: true; backup: string | null; size: number }>('/config/bridge', {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
body: JSON.stringify(bridge),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
saveCredentials: (credentials: Credentials) =>
|
|
51
|
+
request<{ ok: true; backup: string | null; size: number }>('/config/credentials', {
|
|
52
|
+
method: 'PUT',
|
|
53
|
+
body: JSON.stringify(credentials),
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
saveSettings: (settings: Settings) =>
|
|
57
|
+
request<{ ok: true; backup: string | null; size: number }>('/config/settings', {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
body: JSON.stringify(settings),
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
listBackups: () => request<BackupInfo[]>('/backups'),
|
|
63
|
+
|
|
64
|
+
createBackup: (scope: 'bridge' | 'credentials' | 'settings') =>
|
|
65
|
+
request<{ filename: string }>('/backups', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: JSON.stringify({ scope }),
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
restoreBackup: (filename: string) =>
|
|
71
|
+
request<{ scope: string }>('/backups/restore', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({ filename }),
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
deleteBackup: (filename: string) =>
|
|
77
|
+
request<{ ok: true }>(`/backups/${encodeURIComponent(filename)}`, {
|
|
78
|
+
method: 'DELETE',
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
listProviderPresets: () => request<ProviderPreset[]>('/providers/presets'),
|
|
82
|
+
|
|
83
|
+
resetCatalog: () =>
|
|
84
|
+
request<{ ok: true; backup: string | null; size: number; modelCount: number; switchedActiveFrom: string | null }>(
|
|
85
|
+
'/config/reset-catalog',
|
|
86
|
+
{ method: 'POST' }
|
|
87
|
+
),
|
|
88
|
+
|
|
89
|
+
listLogs: () => request<LogEntry[]>('/logs'),
|
|
90
|
+
|
|
91
|
+
// Relay
|
|
92
|
+
getRelayStatus: () => request<RelayStatus>('/relay/status'),
|
|
93
|
+
startRelay: () => request<{ ok: true }>('/relay/start', { method: 'POST' }),
|
|
94
|
+
stopRelay: () => request<{ ok: true }>('/relay/stop', { method: 'POST' }),
|
|
95
|
+
restartRelay: () => request<{ ok: true }>('/relay/restart', { method: 'POST' }),
|
|
96
|
+
updateRelayConfig: (patch: Record<string, unknown>) =>
|
|
97
|
+
request<{ port: number; deepseekBaseUrl: string }>('/relay/config', {
|
|
98
|
+
method: 'PUT',
|
|
99
|
+
body: JSON.stringify(patch),
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// Install
|
|
104
|
+
getInstallStatus: () =>
|
|
105
|
+
request<{
|
|
106
|
+
projectRoot: string
|
|
107
|
+
hasNodeModules: boolean
|
|
108
|
+
hasPackageJson: boolean
|
|
109
|
+
needsInstall: boolean
|
|
110
|
+
isRunning: boolean
|
|
111
|
+
logCount: number
|
|
112
|
+
node: { ok: boolean; version?: string; error?: string }
|
|
113
|
+
npm: { ok: boolean; version?: string; error?: string }
|
|
114
|
+
}>('/install/status'),
|
|
115
|
+
runInstall: () =>
|
|
116
|
+
request<{ started: boolean; pid?: number }>('/install/run', { method: 'POST' }),
|
|
117
|
+
cancelInstall: () =>
|
|
118
|
+
request<{ killed: boolean }>('/install/cancel', { method: 'POST' }),
|
|
119
|
+
}
|
|
120
|
+
|
package/src/lib/store.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// BHG-helper — zustand store
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
import { api } from './api'
|
|
4
|
+
import type {
|
|
5
|
+
BackupInfo,
|
|
6
|
+
BridgeConfig,
|
|
7
|
+
ConfigPayload,
|
|
8
|
+
Credentials,
|
|
9
|
+
LogEntry,
|
|
10
|
+
ProviderPreset,
|
|
11
|
+
Settings,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
interface CockpitState {
|
|
15
|
+
// data
|
|
16
|
+
config: ConfigPayload | null
|
|
17
|
+
presets: ProviderPreset[]
|
|
18
|
+
logs: LogEntry[]
|
|
19
|
+
// ui
|
|
20
|
+
loading: boolean
|
|
21
|
+
saving: boolean
|
|
22
|
+
lastSavedAt: string | null
|
|
23
|
+
errorMessage: string | null
|
|
24
|
+
|
|
25
|
+
// actions
|
|
26
|
+
loadAll: () => Promise<void>
|
|
27
|
+
saveBridge: (bridge: BridgeConfig) => Promise<void>
|
|
28
|
+
saveCredentials: (c: Credentials) => Promise<void>
|
|
29
|
+
saveSettings: (s: Settings) => Promise<void>
|
|
30
|
+
setActiveModel: (id: string) => Promise<void>
|
|
31
|
+
refreshBackups: () => Promise<void>
|
|
32
|
+
createBackup: (scope: 'bridge' | 'credentials' | 'settings') => Promise<void>
|
|
33
|
+
restoreBackup: (filename: string) => Promise<void>
|
|
34
|
+
deleteBackup: (filename: string) => Promise<void>
|
|
35
|
+
appendLog: (entry: LogEntry) => void
|
|
36
|
+
clearError: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const useStore = create<CockpitState>((set, get) => ({
|
|
40
|
+
config: null,
|
|
41
|
+
presets: [],
|
|
42
|
+
logs: [],
|
|
43
|
+
loading: false,
|
|
44
|
+
saving: false,
|
|
45
|
+
lastSavedAt: null,
|
|
46
|
+
errorMessage: null,
|
|
47
|
+
|
|
48
|
+
loadAll: async () => {
|
|
49
|
+
set({ loading: true, errorMessage: null })
|
|
50
|
+
try {
|
|
51
|
+
const [config, presets, logs] = await Promise.all([
|
|
52
|
+
api.getConfig(),
|
|
53
|
+
api.listProviderPresets(),
|
|
54
|
+
api.listLogs(),
|
|
55
|
+
])
|
|
56
|
+
set({ config, presets, logs, loading: false })
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
59
|
+
set({ loading: false, errorMessage: msg })
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
saveBridge: async (bridge) => {
|
|
64
|
+
set({ saving: true, errorMessage: null })
|
|
65
|
+
try {
|
|
66
|
+
await api.saveBridge(bridge)
|
|
67
|
+
const config = await api.getConfig()
|
|
68
|
+
set({ config, saving: false, lastSavedAt: new Date().toISOString() })
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
71
|
+
set({ saving: false, errorMessage: msg })
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
saveCredentials: async (credentials) => {
|
|
76
|
+
set({ saving: true, errorMessage: null })
|
|
77
|
+
try {
|
|
78
|
+
await api.saveCredentials(credentials)
|
|
79
|
+
const config = await api.getConfig()
|
|
80
|
+
set({ config, saving: false, lastSavedAt: new Date().toISOString() })
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
83
|
+
set({ saving: false, errorMessage: msg })
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
saveSettings: async (settings) => {
|
|
88
|
+
set({ saving: true, errorMessage: null })
|
|
89
|
+
try {
|
|
90
|
+
await api.saveSettings(settings)
|
|
91
|
+
const config = await api.getConfig()
|
|
92
|
+
set({ config, saving: false, lastSavedAt: new Date().toISOString() })
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
95
|
+
set({ saving: false, errorMessage: msg })
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
setActiveModel: async (id) => {
|
|
100
|
+
const cur = get().config?.settings
|
|
101
|
+
if (!cur) return
|
|
102
|
+
const next: Settings = { ...cur, model: id }
|
|
103
|
+
await get().saveSettings(next)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
refreshBackups: async () => {
|
|
107
|
+
try {
|
|
108
|
+
const backups = await api.listBackups()
|
|
109
|
+
const config = get().config
|
|
110
|
+
if (config) set({ config: { ...config, backups } })
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
createBackup: async (scope) => {
|
|
117
|
+
try {
|
|
118
|
+
await api.createBackup(scope)
|
|
119
|
+
await get().refreshBackups()
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
122
|
+
set({ errorMessage: msg })
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
restoreBackup: async (filename) => {
|
|
127
|
+
try {
|
|
128
|
+
await api.restoreBackup(filename)
|
|
129
|
+
await get().loadAll()
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
132
|
+
set({ errorMessage: msg })
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
deleteBackup: async (filename) => {
|
|
137
|
+
try {
|
|
138
|
+
await api.deleteBackup(filename)
|
|
139
|
+
await get().refreshBackups()
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
142
|
+
set({ errorMessage: msg })
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
appendLog: (entry) => set((s) => ({ logs: [...s.logs, entry].slice(-300) })),
|
|
147
|
+
clearError: () => set({ errorMessage: null }),
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
// SSE log stream subscriber
|
|
151
|
+
export function startLogStream(): () => void {
|
|
152
|
+
const es = new EventSource('/api/logs/stream')
|
|
153
|
+
const handler = (e: MessageEvent) => {
|
|
154
|
+
try {
|
|
155
|
+
const data = JSON.parse(e.data) as LogEntry
|
|
156
|
+
useStore.getState().appendLog(data)
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
es.onmessage = handler
|
|
162
|
+
es.onerror = () => {
|
|
163
|
+
// silent — server will retry
|
|
164
|
+
}
|
|
165
|
+
return () => es.close()
|
|
166
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// BHG-helper — TypeScript types
|
|
2
|
+
|
|
3
|
+
export interface ModelEntry {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
tier: 'large' | 'middle' | 'small'
|
|
7
|
+
provider: string
|
|
8
|
+
context_window: number
|
|
9
|
+
default_max_tokens: number
|
|
10
|
+
max_tokens: number
|
|
11
|
+
pricing: {
|
|
12
|
+
currency: string
|
|
13
|
+
quota_type: string
|
|
14
|
+
pricing_group: string
|
|
15
|
+
input_per_1m_tokens: number
|
|
16
|
+
output_per_1m_tokens: number
|
|
17
|
+
cache_read_per_1m_tokens: number
|
|
18
|
+
cache_write_per_1m_tokens: number
|
|
19
|
+
}
|
|
20
|
+
description: string
|
|
21
|
+
protocol: 'anthropic' | 'openai'
|
|
22
|
+
enabled: boolean
|
|
23
|
+
available: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BridgeModelCatalogCache {
|
|
27
|
+
models: ModelEntry[]
|
|
28
|
+
defaults: { large: string; middle: string; small: string }
|
|
29
|
+
pricing_version?: string
|
|
30
|
+
pricing_group?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BridgeConfig {
|
|
34
|
+
bridgeModelCatalogCache?: BridgeModelCatalogCache
|
|
35
|
+
additionalModelOptionsCache?: Array<{
|
|
36
|
+
value: string
|
|
37
|
+
label: string
|
|
38
|
+
description: string
|
|
39
|
+
descriptionForModel: string
|
|
40
|
+
}>
|
|
41
|
+
oauthAccount?: null | {
|
|
42
|
+
accountUuid: string
|
|
43
|
+
organizationUuid: string
|
|
44
|
+
hasExtraUsageEnabled?: boolean
|
|
45
|
+
billingType?: string
|
|
46
|
+
subscriptionCreatedAt?: string
|
|
47
|
+
displayName?: string
|
|
48
|
+
}
|
|
49
|
+
[key: string]: unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Credentials {
|
|
53
|
+
claudeAiOauth?: {
|
|
54
|
+
accessToken: string
|
|
55
|
+
newApiApiKey: string
|
|
56
|
+
refreshToken: string
|
|
57
|
+
expiresAt: number
|
|
58
|
+
scopes: string[]
|
|
59
|
+
subscriptionType: string
|
|
60
|
+
rateLimitTier: string
|
|
61
|
+
newApiSessionUserId: number
|
|
62
|
+
newApiSessionCookie: string
|
|
63
|
+
}
|
|
64
|
+
[key: string]: unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Settings {
|
|
68
|
+
env?: Record<string, string>
|
|
69
|
+
includeCoAuthoredBy?: boolean
|
|
70
|
+
model: string
|
|
71
|
+
skipDangerousModePermissionPrompt?: boolean
|
|
72
|
+
hasCompletedOnboarding?: boolean
|
|
73
|
+
language?: string
|
|
74
|
+
[key: string]: unknown
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BackupInfo {
|
|
78
|
+
filename: string
|
|
79
|
+
scope: 'bridge' | 'credentials' | 'settings' | 'unknown'
|
|
80
|
+
size: number
|
|
81
|
+
createdAt: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ConfigPayload {
|
|
85
|
+
bhgHelperDir: string
|
|
86
|
+
files: {
|
|
87
|
+
bridge: string
|
|
88
|
+
credentials: string
|
|
89
|
+
settings: string
|
|
90
|
+
}
|
|
91
|
+
bridge: BridgeConfig | null
|
|
92
|
+
credentials: Credentials | null
|
|
93
|
+
settings: Settings | null
|
|
94
|
+
backups: BackupInfo[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ProviderPreset {
|
|
98
|
+
id: string
|
|
99
|
+
name: string
|
|
100
|
+
protocol: 'anthropic' | 'openai'
|
|
101
|
+
baseUrl?: string
|
|
102
|
+
notes: string
|
|
103
|
+
models: Array<{
|
|
104
|
+
id: string
|
|
105
|
+
label: string
|
|
106
|
+
tier: 'large' | 'middle' | 'small'
|
|
107
|
+
description: string
|
|
108
|
+
}>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface LogEntry {
|
|
112
|
+
id: number
|
|
113
|
+
ts: string
|
|
114
|
+
level: 'INFO' | 'OK' | 'WARN' | 'ERR'
|
|
115
|
+
scope: string
|
|
116
|
+
message: string
|
|
117
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useStore } from '../lib/store'
|
|
3
|
+
import Page, { Panel } from '../components/Page'
|
|
4
|
+
import { Trash2 } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export default function ConsolePage() {
|
|
7
|
+
const logs = useStore((s) => s.logs)
|
|
8
|
+
const bodyRef = useRef<HTMLDivElement | null>(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
|
|
12
|
+
}, [logs.length])
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Page title="事件日志" code="06" subtitle="所有文件读写 / API 调用 / 备份事件实时回放">
|
|
16
|
+
<Panel title={`EVENT STREAM · ${logs.length} 条`}>
|
|
17
|
+
<div
|
|
18
|
+
ref={bodyRef}
|
|
19
|
+
style={{
|
|
20
|
+
maxHeight: 'calc(100vh - 280px)',
|
|
21
|
+
overflowY: 'auto',
|
|
22
|
+
background: 'var(--bg-0)',
|
|
23
|
+
border: '1px solid var(--line-2)',
|
|
24
|
+
padding: 'var(--sp-2)',
|
|
25
|
+
fontFamily: 'var(--font-mono)',
|
|
26
|
+
fontSize: 11,
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{logs.length === 0 ? (
|
|
30
|
+
<div className="empty">
|
|
31
|
+
<div className="empty-title">— 还没有事件 —</div>
|
|
32
|
+
<div>所有操作会实时显示在此处</div>
|
|
33
|
+
</div>
|
|
34
|
+
) : (
|
|
35
|
+
logs.map((l) => (
|
|
36
|
+
<div key={l.id} className={'log-row ' + l.level}>
|
|
37
|
+
<span className="log-ts">{l.ts}</span>
|
|
38
|
+
<span className={'log-level ' + l.level}>{l.level}</span>
|
|
39
|
+
<span className="log-scope">{l.scope}</span>
|
|
40
|
+
<span className="log-msg">{l.message}</span>
|
|
41
|
+
</div>
|
|
42
|
+
))
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
</Panel>
|
|
46
|
+
</Page>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useStore } from '../lib/store'
|
|
2
|
+
import Page, { Panel, StatusCard } from '../components/Page'
|
|
3
|
+
import { RefreshCw, Server, ExternalLink, Terminal } from 'lucide-react'
|
|
4
|
+
import { api } from '../lib/api'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
|
|
7
|
+
export default function Dashboard() {
|
|
8
|
+
const loadAll = useStore((s) => s.loadAll)
|
|
9
|
+
const [relay, setRelay] = useState<{ running: boolean; hasKey: boolean; models: string[] } | null>(null)
|
|
10
|
+
|
|
11
|
+
async function refresh() {
|
|
12
|
+
try {
|
|
13
|
+
const s = await api.getRelayStatus()
|
|
14
|
+
const m = await fetch('http://127.0.0.1:8787/v1/models').then(r => r.json()).catch(() => null)
|
|
15
|
+
setRelay({
|
|
16
|
+
running: s.running,
|
|
17
|
+
hasKey: !!(s.info?.deepseekApiKey || s.config?.deepseekApiKey),
|
|
18
|
+
models: m?.data?.map((d: { id: string }) => d.id) ?? [],
|
|
19
|
+
})
|
|
20
|
+
} catch {
|
|
21
|
+
setRelay({ running: false, hasKey: false, models: [] })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
void refresh()
|
|
27
|
+
const t = setInterval(refresh, 5000)
|
|
28
|
+
return () => clearInterval(t)
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Page
|
|
33
|
+
title="主控台"
|
|
34
|
+
code="01"
|
|
35
|
+
subtitle="Claude Code → DeepSeek 本地中转"
|
|
36
|
+
actions={
|
|
37
|
+
<button className="btn" onClick={() => { void loadAll(); void refresh() }}>
|
|
38
|
+
<RefreshCw size={14} /> 刷新
|
|
39
|
+
</button>
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
<div className="panel-grid cols-3" style={{ marginBottom: 'var(--sp-5)' }}>
|
|
43
|
+
<StatusCard
|
|
44
|
+
tone={relay?.running ? 'green' : 'amber'}
|
|
45
|
+
label="中转服务"
|
|
46
|
+
value={relay?.running ? 'RUNNING :8787' : 'STOPPED'}
|
|
47
|
+
meta={relay?.running ? 'claude.exe 可用' : '去「中转服务」启动'}
|
|
48
|
+
/>
|
|
49
|
+
<StatusCard
|
|
50
|
+
tone={relay?.hasKey ? 'green' : 'amber'}
|
|
51
|
+
label="DeepSeek API Key"
|
|
52
|
+
value={relay?.hasKey ? '已配置' : '未配置'}
|
|
53
|
+
meta={relay?.hasKey ? '中转可调用' : '需要填入 Key'}
|
|
54
|
+
/>
|
|
55
|
+
<StatusCard
|
|
56
|
+
tone={(relay?.models?.length ?? 0) > 0 ? 'green' : 'amber'}
|
|
57
|
+
label="可用模型"
|
|
58
|
+
value={`${relay?.models?.length ?? 0} 个`}
|
|
59
|
+
meta={relay?.models?.[0] || '中转未启'}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Panel title="HOW IT WORKS · 工作原理">
|
|
64
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, alignItems: 'center', padding: 'var(--sp-3) 0' }}>
|
|
65
|
+
<Block title="claude.exe" sub="官方 CLI" body="读 ~/.claude/settings.json" />
|
|
66
|
+
<Block title="中转 :8787" sub="本地服务" body="Anthropic → OpenAI 翻译" highlight />
|
|
67
|
+
<Block title="DeepSeek" sub="api.deepseek.com" body="真实模型调用" />
|
|
68
|
+
</div>
|
|
69
|
+
</Panel>
|
|
70
|
+
|
|
71
|
+
<Panel title="QUICK START · 3 步">
|
|
72
|
+
<ol style={{ margin: 0, paddingLeft: 20, lineHeight: 1.9, fontSize: 13, color: 'var(--ink-1)' }}>
|
|
73
|
+
<li>去 <a href="https://platform.deepseek.com" target="_blank" rel="noreferrer" style={{ color: 'var(--accent-green)' }}>platform.deepseek.com <ExternalLink size={11} /></a> 申请 API Key</li>
|
|
74
|
+
<li>进 <a href="#/relay" style={{ color: 'var(--accent-green)' }}>「中转服务」</a> 填 Key → 保存 → 启动</li>
|
|
75
|
+
<li>新开终端跑 <code style={{ background: 'var(--bg-2)', padding: '2px 6px', borderRadius: 3, fontSize: 12 }}>claude</code></li>
|
|
76
|
+
</ol>
|
|
77
|
+
</Panel>
|
|
78
|
+
|
|
79
|
+
<div style={{ marginTop: 'var(--sp-3)', color: 'var(--ink-3)', fontSize: 11, display: 'flex', gap: 12, alignItems: 'center', fontFamily: 'var(--font-mono)' }}>
|
|
80
|
+
<Terminal size={12} />
|
|
81
|
+
端口: Vite 5173 · API 3001 · 中转 8787 · 全部仅本机监听
|
|
82
|
+
</div>
|
|
83
|
+
</Page>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Block({ title, sub, body, highlight }: { title: string; sub: string; body: string; highlight?: boolean }) {
|
|
88
|
+
return (
|
|
89
|
+
<div style={{
|
|
90
|
+
padding: 'var(--sp-3)',
|
|
91
|
+
background: highlight ? 'rgba(16, 185, 129, 0.08)' : 'var(--bg-1)',
|
|
92
|
+
border: `1px solid ${highlight ? 'rgba(16, 185, 129, 0.3)' : 'var(--line-1)'}`,
|
|
93
|
+
borderRadius: 8,
|
|
94
|
+
textAlign: 'center',
|
|
95
|
+
}}>
|
|
96
|
+
<div style={{ fontSize: 14, fontWeight: 600, color: highlight ? 'var(--accent-green)' : 'var(--ink-0)' }}>{title}</div>
|
|
97
|
+
<div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2, textTransform: 'uppercase', letterSpacing: 0.5 }}>{sub}</div>
|
|
98
|
+
<div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 6, fontFamily: 'var(--font-mono)' }}>{body}</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|