Spaces:
Sleeping
Sleeping
package main | |
import ( | |
"bufio" | |
"bytes" | |
"compress/gzip" | |
"crypto/sha256" | |
"encoding/hex" | |
"encoding/json" | |
"fmt" | |
"html/template" | |
"io" | |
"log" | |
"net/http" | |
"net/url" | |
"os" | |
"strings" | |
"time" | |
"github.com/gin-gonic/gin" | |
"github.com/google/uuid" | |
"github.com/joho/godotenv" | |
"github.com/patrickmn/go-cache" | |
"github.com/tidwall/gjson" | |
) | |
const tokenUrl = "https://api.github.com/copilot_internal/v2/token" | |
const completionsUrl = "https://api.githubcopilot.com/chat/completions" | |
const embeddingsUrl = "https://api.githubcopilot.com/embeddings" | |
var requestUrl = "" | |
type Model struct { | |
ID string `json:"id"` | |
Object string `json:"object"` | |
Created int `json:"created"` | |
OwnedBy string `json:"owned_by"` | |
Root string `json:"root"` | |
Parent *string `json:"parent"` | |
} | |
type ModelList struct { | |
Object string `json:"object"` | |
Data []Model `json:"data"` | |
} | |
var version = "v0.6.1" | |
var port = "8081" | |
var client_id = "Iv1.b507a08c87ecfe98" | |
func main() { | |
err := godotenv.Load() | |
if err == nil { | |
// 从环境变量中获取配置值 | |
portEnv := os.Getenv("PORT") | |
if portEnv != "" { | |
port = portEnv | |
version = os.Getenv("VERSION") | |
} | |
} | |
log.Printf("Server is running on port %s, version: %s\n", port, version) | |
log.Printf("client_id: %s\n", client_id) | |
gin.SetMode(gin.ReleaseMode) | |
r := gin.Default() | |
// CORS 中间件 | |
r.Use(func(c *gin.Context) { | |
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") | |
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") | |
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") | |
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") | |
if c.Request.Method == "OPTIONS" { | |
c.AbortWithStatus(204) | |
return | |
} | |
c.Next() | |
}) | |
r.GET("/", func(c *gin.Context) { | |
c.String(http.StatusOK, ` | |
curl --location 'http://127.0.0.1:8081/v1/chat/completions' \ | |
--header 'Content-Type: application/json' \ | |
--header 'Authorization: Bearer ghu_xxx' \ | |
--data '{ | |
"model": "gpt-4", | |
"messages": [{"role": "user", "content": "hi"}] | |
}'`) | |
}) | |
r.GET("/v1/models", func(c *gin.Context) { | |
c.JSON(http.StatusOK, models()) | |
}) | |
r.POST("/v1/chat/completions", func(c *gin.Context) { | |
c.Header("Cache-Control", "no-cache, must-revalidate") | |
c.Header("Connection", "keep-alive") | |
requestUrl = completionsUrl | |
forwardRequest(c) | |
}) | |
r.POST("/v1/embeddings", func(c *gin.Context) { | |
c.Header("Cache-Control", "no-cache, must-revalidate") | |
c.Header("Connection", "keep-alive") | |
requestUrl = embeddingsUrl | |
forwardRequest(c) | |
}) | |
// 获取ghu | |
t, err := loadTemplate() | |
if err != nil { | |
panic(err) | |
} | |
r.SetHTMLTemplate(t) | |
r.GET("/auth", func(c *gin.Context) { | |
// 获取设备授权码 | |
deviceCode, userCode, err := getDeviceCode() | |
if err != nil { | |
c.String(http.StatusOK, "获取设备码失败:"+err.Error()) | |
return | |
} | |
// 使用 deviceCode 和 userCode | |
fmt.Println("Device Code: ", deviceCode) | |
fmt.Println("User Code: ", userCode) | |
c.HTML(http.StatusOK, "/html/auth.tmpl", gin.H{ | |
"title": "Get Copilot Token", | |
"deviceCode": deviceCode, | |
"userCode": userCode, | |
}) | |
}) | |
r.POST("/auth/check", func(c *gin.Context) { | |
returnData := map[string]string{ | |
"code": "1", | |
"msg": "", | |
"data": "", | |
} | |
deviceCode := c.PostForm("deviceCode") | |
if deviceCode == "" { | |
returnData["msg"] = "device code null" | |
c.JSON(http.StatusOK, returnData) | |
return | |
} | |
token, err := checkUserCode(deviceCode) | |
if err != nil { | |
returnData["msg"] = err.Error() | |
c.JSON(http.StatusOK, returnData) | |
return | |
} | |
if token == "" { | |
returnData["msg"] = "token null" | |
c.JSON(http.StatusOK, returnData) | |
return | |
} | |
returnData["code"] = "0" | |
returnData["msg"] = "success" | |
returnData["data"] = token | |
c.JSON(http.StatusOK, returnData) | |
return | |
}) | |
r.POST("/auth/checkGhu", func(c *gin.Context) { | |
returnData := map[string]string{ | |
"code": "1", | |
"msg": "", | |
"data": "", | |
} | |
ghu := c.PostForm("ghu") | |
if ghu == "" { | |
returnData["msg"] = "ghu null" | |
c.JSON(http.StatusOK, returnData) | |
return | |
} | |
if !strings.HasPrefix(ghu, "gh") { | |
returnData["msg"] = "ghu 格式错误" | |
c.JSON(http.StatusOK, returnData) | |
return | |
} | |
info := checkGhuToken(ghu) | |
returnData["code"] = "0" | |
returnData["msg"] = "success" | |
returnData["data"] = info | |
c.JSON(http.StatusOK, returnData) | |
return | |
}) | |
r.Run(":" + port) | |
} | |
func forwardRequest(c *gin.Context) { | |
var jsonBody map[string]interface{} | |
if err := c.ShouldBindJSON(&jsonBody); err != nil { | |
c.JSON(http.StatusBadRequest, gin.H{"error": "Request body is missing or not in JSON format"}) | |
return | |
} | |
ghuToken := strings.Split(c.GetHeader("Authorization"), " ")[1] | |
if !strings.HasPrefix(ghuToken, "gh") { | |
c.JSON(http.StatusBadRequest, gin.H{"error": "auth token not found"}) | |
log.Printf("token 格式错误:%s\n", ghuToken) | |
return | |
} | |
// 检查 token 是否有效 | |
if !checkToken(ghuToken) { | |
c.JSON(http.StatusBadRequest, gin.H{"error": "auth token is invalid"}) | |
log.Printf("token 无效:%s\n", ghuToken) | |
return | |
} | |
accToken, err := getAccToken(ghuToken) | |
if accToken == "" { | |
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
return | |
} | |
sessionId := fmt.Sprintf("%s%d", uuid.New().String(), time.Now().UnixNano()/int64(time.Millisecond)) | |
machineID := sha256.Sum256([]byte(uuid.New().String())) | |
machineIDStr := hex.EncodeToString(machineID[:]) | |
accHeaders := getAccHeaders(accToken, uuid.New().String(), sessionId, machineIDStr) | |
client := &http.Client{} | |
jsonData, err := json.Marshal(jsonBody) | |
if err != nil { | |
c.AbortWithError(http.StatusInternalServerError, err) | |
return | |
} | |
isStream := gjson.GetBytes(jsonData, "stream").String() == "true" | |
req, err := http.NewRequest("POST", requestUrl, bytes.NewBuffer(jsonData)) | |
if err != nil { | |
c.AbortWithError(http.StatusInternalServerError, err) | |
} | |
for key, value := range accHeaders { | |
req.Header.Add(key, value) | |
} | |
resp, err := client.Do(req) | |
if err != nil { | |
return | |
} | |
defer resp.Body.Close() | |
if resp.StatusCode != http.StatusOK { | |
bodyBytes, err := io.ReadAll(resp.Body) | |
if err != nil { | |
log.Fatal(err) | |
} | |
bodyString := string(bodyBytes) | |
log.Printf("对话失败:%d, %s ", resp.StatusCode, bodyString) | |
cache := cache.New(5*time.Minute, 10*time.Minute) | |
cache.Delete(ghuToken) | |
c.AbortWithError(resp.StatusCode, fmt.Errorf(bodyString)) | |
return | |
} | |
c.Header("Content-Type", "application/json; charset=utf-8") | |
if isStream { | |
returnStream(c, resp) | |
} else { | |
returnJson(c, resp) | |
} | |
return | |
} | |
func returnJson(c *gin.Context, resp *http.Response) { | |
c.Header("Content-Type", "application/json; charset=utf-8") | |
body, err := io.ReadAll(resp.Body.(io.Reader)) | |
if err != nil { | |
c.AbortWithError(http.StatusInternalServerError, err) | |
return | |
} | |
c.Writer.Write(body) | |
return | |
} | |
func returnStream(c *gin.Context, resp *http.Response) { | |
c.Header("Content-Type", "text/event-stream; charset=utf-8") | |
// 创建一个新的扫描器 | |
scanner := bufio.NewScanner(resp.Body) | |
// 使用Scan方法来读取流 | |
for scanner.Scan() { | |
line := scanner.Bytes() | |
// 替换 "content":null 为 "content":"" | |
modifiedLine := bytes.Replace(line, []byte(`"content":null`), []byte(`"content":""`), -1) | |
// 将修改后的数据写入响应体 | |
if _, err := c.Writer.Write(modifiedLine); err != nil { | |
c.AbortWithError(http.StatusInternalServerError, err) | |
return | |
} | |
// 添加一个换行符 | |
if _, err := c.Writer.Write([]byte("\n")); err != nil { | |
c.AbortWithError(http.StatusInternalServerError, err) | |
return | |
} | |
} | |
if scanner.Err() != nil { | |
// 处理来自扫描器的任何错误 | |
c.AbortWithError(http.StatusInternalServerError, scanner.Err()) | |
return | |
} | |
return | |
} | |
func models() ModelList { | |
jsonStr := `{ | |
"object": "list", | |
"data": [ | |
{"id": "text-search-babbage-doc-001","object": "model","created": 1651172509,"owned_by": "openai-dev"}, | |
{"id": "gpt-4-0613","object": "model","created": 1686588896,"owned_by": "openai"}, | |
{"id": "gpt-4", "object": "model", "created": 1687882411, "owned_by": "openai"}, | |
{"id": "babbage", "object": "model", "created": 1649358449, "owned_by": "openai"}, | |
{"id": "gpt-3.5-turbo-0613", "object": "model", "created": 1686587434, "owned_by": "openai"}, | |
{"id": "text-babbage-001", "object": "model", "created": 1649364043, "owned_by": "openai"}, | |
{"id": "gpt-3.5-turbo", "object": "model", "created": 1677610602, "owned_by": "openai"}, | |
{"id": "gpt-3.5-turbo-1106", "object": "model", "created": 1698959748, "owned_by": "system"}, | |
{"id": "curie-instruct-beta", "object": "model", "created": 1649364042, "owned_by": "openai"}, | |
{"id": "gpt-3.5-turbo-0301", "object": "model", "created": 1677649963, "owned_by": "openai"}, | |
{"id": "gpt-3.5-turbo-16k-0613", "object": "model", "created": 1685474247, "owned_by": "openai"}, | |
{"id": "text-embedding-ada-002", "object": "model", "created": 1671217299, "owned_by": "openai-internal"}, | |
{"id": "davinci-similarity", "object": "model", "created": 1651172509, "owned_by": "openai-dev"}, | |
{"id": "curie-similarity", "object": "model", "created": 1651172510, "owned_by": "openai-dev"}, | |
{"id": "babbage-search-document", "object": "model", "created": 1651172510, "owned_by": "openai-dev"}, | |
{"id": "curie-search-document", "object": "model", "created": 1651172508, "owned_by": "openai-dev"}, | |
{"id": "babbage-code-search-code", "object": "model", "created": 1651172509, "owned_by": "openai-dev"}, | |
{"id": "ada-code-search-text", "object": "model", "created": 1651172510, "owned_by": "openai-dev"}, | |
{"id": "text-search-curie-query-001", "object": "model", "created": 1651172509, "owned_by": "openai-dev"}, | |
{"id": "text-davinci-002", "object": "model", "created": 1649880484, "owned_by": "openai"}, | |
{"id": "ada", "object": "model", "created": 1649357491, "owned_by": "openai"}, | |
{"id": "text-ada-001", "object": "model", "created": 1649364042, "owned_by": "openai"}, | |
{"id": "ada-similarity", "object": "model", "created": 1651172507, "owned_by": "openai-dev"}, | |
{"id": "code-search-ada-code-001", "object": "model", "created": 1651172507, "owned_by": "openai-dev"}, | |
{"id": "text-similarity-ada-001", "object": "model", "created": 1651172505, "owned_by": "openai-dev"}, | |
{"id": "text-davinci-edit-001", "object": "model", "created": 1649809179, "owned_by": "openai"}, | |
{"id": "code-davinci-edit-001", "object": "model", "created": 1649880484, "owned_by": "openai"}, | |
{"id": "text-search-curie-doc-001", "object": "model", "created": 1651172509, "owned_by": "openai-dev"}, | |
{"id": "text-curie-001", "object": "model", "created": 1649364043, "owned_by": "openai"}, | |
{"id": "curie", "object": "model", "created": 1649359874, "owned_by": "openai"}, | |
{"id": "davinci", "object": "model", "created": 1649359874, "owned_by": "openai"}, | |
{"id": "gpt-4-0314", "object": "model", "created": 1687882410, "owned_by": "openai"} | |
] | |
}` | |
var modelList ModelList | |
json.Unmarshal([]byte(jsonStr), &modelList) | |
return modelList | |
} | |
func getAccToken(ghuToken string) (string, error) { | |
var accToken = "" | |
cache := cache.New(15*time.Minute, 60*time.Minute) | |
cacheToken, found := cache.Get(ghuToken) | |
if found { | |
accToken = cacheToken.(string) | |
} else { | |
client := &http.Client{} | |
req, err := http.NewRequest("GET", tokenUrl, nil) | |
if err != nil { | |
return accToken, err | |
} | |
headers := getHeaders(ghuToken) | |
for key, value := range headers { | |
req.Header.Add(key, value) | |
} | |
resp, err := client.Do(req) | |
if err != nil { | |
return accToken, err | |
} | |
defer resp.Body.Close() | |
var reader interface{} | |
switch resp.Header.Get("Content-Encoding") { | |
case "gzip": | |
reader, err = gzip.NewReader(resp.Body) | |
if err != nil { | |
return accToken, fmt.Errorf("数据解压失败") | |
} | |
default: | |
reader = resp.Body | |
} | |
body, err := io.ReadAll(reader.(io.Reader)) | |
if err != nil { | |
return accToken, fmt.Errorf("数据读取失败") | |
} | |
if resp.StatusCode == http.StatusOK { | |
accToken = gjson.GetBytes(body, "token").String() | |
if accToken == "" { | |
return accToken, fmt.Errorf("acc_token 未返回") | |
} | |
cache.Set(ghuToken, accToken, 14*time.Minute) | |
} else { | |
log.Printf("获取 acc_token 请求失败:%d, %s ", resp.StatusCode, string(body)) | |
return accToken, fmt.Errorf("获取 acc_token 请求失败: %d", resp.StatusCode) | |
} | |
} | |
return accToken, nil | |
} | |
func checkToken(ghuToken string) bool { | |
client := &http.Client{} | |
url := "https://api.github.com/user" | |
req, err := http.NewRequest("GET", url, nil) | |
if err != nil { | |
fmt.Println(err) | |
return false | |
} | |
req.Header.Add("Accept", "application/vnd.github+json") | |
req.Header.Add("Authorization", "Bearer "+ghuToken) | |
req.Header.Add("X-GitHub-Api-Version", "2022-11-28") | |
resp, err := client.Do(req) | |
if err != nil { | |
fmt.Println(err) | |
return false | |
} | |
defer resp.Body.Close() | |
return resp.StatusCode == http.StatusOK | |
} | |
func getHeaders(ghoToken string) map[string]string { | |
return map[string]string{ | |
"Host": "api.github.com", | |
"Authorization": "token " + ghoToken, | |
"Editor-Version": "vscode/1.85.1", | |
"Editor-Plugin-Version": "copilot-chat/0.11.1", | |
"User-Agent": "GitHubCopilotChat/0.11.1", | |
"Accept": "*/*", | |
"Accept-Encoding": "gzip, deflate, br", | |
} | |
} | |
func getAccHeaders(accessToken, uuid string, sessionId string, machineId string) map[string]string { | |
return map[string]string{ | |
"Host": "api.githubcopilot.com", | |
"Authorization": "Bearer " + accessToken, | |
"X-Request-Id": uuid, | |
"X-Github-Api-Version": "2023-07-07", | |
"Vscode-Sessionid": sessionId, | |
"Vscode-machineid": machineId, | |
"Editor-Version": "vscode/1.85.1", | |
"Editor-Plugin-Version": "copilot-chat/0.11.1", | |
"Openai-Organization": "github-copilot", | |
"Openai-Intent": "conversation-panel", | |
"Content-Type": "application/json", | |
"User-Agent": "GitHubCopilotChat/0.11.1", | |
"Copilot-Integration-Id": "vscode-chat", | |
"Accept": "*/*", | |
"Accept-Encoding": "gzip, deflate, br", | |
} | |
} | |
func getDeviceCode() (string, string, error) { | |
requestUrl := "https://github.com/login/device/code" | |
body := url.Values{} | |
headers := map[string]string{ | |
"Accept": "application/json", | |
} | |
body.Set("client_id", client_id) | |
res, err := handleRequest("POST", body, requestUrl, headers) | |
deviceCode := gjson.Get(res, "device_code").String() | |
userCode := gjson.Get(res, "user_code").String() | |
if deviceCode == "" { | |
return "", "", fmt.Errorf("device code null") | |
} | |
if userCode == "" { | |
return "", "", fmt.Errorf("user code null") | |
} | |
return deviceCode, userCode, err | |
} | |
func checkUserCode(deviceCode string) (string, error) { | |
requestUrl := "https://github.com/login/oauth/access_token" | |
body := url.Values{} | |
headers := map[string]string{ | |
"Accept": "application/json", | |
} | |
body.Set("client_id", client_id) | |
body.Set("device_code", deviceCode) | |
body.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") | |
res, err := handleRequest("POST", body, requestUrl, headers) | |
if err != nil { | |
return "", err | |
} | |
token := gjson.Get(res, "access_token").String() | |
return token, nil | |
} | |
func checkGhuToken(ghuToken string) string { | |
requestUrl := "https://api.github.com/copilot_internal/v2/token" | |
body := url.Values{} | |
headers := map[string]string{ | |
"Authorization": "Bearer " + ghuToken, | |
"editor-version": "JetBrains-IU/232.10203.10", | |
"editor-plugin-version": "copilot-intellij/1.3.3.3572", | |
"User-Agent": "GithubCopilot/1.129.0", | |
"Host": "api.github.com", | |
} | |
res, err := handleRequest("GET", body, requestUrl, headers) | |
if err != nil { | |
return "查询失败" | |
} | |
info := gjson.Get(res, "sku").String() | |
if info != "" { | |
return info | |
} else { | |
return "未订阅" | |
} | |
} | |
func handleRequest(method string, body url.Values, requestUrl string, headers map[string]string) (string, error) { | |
client := &http.Client{} | |
req, err := http.NewRequest(method, requestUrl, bytes.NewBuffer([]byte(body.Encode()))) | |
if err != nil { | |
return "", err | |
} | |
for key, value := range headers { | |
req.Header.Add(key, value) | |
} | |
resp, err := client.Do(req) | |
if err != nil { | |
return "", err | |
} | |
defer resp.Body.Close() | |
respBody, err := io.ReadAll(resp.Body) | |
if err != nil { | |
return "", fmt.Errorf("status code: %d, read body error", resp.StatusCode) | |
} | |
return string(respBody), nil | |
} | |
func loadTemplate() (*template.Template, error) { | |
t := template.New("") | |
for name, file := range Assets.Files { | |
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { | |
continue | |
} | |
h, err := io.ReadAll(file) | |
if err != nil { | |
return nil, err | |
} | |
t, err = t.New(name).Parse(string(h)) | |
if err != nil { | |
return nil, err | |
} | |
} | |
return t, nil | |
} | |