refactor: complete backend rewrite for multi-protocol proxy

Major rewrite of the Go backend to support:
- Three API format imports: openai, anthropic, compatible
- Three protocol conversions: Chat Completions, Responses, Messages
- Hub-and-spoke architecture with Chat as intermediate format

Deleted:
- opencat.go (old entry)
- store/, team/, pkg/team/, pkg/store/ (old data layer)
- internal/model/, internal/consts/ (old types)
- internal/service/team/, internal/controller/team/ (old handlers)
- llm/ (removed LLM client library, pure proxy mode)
- dist/, assets/ (old build artifacts)

Added:
- internal/store/ — 9 GORM models + multi-DB support
- internal/pkg/ — crypto (AES-GCM), apikey, jwt, ratelimit, resp, tokenizer
- internal/channel/ — channel selection, weighted LB, health checks
- internal/proxy/convert/ — 6 protocol conversion functions + SSE streaming
- internal/proxy/ — gateway with request dispatch and upstream selection
- internal/usage/ — async usage recorder with batch writes
- internal/api/ — management API (auth, users, keys, channels, models)
- Makefile for build/test/deploy

Fixed API to match frontend expectations:
- Login response wraps token in { data: { token } }
- GET /api/profile route added
- Profile response wraps user in { code, data }
- Role returned as number (10=admin, 1=user)
This commit is contained in:
Sakurasan committed 2026-08-30 11:49:31 +08:00
1 parent aa0d87f132
commit ef3025dd80
127 files changed
+4623 -10500

No files matched your search

