refactor remove upstream env dependency (#551)
This commit is contained in:
+1
-3
@@ -35,7 +35,6 @@ All key runtime settings are configured from compose variables.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PORT` | `6060` | Web UI listen port |
|
| `PORT` | `6060` | Web UI listen port |
|
||||||
| `BIND_HOST` | `0.0.0.0` | Optional Web UI bind host. Defaults to IPv4 for stable WSL/Windows access. Set `::` explicitly if you want IPv6 listening. |
|
| `BIND_HOST` | `0.0.0.0` | Optional Web UI bind host. Defaults to IPv4 for stable WSL/Windows access. Set `::` explicitly if you want IPv6 listening. |
|
||||||
| `UPSTREAM` | `http://hermes-agent:8642` | Hermes gateway URL (container internal) |
|
|
||||||
| `HERMES_BIN` | `/opt/hermes/.venv/bin/hermes` | Path to Hermes CLI binary |
|
| `HERMES_BIN` | `/opt/hermes/.venv/bin/hermes` | Path to Hermes CLI binary |
|
||||||
| `HERMES_AGENT_IMAGE` | `nousresearch/hermes-agent:latest` | Hermes Agent base image |
|
| `HERMES_AGENT_IMAGE` | `nousresearch/hermes-agent:latest` | Hermes Agent base image |
|
||||||
| `WEBUI_IMAGE` | `hermes-web-ui-local:latest` | Web UI image (set to `ekkoye8888/hermes-web-ui:latest` to use pre-built) |
|
| `WEBUI_IMAGE` | `hermes-web-ui-local:latest` | Web UI image (set to `ekkoye8888/hermes-web-ui:latest` to use pre-built) |
|
||||||
@@ -79,10 +78,9 @@ AUTH_DISABLED=false
|
|||||||
|
|
||||||
## Code Runtime Behavior
|
## Code Runtime Behavior
|
||||||
|
|
||||||
- Server upstream comes from `UPSTREAM` env (`packages/server/src/config.ts`).
|
|
||||||
- Hermes CLI binary comes from `HERMES_BIN` env (`packages/server/src/services/hermes-cli.ts`).
|
- Hermes CLI binary comes from `HERMES_BIN` env (`packages/server/src/services/hermes-cli.ts`).
|
||||||
- If `HERMES_BIN` is not provided, code falls back to `hermes` in `PATH`.
|
- If `HERMES_BIN` is not provided, code falls back to `hermes` in `PATH`.
|
||||||
- Profile switching dynamically resolves upstream URLs via `GatewayManager` — the `UPSTREAM` env only sets the default profile gateway.
|
- Profile switching dynamically resolves upstream URLs via `GatewayManager`.
|
||||||
|
|
||||||
## Common Operations
|
## Common Operations
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/auth/locked-ips": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Get locked-ips",
|
||||||
|
"description": "GET /api/auth/locked-ips",
|
||||||
|
"operationId": "listLockedIps",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Auth"
|
||||||
|
],
|
||||||
|
"summary": "Delete locked-ips",
|
||||||
|
"description": "DELETE /api/auth/locked-ips",
|
||||||
|
"operationId": "unlockIpHandler",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/auth/login": {
|
"/api/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const config = {
|
|||||||
port: parseInt(process.env.PORT || '8648', 10),
|
port: parseInt(process.env.PORT || '8648', 10),
|
||||||
// Default to IPv4 for stable WSL/Windows browser access. Use BIND_HOST=:: explicitly for IPv6.
|
// Default to IPv4 for stable WSL/Windows browser access. Use BIND_HOST=:: explicitly for IPv6.
|
||||||
host: getListenHost(),
|
host: getListenHost(),
|
||||||
upstream: process.env.UPSTREAM || 'http://127.0.0.1:8642',
|
|
||||||
uploadDir: process.env.UPLOAD_DIR || resolve(homedir(), '.hermes-web-ui', 'upload'),
|
uploadDir: process.env.UPLOAD_DIR || resolve(homedir(), '.hermes-web-ui', 'upload'),
|
||||||
dataDir: resolve(__dirname, '..', 'data'),
|
dataDir: resolve(__dirname, '..', 'data'),
|
||||||
corsOrigins: process.env.CORS_ORIGINS || '*',
|
corsOrigins: process.env.CORS_ORIGINS || '*',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { existsSync, readFileSync } from 'fs'
|
|||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import * as hermesCli from '../services/hermes/hermes-cli'
|
import * as hermesCli from '../services/hermes/hermes-cli'
|
||||||
import { getGatewayManagerInstance } from '../services/gateway-bootstrap'
|
import { getGatewayManagerInstance } from '../services/gateway-bootstrap'
|
||||||
import { config } from '../config'
|
|
||||||
|
|
||||||
declare const __APP_VERSION__: string
|
declare const __APP_VERSION__: string
|
||||||
|
|
||||||
@@ -73,7 +72,10 @@ export async function healthCheck(ctx: any) {
|
|||||||
let gatewayOk = false
|
let gatewayOk = false
|
||||||
try {
|
try {
|
||||||
const mgr = getGatewayManagerInstance()
|
const mgr = getGatewayManagerInstance()
|
||||||
const upstream = mgr?.getUpstream() || config.upstream
|
const upstream = mgr?.getUpstream()
|
||||||
|
if (!upstream) {
|
||||||
|
throw new Error('GatewayManager not initialized')
|
||||||
|
}
|
||||||
const res = await fetch(`${upstream.replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(5000) })
|
const res = await fetch(`${upstream.replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(5000) })
|
||||||
gatewayOk = res.ok
|
gatewayOk = res.ok
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { Context } from 'koa'
|
import type { Context } from 'koa'
|
||||||
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
||||||
import { config } from '../../config'
|
|
||||||
|
|
||||||
function getUpstream(profile: string): string {
|
function getUpstream(profile: string): string {
|
||||||
const mgr = getGatewayManagerInstance()
|
const mgr = getGatewayManagerInstance()
|
||||||
return mgr ? mgr.getUpstream(profile) : config.upstream.replace(/\/$/, '')
|
if (!mgr) {
|
||||||
|
throw new Error('GatewayManager not initialized')
|
||||||
|
}
|
||||||
|
return mgr.getUpstream(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApiKey(profile: string): string | null {
|
function getApiKey(profile: string): string | null {
|
||||||
@@ -54,7 +56,15 @@ async function readUpstreamError(res: Response): Promise<unknown> {
|
|||||||
|
|
||||||
async function proxyRequest(ctx: Context, upstreamPath: string, method?: string): Promise<void> {
|
async function proxyRequest(ctx: Context, upstreamPath: string, method?: string): Promise<void> {
|
||||||
const profile = resolveProfile(ctx)
|
const profile = resolveProfile(ctx)
|
||||||
const upstream = getUpstream(profile)
|
let upstream: string
|
||||||
|
try {
|
||||||
|
upstream = getUpstream(profile)
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.status = 503
|
||||||
|
ctx.set('Content-Type', 'application/json')
|
||||||
|
ctx.body = { error: { message: e?.message || 'GatewayManager not initialized' } }
|
||||||
|
return
|
||||||
|
}
|
||||||
const params = new URLSearchParams(ctx.search || '')
|
const params = new URLSearchParams(ctx.search || '')
|
||||||
params.delete('token')
|
params.delete('token')
|
||||||
const search = params.toString()
|
const search = params.toString()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { emitWebhook } from '../services/hermes/hermes'
|
|
||||||
import { logger } from '../services/logger'
|
import { logger } from '../services/logger'
|
||||||
|
|
||||||
export async function handleWebhook(ctx: any) {
|
export async function handleWebhook(ctx: any) {
|
||||||
@@ -9,6 +8,5 @@ export async function handleWebhook(ctx: any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.info('Received webhook event: %s', payload.event)
|
logger.info('Received webhook event: %s', payload.event)
|
||||||
emitWebhook(payload)
|
|
||||||
ctx.body = { ok: true }
|
ctx.body = { ok: true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,10 +163,8 @@ export async function bootstrap() {
|
|||||||
const interfaces = safeNetworkInterfaces()
|
const interfaces = safeNetworkInterfaces()
|
||||||
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
||||||
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
||||||
console.log(`Upstream: ${config.upstream}`)
|
|
||||||
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
||||||
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
||||||
logger.info('Upstream: %s', config.upstream)
|
|
||||||
|
|
||||||
// Restore group chat agents after server is ready.
|
// Restore group chat agents after server is ready.
|
||||||
groupChatServer.restoreWhenReady()
|
groupChatServer.restoreWhenReady()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Context } from 'koa'
|
import type { Context } from 'koa'
|
||||||
import { config } from '../../config'
|
|
||||||
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
import { getGatewayManagerInstance } from '../../services/gateway-bootstrap'
|
||||||
import { updateUsage } from '../../db/hermes/usage-store'
|
import { updateUsage } from '../../db/hermes/usage-store'
|
||||||
|
|
||||||
@@ -68,14 +67,14 @@ function resolveProfile(ctx: Context): string {
|
|||||||
/** Resolve upstream URL for a request based on profile header/query */
|
/** Resolve upstream URL for a request based on profile header/query */
|
||||||
function resolveUpstream(ctx: Context): string {
|
function resolveUpstream(ctx: Context): string {
|
||||||
const mgr = getGatewayManager()
|
const mgr = getGatewayManager()
|
||||||
if (mgr) {
|
if (!mgr) {
|
||||||
const profile = resolveProfile(ctx)
|
throw new Error('GatewayManager not initialized')
|
||||||
if (profile && profile !== 'default') {
|
|
||||||
return mgr.getUpstream(profile)
|
|
||||||
}
|
|
||||||
return mgr.getUpstream()
|
|
||||||
}
|
}
|
||||||
return config.upstream.replace(/\/$/, '')
|
const profile = resolveProfile(ctx)
|
||||||
|
if (profile && profile !== 'default') {
|
||||||
|
return mgr.getUpstream(profile)
|
||||||
|
}
|
||||||
|
return mgr.getUpstream()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProxyHeaders(ctx: Context, upstream: string): Record<string, string> {
|
function buildProxyHeaders(ctx: Context, upstream: string): Record<string, string> {
|
||||||
@@ -185,7 +184,14 @@ async function streamSSE(ctx: Context, res: Response, profile: string): Promise<
|
|||||||
|
|
||||||
export async function proxy(ctx: Context) {
|
export async function proxy(ctx: Context) {
|
||||||
const profile = resolveProfile(ctx)
|
const profile = resolveProfile(ctx)
|
||||||
const upstream = resolveUpstream(ctx)
|
let upstream: string
|
||||||
|
try {
|
||||||
|
upstream = resolveUpstream(ctx)
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.status = 503
|
||||||
|
ctx.body = { error: { message: e?.message || 'GatewayManager not initialized' } }
|
||||||
|
return
|
||||||
|
}
|
||||||
const upstreamPath = ctx.path.replace(/^\/api\/hermes\/v1/, '/v1').replace(/^\/api\/hermes/, '/api')
|
const upstreamPath = ctx.path.replace(/^\/api\/hermes\/v1/, '/v1').replace(/^\/api\/hermes/, '/api')
|
||||||
const params = new URLSearchParams(ctx.search || '')
|
const params = new URLSearchParams(ctx.search || '')
|
||||||
params.delete('token')
|
params.delete('token')
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import { config } from '../../config'
|
|
||||||
import { logger } from '../logger'
|
|
||||||
|
|
||||||
const UPSTREAM = config.upstream.replace(/\/$/, '')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an instruction to Hermes Agent via /v1/runs
|
|
||||||
*/
|
|
||||||
export async function sendInstruction(params: {
|
|
||||||
input: string | any[]
|
|
||||||
instructions?: string
|
|
||||||
conversationHistory?: any[]
|
|
||||||
sessionId?: string
|
|
||||||
authToken?: string
|
|
||||||
}): Promise<{ run_id: string; status: string }> {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
if (params.authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${params.authToken}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: any = { input: params.input }
|
|
||||||
if (params.instructions) body.instructions = params.instructions
|
|
||||||
if (params.conversationHistory) body.conversation_history = params.conversationHistory
|
|
||||||
if (params.sessionId) body.session_id = params.sessionId
|
|
||||||
|
|
||||||
const res = await fetch(`${UPSTREAM}/v1/runs`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text()
|
|
||||||
throw new Error(`Hermes API error ${res.status}: ${text}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get run status (poll /v1/runs/:id if supported)
|
|
||||||
*/
|
|
||||||
export async function getRunStatus(runId: string): Promise<any> {
|
|
||||||
const res = await fetch(`${UPSTREAM}/v1/runs/${runId}`)
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to get run status: ${res.status}`)
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to SSE events for a run
|
|
||||||
*/
|
|
||||||
export async function* streamRunEvents(runId: string, authToken?: string): AsyncGenerator<any> {
|
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
if (authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${authToken}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${UPSTREAM}/v1/runs/${runId}/events`, { headers })
|
|
||||||
if (!res.ok || !res.body) {
|
|
||||||
throw new Error(`Failed to stream run events: ${res.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = res.body.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
let buffer = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
|
||||||
|
|
||||||
const lines = buffer.split('\n')
|
|
||||||
buffer = lines.pop() || ''
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith('data: ')) {
|
|
||||||
const data = line.slice(6).trim()
|
|
||||||
if (data === '[DONE]') return
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(data)
|
|
||||||
yield event
|
|
||||||
if (event.event === 'run.completed' || event.event === 'run.failed') return
|
|
||||||
} catch { /* skip malformed lines */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Health check
|
|
||||||
*/
|
|
||||||
export async function healthCheck(): Promise<{ status: string; version?: string }> {
|
|
||||||
const res = await fetch(`${UPSTREAM}/health`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch available models
|
|
||||||
*/
|
|
||||||
export async function fetchModels(): Promise<{ data: Array<{ id: string }> }> {
|
|
||||||
const res = await fetch(`${UPSTREAM}/v1/models`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webhook callback registry
|
|
||||||
type WebhookCallback = (payload: any) => void | Promise<void>
|
|
||||||
const webhookCallbacks: WebhookCallback[] = []
|
|
||||||
|
|
||||||
export function onWebhook(callback: WebhookCallback) {
|
|
||||||
webhookCallbacks.push(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emitWebhook(payload: any) {
|
|
||||||
for (const cb of webhookCallbacks) {
|
|
||||||
const result = cb(payload)
|
|
||||||
if (result && typeof result.catch === 'function') {
|
|
||||||
result.catch((err: Error) => logger.error(err, 'Webhook callback error'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -137,7 +137,6 @@ export default {
|
|||||||
['AUTH_TOKEN', 'Custom auth token (overrides auto-generated)'],
|
['AUTH_TOKEN', 'Custom auth token (overrides auto-generated)'],
|
||||||
['PORT', 'Server listen port (default: 8648)'],
|
['PORT', 'Server listen port (default: 8648)'],
|
||||||
['BIND_HOST', 'Server bind host (default: 0.0.0.0). Set :: explicitly to enable IPv6 listening.'],
|
['BIND_HOST', 'Server bind host (default: 0.0.0.0). Set :: explicitly to enable IPv6 listening.'],
|
||||||
['UPSTREAM', 'Hermes gateway URL (default: http://127.0.0.1:8642)'],
|
|
||||||
['UPLOAD_DIR', 'Custom upload directory path'],
|
['UPLOAD_DIR', 'Custom upload directory path'],
|
||||||
['CORS_ORIGINS', 'CORS origin config (default: *)'],
|
['CORS_ORIGINS', 'CORS origin config (default: *)'],
|
||||||
['HERMES_BIN', 'Custom path to hermes CLI binary'],
|
['HERMES_BIN', 'Custom path to hermes CLI binary'],
|
||||||
@@ -145,7 +144,7 @@ export default {
|
|||||||
},
|
},
|
||||||
gateway: {
|
gateway: {
|
||||||
title: 'Gateway Management',
|
title: 'Gateway Management',
|
||||||
content: 'The gateway is the Hermes Agent process that handles AI conversations. Hermes Web UI manages the gateway lifecycle — start, stop, and monitor from the Gateways page. Multiple gateways can run with different profiles.',
|
content: 'The gateway is the Hermes Agent process that handles AI conversations. Hermes Web UI manages the gateway lifecycle — start, stop, and monitor from the Gateways page. Multiple gateways can run with different profiles, and each profile resolves its own gateway host/port from its Hermes config.',
|
||||||
},
|
},
|
||||||
profiles: {
|
profiles: {
|
||||||
title: 'Profiles',
|
title: 'Profiles',
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ export default {
|
|||||||
['AUTH_TOKEN', '自定义认证令牌(覆盖自动生成的令牌)'],
|
['AUTH_TOKEN', '自定义认证令牌(覆盖自动生成的令牌)'],
|
||||||
['PORT', '服务器监听端口(默认:8648)'],
|
['PORT', '服务器监听端口(默认:8648)'],
|
||||||
['BIND_HOST', '服务器绑定地址(默认:0.0.0.0)。如需 IPv6,请显式设置为 ::。'],
|
['BIND_HOST', '服务器绑定地址(默认:0.0.0.0)。如需 IPv6,请显式设置为 ::。'],
|
||||||
['UPSTREAM', 'Hermes 网关 URL(默认:http://127.0.0.1:8642)'],
|
|
||||||
['UPLOAD_DIR', '自定义上传目录路径'],
|
['UPLOAD_DIR', '自定义上传目录路径'],
|
||||||
['CORS_ORIGINS', 'CORS 来源配置(默认:*)'],
|
['CORS_ORIGINS', 'CORS 来源配置(默认:*)'],
|
||||||
['HERMES_BIN', '自定义 hermes CLI 二进制路径'],
|
['HERMES_BIN', '自定义 hermes CLI 二进制路径'],
|
||||||
@@ -145,7 +144,7 @@ export default {
|
|||||||
},
|
},
|
||||||
gateway: {
|
gateway: {
|
||||||
title: '网关管理',
|
title: '网关管理',
|
||||||
content: '网关是处理 AI 对话的 Hermes Agent 进程。Hermes Web UI 管理网关生命周期——在网关页面启动、停止和监控。不同配置可运行多个网关。',
|
content: '网关是处理 AI 对话的 Hermes Agent 进程。Hermes Web UI 管理网关生命周期——在网关页面启动、停止和监控。不同配置可运行多个网关,且每个 profile 都会从各自的 Hermes 配置中解析网关 host/port。',
|
||||||
},
|
},
|
||||||
profiles: {
|
profiles: {
|
||||||
title: '配置文件',
|
title: '配置文件',
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
vi.mock('../../packages/server/src/config', () => ({
|
|
||||||
config: { upstream: 'http://127.0.0.1:8642' },
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../../packages/server/src/services/gateway-bootstrap', () => ({
|
vi.mock('../../packages/server/src/services/gateway-bootstrap', () => ({
|
||||||
getGatewayManagerInstance: () => null,
|
getGatewayManagerInstance: () => ({
|
||||||
|
getUpstream: () => 'http://127.0.0.1:8642',
|
||||||
|
getApiKey: () => null,
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockFetch = vi.fn()
|
const mockFetch = vi.fn()
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
// Mock config
|
|
||||||
vi.mock('../../packages/server/src/config', () => ({
|
|
||||||
config: { upstream: 'http://127.0.0.1:8642' },
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('../../packages/server/src/services/gateway-bootstrap', () => ({
|
vi.mock('../../packages/server/src/services/gateway-bootstrap', () => ({
|
||||||
getGatewayManagerInstance: () => null,
|
getGatewayManagerInstance: () => ({
|
||||||
|
getUpstream: () => 'http://127.0.0.1:8642',
|
||||||
|
getApiKey: () => null,
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock updateUsage so we can assert calls without real DB
|
// Mock updateUsage so we can assert calls without real DB
|
||||||
|
|||||||
Reference in New Issue
Block a user