fix(phase17): 401 auth, provider config from server, remove hardcoded PROVIDERS
Problems fixed: 1. 401 unauthorized on chat — OLLAMA_API_KEY was not set in containers - Created docker/.env with real API key - Added OLLAMA_BASE_URL + OLLAMA_API_KEY to control-center in docker-compose.yml 2. AgentDetailModal/AgentCreateModal showed hardcoded providers list (Ollama, OpenAI, Anthropic, Mistral, Groq) regardless of what is configured - Removed const PROVIDERS = [...] from both modals - Now loads providers via trpc.config.providers (server-side) - Only shows providers that are actually configured in env 3. Settings.tsx had API key hardcoded in frontend source code (security issue) - API key removed from frontend - New trpc.config.providers endpoint returns masked key (first 8 chars + ***) - Shows red warning badge 'NO KEY — chat will fail' if key is missing - Base URL read from server env, not hardcoded New tRPC endpoint: config.providers - Returns list of configured providers with name, baseUrl, hasKey, maskedKey - Provider name auto-detected from URL (ollama.com → 'Ollama Cloud', etc.)
This commit is contained in:
1
.git-credentials
Normal file
1
.git-credentials
Normal file
@@ -0,0 +1 @@
|
||||
https://x-access-token:ghs_b4NOitjlosRPPypJr3KupAZqrOXlxr4fq5Z9@github.com
|
||||
@@ -28,11 +28,7 @@ const AGENT_ROLES = [
|
||||
{ value: "monitor", label: "Monitor - System monitoring" },
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
{ value: "Ollama", label: "Ollama (Local/Cloud)" },
|
||||
{ value: "OpenAI", label: "OpenAI (GPT)" },
|
||||
{ value: "Anthropic", label: "Anthropic (Claude)" },
|
||||
];
|
||||
// Providers are loaded dynamically from server config — no hardcoded list
|
||||
|
||||
export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateModalProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -51,6 +47,11 @@ export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateM
|
||||
const { data: modelsData, isLoading: modelsLoading } = trpc.ollama.models.useQuery(undefined, {
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const { data: configData } = trpc.config.providers.useQuery(undefined, {
|
||||
staleTime: 300_000,
|
||||
});
|
||||
// Only providers configured on server
|
||||
const connectedProviders = configData?.providers ?? [];
|
||||
|
||||
const createMutation = trpc.agents.create.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -157,11 +158,17 @@ export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateM
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDERS.map((provider) => (
|
||||
<SelectItem key={provider.value} value={provider.value}>
|
||||
{provider.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{connectedProviders.length > 0
|
||||
? connectedProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={p.name}>
|
||||
<span className="flex items-center gap-2">
|
||||
{p.name}
|
||||
<span className="text-[10px] text-neon-green font-mono">● connected</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))
|
||||
: <SelectItem value={formData.provider}>{formData.provider}</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,6 @@ interface AgentDetailModalProps {
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
const PROVIDERS = ["Ollama", "OpenAI", "Anthropic", "Mistral", "Groq"];
|
||||
const TOOL_OPTIONS = ["http_get", "http_post", "shell_exec", "file_read", "file_write", "docker_list", "docker_exec", "docker_logs", "browser_navigate"];
|
||||
|
||||
function toNum(v: string | number | null | undefined, fallback = 0): number {
|
||||
@@ -70,6 +69,11 @@ export function AgentDetailModal({ agent, open, onOpenChange, onSave }: AgentDet
|
||||
const { data: modelsData, isLoading: modelsLoading } = trpc.ollama.models.useQuery(undefined, {
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const { data: configData } = trpc.config.providers.useQuery(undefined, {
|
||||
staleTime: 300_000,
|
||||
});
|
||||
// Only show providers that are actually configured on the server
|
||||
const connectedProviders = configData?.providers ?? [];
|
||||
const { data: history = [] } = trpc.agents.history.useQuery(
|
||||
{ id: agent?.id ?? 0, limit: 20 },
|
||||
{ enabled: !!agent && open }
|
||||
@@ -194,7 +198,17 @@ export function AgentDetailModal({ agent, open, onOpenChange, onSave }: AgentDet
|
||||
<Select value={form.provider ?? ""} onValueChange={(v) => setForm({ ...form, provider: v, model: "" })}>
|
||||
<SelectTrigger className="font-mono"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDERS.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
{connectedProviders.length > 0
|
||||
? connectedProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={p.name}>
|
||||
<span className="flex items-center gap-2">
|
||||
{p.name}
|
||||
<span className="text-[10px] text-neon-green font-mono">● connected</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))
|
||||
: <SelectItem value={form.provider ?? "ollama"}>{form.provider ?? "Ollama"}</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -53,11 +53,16 @@ export default function Settings() {
|
||||
|
||||
// Реальные данные из Ollama API
|
||||
const healthQuery = trpc.ollama.health.useQuery(undefined, {
|
||||
refetchInterval: 30_000, // Обновлять каждые 30 секунд
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
const modelsQuery = trpc.ollama.models.useQuery(undefined, {
|
||||
refetchInterval: 60_000, // Обновлять каждые 60 секунд
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
// Server-side provider configuration (API key masked)
|
||||
const configQuery = trpc.config.providers.useQuery(undefined, {
|
||||
staleTime: 300_000,
|
||||
});
|
||||
const primaryProvider = configQuery.data?.providers?.[0];
|
||||
|
||||
const ollamaStatus = healthQuery.data?.connected ? "connected" : healthQuery.isLoading ? "unchecked" : "error";
|
||||
const ollamaLatency = healthQuery.data?.latencyMs ?? 0;
|
||||
@@ -133,7 +138,7 @@ export default function Settings() {
|
||||
)}
|
||||
<div>
|
||||
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
||||
Ollama Cloud
|
||||
{primaryProvider?.name ?? "Ollama Cloud"}
|
||||
<Badge variant="outline" className="text-[9px] font-mono bg-primary/10 text-primary border-primary/20">
|
||||
LIVE
|
||||
</Badge>
|
||||
@@ -156,12 +161,12 @@ export default function Settings() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Base URL */}
|
||||
{/* Base URL — from server config */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[11px] font-mono text-muted-foreground">BASE URL</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value="https://ollama.com/v1"
|
||||
value={primaryProvider?.baseUrl ?? "https://ollama.com/v1"}
|
||||
className="bg-secondary/30 border-border/30 font-mono text-xs h-8"
|
||||
readOnly
|
||||
/>
|
||||
@@ -182,15 +187,17 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
{/* API Key — masked, read from server env */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[11px] font-mono text-muted-foreground">API KEY</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showKeys["ollama"] ? "text" : "password"}
|
||||
value="feaa56e2dff045af989346ca74cb33a6.xzJ-plOVSgTL1FbmL8PZZ3Wx"
|
||||
className="bg-secondary/30 border-border/30 font-mono text-xs h-8 pr-10"
|
||||
value={primaryProvider?.maskedKey ?? (primaryProvider?.hasKey ? "(configured)" : "(not set)")}
|
||||
className={`bg-secondary/30 border-border/30 font-mono text-xs h-8 pr-10 ${
|
||||
primaryProvider?.hasKey ? "" : "text-neon-amber"
|
||||
}`}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
@@ -200,15 +207,17 @@ export default function Settings() {
|
||||
{showKeys["ollama"] ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 text-[11px] border-border/50 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => toast("Feature coming soon")}
|
||||
>
|
||||
<Key className="w-3 h-3 mr-1" /> Edit
|
||||
</Button>
|
||||
{!primaryProvider?.hasKey && (
|
||||
<Badge variant="outline" className="text-[10px] font-mono bg-neon-red/15 text-neon-red border-neon-red/30 whitespace-nowrap">
|
||||
NO KEY — chat will fail
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!primaryProvider?.hasKey && (
|
||||
<p className="text-[10px] text-neon-amber font-mono">
|
||||
Set OLLAMA_API_KEY in docker/.env and restart containers
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="bg-border/30" />
|
||||
|
||||
@@ -145,6 +145,9 @@ services:
|
||||
DATABASE_URL: "mysql://${MYSQL_USER:-goclaw}:${MYSQL_PASSWORD:-goClawPass123}@db:3306/${MYSQL_DATABASE:-goclaw}"
|
||||
GATEWAY_URL: "http://gateway:18789"
|
||||
JWT_SECRET: "${JWT_SECRET:-change-me-in-production}"
|
||||
# ── LLM Provider (same as gateway, used by Node.js tRPC proxy) ──────
|
||||
OLLAMA_BASE_URL: "${LLM_BASE_URL:-${OLLAMA_BASE_URL:-https://ollama.com/v1}}"
|
||||
OLLAMA_API_KEY: "${LLM_API_KEY:-${OLLAMA_API_KEY:-}}"
|
||||
VITE_APP_ID: "${VITE_APP_ID:-}"
|
||||
OAUTH_SERVER_URL: "${OAUTH_SERVER_URL:-}"
|
||||
VITE_OAUTH_PORTAL_URL: "${VITE_OAUTH_PORTAL_URL:-}"
|
||||
|
||||
@@ -31,6 +31,46 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* System config — returns server-side LLM provider configuration.
|
||||
* API keys are masked (never sent to frontend in full).
|
||||
* Used by Settings page and AgentDetailModal to show real connected providers.
|
||||
*/
|
||||
config: router({
|
||||
providers: publicProcedure.query(async () => {
|
||||
const { ENV } = await import("./_core/env");
|
||||
const baseUrl = ENV.ollamaBaseUrl || "https://ollama.com/v1";
|
||||
const apiKey = ENV.ollamaApiKey || "";
|
||||
const hasKey = apiKey.length > 0;
|
||||
// Mask key: show first 8 chars + ****
|
||||
const maskedKey = hasKey
|
||||
? `${apiKey.slice(0, 8)}${"*".repeat(Math.max(0, apiKey.length - 8))}`
|
||||
: "";
|
||||
|
||||
// Determine provider name from base URL
|
||||
let providerName = "Ollama Cloud";
|
||||
if (baseUrl.includes("openai.com")) providerName = "OpenAI";
|
||||
else if (baseUrl.includes("anthropic.com")) providerName = "Anthropic";
|
||||
else if (baseUrl.includes("groq.com")) providerName = "Groq";
|
||||
else if (baseUrl.includes("mistral.ai")) providerName = "Mistral";
|
||||
else if (baseUrl.includes("ollama.com")) providerName = "Ollama Cloud";
|
||||
else providerName = "Custom";
|
||||
|
||||
return {
|
||||
providers: [
|
||||
{
|
||||
id: "primary",
|
||||
name: providerName,
|
||||
baseUrl,
|
||||
hasKey,
|
||||
maskedKey,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* Ollama API — серверный прокси для безопасного доступа
|
||||
* Приоритет: Go Gateway → прямой Ollama
|
||||
|
||||
Reference in New Issue
Block a user