M0-M4: 推倒重来基线(基建+用户/密钥/核心代理+前端+管理后台+三协议互转)

- 后端 Go+Gin+GORM: 配置(OT_ env)/SQLite/Postgres 双驱动、用户体系(argon2id+JWT access/refresh)、
  API Key(sk- 48位, 仅存 SHA-256 哈希)
- 代理网关: /v1/chat/completions、/v1/responses、/v1/messages、/v1/models;错误按客户端协议返回
- 三协议互转(convert 包): Chat↔Messages↔Responses 请求/响应 + 流式 SSE 逐事件转换(直通优先)
- 用量计费: 异步批量记账、余额扣减、balance_logs、usage_daily 日聚合
- 管理 API: 用户/渠道 CRUD+测试+模型导入/模型定价+绑定/统计/系统配置
- 前端 Vue3+TS+Tailwind(taste-skill 设计 tokens): Landing/登录注册/控制台/管理后台,
  自建组件+Phosphor 图标+自建 SVG 趋势图, 已过 web-design-guidelines 复查
- mock 上游: OpenAI+Anthropic 双协议模拟(含流式)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 15:34:06 +08:00
co-authored by Claude
parent b25e9ec8a7
commit ec4de8d913
92 changed files with 6203 additions and 3064 deletions
+62 -13
View File
@@ -1,5 +1,5 @@
// mockupstream 本地 mock OpenAI 上游服务(联调代理链路,无需真实 key)。
// 支持 /v1/chat/completions 与 /v1/responses,含流式与非流式。
// 支持 /v1/chat/completions 与 /v1/responses,含流式与非流式;/v1/models 返回模型列表。
package main
import (
@@ -46,22 +46,72 @@ func main() {
http.HandleFunc("/v1/models", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"object":"list","data":[{"id":"gpt-4o-mini","object":"model"},{"id":"gpt-4o","object":"model"}]}`)
fmt.Fprint(w, `{"object":"list","data":[{"id":"gpt-4o-mini","object":"model"},{"id":"gpt-4o","object":"model"},{"id":"claude-sonnet-5","object":"model"}]}`)
})
// Anthropic Messages 端点(provider=anthropic 的渠道走这里)
http.HandleFunc("/v1/messages", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var req struct {
Model string `json:"model"`
Stream bool `json:"stream"`
}
_ = json.Unmarshal(body, &req)
if req.Stream {
streamMessages(w, req.Model)
return
}
replyMessages(w, req.Model)
})
log.Printf("mock upstream listening on %s", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}
func replyMessages(w http.ResponseWriter, model string) {
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{
"id": "msg_mock789",
"type": "message",
"role": "assistant",
"model": model,
"content": []any{map[string]any{"type": "text", "text": "这是 mock Anthropic 上游的回复。"}},
"stop_reason": "end_turn",
"usage": map[string]any{"input_tokens": 14, "output_tokens": 10},
}
_ = json.NewEncoder(w).Encode(resp)
}
func streamMessages(w http.ResponseWriter, model string) {
w.Header().Set("Content-Type", "text/event-stream")
fl, _ := w.(http.Flusher)
events := []map[string]any{
{"type": "message_start", "message": map[string]any{"id": "msg_mock789", "type": "message", "role": "assistant", "model": model, "content": []any{}, "usage": map[string]any{"input_tokens": 14, "output_tokens": 0}}},
{"type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "text", "text": ""}},
{"type": "content_block_delta", "index": 0, "delta": map[string]any{"type": "text_delta", "text": "这是"}},
{"type": "content_block_delta", "index": 0, "delta": map[string]any{"type": "text_delta", "text": " Anthropic"}},
{"type": "content_block_stop", "index": 0},
{"type": "message_delta", "delta": map[string]any{"stop_reason": "end_turn", "stop_sequence": nil}, "usage": map[string]any{"output_tokens": 10}},
{"type": "message_stop"},
}
for _, e := range events {
b, _ := json.Marshal(e)
typ, _ := e["type"].(string)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", typ, b)
fl.Flush()
time.Sleep(40 * time.Millisecond)
}
}
func replyChat(w http.ResponseWriter, model string) {
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{
"id": "chatcmpl-mock123",
"object": "chat.completion",
"model": model,
"id": "chatcmpl-mock123",
"object": "chat.completion",
"model": model,
"choices": []any{map[string]any{
"index": 0,
"message": map[string]any{"role": "assistant", "content": "你好,这是 mock 上游的回复。"},
"index": 0,
"message": map[string]any{"role": "assistant", "content": "你好,这是 mock 上游的回复。"},
"finish_reason": "stop",
}},
"usage": map[string]any{"prompt_tokens": 12, "completion_tokens": 9, "total_tokens": 21},
@@ -80,7 +130,6 @@ func streamChat(w http.ResponseWriter, model string) {
"choices": []any{map[string]any{
"index": 0,
"delta": map[string]any{"content": c},
"finish_reason": nil,
}},
}
if i == len(chunks)-1 {
@@ -107,11 +156,11 @@ func streamChat(w http.ResponseWriter, model string) {
func replyResponses(w http.ResponseWriter, model string) {
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{
"id": "resp_mock456",
"object": "response",
"model": model,
"status": "completed",
"output": []any{map[string]any{
"id": "resp_mock456",
"object": "response",
"model": model,
"status": "completed",
"output": []any{map[string]any{
"type": "message", "role": "assistant",
"content": []any{map[string]any{"type": "output_text", "text": "这是 Responses API 的 mock 回复。"}},
}},