-1
View File
@@ -1 +0,0 @@
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&n(i)}).observe(document,{childList:!0,subtree:!0});function s(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function n(r){if(r.ep)return;r.ep=!0;const o=s(r);fetch(r.href,o)}})();function bs(e,t){const s=Object.create(null),n=e.split(",");for(let r=0;r<n.length;r++)s[n[r]]=!0;return t?r=>!!s[r.toLowerCase()]:r=>!!s[r]}const L={},We=[],fe=()=>{},yr=()=>!1,wr=/^on[^a-z]/,Ut=e=>wr.test(e),xs=e=>e.startsWith("onUpdate:"),J=Object.assign,vs=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Er=Object.prototype.hasOwnProperty,j=(e,t)=>Er.call(e,t),T=Array.isArray,qe=e=>Lt(e)==="[object Map]",In=e=>Lt(e)==="[object Set]",A=e=>typeof e=="function",q=e=>typeof e=="string",ys=e=>typeof e=="symbol",D=e=>e!==null&&typeof e=="object",Tn=e=>D(e)&&A(e.then)&&A(e.catch),Pn=Object.prototype.toString,Lt=e=>Pn.call(e),Cr=e=>Lt(e).slice(8,-1),An=e=>Lt(e)==="[object Object]",ws=e=>q(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,It=bs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Kt=e=>{const t=Object.create(null);return s=>t[s]||(t[s]=e(s))},Or=/-(\w)/g,Je=Kt(e=>e.replace(Or,(t,s)=>s?s.toUpperCase():"")),Ir=/\B([A-Z])/g,Ze=Kt(e=>e.replace(Ir,"-$1").toLowerCase()),Mn=Kt(e=>e.charAt(0).toUpperCase()+e.slice(1)),kt=Kt(e=>e?`on${Mn(e)}`:""),ct=(e,t)=>!Object.is(e,t),Tt=(e,t)=>{for(let s=0;s<e.length;s++)e[s](t)},Rt=(e,t,s)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:s})},rs=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Js;const os=()=>Js||(Js=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Es(e){if(T(e)){const t={};for(let s=0;s<e.length;s++){const n=e[s],r=q(n)?Mr(n):Es(n);if(r)for(const o in r)t[o]=r[o]}return t}else{if(q(e))return e;if(D(e))return e}}const Tr=/;(?![^(]*\))/g,Pr=/:([^]+)/,Ar=new RegExp("\\/\\*.*?\\*\\/","gs");function Mr(e){const t={};return e.replace(Ar,"").split(Tr).forEach(s=>{if(s){const n=s.split(Pr);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Cs(e){let t="";if(q(e))t=e;else if(T(e))for(let s=0;s<e.length;s++){const n=Cs(e[s]);n&&(t+=n+" ")}else if(D(e))for(const s in e)e[s]&&(t+=s+" ");return t.trim()}const Fr="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",Rr=bs(Fr);function Fn(e){return!!e||e===""}const jr=e=>q(e)?e:e==null?"":T(e)||D(e)&&(e.toString===Pn||!A(e.toString))?JSON.stringify(e,Rn,2):String(e),Rn=(e,t)=>t&&t.__v_isRef?Rn(e,t.value):qe(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,r])=>(s[`${n} =>`]=r,s),{})}:In(t)?{[`Set(${t.size})`]:[...t.values()]}:D(t)&&!T(t)&&!An(t)?String(t):t;let oe;class Nr{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=oe,!t&&oe&&(this.index=(oe.scopes||(oe.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const s=oe;try{return oe=this,t()}finally{oe=s}}}on(){oe=this}off(){oe=this.parent}stop(t){if(this._active){let s,n;for(s=0,n=this.effects.length;s<n;s++)this.effects[s].stop();for(s=0,n=this.cleanups.length;s<n;s++)this.cleanups[s]();if(this.scopes)for(s=0,n=this.scopes.length;s<n;s++)this.scopes[s].stop(!0);if(!this.detached&&this.parent&&!t){const r=this.parent.scopes.pop();r&&r!==this&&(this.parent.scopes[this.index]=r,r.index=this.index)}this.parent=void 0,this._active=!1}}}function Sr(e,t=oe){t&&t.active&&t.effects.push(e)}function Hr(){return oe}const Os=e=>{const t=new Set(e);return t.w=0,t.n=0,t},jn=e=>(e.w&Te)>0,Nn=e=>(e.n&Te)>0,Ur=({deps:e})=>{if(e.length)for(let t=0;t<e.length;t++)e[t].w|=Te},Lr=e=>{const{deps:t}=e;if(t.length){let s=0;for(let n=0;n<t.length;n++){const r=t[n];jn(r)&&!Nn(r)?r.delete(e):t[s++]=r,r.w&=~Te,r.n&=~Te}t.length=s}},is=new WeakMap;let rt=0,Te=1;const ls=30;let ie;const Ue=Symbol(""),cs=Symbol("");class Is{constructor(t,s=null,n){this.fn=t,this.scheduler=s,this.active=!0,this.deps=[],this.parent=void 0,Sr(this,n)}run(){if(!this.active)return this.fn();let t=ie,s=Ce;for(;t;){if(t===this)return;t=t.parent}try{return this.parent=ie,ie=this,Ce=!0,Te=1<<++rt,rt<=ls?Ur(this):Vs(this),this.fn()}finally{rt<=ls&&Lr(this),Te=1<<--rt,ie=this.parent,Ce=s,this.parent=void 0,this.deferStop&&this.stop()}}stop(){ie===this?this.deferStop=!0:this.active&&(Vs(this),this.onStop&&this.onStop(),this.active=!1)}}function Vs(e){const{deps:t}=e;if(t.length){for(let s=0;s<t.length;s++)t[s].delete(e);t.length=0}}let Ce=!0;const Line truncated
-1
View File
@@ -1 +0,0 @@
*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.top-0{top:0px}.z-10{z-index:10}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-60{margin-left:15rem;margin-right:15rem}.my-0{margin-top:0;margin-bottom:0}.my-12{margin-top:3rem;margin-bottom:3rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1remLine truncated
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><path fill="#8ce7f2" d="M44.475,24.803c0.774-2.528,0.53-5.23-0.722-7.583c-2.018-3.792-6.225-5.638-10.284-5.086 c-1.802-1.935-4.265-3.074-6.929-3.166c-4.294-0.149-7.996,2.572-9.547,6.363c-2.576,0.593-4.794,2.156-6.206,4.417 c-2.275,3.643-1.771,8.21,0.737,11.449c-0.774,2.528-0.53,5.23,0.722,7.583c2.018,3.792,6.225,5.638,10.284,5.086 c1.802,1.935,4.265,3.074,6.929,3.167c4.293,0.148,7.996-2.573,9.547-6.364c2.576-0.593,4.794-2.156,6.205-4.417 C47.486,32.608,46.982,28.042,44.475,24.803z"/><path fill="#18193f" d="M38.844,17.559l-7.523-4.343c-0.493-0.284-1.1-0.285-1.594-0.003l-10.245,5.855l0.021-4.018 l7.913-4.569c3.445-1.989,7.938-1.371,10.44,1.722c0.594,0.734,1.04,1.539,1.341,2.382c0.211,0.592,0.772,0.984,1.4,0.984 c1.037,0,1.772-1.03,1.421-2.006c-0.416-1.158-1.033-2.265-1.853-3.275c-2.488-3.065-6.393-4.357-10.151-3.807 c-1.987-2.124-4.699-3.373-7.63-3.473c-4.733-0.161-8.814,2.839-10.525,7.018c-2.842,0.654-5.289,2.378-6.847,4.873 c-3.318,5.313-1.284,12.41,4.142,15.543l7.523,4.343c0.493,0.284,1.1,0.285,1.594,0.003l10.245-5.855l-0.021,4.018l-7.902,4.563 c-3.448,1.991-7.945,1.378-10.451-1.715c-0.591-0.73-1.035-1.53-1.336-2.368c-0.212-0.591-0.772-0.982-1.4-0.982h0 c-1.039,0-1.774,1.033-1.421,2.01c0.326,0.901,0.774,1.771,1.344,2.589c2.43,3.487,6.613,5.039,10.645,4.465 c1.987,2.129,4.7,3.381,7.634,3.483c4.736,0.163,8.82-2.838,10.531-7.02c2.841-0.654,5.288-2.378,6.844-4.872 C46.303,27.788,44.269,20.691,38.844,17.559z M34,33.723c0,4.324-3.313,8.077-7.633,8.269c-1.837,0.082-3.585-0.463-5.024-1.496 c0.274-0.13,0.546-0.266,0.812-0.42l7.521-4.342c0.493-0.285,0.799-0.81,0.802-1.38l0.054-9.883c0.003-0.55-0.441-0.999-0.992-1 c-0.549-0.002-0.995,0.441-0.998,0.99l-0.011,2.172L18.498,32.37l-7.918-4.571c-3.745-2.163-5.339-6.908-3.345-10.745 c0.848-1.633,2.196-2.875,3.812-3.605C11.022,13.753,11,14.058,11,14.367v8.684c0,0.569,0.302,1.095,0.794,1.382l8.73,5.055 c0.475,0.275,1.082,0.113,1.358-0.361c0.277-0.476,0.114-1.085-0.362-1.361L14,23.42v-9.143c0-4.325,3.313-8.077,7.634-8.269 c1.835-0.081,3.582,0.462,5.02,1.494c-0.264,0.127-0.526,0.259-0.782,0.407l-7.548,4.357c-0.494,0.285-0.799,0.81-0.802,1.38 l-0.054,9.797c-0.003,0.55,0.441,0.999,0.992,1c0.549,0.002,0.995-0.441,0.998-0.99l0.011-2.087l4.552-2.603L34,24.58V33.723z M40.765,30.946c-0.848,1.633-2.195,2.875-3.812,3.604C36.978,34.248,37,33.944,37,33.636v-8.687c0-0.569-0.302-1.095-0.794-1.382 l-10.191-5.943l3.487-1.994l7.918,4.571C41.165,22.364,42.759,27.109,40.765,30.946z"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

+21 -7
View File
@@ -1,15 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html lang="en" data-theme="emerald">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/logo-4312ea85.svg" />
<link rel="icon" type="image/svg+xml" href="/assets/logo-BYScUf44.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>opencatd-open</title>
<script type="module" crossorigin src="/assets/index-9a4663b1.js"></script>
<link rel="stylesheet" href="/assets/index-ef8ba4ac.css">
<meta name="description" content="OpenTeam — an open-source, team-shared service compatible with the OpenAI API." />
<meta name="theme-color" content="#ffffff" />
<title>OpenTeam</title>
<script>
// 恢复主题偏好(light/dark/auto,auto 跟随系统),避免闪烁
try {
var __p = localStorage.getItem('theme') || 'auto'
var __dark = __p === 'dark' || (__p === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.setAttribute('data-theme', __dark ? 'dark' : 'emerald')
} catch (e) {
document.documentElement.setAttribute('data-theme', 'emerald')
}
</script>
<script type="module" crossorigin src="/assets/index-BJCJroRI.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DK3Fl9T5.js">
<link rel="modulepreload" crossorigin href="/assets/components-C0OpMyPR.js">
<link rel="stylesheet" crossorigin href="/assets/components-Bx5gkR8i.css">
<link rel="stylesheet" crossorigin href="/assets/index-DgCnyds5.css">
</head>
<body>
<div id="app"></div>
</body>
</html>