CrossRelay stage 0+1: Go gateway with Anthropic<->Chat Completions relay, Tauri shell, bilingual management UI

- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens,
  management API /api/* (providers CRUD, apply/restore, settings, backups, logs)
- Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming
  state machine keyed by content block index (text, tool_use, parallel calls)
- ~/.claude/settings.json guarded read/write: timestamped backups, managed env
  keys only, one-click restore
- Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout,
  stdin pipe watchdog prevents orphans, kills the child on exit
- React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query
- Tests: Go table-driven + httptest integration (all green), vitest + RTL
This commit is contained in:
kate522 committed 2026-10-18 17:19:13 +08:00
1 parent 8ee5e98ea3
commit f8acf31ebc
71 files changed
+15727 -1

No files matched your search

+295
View File
@@ -0,0 +1,295 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, Pencil } from "lucide-react";
import { PRESETS } from "@/lib/types";
import type { Models, Provider } from "@/lib/types";
import { useProviders, useProviderMutations } from "@/lib/queries";
import { useToast } from "@/components/toast";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input, Label } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
interface FormState {
id?: string;
name: string;
preset: string;
base_url: string;
api_key: string;
models: Models;
}
const emptyForm: FormState = {
name: "",
preset: "",
base_url: "",
api_key: "",
models: { default: "", sonnet: "", haiku: "", opus: "", extra: {} },
};
export default function Providers() {
const { t } = useTranslation();
const { data, isLoading, isError } = useProviders();
const mutations = useProviderMutations();
const { show } = useToast();
const [form, setForm] = useState<FormState | null>(null);
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});
const [lastAppliedEnv, setLastAppliedEnv] = useState<Record<string, string> | null>(null);
const providers = data?.items ?? [];
const openCreate = (presetKey = "") => {
const preset = PRESETS.find((p) => p.key === presetKey);
setErrors({});
setForm({
...emptyForm,
preset: presetKey,
name: presetKey ? presetKey : "",
base_url: preset?.base_url ?? "",
models: preset ? { ...preset.models } : { ...emptyForm.models },
});
};
const openEdit = (provider: Provider) => {
setErrors({});
setForm({
id: provider.id,
name: provider.name,
preset: provider.preset ?? "",
base_url: provider.base_url,
api_key: "",
models: {
default: provider.models.default ?? "",
sonnet: provider.models.sonnet ?? "",
haiku: provider.models.haiku ?? "",
opus: provider.models.opus ?? "",
extra: { ...(provider.models.extra ?? {}) },
},
});
};
const validate = (): boolean => {
if (!form) return false;
const next: Partial<Record<keyof FormState, string>> = {};
if (!form.name.trim()) next.name = t("providers.validationName");
if (!/^https?:\/\/.+/.test(form.base_url.trim())) next.base_url = t("providers.validationBaseUrl");
if (!form.id && !form.api_key.trim()) next.api_key = t("providers.validationApiKey");
setErrors(next);
return Object.keys(next).length === 0;
};
const submit = () => {
if (!form || !validate()) return;
const input = {
name: form.name.trim(),
preset: form.preset,
base_url: form.base_url.trim(),
api_key: form.api_key.trim(),
models: {
default: form.models.default?.trim(),
sonnet: form.models.sonnet?.trim(),
haiku: form.models.haiku?.trim(),
opus: form.models.opus?.trim(),
extra: Object.fromEntries(
Object.entries(form.models.extra ?? {}).filter(([k, v]) => k.trim() && v.trim()),
),
},
};
const onSuccess = () => {
show(form.id ? t("providers.updated") : t("providers.created"));
setForm(null);
};
const onError = (err: Error) => show(err.message, "error");
if (form.id) {
mutations.update.mutate({ id: form.id, input }, { onSuccess, onError });
} else {
mutations.create.mutate(input, { onSuccess, onError });
}
};
const apply = (provider: Provider) => {
mutations.apply.mutate(provider.id, {
onSuccess: (result) => {
setLastAppliedEnv(result.env);
show(`${t("providers.activated")}: ${provider.name}`);
},
onError: (err) => show(err.message, "error"),
});
};
if (isLoading) return <div className="text-sm text-zinc-500">{t("common.loading")}</div>;
if (isError) return <div className="text-sm text-red-600">{t("common.error")}</div>;
return (
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t("providers.title")}</h2>
<Button size="sm" onClick={() => openCreate()}>
<Plus size={14} aria-hidden />
{t("providers.add")}
</Button>
</div>
{form && (
<Card>
<CardHeader>
<CardTitle>{form.id ? t("providers.edit") : t("providers.add")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1">
<Label>{t("providers.name")}</Label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="DeepSeek"
/>
{errors.name && <span className="text-xs text-red-600">{errors.name}</span>}
</label>
<label className="flex flex-col gap-1">
<Label>{t("providers.preset")}</Label>
<Select
value={form.preset}
onChange={(e) => {
const key = e.target.value;
const preset = PRESETS.find((p) => p.key === key);
setForm({
...form,
preset: key,
base_url: preset?.base_url ?? form.base_url,
models: preset ? { ...preset.models } : form.models,
});
}}
>
<option value="">{t("providers.presetCustom")}</option>
{PRESETS.map((p) => (
<option key={p.key} value={p.key}>
{p.key}
</option>
))}
</Select>
</label>
<label className="flex flex-col gap-1">
<Label>{t("providers.baseUrl")}</Label>
<Input
value={form.base_url}
onChange={(e) => setForm({ ...form, base_url: e.target.value })}
placeholder="https://api.deepseek.com/v1"
/>
{errors.base_url && <span className="text-xs text-red-600">{errors.base_url}</span>}
</label>
<label className="flex flex-col gap-1">
<Label>
{t("providers.apiKey")}
{form.id && <span className="ml-1 text-zinc-400">({t("providers.keyHint")})</span>}
</Label>
<Input
type="password"
value={form.api_key}
onChange={(e) => setForm({ ...form, api_key: e.target.value })}
placeholder="sk-..."
/>
{errors.api_key && <span className="text-xs text-red-600">{errors.api_key}</span>}
</label>
</div>
<fieldset className="flex flex-col gap-2">
<legend className="text-xs font-medium text-zinc-600">{t("providers.models")}</legend>
<div className="grid grid-cols-2 gap-3">
{(["default", "sonnet", "haiku", "opus"] as const).map((role) => (
<label key={role} className="flex flex-col gap-1">
<Label>{t(`providers.${role}Model`)}</Label>
<Input
value={form.models[role] ?? ""}
onChange={(e) =>
setForm({ ...form, models: { ...form.models, [role]: e.target.value } })
}
/>
</label>
))}
</div>
</fieldset>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => setForm(null)}>
{t("common.cancel")}
</Button>
<Button size="sm" onClick={submit}>
{t("common.save")}
</Button>
</div>
</CardContent>
</Card>
)}
{lastAppliedEnv && (
<Card>
<CardHeader>
<CardTitle>{t("providers.appliedEnv")}</CardTitle>
</CardHeader>
<CardContent>
<ul className="grid grid-cols-2 gap-1 font-mono text-xs text-zinc-600">
{Object.entries(lastAppliedEnv)
.filter(([key]) => key.startsWith("ANTHROPIC_"))
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => (
<li key={key}>
<span className="text-zinc-400">{key}</span> = {value}
</li>
))}
</ul>
</CardContent>
</Card>
)}
{providers.length === 0 ? (
<Card>
<CardContent className="py-8 text-center text-sm text-zinc-500">
{t("providers.empty")}
</CardContent>
</Card>
) : (
<div className="flex flex-col gap-3">
{providers.map((provider) => (
<Card key={provider.id}>
<CardContent className="flex items-center gap-4 py-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{provider.name}</span>
{provider.enabled_for_claude && <Badge tone="success">{t("providers.activated")}</Badge>}
{provider.preset && <Badge>{provider.preset}</Badge>}
</div>
<div className="truncate text-xs text-zinc-500">{provider.base_url}</div>
</div>
<div className="flex shrink-0 gap-2">
<Button variant="outline" size="sm" onClick={() => apply(provider)}>
{t("providers.activate")}
</Button>
<Button variant="ghost" size="icon" aria-label={t("common.edit")} onClick={() => openEdit(provider)}>
<Pencil size={14} aria-hidden />
</Button>
<Button
variant="ghost"
size="icon"
aria-label={t("common.delete")}
onClick={() => {
if (window.confirm(t("providers.deleteConfirm"))) {
mutations.remove.mutate(provider.id, {
onSuccess: () => show(t("providers.deleted")),
onError: (err) => show(err.message, "error"),
});
}
}}
>
<Trash2 size={14} aria-hidden />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}