提交
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
docs
|
||||||
|
nginx
|
||||||
|
out
|
||||||
|
cert
|
||||||
101
.env.example
Normal file
101
.env.example
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# 通用配置
|
||||||
|
ENV=production
|
||||||
|
|
||||||
|
# HTTP请求的端口号 ,非必要请勿更改
|
||||||
|
PORT=1188
|
||||||
|
|
||||||
|
# HTTPS请求的端口号, 如果与本地冲突可更改, 然后自行反代 http 端口实现 https 访问
|
||||||
|
HTTPS_PORT=443
|
||||||
|
|
||||||
|
# 主机地址, 一般不需要更改
|
||||||
|
HOST=0.0.0.0
|
||||||
|
|
||||||
|
# JWT秘钥, 建议立即修改 (每次修改后都需要重新登录插件后才可正常使用)
|
||||||
|
TOKEN_SALT=TiXrNaj4avvMuD4w
|
||||||
|
|
||||||
|
# 登录页面访问密码, 用于部署在公共服务器上防止他人盗用服务, 默认空:表示不设置
|
||||||
|
LOGIN_PASSWORD=
|
||||||
|
|
||||||
|
# VS2022登录GitHub Copilot插件所需的客户端ID (请勿更改)
|
||||||
|
VS_COPILOT_CLIENT_ID=a200baed193bb2088a6e
|
||||||
|
VS_COPILOT_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# 语言环境, 默认中文(zh-CN)
|
||||||
|
CHAT_LOCALE=zh-CN
|
||||||
|
|
||||||
|
# 全局 http 请求超时,单位秒
|
||||||
|
HTTP_CLIENT_TIMEOUT=60
|
||||||
|
|
||||||
|
# 代码补全服务配置
|
||||||
|
CODEX_API_BASE=https://api.deepseek.com/beta/v1/completions
|
||||||
|
|
||||||
|
# 支持多个轮询APIKEY,用英文逗号分隔
|
||||||
|
CODEX_API_KEY=sk-
|
||||||
|
|
||||||
|
# 代码补全服务的模型名称
|
||||||
|
CODEX_API_MODEL_NAME=deepseek-chat
|
||||||
|
|
||||||
|
# 代码补全模型的最大响应tokens, 如果是Ollama建议设置小一点, 避免直接补全一长串代码
|
||||||
|
CODEX_MAX_TOKENS=500
|
||||||
|
|
||||||
|
# 代码补全模型温度超参数, 数值越大对补全质量影响越大. 如果要跟随插件动态设置,请设置为-1 (默认值为 `1`, 可以调整为 `0.1-1.0` 之间的值.)
|
||||||
|
CODEX_TEMPERATURE=1
|
||||||
|
|
||||||
|
# 代码补全模型类型, 用于兼容本地模型, 可选值: default/ollama
|
||||||
|
CODEX_SERVICE_TYPE=default
|
||||||
|
|
||||||
|
# 限制代码补全 `prompt` 和 `suffix` 的行数, 可减少代码补全时消耗的tokens, 这可能会略微影响代码补全质量. (默认: 0, 表示不限制; 大于 0 表示限制 xx 行)
|
||||||
|
CODEX_LIMIT_PROMPT=0
|
||||||
|
|
||||||
|
# 对话服务请求地址, 理论支持任何符合OpenAI接口规范的模型
|
||||||
|
CHAT_API_BASE=https://api.deepseek.com/v1/chat/completions
|
||||||
|
|
||||||
|
# 对话服务请求的API KEY, 不支持多个API KEY
|
||||||
|
CHAT_API_KEY=sk-
|
||||||
|
|
||||||
|
# 对话服务的模型名称
|
||||||
|
CHAT_API_MODEL_NAME=deepseek-chat
|
||||||
|
|
||||||
|
# 对话服务模型的最大响应tokens
|
||||||
|
CHAT_MAX_TOKENS=4096
|
||||||
|
|
||||||
|
# 是否允许使用工具, 默认开启 (根据自己的模型支持来设置)
|
||||||
|
CHAT_USE_TOOLS=true
|
||||||
|
|
||||||
|
# 默认的服务请求地址, 必须开启https. 可以替换任何二级域名, 但后续的服务域名必须与此域名有关
|
||||||
|
DEFAULT_BASE_URL=https://copilot.supercopilot.top
|
||||||
|
|
||||||
|
# 补全防抖时间, 单位:毫秒
|
||||||
|
COPILOT_DEBOUNCE=200
|
||||||
|
|
||||||
|
# 默认的API服务请求地址, 必须开启https. 域名 `api` 前缀必须固定
|
||||||
|
API_BASE_URL=https://api.copilot.supercopilot.top
|
||||||
|
|
||||||
|
# 默认的代理服务请求地址, 必须开启https. 域名 `copilot-proxy` 前缀必须固定
|
||||||
|
PROXY_BASE_URL=https://copilot-proxy.copilot.supercopilot.top
|
||||||
|
|
||||||
|
# 默认的心跳服务请求地址, 必须开启https. 域名 `copilot-telemetry-service` 前缀必须固定
|
||||||
|
TELEMETRY_BASE_URL=https://copilot-telemetry-service.copilot.supercopilot.top
|
||||||
|
|
||||||
|
# copilot的客户端类型, 用于区分是否使用官方copilot服务 (可选值: default/github)
|
||||||
|
COPILOT_CLIENT_TYPE=default
|
||||||
|
|
||||||
|
# 支持多个轮询token,用英文逗号分隔
|
||||||
|
COPILOT_GHU_TOKEN=ghu_xxxx
|
||||||
|
|
||||||
|
# 在使用官方Copilot服务的时候是否全代理请求
|
||||||
|
COPILOT_PROXY_ALL=false
|
||||||
|
|
||||||
|
# github copilot 官方账号类型, 企业版账号需要调整此参数, 否则在全代理模式下无法正常使用 (可选值: individual/business)
|
||||||
|
COPILOT_ACCOUNT_TYPE=individual
|
||||||
|
|
||||||
|
# Copilot伪装token下发的有效期,单位秒 (如果是共享给他人的服务建议使用默认值, 自用的话可以设置很大来避免github copilot插件偶尔断连的问题)
|
||||||
|
DISGUISE_COPILOT_TOKEN_EXPIRES_AT=1800
|
||||||
|
|
||||||
|
# Embedding模型配置
|
||||||
|
EMBEDDING_API_BASE=http://127.0.0.1:5012/v1/embeddings
|
||||||
|
EMBEDDING_API_KEY=
|
||||||
|
EMBEDDING_API_MODEL_NAME=m3e
|
||||||
|
EMBEDDING_DIMENSION_SIZE=1536
|
||||||
|
|
||||||
|
LIGHTWEIGHT_MODEL=gpt-4o-mini
|
||||||
77
.gitee/ISSUE_TEMPLATE/bug.yml
Normal file
77
.gitee/ISSUE_TEMPLATE/bug.yml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: Bug 反馈
|
||||||
|
description: 当你在代码中发现了一个 Bug,导致应用崩溃或抛出异常,或者某些地方看起来不对劲。
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢对项目的支持与关注。在提出问题之前,请确保你已查看相关开发或使用文档,以及已搜索现有的问题列表,以避免重复问题。
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: 这个问题是否已经存在?
|
||||||
|
options:
|
||||||
|
- label: 我已经搜索过现有的 [Issues列表](https://gitee.com/ripperTs/github-copilot-proxies/issues)
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: 是否关闭了代理工具?
|
||||||
|
options:
|
||||||
|
- label: 我确定已经关闭了所有代理工具 (无论是系统代理还是其他代理方式) 后依旧无法解决问题
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 遇到的问题
|
||||||
|
description: 请详细告诉我们如何复现你遇到的问题,如涉及代码,可提供一个最小代码示例,并使用反引号```附上它
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 完整的环境变量配置
|
||||||
|
description: 请提供你的环境变量配置,如果内容敏感不想让其他人看见, 勾选最下方 **添加内容风险标识** 即可。
|
||||||
|
value: |
|
||||||
|
```
|
||||||
|
# 请在这里粘贴你的环境变量配置
|
||||||
|
|
||||||
|
```
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 部署方式
|
||||||
|
description: 你当前是如何部署项目的?
|
||||||
|
options:
|
||||||
|
- Docker 部署(默认)
|
||||||
|
- 下载的可执行文件
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: IDE 类型
|
||||||
|
description: 你使用的 ide 是什么?
|
||||||
|
options:
|
||||||
|
- 'Jetbrains系列 (如: IntelliJ IDEA, PyCharm等)'
|
||||||
|
- VSCode
|
||||||
|
- Visual Studio 2022
|
||||||
|
- HBuilderX
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Github Copilot 插件版本
|
||||||
|
description: 填写你当前使用的 Github Copilot 插件版本
|
||||||
|
placeholder: 例如:v1.5.29 (已是最新版本)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 截图或视频
|
||||||
|
description: 如果可以的话,上传任何关于 bug 的截图。
|
||||||
|
value: |
|
||||||
|
[在这里上传图片]
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 软件版本
|
||||||
|
description: 你当前正在使用我们软件的哪个版本/分支?
|
||||||
|
placeholder: 例如:v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
8
.gitee/ISSUE_TEMPLATE/config.yml
Normal file
8
.gitee/ISSUE_TEMPLATE/config.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 环境变量参数说明
|
||||||
|
url: https://gitee.com/ripperTs/github-copilot-proxies/blob/master/PARAM.md
|
||||||
|
about: 提供了所有用到的环境变量参数、可选值、默认值等说明
|
||||||
|
- name: 免费的公共服务端点
|
||||||
|
url: https://mycopilot.noteo.cn
|
||||||
|
about: 提供了免费的公共服务端点,代码会与此仓库版本保持一致,仅用于测试连通性
|
||||||
20
.gitee/ISSUE_TEMPLATE/feature.yml
Normal file
20
.gitee/ISSUE_TEMPLATE/feature.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: 功能建议
|
||||||
|
description: 对本项目提出一个功能建议
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢提出功能建议,我们将仔细考虑!
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 你的建议是什么?
|
||||||
|
description: 清晰并简洁地描述哦~
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: 意向参与贡献
|
||||||
|
options:
|
||||||
|
- label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区
|
||||||
|
required: false
|
||||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
go.sum
|
||||||
|
configs/*_config.yml
|
||||||
|
bin/*
|
||||||
|
test/*
|
||||||
|
*_config.yml
|
||||||
|
apiclient_key.pem
|
||||||
|
.env
|
||||||
|
docker-compose-local.yml
|
||||||
|
out
|
||||||
|
logs
|
||||||
|
cert
|
||||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 使用官方 Golang 镜像作为构建环境
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 检查 go.mod 是否存在,如果不存在则初始化一个新的模块
|
||||||
|
RUN if [ ! -f go.mod ]; then \
|
||||||
|
go mod init myapp; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载依赖
|
||||||
|
RUN go mod tidy
|
||||||
|
|
||||||
|
# 构建应用
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||||
|
|
||||||
|
# 使用轻量级的 alpine 镜像
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# 从 builder 阶段复制构建的二进制文件
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
COPY .env.example .env
|
||||||
|
COPY models.json models.json
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 1188
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
# 运行应用
|
||||||
|
CMD ["./main"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Ripper
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
20
Makefile
Normal file
20
Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
BINARY_NAME=copilot-proxies
|
||||||
|
OUT_DIR=out
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build: prepare
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o ${OUT_DIR}/${BINARY_NAME}-linux-amd64 main.go
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o ${OUT_DIR}/${BINARY_NAME}-windows-amd64.exe main.go
|
||||||
|
GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" -o ${OUT_DIR}/${BINARY_NAME}-windows-amd64-gui.exe main.go
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o ${OUT_DIR}/${BINARY_NAME}-darwin-amd64 main.go
|
||||||
|
GOOS=linux GOARCH=arm64 go build -o ${OUT_DIR}/${BINARY_NAME}-linux-arm64 main.go
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -o ${OUT_DIR}/${BINARY_NAME}-darwin-arm64 main.go
|
||||||
|
|
||||||
|
prepare:
|
||||||
|
mkdir -p ${OUT_DIR}
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf ${OUT_DIR}
|
||||||
|
|
||||||
|
.PHONY: all build prepare clean
|
||||||
65
PARAM.md
Normal file
65
PARAM.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 环境变量参数说明
|
||||||
|
|
||||||
|
## ENV默认参数说明
|
||||||
|
|
||||||
|
| 参数 | 描述 | 类型 | 默认值 |
|
||||||
|
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------|
|
||||||
|
| ENV | 当前环境 (默认: production 表示生产环境, development 表示开发环境) | string | production |
|
||||||
|
| PORT | HTTP请求的端口号 ,非必要请勿更改 | int | 1188 |
|
||||||
|
| HTTPS_PORT | HTTPS请求的端口号 ,非必要请勿更改 | int | 443 |
|
||||||
|
| HOST | 主机地址 | int | 0.0.0.0 |
|
||||||
|
| LOGIN_PASSWORD | `login/device` 页面的访问密码, 用于部署在公共服务器上防止他人盗用服务, 默认空:表示不设置 | string | |
|
||||||
|
| TOKEN_SALT | JWT秘钥 **建议修改** | string | 7L3Gqrn24TUWzLwG |
|
||||||
|
| VS_COPILOT_CLIENT_ID | VS2022登录GitHub Copilot插件所需的客户端ID | string | a200baed193bb2088a6e |
|
||||||
|
| VS_COPILOT_CLIENT_SECRET | VS2022登录GitHub Copilot插件所需的客户端秘钥 | string | |
|
||||||
|
| HTTP_CLIENT_TIMEOUT | 全局 http 请求超时,单位秒 | int | 60 |
|
||||||
|
| ~~CERT_FILE~~ | HTTPS域名证书 **v0.1.0 已废弃** | string | ssl/mycopilot.crt |
|
||||||
|
| ~~KEY_FILE~~ | HTTPS域名证书秘钥 **v0.1.0 已废弃** | string | ssl/mycopilot.key |
|
||||||
|
| CODEX_API_BASE | 代码补全服务地址 , 详细参考[代码补全服务地址](#代码补全服务地址) | string | https://api.deepseek.com/beta/v1/completions |
|
||||||
|
| CODEX_API_KEY | 代码补全服务的API KEY, 支持多个轮询token,用英文逗号分隔 | string | |
|
||||||
|
| CODEX_API_MODEL_NAME | 代码补全服务的模型名称 | string | |
|
||||||
|
| CODEX_MAX_TOKENS | 代码补全模型的最大响应tokens, 如果是Ollama建议设置小一点, 避免直接补全一长串代码 | int | 500 |
|
||||||
|
| CODEX_TEMPERATURE | 代码补全模型温度超参数,deepseek模型官方推荐设置为1, 如果要跟随插件动态设置,请设置为-1 (默认值为 `1`, 可以调整为 `0.1-1.0` 之间的值.) | int | 0 |
|
||||||
|
| CODEX_SERVICE_TYPE | 代码补全模型类型, 用于兼容本地模型 <br/>可选值: `default` `ollama` | string | default |
|
||||||
|
| CODEX_LIMIT_PROMPT | 限制代码补全 `prompt` 和 `suffix` 的行数, 可减少代码补全时消耗的tokens, 这可能会略微影响代码补全质量. <br/>(默认: 0, 表示不限制; 大于 0 表示限制 xx 行) | int | 0 |
|
||||||
|
| COPILOT_DEBOUNCE | 补全防抖时间, 单位:毫秒 | int | 200 |
|
||||||
|
| CHAT_API_BASE | 对话服务请求地址, 理论支持任何符合 `OpenAI` 接口规范的模型 | string | https://api.deepseek.com/v1/chat/completions |
|
||||||
|
| CHAT_API_KEY | 对话服务请求的API KEY | string | |
|
||||||
|
| CHAT_API_MODEL_NAME | 对话服务请求的模型名称 | string | deepseek-chat |
|
||||||
|
| CHAT_MAX_TOKENS | 对话模型的最大响应tokens , 常见的模型响应tokens是4k, 如果支持8k可以手动调整 | int | 4096 |
|
||||||
|
| CHAT_LOCALE | 指定国家,可实现中文回答 | string | zh_CN |
|
||||||
|
| CHAT_USE_TOOLS | 是否支持使用工具, 默认开启 (根据自己的模型支持来设置) | bool | true |
|
||||||
|
| EMBEDDING_API_BASE | Embedding模型接口 (**支持任意符合 `OpenAI` 接口格式的 Embedding 模型**) | string | 示例: http://127.0.0.1:5012/v1/embeddings |
|
||||||
|
| EMBEDDING_API_KEY | Embedding接口鉴权秘钥 | string | |
|
||||||
|
| EMBEDDING_API_MODEL_NAME | Embedding模型名称 | string | m3e |
|
||||||
|
| EMBEDDING_DIMENSION_SIZE | Embedding 模型维度 | int | 1536 |
|
||||||
|
| DEFAULT_BASE_URL | 默认的服务请求地址, 必须开启https. 可以替换任何二级域名, 但后续的服务域名必须与此域名有关 | string | https://mycopilot.com |
|
||||||
|
| API_BASE_URL | 默认的API服务请求地址, 必须开启https. 域名 `api` 前缀必须固定 | string | https://api.mycopilot.com |
|
||||||
|
| PROXY_BASE_URL | 默认的代理服务请求地址, 必须开启https. 域名 `copilot-proxy` 前缀必须固定 | string | https://copilot-proxy.mycopilot.com |
|
||||||
|
| TELEMETRY_BASE_URL | 默认的心跳服务请求地址, 必须开启https. 域名 `copilot-telemetry-service` 前缀必须固定 | string | https://copilot-telemetry-service.mycopilot.com |
|
||||||
|
| COPILOT_CLIENT_TYPE | copilot的客户端类型, 用于区分是否使用官方copilot服务<br/>可选值: `default` `github` | string | default |
|
||||||
|
| COPILOT_GHU_TOKEN | 官方copilot服务的ghu token, 如果 `COPILOT_CLIENT_TYPE` 值为 `github` 的时候必填<br/>支持多个轮询token,用英文逗号分隔<br/>获取方法: 程序启动后访问 [获取 GitHub GHU](http://127.0.0.1:1188/github/login/device/code) 页面按提示操作即可 | string | |
|
||||||
|
| COPILOT_PROXY_ALL | 在使用官方Copilot服务的时候是否全代理 (可选值: `false` `true`) <br/> **有封号的风险, 请自行甄别后慎重使用** | bool | false |
|
||||||
|
| COPILOT_ACCOUNT_TYPE | github copilot 官方账号类型 (可选值: `individual` `business`)<br/> 企业版账号需要调整此参数, 否则在全代理模式下无法正常使用 | string | individual |
|
||||||
|
| DISGUISE_COPILOT_TOKEN_EXPIRES_AT | Copilot伪装token下发的有效期,单位秒 (如果是共享给他人的服务建议使用默认值, 自用的话可以设置很大来避免github copilot插件偶尔断连的问题) | int | 1800 |
|
||||||
|
| ~~DASHSCOPE_API_KEY~~ | ~~阿里灵石API KEY, 目前用于embedding模型服务, [API-KEY的获取与配置](https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key)~~ | string | |
|
||||||
|
| LIGHTWEIGHT_MODEL | 轻量模型名称, 关键字即可无需全部模型名称, 比如gpt-4o-mini-0429, 直接使用gpt-4o-mini即可, 符合轻量模型的调用走代码补全接口, 节省成本 | string | |
|
||||||
|
|
||||||
|
以上环境变量参数配置可以手动在以下几个地方更改进行覆盖默认的设置:
|
||||||
|
|
||||||
|
- 二进制文件同级目录下的 `.env` 文件, 如果没有可自行创建
|
||||||
|
- 系统的环境变量中设置, 例如 `export PORT=1188`
|
||||||
|
- `docker-compose.yml` 文件中的 `environment` 配置项
|
||||||
|
|
||||||
|
## 代码补全服务地址
|
||||||
|
|
||||||
|
兼容支持 `OpenAI` Chat 接口参数规范的所有地址, 下面是一些兼容常用的地址:
|
||||||
|
|
||||||
|
| 服务地址 | 描述 |
|
||||||
|
|--------------------------------------------------------------------|------------------------------------------|
|
||||||
|
| https://api.deepseek.com/beta/v1/completions | DeepSeek 官方API, 这里使用Beta地址是为了 8k 的prompt |
|
||||||
|
| https://api.siliconflow.cn/v1/completions | 硅基流动 官方API |
|
||||||
|
| https://api.mistral.ai/v1/fim/completions | Mistral 官方API |
|
||||||
|
| http://127.0.0.1:11434/v1/chat/completions | Ollama的Chat对话接口 |
|
||||||
|
| http://127.0.0.1:11434/api/generate | Ollama代码生成, 主要适配了 `suffix` 后缀参数的模型 |
|
||||||
|
| https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions | 阿里百炼平台API |
|
||||||
278
README.md
Normal file
278
README.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Github Copilot 后端代理服务
|
||||||
|
|
||||||
|
借助其他支持 `FIM` 接口的模型(如DeepSeek)来接管GitHub Copilot插件服务端, 廉价的模型+强大的补全插件相结合, 使得开发者可以更加高效的编写代码。
|
||||||
|
|
||||||
|
✨ **免费的公共服务端点: [mycopilot.noteo.cn](https://mycopilot.noteo.cn/help), 感谢 [硅基流动](https://cloud.siliconflow.cn/i/NO6ShUc3) 提供免费的模型服务**
|
||||||
|
|
||||||
|
🚨 **破坏性更新提示: `v0.1.0` 版本更新之后需要做配置调整, 具体参考: [升级指南](https://gitee.com/ripperTs/github-copilot-proxies/releases/tag/v0.1.0) 部分内容.**
|
||||||
|
|
||||||
|
🚨 **破坏性更新提示: `v0.1.6` 版本更新对 `Embeddings模型` 进行了调整, 如果还想继续使用阿里灵石的模型, 请自行借助 `One API` 之类的中转系统进行接入.**
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- [x] 使用 `docker-compose` 部署, 简单方便
|
||||||
|
- [x] 多种IDE, 如: [VSCode](#vscode), [Jetbrains IDE系列](#jetbrains-ide系列), [Visual Studio 2022](#visual-studio-2022), [HBuilderX](#hbuilderx)
|
||||||
|
- [x] 任意符合 `OpenAI` 接口规范的模型, 和 `Ollama` 部署的本地模型
|
||||||
|
- [x] `GitHub Copilot` 插件各种API接口**全接管**, 无需担心插件升级导致服务失效
|
||||||
|
- [x] 代码补全请求防抖设置和自定义 prompt 精简, 避免过度消耗 Tokens
|
||||||
|
- [x] 使用 Github Copilot 官方服务, 参考: [使用GitHub Copilot官方服务](#使用github-copilot官方服务)
|
||||||
|
- [x] VSCode 对话编辑模式
|
||||||
|
- [x] 代码补全APIKEY支持多个轮询, 避免限频
|
||||||
|
- [x] 无需自有域名, 自动配置和续签 `Let's Encrypt` SSL证书 (每 60 天自动更新一次证书, 自动重载 https 服务)
|
||||||
|
- [x] 局域网共享, 可多台电脑共享一个服务端, 参考: [局域网共享方案](#局域网共享方案)
|
||||||
|
- [x] 完全纯离线部署, 无需任何外部网络支持, 参考: [纯内网离线部署方案](#纯内网离线部署方案)
|
||||||
|
- [x] 本地部署的 Embeddings 模型支持, 参考: [README.md](embeddings/README.md)
|
||||||
|
|
||||||
|
## 如何使用?
|
||||||
|
|
||||||
|
**在使用之前确保自己的环境是干净的, 也就是说不能使用过其他的激活服务, 可以先检查自己的环境变量将 `GITHUB` `COPILOT` 相关的环境变量删除, 然后将插件更新最新版本后重启IDE即可.**
|
||||||
|
|
||||||
|
**⚠️ 如果你本地有使用科学上网工具, 那必须将域名 `copilot.supercopilot.top` 系列域名添加直连名单中, 否则无法正常使用!**
|
||||||
|
|
||||||
|
|
||||||
|
### 快速使用步骤
|
||||||
|
|
||||||
|
1. **部署服务**: 可以使用[下载文件直接部署使用](#下载文件直接部署使用) 或 使用[docker部署](#docker部署).
|
||||||
|
2. **配置IDE**: 详细参考下面的[IDE设置方法](#ide设置方法).
|
||||||
|
3. **重启IDE**: 点击登录 `GitHub Copilot` 插件即可.
|
||||||
|
|
||||||
|
### Docker部署
|
||||||
|
|
||||||
|
**(推荐)** 懒人推荐使用此方案, 比较简单
|
||||||
|
**模型API KEY 替换为你的**, 然后执行以下命令即可启动服务:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 更新服务
|
||||||
|
1. docker-compose pull
|
||||||
|
2. docker-compose down
|
||||||
|
3. docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
镜像全部上传到阿里云容器镜像服务, 每个版本都有对应的镜像可使用或回滚.
|
||||||
|
|
||||||
|
### 下载文件直接部署使用
|
||||||
|
|
||||||
|
1. 下载最新版本的对应系统的可执行文件访问 [releases](https://gitee.com/ripperTs/github-copilot-proxies/releases).
|
||||||
|
2. 在可执行文件同级目录下创建 `.env` 文件, 参考 [.env.example](.env.example) 文件配置.
|
||||||
|
3. 启动服务后然后按照[IDE设置方法](#ide设置方法)配置IDE.
|
||||||
|
4. 重启IDE,登录 `GitHub Copilot` 插件.
|
||||||
|
|
||||||
|
### 自有服务器部署
|
||||||
|
|
||||||
|
1. 使用 `docker-compose` 或下载可执行文件运行起程序 (如果已有 nginx, 避免 443 端口占用可直接修改其他端口, 后面借助nginx 反向代理实现 https)
|
||||||
|
2. 解析四个域名到服务器IP, 假设你的域名是: `domain.com`, 那么你需要解析的域名分别是 (**特别注意: 域名前缀不可变**):
|
||||||
|
```
|
||||||
|
domain.com
|
||||||
|
api.domain.com
|
||||||
|
copilot-proxy.domain.com
|
||||||
|
copilot-telemetry-service.domain.com
|
||||||
|
```
|
||||||
|
3. 将四个域名全部配置好 `SSL` 证书
|
||||||
|
4. 配置 Nginx 反向代理或伪静态规则, 参考配置如下:
|
||||||
|
```nginx
|
||||||
|
location ^~ /
|
||||||
|
{
|
||||||
|
proxy_pass http://127.0.0.1:1188/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header REMOTE-HOST $remote_addr;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
# proxy_hide_header Upgrade;
|
||||||
|
|
||||||
|
add_header X-Cache $upstream_cache_status;
|
||||||
|
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_max_temp_file_size 0;
|
||||||
|
client_max_body_size 10m;
|
||||||
|
client_body_buffer_size 128k;
|
||||||
|
proxy_connect_timeout 90;
|
||||||
|
proxy_send_timeout 90;
|
||||||
|
proxy_read_timeout 90;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 4 32k;
|
||||||
|
proxy_busy_buffers_size 64k;
|
||||||
|
proxy_temp_file_write_size 64k;
|
||||||
|
|
||||||
|
#Set Nginx Cache
|
||||||
|
|
||||||
|
|
||||||
|
set $static_filer5CIeZff 0;
|
||||||
|
if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" )
|
||||||
|
{
|
||||||
|
set $static_filer5CIeZff 1;
|
||||||
|
expires 1m;
|
||||||
|
}
|
||||||
|
if ( $static_filer5CIeZff = 0 )
|
||||||
|
{
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
5. 最后将以上域名修改到对应的环境变量配置文件中.
|
||||||
|
6. 最终使用 https 方式访问四个域名必须是正常的, 不能有任何问题, 否则插件无法正常使用.
|
||||||
|
|
||||||
|
### 环境变量参数说明
|
||||||
|
|
||||||
|
详细参考: [环境变量参数说明](PARAM.md)
|
||||||
|
|
||||||
|
## IDE设置方法
|
||||||
|
|
||||||
|
### VSCode
|
||||||
|
|
||||||
|
1. 安装插件: `GitHub Copilot`
|
||||||
|
2. 修改 VSCode 的 settings.json 文件, 添加以下配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"github.copilot.advanced": {
|
||||||
|
"authProvider": "github-enterprise",
|
||||||
|
"debug.overrideCAPIUrl": "https://api.copilot.supercopilot.top",
|
||||||
|
"debug.overrideProxyUrl": "https://copilot-proxy.copilot.supercopilot.top",
|
||||||
|
"debug.chatOverrideProxyUrl": "https://api.copilot.supercopilot.top/chat/completions",
|
||||||
|
"debug.overrideFastRewriteEngine": "v1/engines/copilot-centralus-h100",
|
||||||
|
"debug.overrideFastRewriteUrl": "https://api.copilot.supercopilot.top"
|
||||||
|
},
|
||||||
|
"github-enterprise.uri": "https://copilot.supercopilot.top"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jetbrains IDE系列
|
||||||
|
|
||||||
|
1. 找到`设置` > `语言与框架` > `GitHub Copilot` > `Authentication Provider`
|
||||||
|
2. 填写的值为: `copilot.supercopilot.top`
|
||||||
|
|
||||||
|
### Visual Studio 2022
|
||||||
|
|
||||||
|
1. 更新到最新版本(内置 Copilot 版本)至少是 `17.10.x` 以上
|
||||||
|
2. 首先, 开启 `Github Enterprise` 账户支持:工具->环境->账户->勾选 `包含 Github Enterprise 服务器账户`
|
||||||
|
3. 然后, 重启你的 `Visual Studio 2022` 编辑器
|
||||||
|
4. 最后, 点击添加 Github 账户,切换到 Github Enterprise 选项卡,输入 `https://copilot.supercopilot.top` 即可。
|
||||||
|
|
||||||
|
### HBuilderX
|
||||||
|
|
||||||
|
⚠️ 注意, 插件中的相关 domain 已经写死无法修改, 所以必须使用默认的 `copilot.supercopilot.top` 域名配置.
|
||||||
|
|
||||||
|
1. 下载 **[copilot-for-hbuilderx-v1.zip](https://pan.quark.cn/s/70e6849970e5)** 插件到本地
|
||||||
|
2. 将插件安装到 plugin目录下, 详细参考: [离线插件安装指南](https://hx.dcloud.net.cn/Tutorial/OfflineInstall)
|
||||||
|
3. 重启 Hbuilder X 后点击登录 `GitHub Copilot` 即可.
|
||||||
|
|
||||||
|
|
||||||
|
## 局域网共享方案
|
||||||
|
如果是局域网多台电脑共用一个服务端,只需要更改hosts文件指向到内网服务器 ip 即可, 例如:
|
||||||
|
在局域网服务器(192.168.80.40)部署了copilot-proxies服务,那么局域网内其他机器仅需要修改host为以下即可,可以工作。 ( [@pennbay](https://gitee.com/pennbay) 提供实测反馈 )
|
||||||
|
```
|
||||||
|
192.168.80.40 copilot.supercopilot.top
|
||||||
|
192.168.80.40 api.copilot.supercopilot.top
|
||||||
|
192.168.80.40 copilot-proxy.copilot.supercopilot.top
|
||||||
|
192.168.80.40 copilot-telemetry-service.copilot.supercopilot.top
|
||||||
|
```
|
||||||
|
|
||||||
|
## 纯内网离线部署方案
|
||||||
|
`v0.1.0` 版本之后 ssl 证书调整为从网络上下载同步, 这对于纯内网部署造成了一些困难, 下面我提供一个简单的方案你需要做如下操作:
|
||||||
|
|
||||||
|
- 从外网下载最新证书文件, 远程下载地址参考 [certificate.go](pkg/certificate/certificate.go), 注意证书最长只有 60 天, 需要手动更新.
|
||||||
|
- 将两个证书文件放在 `/cert` 目录下.
|
||||||
|
- 因为也无法连接公共 DNS 服务器, 所以也需要更改本机 hosts 文件, 将以下域名手动指向到本机的 `127.0.0.1`:
|
||||||
|
- `copilot.supercopilot.top`
|
||||||
|
- `api.copilot.supercopilot.top`
|
||||||
|
- `copilot-proxy.copilot.supercopilot.top`
|
||||||
|
- `copilot-telemetry-service.copilot.supercopilot.top`
|
||||||
|
- 启动主程序.
|
||||||
|
|
||||||
|
还有一种方案, 依旧使用 `v0.1.0` 之前版本的自签证书, 但这会在未来 `GitHub Copilot` 插件更新后可能无法正常使用.
|
||||||
|
|
||||||
|
## 支持的模型
|
||||||
|
|
||||||
|
> 大部分Chat模型都兼容, 因此下面列出的模型是支持 FIM 的模型, 也就是说支持补全功能.
|
||||||
|
|
||||||
|
| 模型名称 (区分大小写) | 类型 | 接入地址 | 说明 |
|
||||||
|
|--------------------------------------------------------------------------------------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------|-----------------------------|
|
||||||
|
| [Qwen/Qwen2.5-Coder-7B-Instruct](https://docs.siliconflow.cn/features/fim) | 免费 | <details><summary>查看地址</summary>`https://api.siliconflow.cn/v1/completions`</details> | 硅基流动官方支持的 FIM 补全模型, 完美适配且免费 |
|
||||||
|
| [DeepSeek (API)](https://www.deepseek.com/) | 付费 | <details><summary>查看地址</summary>`https://api.deepseek.com/beta/v1/completions`</details> | 👍🏻完美适配且价格实惠, 推荐使用 |
|
||||||
|
| [deepseek-ai/DeepSeek-V2.5](https://docs.siliconflow.cn/features/fim) | 付费 | <details><summary>查看地址</summary>`https://api.siliconflow.cn/v1/completions`</details> | 硅基流动官方支持的 FIM 补全模型, 完美适配 |
|
||||||
|
| [deepseek-ai/DeepSeek-Coder-V2-Instruct](https://docs.siliconflow.cn/features/fim) | 付费 | <details><summary>查看地址</summary>`https://api.siliconflow.cn/v1/completions`</details> | 硅基流动官方支持的 FIM 补全模型, 完美适配 |
|
||||||
|
| [codestral-latest (API)](https://docs.mistral.ai/api/#tag/fim) | 免费 / 付费 | <details><summary>查看地址</summary>`https://api.mistral.ai/v1/fim/completions`</details> | Mistral 出品, 免费计划有非常严重的频率限制 |
|
||||||
|
| [stable-code](https://ollama.com/library/stable-code) | 免费 | <details><summary>查看地址</summary>`http://127.0.0.1:11434/v1/chat/completions`</details> | Ollama部署本地的超小量级补全模型 |
|
||||||
|
| [codegemma](https://ollama.com/library/codegemma) | 免费 | <details><summary>查看地址</summary>`http://127.0.0.1:11434/v1/chat/completions`</details> | Ollama部署本地的补全模型 |
|
||||||
|
| [codellama](https://ollama.com/library/codellama) | 免费 | <details><summary>查看地址</summary>`http://127.0.0.1:11434/v1/chat/completions`</details> | Ollama部署本地的补全模型 |
|
||||||
|
| [qwen-coder-turbo-latest](https://help.aliyun.com/zh/model-studio/user-guide/qwen-coder?spm=a2c4g.11186623.0.0.a5234823I6LvAG) | 收费 | <details><summary>查看地址</summary>`https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions`</details> | 阿里通义代码补全模型 |
|
||||||
|
| [mike/deepseek-coder-v2](https://ollama.com/mike/deepseek-coder-v2) | 免费 | <details><summary>查看地址</summary>`http://127.0.0.1:11434/api/generate`</details> | Ollama支持的 `suffix` 参数方式实现 |
|
||||||
|
| [deepseek-coder-v2](https://ollama.com/library/deepseek-coder-v2) | 免费 | <details><summary>查看地址</summary>`http://127.0.0.1:11434/api/generate`</details> | Ollama支持的 `suffix` 参数方式实现 |
|
||||||
|
|
||||||
|
**💡以上接入的模型除了 `DeepSeek` 模型与 `硅基流动` 模型之外, 效果均不理想, 这里仅做接入更多模型的Demo参考.**,
|
||||||
|
理论上后续如果有API支持标准的FIM补全, 都可以接入.
|
||||||
|
|
||||||
|
## 使用Github Copilot官方服务
|
||||||
|
|
||||||
|
> 前提条件: 必须有官方正版的 `GitHub Copilot` 订阅权限, 否则无法使用.
|
||||||
|
|
||||||
|
**应用场景:**
|
||||||
|
|
||||||
|
- 适用于 **"月抛"** 的Github账号, 避免每个月切换Github账号后都要重复登录多个IDE中的插件操作, 只需要更改环境变量中的
|
||||||
|
`COPILOT_GHU_TOKEN` 参数一处即可.
|
||||||
|
- 适用于 **"多人共享"** 的Github账号, 共享者只需要使用此服务即可, 不需要告知Github账号密码.
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
- 设置环境变量参数 `COPILOT_CLIENT_TYPE=github` (设置此参数后其他的Copilot相关配置都可以不用设置了, 因为这里已经使用了官方的服务).
|
||||||
|
- 启动服务访问 [https://copilot.supercopilot.top/github/login/device/code](https://copilot.supercopilot.top/github/login/device/code) 获取 `ghu_` 的参数
|
||||||
|
- 将获取到的 `ghu_` 参数填写到 `COPILOT_GHU_TOKEN` 环境变量中.
|
||||||
|
- 重启服务, 重启IDE即可.
|
||||||
|
|
||||||
|
### 全代理模式
|
||||||
|
> 即所有请求走依旧服务端,然后由服务端发起请求到github, 在多人共享账号的情况下所有请求全部统一出口, 可以略微降低被风控的情况.
|
||||||
|
|
||||||
|
- 设置环境变量参数 `COPILOT_PROXY_ALL=true` (默认值为 `false`).
|
||||||
|
- 重启服务即可.
|
||||||
|
- 全代理模式的 `/embeddings`和 `/chunks` 接口即将推出.
|
||||||
|
|
||||||
|
**🚨 全代理模式有封号的风险, 请自行甄别谨慎使用.** 补全和对话接口的请求频率都有阀值限制的, 共享人数过多肯定会触发风控.
|
||||||
|
|
||||||
|
## Embeddings模型配置
|
||||||
|
|
||||||
|
> 目前仅 VSCode 最新版本的 `Github Copilot Chat` 插件支持使用 Embeddings 模型, 其他IDE可以不用考虑.
|
||||||
|
|
||||||
|
支持使用任意符合 `OpenAI` 接口格式的模型, 推荐本地Docker部署bge-m3的模型, 具体步骤如下参考: [README.md](embeddings/README.md) 中的Docker运行部分内容.
|
||||||
|
|
||||||
|
**然后配置服务端**
|
||||||
|
修改下面相关环境变量文件内容:
|
||||||
|
```
|
||||||
|
EMBEDDING_API_BASE=http://127.0.0.1/v1/embeddings
|
||||||
|
EMBEDDING_API_KEY=sk-aaabbbcccdddeeefffggghhhiiijjjkkk
|
||||||
|
EMBEDDING_API_MODEL_NAME=bge-m3
|
||||||
|
EMBEDDING_DIMENSION_SIZE=1024
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你是 `One API` 之类的中转站, 也可以按照上面环境变量内容直接接入.
|
||||||
|
|
||||||
|
⚠️ 如果使用第三方API的Embedding模型, 可能会有隐私相关风险以及请求限频问题.
|
||||||
|
|
||||||
|
## 问题排查
|
||||||
|
如果本地部署遇到了 `无法登录` `无法对话` `无法补全` 等问题, 可以参考下面的排查方法:
|
||||||
|
1. 确认最新版本服务
|
||||||
|
2. `ping copilot.supercopilot.top` 是否指向 `127.0.0.1` ,如果不是则表明受到了代理工具影响
|
||||||
|
3. 访问 [https://copilot.supercopilot.top](https://copilot.supercopilot.top/help) 是否出现帮助页面
|
||||||
|
4. 检查目录下是否含有 `cert` 目录
|
||||||
|
5. 特别注意检查 https 端口不可被占用
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 请勿将本服务用于商业用途, 仅供学习交流使用
|
||||||
|
2. 请勿将本服务用于非法用途, 一切后果自负
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
- [copilot_take_over](https://gitee.com/LoveA/copilot_take_over)
|
||||||
|
- [override](https://github.com/linux-do/override)
|
||||||
|
- [硅基流动](https://cloud.siliconflow.cn/i/NO6ShUc3)
|
||||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: "3.3"
|
||||||
|
services:
|
||||||
|
copilot-app:
|
||||||
|
container_name: copilot-app
|
||||||
|
#image: registry.cn-hangzhou.aliyuncs.com/ripper/copilot-proxies:latest
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 1188:1188
|
||||||
|
#- "443:443"
|
||||||
|
environment:
|
||||||
|
- PORT=1188
|
||||||
|
- LOGIN_PASSWORD=1188
|
||||||
|
- CHAT_LOCALE=zh-CN
|
||||||
|
- HTTP_CLIENT_TIMEOUT=60
|
||||||
|
- CODEX_API_BASE=https://for.shuo.bar/v1/chat/completions
|
||||||
|
- CODEX_API_KEY=sk-oc25vXlSaSoi1tURqJ2ZfhyGj3jRkpPp8TrBIZHE6aEEz1Ov # 代码补全API地址
|
||||||
|
- CODEX_API_MODEL_NAME=glm-4.5 # 代码补全API密钥, 支持多个轮询APIKEY,用英文逗号分隔
|
||||||
|
- CODEX_MAX_TOKENS=15000 # 代码补全API模型名称
|
||||||
|
- CODEX_TEMPERATURE=-1 # 代码补全API最大返回token数
|
||||||
|
- CODEX_SERVICE_TYPE=default # 代码补全API温度, 默认值为:1, deepseek模型官方推荐设置为1, 如果要跟随插件动态设置,请设置为-1
|
||||||
|
- CODEX_LIMIT_PROMPT=20 # 补全服务类型, 默认值为:default, 可选值: ollama
|
||||||
|
- CHAT_API_BASE=https://for.shuo.bar/v1/chat/completions # 限制prompt行数,可减少代码补全时消耗的tokens
|
||||||
|
- CHAT_API_KEY=sk-oc25vXlSaSoi1tURqJ2ZfhyGj3jRkpPp8TrBIZHE6aEEz1Ov # 聊天补全API地址
|
||||||
|
- CHAT_API_MODEL_NAME=glm-4.5 # 聊天补全API密钥
|
||||||
|
- CHAT_MAX_TOKENS=40960 # 聊天补全API模型名称
|
||||||
|
- CHAT_USE_TOOLS=true # 聊天API最大返回token数
|
||||||
|
- DEFAULT_BASE_URL=https://mycopilot.01061220.xyz
|
||||||
|
- COPILOT_DEBOUNCE=200
|
||||||
|
- API_BASE_URL=https://api.mycopilot.01061220.xyz # 补全防抖时间(毫秒)
|
||||||
|
- PROXY_BASE_URL=https://copilot-proxy.mycopilot.01061220.xyz
|
||||||
|
- TELEMETRY_BASE_URL=https://copilot-telemetry-service.mycopilot.01061220.xyz
|
||||||
|
- COPILOT_CLIENT_TYPE=default
|
||||||
|
- COPILOT_GHU_TOKEN=ghu_xxxx # copilot的客户端类型, 用于区分是否使用官方copilot服务
|
||||||
|
- COPILOT_PROXY_ALL=false # 支持多个轮询token,用英文逗号分隔
|
||||||
|
- COPILOT_ACCOUNT_TYPE=individual # 在使用官方Copilot服务的时候是否全代理请求
|
||||||
|
- DISGUISE_COPILOT_TOKEN_EXPIRES_AT=1800 # github copilot 官方账号类型, 企业版账号需要调整此参数, 否则在全代理模式下无法正常使用 (可选值: individual/business)
|
||||||
|
- EMBEDDING_API_BASE=http://127.0.0.1:5012/v1/embeddings # Copilot伪装token下发的有效期,单位秒
|
||||||
|
- EMBEDDING_API_KEY=
|
||||||
|
- EMBEDDING_API_MODEL_NAME=m3e
|
||||||
|
- EMBEDDING_DIMENSION_SIZE=1536
|
||||||
|
- LIGHTWEIGHT_MODEL=do-not-use
|
||||||
|
- VS_COPILOT_CLIENT_ID=a200baed193bb2088a6e
|
||||||
|
- VS_COPILOT_CLIENT_SECRET=
|
||||||
|
- TOKEN_SALT=1188
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./models.json:/root/models.json
|
||||||
|
networks: {}
|
||||||
37
embeddings/.gitignore
vendored
Normal file
37
embeddings/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
*/target
|
||||||
|
*/tomcat
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
*.class
|
||||||
|
target/
|
||||||
|
tomcat/
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.classpath
|
||||||
|
src/main/resources/META-INF
|
||||||
|
.DS_Store
|
||||||
|
logs/*
|
||||||
|
.idea/*
|
||||||
|
application-local.properties
|
||||||
|
rebel.xml
|
||||||
|
LOG_DIR_IS_UNDEFINED
|
||||||
|
index/*
|
||||||
|
lora.json
|
||||||
|
ptuning.json
|
||||||
|
lora
|
||||||
|
applogs
|
||||||
|
__pycache__
|
||||||
|
answers.json
|
||||||
|
answers.jsonl
|
||||||
|
answers_back.json
|
||||||
|
keys.pkl
|
||||||
|
data.pkl
|
||||||
|
make_dataset_schedule.d
|
||||||
|
config.json
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
temp/
|
||||||
|
.ipynb_checkpoints
|
||||||
|
/modules/*
|
||||||
|
cert_cache/*
|
||||||
|
models
|
||||||
18
embeddings/Dockerfile
Normal file
18
embeddings/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 使用官方Python运行时作为父镜像
|
||||||
|
FROM registry.cn-hangzhou.aliyuncs.com/ripper/python:3.9-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 将当前目录内容复制到容器的/app中
|
||||||
|
ADD . /app
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
# 安装程序需要的包
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
# 运行时监听的端口
|
||||||
|
EXPOSE 6008
|
||||||
|
|
||||||
|
# 运行app.py时的命令及其参数
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "6008"]
|
||||||
55
embeddings/README.md
Normal file
55
embeddings/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 本地 Embeddings 模型API服务
|
||||||
|
|
||||||
|
下载本地 `Embedding模型` 并转为 `OpenAI` 接口格式的 API 服务。
|
||||||
|
|
||||||
|
## 准备工作
|
||||||
|
- Python 3.9+
|
||||||
|
- 选择合适的模型文件 (根据效果自行测试), 程序支持自动提升维度或降级维度到指定维度(接口中传递的 `dimensions` 参数, 默认为512)
|
||||||
|
- 下载模型文件,放置在 `./models` 目录下, 国内下载可以去 [魔搭社区](https://www.modelscope.cn/models/BAAI/bge-m3), 速度不受影响
|
||||||
|
|
||||||
|
## 环境变量参数
|
||||||
|
- `sk-key`: 服务的 `API KEY`,默认为 `sk-aaabbbcccdddeeefffggghhhiiijjjkkk`
|
||||||
|
- `auto-dim`: 是否自动进行维度操作, 若为 `true` 则会自动提升或降级维度到512, 默认为 `false`
|
||||||
|
- `model-name`: 模型目录名称, 默认为 `bge-m3`, 注意必须在models文件夹下有对应的模型文件夹
|
||||||
|
|
||||||
|
|
||||||
|
## 运行服务
|
||||||
|
```shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request POST 'http://127.0.0.1:6008/v1/embeddings' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'Authorization: Bearer sk-aaabbbcccdddeeefffggghhhiiijjjkkk' \
|
||||||
|
--data-raw '{
|
||||||
|
"input": [
|
||||||
|
"解析当前项目"
|
||||||
|
],
|
||||||
|
"model": "text-embedding-3-small",
|
||||||
|
"dimensions": 512
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker 运行
|
||||||
|
|
||||||
|
将 [docker-compose.yml](docker-compose.yml) 文件放在任意目录下, 然后执行命令:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
镜像内已经将模型打包好了, 所以首次执行会比较慢, 如果需要更新或新增模型, 直接将新的模型文件放在 `./models` 目录下, 更改环境变量 `model-name` 即可
|
||||||
|
|
||||||
|
然后重新启动服务即可:
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# 如果未生效, 可以先停止再启动
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
14
embeddings/docker-compose.yml
Normal file
14
embeddings/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
embeddings2openai:
|
||||||
|
container_name: embeddings2openai
|
||||||
|
image: registry.cn-hangzhou.aliyuncs.com/ripper/embeddings2openai:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 6008:6008
|
||||||
|
environment:
|
||||||
|
- sk-key=sk-aaabbbcccdddeeefffggghhhiiijjjkkk
|
||||||
|
- auto-dim=false
|
||||||
|
- model-name=bge-m3
|
||||||
|
# volumes:
|
||||||
|
# - ./models:/app/models # 如果需要自己更改模型, 将模型文件放到models文件夹下, 并取消注释
|
||||||
171
embeddings/main.py
Normal file
171
embeddings/main.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
from fastapi import FastAPI, Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import uvicorn
|
||||||
|
import tiktoken
|
||||||
|
import numpy as np
|
||||||
|
from scipy.interpolate import interp1d
|
||||||
|
from typing import List, Optional
|
||||||
|
from sklearn.preprocessing import PolynomialFeatures
|
||||||
|
from sklearn.decomposition import PCA
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 接口秘钥环境变量传入
|
||||||
|
sk_key = os.environ.get('sk-key', 'sk-aaabbbcccdddeeefffggghhhiiijjjkkk')
|
||||||
|
# 是否自动进行维度操作的环境变量,默认为false
|
||||||
|
auto_dim = os.environ.get('auto-dim', 'false').lower() == 'true'
|
||||||
|
# 模型名称, 必须在models文件夹下有对应的模型文件夹
|
||||||
|
model_name = os.environ.get('model-name', 'bge-m3')
|
||||||
|
|
||||||
|
# 创建一个FastAPI实例
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建一个HTTPBearer实例
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
# 预加载模型
|
||||||
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 检测是否有GPU可用,如果有则使用cuda设备,否则使用cpu设备
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
print('本次加载模型的设备为GPU: ', torch.cuda.get_device_name(0))
|
||||||
|
else:
|
||||||
|
print('本次加载模型的设备为CPU.')
|
||||||
|
|
||||||
|
print(f'加载模型: {model_name}')
|
||||||
|
model = SentenceTransformer(f'./models/{model_name}',device=device)
|
||||||
|
|
||||||
|
# 创建PCA降维模型
|
||||||
|
pca = None
|
||||||
|
|
||||||
|
class EmbeddingRequest(BaseModel):
|
||||||
|
input: List[str]
|
||||||
|
model: str
|
||||||
|
dimensions: Optional[int] = 512
|
||||||
|
|
||||||
|
class EmbeddingResponse(BaseModel):
|
||||||
|
data: list
|
||||||
|
model: str
|
||||||
|
object: str
|
||||||
|
usage: dict
|
||||||
|
|
||||||
|
def num_tokens_from_string(string: str) -> int:
|
||||||
|
"""Returns the number of tokens in a text string."""
|
||||||
|
encoding = tiktoken.get_encoding('cl100k_base')
|
||||||
|
num_tokens = len(encoding.encode(string))
|
||||||
|
return num_tokens
|
||||||
|
|
||||||
|
# 插值法
|
||||||
|
def interpolate_vector(vector, target_length):
|
||||||
|
original_indices = np.arange(len(vector))
|
||||||
|
target_indices = np.linspace(0, len(vector)-1, target_length)
|
||||||
|
f = interp1d(original_indices, vector, kind='linear')
|
||||||
|
return f(target_indices)
|
||||||
|
|
||||||
|
def expand_features(embedding, target_length):
|
||||||
|
poly = PolynomialFeatures(degree=2)
|
||||||
|
expanded_embedding = poly.fit_transform(embedding.reshape(1, -1))
|
||||||
|
expanded_embedding = expanded_embedding.flatten()
|
||||||
|
if len(expanded_embedding) > target_length:
|
||||||
|
# 如果扩展后的特征超过目标长度,可以通过截断或其他方法来减少维度
|
||||||
|
expanded_embedding = expanded_embedding[:target_length]
|
||||||
|
elif len(expanded_embedding) < target_length:
|
||||||
|
# 如果扩展后的特征少于目标长度,可以通过填充或其他方法来增加维度
|
||||||
|
expanded_embedding = np.pad(expanded_embedding, (0, target_length - len(expanded_embedding)))
|
||||||
|
return expanded_embedding
|
||||||
|
|
||||||
|
# 降维方法:使用PCA将向量从1024维降到512维
|
||||||
|
def reduce_dimensions(embeddings, target_dim=512):
|
||||||
|
global pca
|
||||||
|
|
||||||
|
# 将列表转换为numpy数组
|
||||||
|
embeddings_array = np.array(embeddings)
|
||||||
|
|
||||||
|
# 检查样本数量
|
||||||
|
n_samples = embeddings_array.shape[0]
|
||||||
|
n_features = embeddings_array.shape[1]
|
||||||
|
|
||||||
|
# 如果只有一个样本,无法使用PCA,改用插值法
|
||||||
|
if n_samples == 1:
|
||||||
|
return [interpolate_vector(embeddings_array[0], target_dim)]
|
||||||
|
|
||||||
|
# 确保目标维度不超过可能的最大值
|
||||||
|
actual_target_dim = min(target_dim, n_samples, n_features)
|
||||||
|
if actual_target_dim < target_dim:
|
||||||
|
print(f"警告:目标维度{target_dim}超过了可能的最大值,已调整为{actual_target_dim}")
|
||||||
|
|
||||||
|
# 如果是第一次运行或者输入维度变化,重新初始化PCA
|
||||||
|
if pca is None or pca.n_components != actual_target_dim:
|
||||||
|
pca = PCA(n_components=actual_target_dim)
|
||||||
|
# 先拟合再转换
|
||||||
|
reduced_embeddings = pca.fit_transform(embeddings_array)
|
||||||
|
else:
|
||||||
|
# 直接使用已训练的PCA模型转换
|
||||||
|
reduced_embeddings = pca.transform(embeddings_array)
|
||||||
|
|
||||||
|
# 如果实际降维后的维度小于目标维度,使用插值法扩展
|
||||||
|
if actual_target_dim < target_dim:
|
||||||
|
reduced_embeddings = [interpolate_vector(embedding, target_dim) for embedding in reduced_embeddings]
|
||||||
|
|
||||||
|
return list(reduced_embeddings)
|
||||||
|
|
||||||
|
@app.post("/v1/embeddings", response_model=EmbeddingResponse)
|
||||||
|
async def get_embeddings(request: EmbeddingRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
|
|
||||||
|
if credentials.credentials != sk_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid authorization code",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算嵌入向量和tokens数量
|
||||||
|
embeddings = [model.encode(text) for text in request.input]
|
||||||
|
|
||||||
|
# 检查是否需要进行维度操作
|
||||||
|
if auto_dim:
|
||||||
|
# 检查向量维度
|
||||||
|
embedding_dim = len(embeddings[0])
|
||||||
|
|
||||||
|
# 如果维度大于512,则降维到512
|
||||||
|
if embedding_dim > request.dimensions:
|
||||||
|
embeddings = reduce_dimensions(embeddings, target_dim=request.dimensions)
|
||||||
|
# 如果维度小于512,则使用插值法扩展到512
|
||||||
|
elif embedding_dim < request.dimensions:
|
||||||
|
embeddings = [interpolate_vector(embedding, request.dimensions) for embedding in embeddings]
|
||||||
|
|
||||||
|
# 归一化处理
|
||||||
|
embeddings = [embedding / np.linalg.norm(embedding) for embedding in embeddings]
|
||||||
|
# 将numpy数组转换为列表
|
||||||
|
embeddings = [embedding.tolist() for embedding in embeddings]
|
||||||
|
prompt_tokens = sum(len(text.split()) for text in request.input)
|
||||||
|
total_tokens = sum(num_tokens_from_string(text) for text in request.input)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"embedding": embedding,
|
||||||
|
"index": index,
|
||||||
|
"object": "embedding"
|
||||||
|
} for index, embedding in enumerate(embeddings)
|
||||||
|
],
|
||||||
|
"model": model_name,
|
||||||
|
"object": "list",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": prompt_tokens,
|
||||||
|
"total_tokens": total_tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run("main:app", host='0.0.0.0', port=6008, workers=1)
|
||||||
11
embeddings/requirements.txt
Normal file
11
embeddings/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi==0.100.0
|
||||||
|
pydantic==1.10.7
|
||||||
|
sentence-transformers==3.4.1
|
||||||
|
uvicorn==0.23.1
|
||||||
|
tiktoken==0.4.0
|
||||||
|
numpy==2.0.2
|
||||||
|
scipy==1.13.1
|
||||||
|
scikit-learn==1.6.1
|
||||||
|
torch
|
||||||
|
torchvision
|
||||||
|
torchaudio
|
||||||
51
go.mod
Normal file
51
go.mod
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
module ripper
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||||
|
github.com/gomodule/redigo v1.9.2
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
golang.org/x/sync v0.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
60
internal/app/github_auth/access_token.go
Normal file
60
internal/app/github_auth/access_token.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package github_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sha256Sign(data string) string {
|
||||||
|
hash := sha256.Sum256([]byte(data))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccessTokenT() string {
|
||||||
|
t, _ := uuid.NewV4()
|
||||||
|
return t.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func JsonMap2Token(data map[string]interface{}) string {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(data))
|
||||||
|
for k := range data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(";")
|
||||||
|
}
|
||||||
|
sb.WriteString(key)
|
||||||
|
sb.WriteString("=")
|
||||||
|
sb.WriteString(fmt.Sprintf("%v", data[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func JsonMap2SignToken(data map[string]interface{}) string {
|
||||||
|
token := JsonMap2Token(data)
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sign := Token2Sign(token)
|
||||||
|
return token + ";8kp=1:" + sign
|
||||||
|
}
|
||||||
|
|
||||||
|
func Token2Sign(token string) string {
|
||||||
|
sign := sha256Sign(token + fmt.Sprintf(";salt=%s", os.Getenv("TOKEN_SALT")))
|
||||||
|
return sign
|
||||||
|
}
|
||||||
162
internal/app/github_auth/code.go
Normal file
162
internal/app/github_auth/code.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package github_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/gomodule/redigo/redis"
|
||||||
|
"ripper/internal/cache"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientAuthInfo struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
DisplayUserName string `json:"display_user_name,omitempty"`
|
||||||
|
DeviceCode string `json:"device_code"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
CardCode string `json:"card_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientOAuthInfo struct {
|
||||||
|
ClientId string `json:"client_id" form:"client_id"`
|
||||||
|
Code string `json:"code" form:"code"`
|
||||||
|
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||||
|
Scope string `json:"scope" form:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindClientToCode 绑定客户端到代码
|
||||||
|
// clientId 客户端ID
|
||||||
|
// exp 过期时间
|
||||||
|
// return 用户代码, 设备代码, 错误
|
||||||
|
func BindClientToCode(clientId string, exp int) (string, string, error) {
|
||||||
|
genCode := func() string {
|
||||||
|
newUUID, _ := uuid.NewV4()
|
||||||
|
uuidStr := strings.Replace(newUUID.String(), "-", "", -1)
|
||||||
|
return uuidStr[:6]
|
||||||
|
}
|
||||||
|
formattedUUID := genCode()
|
||||||
|
rep := 0
|
||||||
|
redisKey := fmt.Sprintf("copilot.proxy.%s", formattedUUID)
|
||||||
|
repeat, _ := cache.Exist(redisKey)
|
||||||
|
for repeat {
|
||||||
|
if rep > 5 {
|
||||||
|
return "", "", fmt.Errorf("gen code error")
|
||||||
|
}
|
||||||
|
formattedUUID = genCode()
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.%s", formattedUUID)
|
||||||
|
repeat, _ = cache.Exist(redisKey)
|
||||||
|
rep++
|
||||||
|
}
|
||||||
|
devId := GenDevicesCode(40)
|
||||||
|
authInfo := ClientAuthInfo{
|
||||||
|
ClientId: clientId,
|
||||||
|
DeviceCode: devId,
|
||||||
|
UserCode: formattedUUID,
|
||||||
|
}
|
||||||
|
authInfoData, _ := json.Marshal(authInfo)
|
||||||
|
err := cache.Set(redisKey, authInfoData, exp)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.map.%s", devId)
|
||||||
|
err = cache.Set(redisKey, formattedUUID, exp)
|
||||||
|
return formattedUUID, devId, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientAuthInfoByDeviceCode 通过设备代码获取客户端授权信息
|
||||||
|
func GetClientAuthInfoByDeviceCode(deviceCode string) (*ClientAuthInfo, error) {
|
||||||
|
redisKey := fmt.Sprintf("copilot.proxy.map.%s", deviceCode)
|
||||||
|
userCode, err := cache.Get(redisKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.%s", userCode)
|
||||||
|
authInfoData, err := redis.Bytes(cache.Get(redisKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authInfo := &ClientAuthInfo{}
|
||||||
|
err = json.Unmarshal(authInfoData, &authInfo)
|
||||||
|
return authInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOAuthCodeInfoByClientIdAndCode(clientId string, code string) (*ClientOAuthInfo, error) {
|
||||||
|
cacheKey := "oauth2_authorize_" + clientId
|
||||||
|
oauthCodeData, err := redis.Bytes(cache.Get(cacheKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var oauthCode ClientOAuthInfo
|
||||||
|
err = json.Unmarshal(oauthCodeData, &oauthCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if oauthCode.Code != code {
|
||||||
|
return nil, fmt.Errorf("invalid oauth code")
|
||||||
|
}
|
||||||
|
return &oauthCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClientAuthInfo(code string) (ClientAuthInfo, error) {
|
||||||
|
redisKey := fmt.Sprintf("copilot.proxy.%s", code)
|
||||||
|
authInfoData, err := redis.Bytes(cache.Get(redisKey))
|
||||||
|
if err != nil {
|
||||||
|
return ClientAuthInfo{}, err
|
||||||
|
}
|
||||||
|
var authInfo ClientAuthInfo
|
||||||
|
err = json.Unmarshal(authInfoData, &authInfo)
|
||||||
|
return authInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenDevicesCode 生成设备代码
|
||||||
|
func GenDevicesCode(codeLen int) string {
|
||||||
|
var newUUID string
|
||||||
|
for len(newUUID) < 64 {
|
||||||
|
ud, _ := uuid.NewV4()
|
||||||
|
newUUID += strings.Replace(ud.String(), "-", "", -1)
|
||||||
|
}
|
||||||
|
return newUUID[:codeLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateClientAuthStatusByDeviceCode 更新客户端授权码通过设备代码
|
||||||
|
func UpdateClientAuthStatusByDeviceCode(deviceCode string, cardCode string, displayUserName string) error {
|
||||||
|
redisKey := fmt.Sprintf("copilot.proxy.map.%s", deviceCode)
|
||||||
|
uCode, err := cache.Get(redisKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.%s", uCode)
|
||||||
|
authInfoData, err := redis.Bytes(cache.Get(redisKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authInfo := &ClientAuthInfo{}
|
||||||
|
err = json.Unmarshal(authInfoData, &authInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authInfo.CardCode = cardCode
|
||||||
|
if displayUserName != "" {
|
||||||
|
authInfo.DisplayUserName = displayUserName
|
||||||
|
}
|
||||||
|
authInfoData, _ = json.Marshal(authInfo)
|
||||||
|
err = cache.Set(redisKey, authInfoData, -1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveClientAuthInfoByDeviceCode(deviceCode string) error {
|
||||||
|
redisKey := fmt.Sprintf("copilot.proxy.map.%s", deviceCode)
|
||||||
|
uCode, err := cache.Get(redisKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.%s", uCode)
|
||||||
|
err = cache.Del(redisKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
redisKey = fmt.Sprintf("copilot.proxy.map.%s", deviceCode)
|
||||||
|
err = cache.Del(redisKey)
|
||||||
|
return err
|
||||||
|
}
|
||||||
8
internal/cache/cacheable.go
vendored
Normal file
8
internal/cache/cacheable.go
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
type Cacheable interface {
|
||||||
|
Set(key string, value interface{}, ttl int) error
|
||||||
|
Get(key string) (interface{}, error)
|
||||||
|
Exist(key string) (bool, error)
|
||||||
|
Del(key string) error
|
||||||
|
}
|
||||||
100
internal/cache/memory.go
vendored
Normal file
100
internal/cache/memory.go
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MemoryMap 用于内存缓存
|
||||||
|
type MemoryMap struct {
|
||||||
|
cache map[string]interface{}
|
||||||
|
expirations map[string]int64
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryMap() *MemoryMap {
|
||||||
|
m := &MemoryMap{}
|
||||||
|
m.init()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 初始化 MemoryMap 的缓存
|
||||||
|
func (m *MemoryMap) init() {
|
||||||
|
m.cache = make(map[string]interface{})
|
||||||
|
m.expirations = make(map[string]int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryMap) Get(key string) (interface{}, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
expiration, exists := m.expirations[key]
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
if exists && currentTime > expiration {
|
||||||
|
// 键已过期,删除并返回 nil
|
||||||
|
fmt.Printf("Get: key=%s has expired, deleting...\n", key)
|
||||||
|
delete(m.cache, key)
|
||||||
|
delete(m.expirations, key)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := m.cache[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置缓存中的值,并指定过期时间(秒)
|
||||||
|
func (m *MemoryMap) Set(key string, value interface{}, ttl int) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.cache[key] = value
|
||||||
|
if ttl == 0 {
|
||||||
|
// 默认半小时
|
||||||
|
ttl = 30 * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if ttl == -1 {
|
||||||
|
// -1 表示永久缓存,不设置过期时间
|
||||||
|
delete(m.expirations, key)
|
||||||
|
} else {
|
||||||
|
expiration := time.Now().UnixMilli() + int64(ttl*1000)
|
||||||
|
m.expirations[key] = expiration
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryMap) Exist(key string) (bool, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
expiration, exists := m.expirations[key]
|
||||||
|
currentTime := time.Now().UnixMilli()
|
||||||
|
if exists && currentTime > expiration {
|
||||||
|
// 键已过期,删除并返回 nil
|
||||||
|
fmt.Printf("Get: key=%s has expired, deleting...\n", key)
|
||||||
|
delete(m.cache, key)
|
||||||
|
delete(m.expirations, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := m.cache[key]
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryMap) Del(key string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := m.cache[key]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
delete(m.cache, key)
|
||||||
|
delete(m.expirations, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译时检查
|
||||||
|
var _ Cacheable = (*MemoryMap)(nil)
|
||||||
25
internal/cache/operation.go
vendored
Normal file
25
internal/cache/operation.go
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
var cache Cacheable
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cache = NewMemoryMap()
|
||||||
|
|
||||||
|
// 已废弃redis缓存实现
|
||||||
|
/*host := os.Getenv("REDIS_HOST")
|
||||||
|
port := os.Getenv("REDIS_PORT")
|
||||||
|
psw := os.Getenv("REDIS_PASSWORD")
|
||||||
|
cache = NewRedisInstance(host, port, psw)*/
|
||||||
|
}
|
||||||
|
func Set(key string, value interface{}, ttl int) error {
|
||||||
|
return cache.Set(key, value, ttl)
|
||||||
|
}
|
||||||
|
func Get(key string) (interface{}, error) {
|
||||||
|
return cache.Get(key)
|
||||||
|
}
|
||||||
|
func Exist(key string) (bool, error) {
|
||||||
|
return cache.Exist(key)
|
||||||
|
}
|
||||||
|
func Del(key string) error {
|
||||||
|
return cache.Del(key)
|
||||||
|
}
|
||||||
82
internal/cache/redis.go
vendored
Normal file
82
internal/cache/redis.go
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gomodule/redigo/redis"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Psw string
|
||||||
|
Pool *redis.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedisInstance(host string, port string, psw string) *Redis {
|
||||||
|
r := &Redis{Host: host, Port: port, Psw: psw}
|
||||||
|
r.init()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Get(k string) (interface{}, error) {
|
||||||
|
return r.getConn().Do("get", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Set(k string, v interface{}, ttl int) error {
|
||||||
|
_, err := r.getConn().Do("set", k, v, "EX", ttl)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Exist(k string) (bool, error) {
|
||||||
|
return redis.Bool(r.getConn().Do("EXISTS", k))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Del(k string) error {
|
||||||
|
_, err := r.getConn().Do("del", k)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) getConn() redis.Conn {
|
||||||
|
return r.Pool.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) init() {
|
||||||
|
r.Pool = &redis.Pool{
|
||||||
|
// Maximum number of connections allocated by the pool at a given time.
|
||||||
|
// When zero, there is no limit on the number of connections in the pool.
|
||||||
|
//最大活跃连接数,0代表无限
|
||||||
|
MaxActive: 1000,
|
||||||
|
//最大闲置连接数
|
||||||
|
// Maximum number of idle connections in the pool.
|
||||||
|
MaxIdle: 50,
|
||||||
|
//闲置连接的超时时间
|
||||||
|
// Close connections after remaining idle for this duration. If the value
|
||||||
|
// is zero, then idle connections are not closed. Applications should set
|
||||||
|
// the timeout to a value less than the server's timeout.
|
||||||
|
IdleTimeout: time.Second * 100,
|
||||||
|
//定义拨号获得连接的函数
|
||||||
|
// Dial is an application supplied function for creating and configuring a
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// The connection returned from Dial must not be in a special state
|
||||||
|
// (subscribed to pubsub channel, transaction started, ...).
|
||||||
|
Dial: func() (redis.Conn, error) {
|
||||||
|
port, _ := strconv.Atoi(r.Port)
|
||||||
|
c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", r.Host, port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
password := r.Psw
|
||||||
|
if password != "" {
|
||||||
|
if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
153
internal/controller/auth/github.go
Normal file
153
internal/controller/auth/github.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/app/github_auth"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"ripper/internal/response"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postLoginDeviceCodeRequest struct {
|
||||||
|
ClientId string `json:"client_id" form:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type postLoginDeviceCodeResponse struct {
|
||||||
|
DeviceCode string `json:"device_code"` // 设备代码
|
||||||
|
UserCode string `json:"user_code"` // 用户代码
|
||||||
|
VerificationUrl string `json:"verification_uri"` // 验证地址
|
||||||
|
ExpiresIn int `json:"expires_in"` // 过期时间
|
||||||
|
Interval int `json:"interval"` // 间隔时间
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginDeviceRequestInfo struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Authorization string `json:"authorization"`
|
||||||
|
DisplayUserName string `json:"displayUserName,omitempty"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLoginDeviceCode(ctx *gin.Context) {
|
||||||
|
cli := postLoginDeviceCodeRequest{}
|
||||||
|
if err := ctx.ShouldBind(&cli); err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.ClientId == "" {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Client id is required.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, devid, err := github_auth.BindClientToCode(cli.ClientId, 1800)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, postLoginDeviceCodeResponse{
|
||||||
|
DeviceCode: devid,
|
||||||
|
UserCode: uid,
|
||||||
|
VerificationUrl: fmt.Sprintf("%s/login/device?user_code=%s", os.Getenv("DEFAULT_BASE_URL"), uid),
|
||||||
|
ExpiresIn: 1800,
|
||||||
|
Interval: 5,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLoginOauthAccessToken(ctx *gin.Context) {
|
||||||
|
v, exists := ctx.Get("client_auth_info")
|
||||||
|
if !exists {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"error": "authorization_pending",
|
||||||
|
"error_description": "The authorization request is still pending.",
|
||||||
|
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cliAuthInfo := v.(*github_auth.ClientAuthInfo)
|
||||||
|
t := time.Now()
|
||||||
|
t.Add(24 * 3 * time.Hour)
|
||||||
|
u, err := github_auth.GetClientAuthInfo(cliAuthInfo.UserCode)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"error": "access_denied",
|
||||||
|
"error_description": "You must make a new request for a device code.",
|
||||||
|
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
|
||||||
|
UserDisplayName: cliAuthInfo.DisplayUserName,
|
||||||
|
CardCode: u.CardCode,
|
||||||
|
Client: cliAuthInfo.ClientId,
|
||||||
|
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
|
||||||
|
})
|
||||||
|
_ = github_auth.RemoveClientAuthInfoByDeviceCode(cliAuthInfo.ClientId)
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_token": tk,
|
||||||
|
"scope": "",
|
||||||
|
"token_type": "bearer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLoginDevice(ctx *gin.Context) {
|
||||||
|
var info loginDeviceRequestInfo
|
||||||
|
if err := response.BindStruct(ctx, &info); err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: 422,
|
||||||
|
Msg: "请求参数错误",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 验证密码
|
||||||
|
loginPassword := os.Getenv("LOGIN_PASSWORD")
|
||||||
|
if loginPassword != "" && info.Password != loginPassword {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: 422,
|
||||||
|
Msg: "访问密码错误",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查code是否存在
|
||||||
|
authInfo, err := github_auth.GetClientAuthInfo(info.Code)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: 422,
|
||||||
|
Msg: "授权码填写错误",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = github_auth.UpdateClientAuthStatusByDeviceCode(authInfo.DeviceCode, info.Authorization, info.DisplayUserName)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: 500,
|
||||||
|
Msg: "系统异常, 请稍后再试",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.SuccessJson(ctx, "ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoginDevice(ctx *gin.Context) {
|
||||||
|
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
ctx.HTML(http.StatusOK, "code.html", gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHelpPage(ctx *gin.Context) {
|
||||||
|
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
ctx.HTML(http.StatusOK, "help.html", gin.H{})
|
||||||
|
}
|
||||||
105
internal/controller/auth/login.go
Normal file
105
internal/controller/auth/login.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ripper/internal/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientID = "Iv1.b507a08c87ecfe98"
|
||||||
|
deviceCodeURL = "https://github.com/login/device/code"
|
||||||
|
tokenURL = "https://github.com/login/oauth/access_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
type githubLoginDeviceRequest struct {
|
||||||
|
DeviceCode string `form:"device_code" json:"device_code" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDeviceCode returns the device code for GitHub login.
|
||||||
|
func getDeviceCode(c *gin.Context) {
|
||||||
|
body := map[string]string{
|
||||||
|
"client_id": clientID,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := makeRequest(c, http.MethodPost, deviceCodeURL, body)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGhuToken returns the GitHub user token.
|
||||||
|
func getGhuToken(c *gin.Context) {
|
||||||
|
var params githubLoginDeviceRequest
|
||||||
|
if err := c.ShouldBind(¶ms); err != nil {
|
||||||
|
response.FailJson(c, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid request: " + err.Error(),
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]string{
|
||||||
|
"client_id": clientID,
|
||||||
|
"device_code": params.DeviceCode,
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := makeRequest(c, http.MethodPost, tokenURL, body)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGithubLoginDevice returns the login page for GitHub.
|
||||||
|
func getGithubLoginDevice(ctx *gin.Context) {
|
||||||
|
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
ctx.HTML(http.StatusOK, "login.html", gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRequest makes a request to the given URL with the given method and body.
|
||||||
|
func makeRequest(c *gin.Context, method, url string, body map[string]string) (interface{}, error) {
|
||||||
|
jsonBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(c, method, url, bytes.NewBuffer(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("accept", "application/json")
|
||||||
|
req.Header.Set("content-type", "application/json")
|
||||||
|
req.Header.Set("editor-plugin-version", "copilot-intellij/1.5.21.6667")
|
||||||
|
req.Header.Set("copilot-language-server-version", "1.228.0")
|
||||||
|
req.Header.Set("user-agent", "GithubCopilot/1.228.0")
|
||||||
|
req.Header.Set("editor-version", "JetBrains-IU/242.21829.142")
|
||||||
|
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{Timeout: httpClientTimeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
100
internal/controller/auth/oauth.go
Normal file
100
internal/controller/auth/oauth.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/app/github_auth"
|
||||||
|
"ripper/internal/cache"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"ripper/internal/response"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getLoginOauthAuthorizeRequest struct {
|
||||||
|
ClientId string `json:"client_id" form:"client_id"`
|
||||||
|
Prompt string `json:"prompt" form:"prompt"`
|
||||||
|
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||||
|
Scope string `json:"scope" form:"scope"`
|
||||||
|
State string `json:"state" form:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoginOauthAuthorize(ctx *gin.Context) {
|
||||||
|
req := getLoginOauthAuthorizeRequest{}
|
||||||
|
err := ctx.BindQuery(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid request.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vsCopilotClientId := os.Getenv("VS_COPILOT_CLIENT_ID")
|
||||||
|
if req.ClientId != vsCopilotClientId {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthCode := github_auth.GenDevicesCode(20)
|
||||||
|
cai := github_auth.ClientOAuthInfo{
|
||||||
|
ClientId: req.ClientId,
|
||||||
|
Code: oauthCode,
|
||||||
|
Scope: req.Scope,
|
||||||
|
}
|
||||||
|
cacheKey := "oauth2_authorize_" + req.ClientId
|
||||||
|
caiInfo, _ := json.Marshal(cai)
|
||||||
|
err = cache.Set(cacheKey, caiInfo, 300)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Internal error.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the client's redirect_uri
|
||||||
|
browserSessionId := github_auth.GenDevicesCode(64)
|
||||||
|
ctx.Redirect(302, req.RedirectUri+"?browserSessionId="+browserSessionId+"&code="+oauthCode+"&state="+req.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postLoginOauthAccessTokenForVs2022(ctx *gin.Context) {
|
||||||
|
v, exists := ctx.Get("client_auth_info")
|
||||||
|
if !exists {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cliAuthInfo := v.(*github_auth.ClientOAuthInfo)
|
||||||
|
t := time.Now()
|
||||||
|
t.Add(24 * 3 * time.Hour)
|
||||||
|
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
|
||||||
|
CardCode: cliAuthInfo.Code,
|
||||||
|
Client: cliAuthInfo.ClientId,
|
||||||
|
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
|
||||||
|
})
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_token": tk,
|
||||||
|
"scope": cliAuthInfo.Scope,
|
||||||
|
"token_type": "bearer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSiteSha(ctx *gin.Context) {
|
||||||
|
ctx.Header("X-GitHub-Request-Id", "C0E1:6A1A:1A1F:2A1D:1A1F:1A1F:1A1F:1A1F")
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoginConfig(ctx *gin.Context) {
|
||||||
|
loginPassword := os.Getenv("LOGIN_PASSWORD")
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"is_login_password": loginPassword != "",
|
||||||
|
})
|
||||||
|
}
|
||||||
42
internal/controller/auth/router_register.go
Normal file
42
internal/controller/auth/router_register.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GinApi(g *gin.RouterGroup) {
|
||||||
|
g.GET("/help", getHelpPage)
|
||||||
|
// 启动设备代码登录流程
|
||||||
|
g.POST("/login/device/code", postLoginDeviceCode)
|
||||||
|
g.POST("/login/device", postLoginDevice)
|
||||||
|
g.GET("/login/device", getLoginDevice)
|
||||||
|
g.POST("/login/oauth/access_token", func(ctx *gin.Context) {
|
||||||
|
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
|
||||||
|
middleware.AuthCodeFlowCheckAuth(ctx)
|
||||||
|
} else {
|
||||||
|
middleware.DeviceCodeCheckAuth(ctx)
|
||||||
|
}
|
||||||
|
}, func(ctx *gin.Context) {
|
||||||
|
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
|
||||||
|
postLoginOauthAccessTokenForVs2022(ctx)
|
||||||
|
} else {
|
||||||
|
postLoginOauthAccessToken(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// oauth2 登录
|
||||||
|
g.GET("/login/oauth/authorize", getLoginOauthAuthorize)
|
||||||
|
|
||||||
|
// enterprise 验证
|
||||||
|
g.GET("/site/sha", getSiteSha)
|
||||||
|
|
||||||
|
// 获取登录页面配置
|
||||||
|
g.GET("/login/config", getLoginConfig)
|
||||||
|
|
||||||
|
// GitHub模拟登录获取 ghu_token
|
||||||
|
g.GET("/github/login/device/code", getGithubLoginDevice)
|
||||||
|
g.POST("/github/login/device/code", getDeviceCode)
|
||||||
|
g.POST("/github/login/ghu-token", getGhuToken)
|
||||||
|
}
|
||||||
29
internal/controller/copilot/agents.go
Normal file
29
internal/controller/copilot/agents.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAgents 获取代理列表
|
||||||
|
func GetAgents(c *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"agents": []gin.H{
|
||||||
|
{
|
||||||
|
"id": "github/copilot-workspace",
|
||||||
|
"name": "@workspace",
|
||||||
|
"description": "Ask questions and get answers about your codebase.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "github",
|
||||||
|
"model": "gpt-4o-mini-2024-07-18",
|
||||||
|
"capabilities": "workspace",
|
||||||
|
"default_model": "gpt-4o-mini-2024-07-18",
|
||||||
|
"capabilities_model": "gpt-4o-mini-2024-07-18",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
188
internal/controller/copilot/chat_completions.go
Normal file
188
internal/controller/copilot/chat_completions.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatCompletions chat对话接口
|
||||||
|
func ChatCompletions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// 添加响应头, 解决vscode校验github所属问题
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
|
if nil != err {
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiModelName := gjson.GetBytes(body, "model").String()
|
||||||
|
// 默认设置的对话模型
|
||||||
|
envModelName := os.Getenv("CHAT_API_MODEL_NAME")
|
||||||
|
// 默认设置的对话请求地址
|
||||||
|
chatAPIURL := os.Getenv("CHAT_API_BASE")
|
||||||
|
// 默认设置的对话模型key
|
||||||
|
apiKey := os.Getenv("CHAT_API_KEY")
|
||||||
|
|
||||||
|
// 轻量模型直接走代码补全接口, 节约成本
|
||||||
|
if strings.Contains(apiModelName, os.Getenv("LIGHTWEIGHT_MODEL")) {
|
||||||
|
envModelName = os.Getenv("CODEX_API_MODEL_NAME")
|
||||||
|
codexAPIURL := os.Getenv("CODEX_API_BASE")
|
||||||
|
parsedURL, err := url.Parse(codexAPIURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("URL解析错误:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chatAPIURL = "https://" + parsedURL.Hostname() + "/v1/chat/completions"
|
||||||
|
apiKey = os.Getenv("CODEX_API_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
|
||||||
|
body, _ = sjson.SetBytes(body, "model", envModelName)
|
||||||
|
body, _ = sjson.SetBytes(body, "stream", true) // 强制流式输出
|
||||||
|
|
||||||
|
if !gjson.GetBytes(body, "function_call").Exists() {
|
||||||
|
messages := gjson.GetBytes(body, "messages").Array()
|
||||||
|
for i, msg := range messages {
|
||||||
|
toolCalls := msg.Get("tool_calls").Array()
|
||||||
|
if len(toolCalls) == 0 {
|
||||||
|
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("messages.%d.tool_calls", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex := len(messages) - 1
|
||||||
|
chatLocale := os.Getenv("CHAT_LOCALE")
|
||||||
|
if chatLocale != "" && !strings.Contains(messages[lastIndex].Get("content").String(), "Respond in the following locale") {
|
||||||
|
body, _ = sjson.SetBytes(body, "messages."+strconv.Itoa(lastIndex)+".content", messages[lastIndex].Get("content").String()+"Respond in the following locale: "+chatLocale+".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ = sjson.DeleteBytes(body, "intent")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "intent_threshold")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "intent_content")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "logprobs") // #IBZYCA
|
||||||
|
|
||||||
|
// 是否支持使用工具, 避免模型不支持相关功能报错
|
||||||
|
chatUseTools, _ := strconv.ParseBool(os.Getenv("CHAT_USE_TOOLS"))
|
||||||
|
if !chatUseTools {
|
||||||
|
body, _ = sjson.DeleteBytes(body, "tools")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "tool_call")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "functions")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "function_call")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "tool_choice")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMaxTokens, _ := strconv.Atoi(os.Getenv("CHAT_MAX_TOKENS"))
|
||||||
|
if int(gjson.GetBytes(body, "max_tokens").Int()) > ChatMaxTokens {
|
||||||
|
body, _ = sjson.SetBytes(body, "max_tokens", ChatMaxTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gjson.GetBytes(body, "n").Int() > 1 {
|
||||||
|
body, _ = sjson.SetBytes(body, "n", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := gjson.GetBytes(body, "messages").Array()
|
||||||
|
userAgent := c.GetHeader("User-Agent")
|
||||||
|
|
||||||
|
// 拦截处理vscode对话首次预处理请求, 减少等待时间
|
||||||
|
firstRole := gjson.GetBytes(body, "messages.0.role").String()
|
||||||
|
firstContent := gjson.GetBytes(body, "messages.0.content").String()
|
||||||
|
if strings.Contains(firstRole, "system") && strings.Contains(firstContent, "You are a helpful AI programming assistant to a user") &&
|
||||||
|
!strings.Contains(firstContent, "If you cannot choose just one category, or if none of the categories seem like they would provide the user with a better result, you must always respond with") &&
|
||||||
|
!gjson.GetBytes(body, "tool_choice").Exists() {
|
||||||
|
_, _ = c.Writer.WriteString("data: [DONE]\n\n")
|
||||||
|
c.Writer.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// vs2022客户端的兼容处理
|
||||||
|
if strings.Contains(userAgent, "VSCopilotClient") {
|
||||||
|
lastMessage := messages[len(messages)-1]
|
||||||
|
messageRole := lastMessage.Get("role").String()
|
||||||
|
messageContent := lastMessage.Get("content").String()
|
||||||
|
if strings.Contains(firstRole, "system") && strings.Contains(firstContent, "You are an AI programming assistant") {
|
||||||
|
vs2022FirstChatTemplate(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if messageRole == "user" && messageContent == "Write a short one-sentence question that I can ask that naturally follows from the previous few questions and answers. It should not ask a question which is already answered in the conversation. It should be a question that you are capable of answering. Reply with only the text of the question and nothing else." {
|
||||||
|
_, _ = c.Writer.WriteString("data: [DONE]\n\n")
|
||||||
|
c.Writer.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, chatAPIURL, io.NopCloser(bytes.NewBuffer(body)))
|
||||||
|
if nil != err {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
c.AbortWithStatus(http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("request conversation failed:", err.Error())
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("request completions failed:", string(body))
|
||||||
|
|
||||||
|
resp.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// vs2022FirstChatTemplate is a template for the first chat completion response
|
||||||
|
func vs2022FirstChatTemplate(c *gin.Context) {
|
||||||
|
fixedOutput := `data: {"id":"f6202f6f-9d13-4518-b34f-65e945b0a1a2","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"b2ab39cb-9a84-4006-b470-93a5965c6d69","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"df5f9ce7-b653-4ffb-8d92-e21856ce1ffc","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":"Explain"},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"fb58d66e-bb16-43f2-8470-2de0c8662533","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||||||
|
|
||||||
|
data: {"id":"22ea16e2-766f-4b10-84d0-68399abc9181","object":"chat.completion.chunk","model":"gpt-4o-mini-2024-07-18","created":1734752124,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop"}]}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
|
||||||
|
`
|
||||||
|
_, _ = c.Writer.WriteString(fixedOutput)
|
||||||
|
c.Writer.Flush()
|
||||||
|
}
|
||||||
273
internal/controller/copilot/chunks.go
Normal file
273
internal/controller/copilot/chunks.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 常量定义
|
||||||
|
const (
|
||||||
|
defaultDimensionSize = 1536 // 默认向量维度
|
||||||
|
markdownFilePrefix = "File: `%s`\n```shell\n"
|
||||||
|
markdownFileSuffix = "```"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取向量维度大小
|
||||||
|
func getDimensionSize() int {
|
||||||
|
dimensionSize := defaultDimensionSize
|
||||||
|
if dimSizeStr := os.Getenv("EMBEDDING_DIMENSION_SIZE"); dimSizeStr != "" {
|
||||||
|
if dimSize, err := strconv.Atoi(dimSizeStr); err == nil {
|
||||||
|
dimensionSize = dimSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dimensionSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算块大小
|
||||||
|
func getChunkSize() int {
|
||||||
|
// 根据维度大小调整块大小,这里设置为维度的1.5倍左右
|
||||||
|
return getDimensionSize() * 3 / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkRequest 表示分块请求
|
||||||
|
type ChunkRequest struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Path string `json:"path" binding:"required"`
|
||||||
|
Embed bool `json:"embed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk 表示内容块
|
||||||
|
type Chunk struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Range Range `json:"range"`
|
||||||
|
Embedding Embedding `json:"embedding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range 表示文本范围
|
||||||
|
type Range struct {
|
||||||
|
Start int `json:"start"`
|
||||||
|
End int `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedding 表示向量嵌入
|
||||||
|
type Embedding struct {
|
||||||
|
Embedding []float32 `json:"embedding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkResponse 表示分块响应
|
||||||
|
type ChunkResponse struct {
|
||||||
|
Chunks []Chunk `json:"chunks"`
|
||||||
|
EmbeddingModel string `json:"embedding_model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkService 处理文本分块和嵌入的服务
|
||||||
|
type ChunkService struct {
|
||||||
|
embeddingClient *EmbeddingClient
|
||||||
|
modelName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunkService 创建新的分块服务
|
||||||
|
func NewChunkService() (*ChunkService, error) {
|
||||||
|
client, err := NewEmbeddingClient(getDimensionSize())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create embedding client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ChunkService{
|
||||||
|
embeddingClient: client,
|
||||||
|
modelName: os.Getenv("EMBEDDING_API_MODEL_NAME"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleChunks 处理分块请求的HTTP处理器
|
||||||
|
func HandleChunks(c *gin.Context) {
|
||||||
|
var req ChunkRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := NewChunkService()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to initialize service: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks := service.SplitIntoChunks(req.Content, req.Path)
|
||||||
|
|
||||||
|
if req.Embed {
|
||||||
|
if err := service.GenerateEmbeddings(c.Request.Context(), chunks); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to generate embeddings: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := ChunkResponse{
|
||||||
|
Chunks: chunks,
|
||||||
|
EmbeddingModel: service.modelName,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitIntoChunks 将内容分割成块
|
||||||
|
func (s *ChunkService) SplitIntoChunks(content, path string) []Chunk {
|
||||||
|
var chunks []Chunk
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
chunkSize := getChunkSize()
|
||||||
|
|
||||||
|
// 预分配切片容量,减少内存重新分配
|
||||||
|
estimatedChunks := len(content)/chunkSize + 1
|
||||||
|
chunks = make([]Chunk, 0, estimatedChunks)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
start := 0
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
lineWithNewline := line + "\n"
|
||||||
|
|
||||||
|
// 如果当前块加上新行会超过chunkSize,并且当前块不为空
|
||||||
|
if sb.Len()+len(lineWithNewline) > chunkSize && sb.Len() > 0 {
|
||||||
|
// 创建新的chunk
|
||||||
|
chunkText := sb.String()
|
||||||
|
chunk := s.createChunk(chunkText, path, start, start+len(chunkText))
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
|
||||||
|
start += len(chunkText)
|
||||||
|
sb.Reset()
|
||||||
|
sb.WriteString(lineWithNewline)
|
||||||
|
} else {
|
||||||
|
sb.WriteString(lineWithNewline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一个chunk
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
chunkText := sb.String()
|
||||||
|
chunk := s.createChunk(chunkText, path, start, start+len(chunkText))
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChunk 创建一个新的内容块
|
||||||
|
func (s *ChunkService) createChunk(text, path string, start, end int) Chunk {
|
||||||
|
// 计算文本的SHA-256哈希
|
||||||
|
hash := sha256.Sum256([]byte(text))
|
||||||
|
|
||||||
|
return Chunk{
|
||||||
|
Hash: fmt.Sprintf("%x", hash),
|
||||||
|
Text: fmt.Sprintf(markdownFilePrefix+"%s"+markdownFileSuffix, path, text),
|
||||||
|
Range: Range{
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
},
|
||||||
|
Embedding: Embedding{
|
||||||
|
Embedding: make([]float32, 0), // 初始化为空切片
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateEmbeddings 为所有块生成嵌入向量
|
||||||
|
func (s *ChunkService) GenerateEmbeddings(ctx context.Context, chunks []Chunk) error {
|
||||||
|
if len(chunks) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于少量块,直接串行处理
|
||||||
|
if len(chunks) <= 5 {
|
||||||
|
return s.generateEmbeddingsSerial(ctx, chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于大量块,使用并行处理
|
||||||
|
return s.generateEmbeddingsParallel(ctx, chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateEmbeddingsSerial 串行生成嵌入向量
|
||||||
|
func (s *ChunkService) generateEmbeddingsSerial(ctx context.Context, chunks []Chunk) error {
|
||||||
|
for i := range chunks {
|
||||||
|
text := s.extractPlainText(chunks[i].Text)
|
||||||
|
|
||||||
|
embedding, err := s.embeddingClient.GetEmbedding(ctx, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate embedding for chunk %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks[i].Embedding.Embedding = embedding
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateEmbeddingsParallel 并行生成嵌入向量
|
||||||
|
func (s *ChunkService) generateEmbeddingsParallel(ctx context.Context, chunks []Chunk) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errChan := make(chan error, len(chunks))
|
||||||
|
|
||||||
|
// 限制并发数量,避免过多的并发请求
|
||||||
|
semaphore := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
for i := range chunks {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// 获取信号量
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
|
||||||
|
text := s.extractPlainText(chunks[idx].Text)
|
||||||
|
|
||||||
|
embedding, err := s.embeddingClient.GetEmbedding(ctx, text)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("failed to generate embedding for chunk %d: %w", idx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks[idx].Embedding.Embedding = embedding
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有goroutine完成
|
||||||
|
wg.Wait()
|
||||||
|
close(errChan)
|
||||||
|
|
||||||
|
// 检查是否有错误
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// 没有错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPlainText 从markdown格式的文本中提取纯文本
|
||||||
|
func (s *ChunkService) extractPlainText(text string) string {
|
||||||
|
// 移除第一行 File: 标记
|
||||||
|
if idx := strings.Index(text, "\n"); idx != -1 {
|
||||||
|
text = text[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 ```shell 和结尾的 ```
|
||||||
|
text = strings.TrimPrefix(text, "```shell\n")
|
||||||
|
text = strings.TrimSuffix(text, "```")
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
373
internal/controller/copilot/code_completions.go
Normal file
373
internal/controller/copilot/code_completions.go
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CodeCompletions 代码补全
|
||||||
|
func CodeCompletions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
debounceTime, _ := strconv.Atoi(os.Getenv("COPILOT_DEBOUNCE"))
|
||||||
|
time.Sleep(time.Duration(debounceTime) * time.Millisecond)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
codexServiceType := os.Getenv("CODEX_SERVICE_TYPE")
|
||||||
|
body = ConstructRequestBody(body, codexServiceType)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, os.Getenv("CODEX_API_BASE"), io.NopCloser(bytes.NewBuffer(body)))
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
apiKeys := strings.Split(os.Getenv("CODEX_API_KEY"), ",")
|
||||||
|
|
||||||
|
// 检查 apiKeys 是否有效
|
||||||
|
if len(apiKeys) == 0 || (len(apiKeys) == 1 && apiKeys[0] == "") {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
randGen := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
selectedKey := strings.TrimSpace(apiKeys[randGen.Intn(len(apiKeys))])
|
||||||
|
|
||||||
|
if selectedKey == "" {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+selectedKey)
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("request completions failed:", err.Error())
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("request completions failed:", string(body))
|
||||||
|
|
||||||
|
abortCodex(c, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
// 处理 Ollama 服务的流式响应
|
||||||
|
if codexServiceType == "ollama" {
|
||||||
|
reader := bufio.NewReader(resp.Body)
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// json解析 line
|
||||||
|
lineJson := gjson.Parse(line)
|
||||||
|
uuid := uuid.Must(uuid.NewV4()).String()
|
||||||
|
done := lineJson.Get("done").Bool()
|
||||||
|
doneReason := lineJson.Get("done_reason").Str
|
||||||
|
response := lineJson.Get("response").Str
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
choice := map[string]interface{}{
|
||||||
|
"text": response,
|
||||||
|
"index": 0,
|
||||||
|
"logprobs": nil,
|
||||||
|
"finish_reason": doneReason,
|
||||||
|
}
|
||||||
|
choices := []map[string]interface{}{choice}
|
||||||
|
constructLineData := map[string]interface{}{
|
||||||
|
"id": uuid,
|
||||||
|
"choices": choices,
|
||||||
|
"created": timestamp,
|
||||||
|
"model": lineJson.Get("model").Str,
|
||||||
|
"system_fingerprint": "fp_1c141eb703",
|
||||||
|
"object": "text_completion",
|
||||||
|
}
|
||||||
|
|
||||||
|
if done && strings.Contains(doneReason, "stop") {
|
||||||
|
usage := map[string]interface{}{
|
||||||
|
"prompt_tokens": lineJson.Get("prompt_eval_count").Int(),
|
||||||
|
"completion_tokens": lineJson.Get("eval_count").Int(),
|
||||||
|
"total_tokens": lineJson.Get("prompt_eval_count").Int(),
|
||||||
|
"prompt_cache_hit_tokens": lineJson.Get("prompt_eval_count").Int(),
|
||||||
|
"prompt_cache_miss_tokens": lineJson.Get("eval_count").Int(),
|
||||||
|
}
|
||||||
|
constructLineData["usage"] = usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将修改后的数据重新编码为 JSON
|
||||||
|
modifiedJSON, err := json.Marshal(constructLineData)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送修改后的数据
|
||||||
|
_, _ = c.Writer.WriteString("data: " + string(modifiedJSON) + "\n\n")
|
||||||
|
c.Writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = c.Writer.WriteString("data: [DONE]\n\n")
|
||||||
|
c.Writer.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理默认服务的响应
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConstructRequestBody 重新构建请求体
|
||||||
|
func ConstructRequestBody(body []byte, codexServiceType string) []byte {
|
||||||
|
envCodexModel := os.Getenv("CODEX_API_MODEL_NAME")
|
||||||
|
body, _ = sjson.SetBytes(body, "model", envCodexModel)
|
||||||
|
body, _ = sjson.SetBytes(body, "stream", true) // 强制流式输出
|
||||||
|
body, _ = sjson.DeleteBytes(body, "extra")
|
||||||
|
body, _ = sjson.DeleteBytes(body, "nwo")
|
||||||
|
|
||||||
|
// 限制 prompt 和 suffix 的长度
|
||||||
|
body = applyPromptLengthLimit(body)
|
||||||
|
|
||||||
|
temperature, _ := strconv.ParseFloat(os.Getenv("CODEX_TEMPERATURE"), 64)
|
||||||
|
if temperature != -1 {
|
||||||
|
body, _ = sjson.SetBytes(body, "temperature", temperature)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeMaxTokens, _ := strconv.Atoi(os.Getenv("CODEX_MAX_TOKENS"))
|
||||||
|
if int(gjson.GetBytes(body, "max_tokens").Int()) > codeMaxTokens {
|
||||||
|
body, _ = sjson.SetBytes(body, "max_tokens", codeMaxTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gjson.GetBytes(body, "n").Int() > 1 {
|
||||||
|
body, _ = sjson.SetBytes(body, "n", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ollama.com/library/stable-code || https://ollama.com/library/codegemma
|
||||||
|
if strings.Contains(envCodexModel, "stable-code") || strings.Contains(envCodexModel, "codegemma") {
|
||||||
|
return constructWithStableCodeModel(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ollama.com/library/codellama
|
||||||
|
if strings.Contains(envCodexModel, "codellama") {
|
||||||
|
return constructWithCodeLlamaModel(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://help.aliyun.com/zh/model-studio/user-guide/qwen-coder?spm=a2c4g.11186623.0.0.a5234823I6LvAG
|
||||||
|
if strings.Contains(envCodexModel, "qwen-coder-turbo") {
|
||||||
|
return constructWithQwenCoderTurboModel(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持 Ollama FIM 的模型, 如:https://ollama.com/library/deepseek-coder-v2
|
||||||
|
if codexServiceType == "ollama" {
|
||||||
|
return constructWithOllamaModel(body, codeMaxTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyPromptLengthLimit 对 prompt 和 suffix 应用长度限制
|
||||||
|
func applyPromptLengthLimit(body []byte) []byte {
|
||||||
|
envLimitPrompt := os.Getenv("CODEX_LIMIT_PROMPT")
|
||||||
|
limitPrompt, err := strconv.Atoi(envLimitPrompt)
|
||||||
|
if err != nil || limitPrompt <= 0 {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
body = limitPromptLength(body, limitPrompt)
|
||||||
|
body = limitSuffixLength(body, limitPrompt)
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitPromptLength 限制 prompt 长度
|
||||||
|
func limitPromptLength(body []byte, limitRows int) []byte {
|
||||||
|
prompt := gjson.GetBytes(body, "prompt")
|
||||||
|
if !prompt.Exists() {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := strings.Split(prompt.Str, "\n")
|
||||||
|
if len(rows) <= limitRows {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留后面的内容
|
||||||
|
newPrompt := strings.Join(rows[len(rows)-limitRows:], "\n")
|
||||||
|
body, _ = sjson.SetBytes(body, "prompt", newPrompt)
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitSuffixLength 限制 suffix 长度
|
||||||
|
func limitSuffixLength(body []byte, limitRows int) []byte {
|
||||||
|
suffix := gjson.GetBytes(body, "suffix")
|
||||||
|
if !suffix.Exists() {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := strings.Split(suffix.Str, "\n")
|
||||||
|
if len(rows) <= limitRows {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留前面的内容
|
||||||
|
newSuffix := strings.Join(rows[:limitRows], "\n")
|
||||||
|
body, _ = sjson.SetBytes(body, "suffix", newSuffix)
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWithCodeLlamaModel 重写codeLlama模型要求的请求体
|
||||||
|
func constructWithCodeLlamaModel(body []byte) []byte {
|
||||||
|
suffix := gjson.GetBytes(body, "suffix")
|
||||||
|
prompt := gjson.GetBytes(body, "prompt")
|
||||||
|
content := fmt.Sprintf("<PRE> %s <SUF> %s <MID>", prompt, suffix)
|
||||||
|
|
||||||
|
return constructWithChatModel(body, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWithStableCodeModel 重写StableCode模型要求的请求体
|
||||||
|
func constructWithStableCodeModel(body []byte) []byte {
|
||||||
|
suffix := gjson.GetBytes(body, "suffix")
|
||||||
|
prompt := gjson.GetBytes(body, "prompt")
|
||||||
|
content := fmt.Sprintf("<fim_prefix>%s<fim_suffix>%s<fim_middle>", prompt, suffix)
|
||||||
|
|
||||||
|
return constructWithChatModel(body, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWithChatModel 重写Chat请求体
|
||||||
|
func constructWithChatModel(body []byte, content string) []byte {
|
||||||
|
// 创建新的 JSON 对象并添加到 body 中
|
||||||
|
messages := []map[string]string{
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ = sjson.SetBytes(body, "messages", messages)
|
||||||
|
|
||||||
|
jsonStr := string(body)
|
||||||
|
jsonStr = strings.ReplaceAll(jsonStr, "\\u003c", "<")
|
||||||
|
jsonStr = strings.ReplaceAll(jsonStr, "\\u003e", ">")
|
||||||
|
return []byte(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWithQwenCoderTurboModel 重写QwenCoderTurbo模型要求的请求体
|
||||||
|
func constructWithQwenCoderTurboModel(body []byte) []byte {
|
||||||
|
if gjson.GetBytes(body, "n").Int() > 1 {
|
||||||
|
body, _ = sjson.SetBytes(body, "n", 1)
|
||||||
|
}
|
||||||
|
suffix := gjson.GetBytes(body, "suffix")
|
||||||
|
prompt := gjson.GetBytes(body, "prompt")
|
||||||
|
codeLanguage := gjson.GetBytes(body, "extra.language")
|
||||||
|
|
||||||
|
messages := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are an expert in " + codeLanguage.Str + " programming, highly skilled at understanding and continuing to write code.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Combined with subsequent code snippets, help me complete the code:\n\n" +
|
||||||
|
"Code subsequent content:\n```" + codeLanguage.Str + "\n" + suffix.Str + "```\n\n" +
|
||||||
|
"Remember:\n" +
|
||||||
|
"- Do not generate content outside of the code.\n" +
|
||||||
|
"- Do not directly fill in all the code content, the maximum number of lines of code should not exceed 5 lines.\n" +
|
||||||
|
"- Answer must refer to the code suffix content, do not exceed the boundary, otherwise repeated code will occur.\n" +
|
||||||
|
"- If you don't know how to answer, just reply with an empty string.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": prompt.Str,
|
||||||
|
"partial": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ = sjson.SetBytes(body, "messages", messages)
|
||||||
|
body, _ = sjson.DeleteBytes(body, "prompt")
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructWithOllamaModel 重写Ollama模型要求的请求体
|
||||||
|
func constructWithOllamaModel(body []byte, codeMaxTokens int) []byte {
|
||||||
|
body, _ = sjson.SetBytes(body, "options.temperature", 0)
|
||||||
|
// stop参数处理
|
||||||
|
stopArray := gjson.GetBytes(body, "stop").Array()
|
||||||
|
stopSlice := make([]interface{}, len(stopArray))
|
||||||
|
for i, v := range stopArray {
|
||||||
|
stopSlice[i] = v.String()
|
||||||
|
}
|
||||||
|
body, _ = sjson.SetBytes(body, "options.stop", stopSlice)
|
||||||
|
body, _ = sjson.SetBytes(body, "stream", true)
|
||||||
|
|
||||||
|
maxTokens := gjson.GetBytes(body, "max_tokens").Int()
|
||||||
|
if int(maxTokens) > codeMaxTokens {
|
||||||
|
body, _ = sjson.SetBytes(body, "options.num_predict", codeMaxTokens)
|
||||||
|
} else {
|
||||||
|
body, _ = sjson.SetBytes(body, "options.num_predict", maxTokens)
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// abortCodex 中断请求
|
||||||
|
func abortCodex(c *gin.Context, status int) {
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
c.String(status, "data: [DONE]\n\n")
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
167
internal/controller/copilot/embedding_client.go
Normal file
167
internal/controller/copilot/embedding_client.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 常量定义
|
||||||
|
const (
|
||||||
|
defaultTimeout = 30 * time.Second
|
||||||
|
contentTypeJSON = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmbeddingRequest 表示向嵌入API发送的请求
|
||||||
|
type EmbeddingRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Input []string `json:"input"`
|
||||||
|
Dimensions int `json:"dimensions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddingResponse 表示从嵌入API接收的响应
|
||||||
|
type EmbeddingResponse struct {
|
||||||
|
Data []EmbeddingData `json:"data"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Usage Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddingData 表示单个嵌入数据
|
||||||
|
type EmbeddingData struct {
|
||||||
|
Embedding []float32 `json:"embedding"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage 表示API使用情况
|
||||||
|
type Usage struct {
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除未使用的类型
|
||||||
|
// Parameters 和 EmbeddingsRequest, EmbeddingsResponse 已被移除
|
||||||
|
|
||||||
|
// EmbeddingClient 封装了与嵌入API交互的功能
|
||||||
|
type EmbeddingClient struct {
|
||||||
|
apiURL string
|
||||||
|
apiKey string
|
||||||
|
model string
|
||||||
|
dimensions int
|
||||||
|
httpClient *http.Client
|
||||||
|
clientMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmbeddingClient 创建一个新的嵌入客户端
|
||||||
|
func NewEmbeddingClient(dimensions int) (*EmbeddingClient, error) {
|
||||||
|
apiURL := os.Getenv("EMBEDDING_API_BASE")
|
||||||
|
apiKey := os.Getenv("EMBEDDING_API_KEY")
|
||||||
|
|
||||||
|
if apiURL == "" || apiKey == "" {
|
||||||
|
return nil, fmt.Errorf("EMBEDDING_API_BASE or EMBEDDING_API_KEY environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("EMBEDDING_API_MODEL_NAME") == "" {
|
||||||
|
return nil, fmt.Errorf("EMBEDDING_API_MODEL_NAME environment variable not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析超时时间,如果未设置或解析失败则使用默认值
|
||||||
|
timeout := defaultTimeout
|
||||||
|
if timeoutStr := os.Getenv("HTTP_CLIENT_TIMEOUT"); timeoutStr != "" {
|
||||||
|
if parsedTimeout, err := time.ParseDuration(timeoutStr + "s"); err == nil {
|
||||||
|
timeout = parsedTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EmbeddingClient{
|
||||||
|
apiURL: apiURL,
|
||||||
|
apiKey: apiKey,
|
||||||
|
model: os.Getenv("EMBEDDING_API_MODEL_NAME"),
|
||||||
|
dimensions: dimensions,
|
||||||
|
httpClient: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetModel 设置嵌入模型
|
||||||
|
func (c *EmbeddingClient) SetModel(model string) {
|
||||||
|
c.clientMutex.Lock()
|
||||||
|
defer c.clientMutex.Unlock()
|
||||||
|
c.model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmbedding 获取单个文本的嵌入
|
||||||
|
func (c *EmbeddingClient) GetEmbedding(ctx context.Context, text string) ([]float32, error) {
|
||||||
|
resp, err := c.GetEmbeddings(ctx, []string{text})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Data) == 0 {
|
||||||
|
return nil, fmt.Errorf("no embeddings returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Data[0].Embedding, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmbeddings 批量获取多个文本的嵌入
|
||||||
|
func (c *EmbeddingClient) GetEmbeddings(ctx context.Context, texts []string) (*EmbeddingResponse, error) {
|
||||||
|
c.clientMutex.RLock()
|
||||||
|
dimensions := c.dimensions
|
||||||
|
c.clientMutex.RUnlock()
|
||||||
|
|
||||||
|
reqBody := EmbeddingRequest{
|
||||||
|
Model: os.Getenv("EMBEDDING_API_MODEL_NAME"),
|
||||||
|
Input: texts,
|
||||||
|
Dimensions: dimensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", contentTypeJSON)
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddingResp EmbeddingResponse
|
||||||
|
if err := json.Unmarshal(body, &embeddingResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &embeddingResp, nil
|
||||||
|
}
|
||||||
58
internal/controller/copilot/embeddings.go
Normal file
58
internal/controller/copilot/embeddings.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmbeddingsAPIRequest 表示嵌入API的请求结构
|
||||||
|
type EmbeddingsAPIRequest struct {
|
||||||
|
Input []string `json:"input" binding:"required"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Dimensions int `json:"dimensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEmbeddings 处理嵌入请求的HTTP处理器
|
||||||
|
func HandleEmbeddings(c *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
var req EmbeddingsAPIRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从环境变量获取维度大小,默认为1536
|
||||||
|
dimensionSize := 1536
|
||||||
|
if dimSizeStr := os.Getenv("EMBEDDING_DIMENSION_SIZE"); dimSizeStr != "" {
|
||||||
|
if dimSize, err := strconv.Atoi(dimSizeStr); err == nil {
|
||||||
|
dimensionSize = dimSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建嵌入客户端
|
||||||
|
client, err := NewEmbeddingClient(dimensionSize)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果请求中指定了模型,则使用请求中的模型
|
||||||
|
if req.Model != "" {
|
||||||
|
client.SetModel(req.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取嵌入,使用请求上下文以支持取消操作
|
||||||
|
resp, err := client.GetEmbeddings(c.Request.Context(), req.Input)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
147
internal/controller/copilot/get_copilot_internal_v2_token.go
Normal file
147
internal/controller/copilot/get_copilot_internal_v2_token.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/app/github_auth"
|
||||||
|
"ripper/internal/cache"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDisguiseCopilotInternalV2Token 返回伪装的token
|
||||||
|
func GetDisguiseCopilotInternalV2Token(ctx *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
ctx.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
trackingId, _ := uuid.NewV4()
|
||||||
|
now := time.Now().Unix()
|
||||||
|
dcAt, _ := strconv.Atoi(os.Getenv("DISGUISE_COPILOT_TOKEN_EXPIRES_AT"))
|
||||||
|
expiresAt := now + int64(dcAt)
|
||||||
|
sku := "copilot_for_business_seat"
|
||||||
|
|
||||||
|
copilotToken := github_auth.JsonMap2SignToken(map[string]interface{}{
|
||||||
|
"tid": trackingId,
|
||||||
|
"exp": expiresAt,
|
||||||
|
"sku": sku,
|
||||||
|
"st": "dotcom",
|
||||||
|
"chat": 1,
|
||||||
|
"u": "github",
|
||||||
|
})
|
||||||
|
|
||||||
|
endpoints := make(map[string]interface{})
|
||||||
|
endpoints["api"] = os.Getenv("API_BASE_URL")
|
||||||
|
endpoints["origin-tracker"] = "https://origin-tracker.individual.githubcopilot.com"
|
||||||
|
endpoints["proxy"] = os.Getenv("PROXY_BASE_URL")
|
||||||
|
endpoints["telemetry"] = os.Getenv("TELEMETRY_BASE_URL")
|
||||||
|
|
||||||
|
gout := gin.H{
|
||||||
|
"annotations_enabled": true,
|
||||||
|
"chat_enabled": true,
|
||||||
|
"chat_jetbrains_enabled": true,
|
||||||
|
"code_quote_enabled": true,
|
||||||
|
"code_review_enabled": false,
|
||||||
|
"codesearch": true,
|
||||||
|
"copilot_ide_agent_chat_gpt4_small_prompt": false,
|
||||||
|
"copilotignore_enabled": false,
|
||||||
|
"endpoints": endpoints,
|
||||||
|
"expires_at": expiresAt,
|
||||||
|
"individual": true,
|
||||||
|
"nes_enabled": false,
|
||||||
|
"prompt_8k": true,
|
||||||
|
"public_suggestions": "disabled",
|
||||||
|
"refresh_in": 1500,
|
||||||
|
"sku": sku,
|
||||||
|
"snippy_load_test_enabled": false,
|
||||||
|
"telemetry": "disabled",
|
||||||
|
"token": copilotToken,
|
||||||
|
"tracking_id": trackingId,
|
||||||
|
"intellij_editor_fetcher": false,
|
||||||
|
"vsc_electron_fetcher": false,
|
||||||
|
"vs_editor_fetcher": false,
|
||||||
|
"vsc_panel_v2": false,
|
||||||
|
"xcode": true,
|
||||||
|
"xcode_chat": true,
|
||||||
|
"limited_user_quotas": nil,
|
||||||
|
"limited_user_reset_date": nil,
|
||||||
|
"vsc_electron_fetcher_v2": false,
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, gout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopilotInternalV2Token 获取github copilot官方token
|
||||||
|
func GetCopilotInternalV2Token(c *gin.Context) {
|
||||||
|
ghuTokens := strings.Split(os.Getenv("COPILOT_GHU_TOKEN"), ",")
|
||||||
|
if len(ghuTokens) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
ghu := ghuTokens[rand.Intn(len(ghuTokens))]
|
||||||
|
if ghu == "" {
|
||||||
|
log.Println("ghu token is empty")
|
||||||
|
c.JSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
"message": "ghu token is empty",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := "copilot_internal_v2_token"
|
||||||
|
token, err := cache.Get(cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||||
|
cache.Del(cacheKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil {
|
||||||
|
c.JSON(http.StatusOK, token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://api.github.com/copilot_internal/v2/token"
|
||||||
|
req, err := http.NewRequestWithContext(c, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("authorization", "token "+ghu)
|
||||||
|
req.Header.Set("editor-plugin-version", "copilot-intellij/1.5.21.6667")
|
||||||
|
req.Header.Set("editor-version", "JetBrains-IU/242.21829.142")
|
||||||
|
req.Header.Set("user-agent", "GithubCopilot/1.228.0")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
c.JSON(resp.StatusCode, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
errorMsg := "获取 Token 失败, 当前 ghu_token 账户可能并未订阅 github copilot 服务!" + ghu
|
||||||
|
c.JSON(resp.StatusCode, gin.H{"error": errorMsg})
|
||||||
|
log.Println(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Set(cacheKey, result, 1500)
|
||||||
|
c.JSON(resp.StatusCode, result)
|
||||||
|
}
|
||||||
373
internal/controller/copilot/github_completions.go
Normal file
373
internal/controller/copilot/github_completions.go
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/cache"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CodexCompletions 全代理GitHub的代码补全接口
|
||||||
|
func CodexCompletions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
urlModelName := c.Param("model-name")
|
||||||
|
debounceTime, _ := strconv.Atoi(os.Getenv("COPILOT_DEBOUNCE"))
|
||||||
|
time.Sleep(time.Duration(debounceTime) * time.Millisecond)
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copilotAccountType := os.Getenv("COPILOT_ACCOUNT_TYPE")
|
||||||
|
url := "https://proxy." + copilotAccountType + ".githubcopilot.com/v1/engines/" + urlModelName + "/completions"
|
||||||
|
req, err := http.NewRequestWithContext(c, "POST", url, bytes.NewBuffer(body))
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求头
|
||||||
|
if err := mergeHeaders(c.Request.Header, req); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("request completions failed:", err.Error())
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("请求GitHub官方补全接口失败:", string(body))
|
||||||
|
|
||||||
|
abortCodex(c, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
c.Header("Content-Type", resp.Header.Get("Content-Type"))
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatsCompletions 全代理GitHub的聊天补全接口
|
||||||
|
func ChatsCompletions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copilotAccountType := os.Getenv("COPILOT_ACCOUNT_TYPE")
|
||||||
|
url := "https://api." + copilotAccountType + ".githubcopilot.com/chat/completions"
|
||||||
|
req, err := http.NewRequestWithContext(c, "POST", url, bytes.NewBuffer(body))
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求头
|
||||||
|
if err := mergeHeaders(c.Request.Header, req); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("request completions failed:", err.Error())
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("请求GitHub官方对话接口失败:", string(body))
|
||||||
|
|
||||||
|
abortCodex(c, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
c.Header("Content-Type", resp.Header.Get("Content-Type"))
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatEditCompletions 聊天编辑补全接口
|
||||||
|
func ChatEditCompletions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copilotAccountType := os.Getenv("COPILOT_ACCOUNT_TYPE")
|
||||||
|
url := "https://proxy." + copilotAccountType + ".githubcopilot.com/v1/engines/copilot-centralus-h100/speculation"
|
||||||
|
req, err := http.NewRequestWithContext(c, "POST", url, bytes.NewBuffer(body))
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求头
|
||||||
|
if err := mergeHeaders(c.Request.Header, req); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("request failed:", err.Error())
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("请求 Chat 编辑接口失败:", string(body))
|
||||||
|
|
||||||
|
abortCodex(c, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
c.Header("Content-Type", resp.Header.Get("Content-Type"))
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthToken 获取GitHub Copilot的临时Token
|
||||||
|
func getAuthToken() (string, error) {
|
||||||
|
ghuTokens := strings.Split(os.Getenv("COPILOT_GHU_TOKEN"), ",")
|
||||||
|
if len(ghuTokens) == 0 {
|
||||||
|
return "", fmt.Errorf("COPILOT_GHU_TOKEN environment variable is empty or malformed")
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
ghu := ghuTokens[rand.Intn(len(ghuTokens))]
|
||||||
|
cacheKey := "github:copilot_internal_v2_token:" + ghu
|
||||||
|
token, err := cache.Get(cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
cache.Del(cacheKey)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if token != nil {
|
||||||
|
return token.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://api.github.com/copilot_internal/v2/token"
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("authorization", "token "+ghu)
|
||||||
|
req.Header.Set("host", "api.github.com")
|
||||||
|
req.Header.Set("accept", "*/*")
|
||||||
|
req.Header.Set("editor-plugin-version", "copilot-intellij/1.5.21.6667")
|
||||||
|
req.Header.Set("copilot-language-server-version", "1.228.0")
|
||||||
|
req.Header.Set("user-agent", "GithubCopilot/1.228.0")
|
||||||
|
req.Header.Set("editor-version", "JetBrains-IU/242.21829.142")
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("获取 Token 失败" + ghu)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 解析json
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
newToken := result["token"].(string)
|
||||||
|
err = cache.Set(cacheKey, newToken, 1500)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeHeaders 合并请求头,固定请求头会覆盖原有请求头
|
||||||
|
func mergeHeaders(originalHeader http.Header, req *http.Request) error {
|
||||||
|
// 复制原始请求头
|
||||||
|
for key, values := range originalHeader {
|
||||||
|
for _, value := range values {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取token
|
||||||
|
token, err := getAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取GitHub Copilot的临时Token失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 固定请求头
|
||||||
|
fixedHeaders := map[string]string{
|
||||||
|
"authorization": "Bearer " + token,
|
||||||
|
"editor-plugin-version": "copilot-intellij/1.5.21.6667",
|
||||||
|
"copilot-language-server-version": "1.228.0",
|
||||||
|
"user-agent": "GithubCopilot/1.228.0",
|
||||||
|
"editor-version": "JetBrains-IU/242.21829.142",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置固定请求头(覆盖原有的)
|
||||||
|
for key, value := range fixedHeaders {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopilotModels 获取GitHub Copilot的模型列表
|
||||||
|
func GetCopilotModels(c *gin.Context) {
|
||||||
|
copilotAccountType := os.Getenv("COPILOT_ACCOUNT_TYPE")
|
||||||
|
url := "https://api." + copilotAccountType + ".githubcopilot.com/models"
|
||||||
|
req, err := http.NewRequestWithContext(c, "GET", url, nil)
|
||||||
|
if nil != err {
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并请求头
|
||||||
|
if err := mergeHeaders(c.Request.Header, req); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: httpClientTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if nil != err {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
abortCodex(c, http.StatusRequestTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("获取模型列表失败:", err.Error())
|
||||||
|
abortCodex(c, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Println("请求GitHub Copilot模型列表失败:", string(body))
|
||||||
|
|
||||||
|
abortCodex(c, resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转发原始响应
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
c.Header("Content-Type", resp.Header.Get("Content-Type"))
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
24
internal/controller/copilot/membership.go
Normal file
24
internal/controller/copilot/membership.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMembership 获取团队成员信息
|
||||||
|
func GetMembership(c *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
teamID := c.Param("teamID")
|
||||||
|
username := c.Param("username")
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Not Found",
|
||||||
|
"documentation_url": "https://docs.github.com/rest/teams/members#get-team-membership-for-a-user-legacy",
|
||||||
|
"status": "404",
|
||||||
|
"teamID": teamID,
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}
|
||||||
49
internal/controller/copilot/meta.go
Normal file
49
internal/controller/copilot/meta.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func V3meta(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cliv3(c *gin.Context) {
|
||||||
|
c.Header("X-OAuth-Scopes", "gist, read:org, repo, user, workflow, write:public_key")
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"current_user_url": "https://api.github.com/user",
|
||||||
|
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
|
||||||
|
"authorizations_url": "https://api.github.com/authorizations",
|
||||||
|
"code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
|
||||||
|
"commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
|
||||||
|
"emails_url": "https://api.github.com/user/emails",
|
||||||
|
"emojis_url": "https://api.github.com/emojis",
|
||||||
|
"events_url": "https://api.github.com/events",
|
||||||
|
"feeds_url": "https://api.github.com/feeds",
|
||||||
|
"followers_url": "https://api.github.com/user/followers",
|
||||||
|
"following_url": "https://api.github.com/user/following{/target}",
|
||||||
|
"gists_url": "https://api.github.com/gists{/gist_id}",
|
||||||
|
"hub_url": "https://api.github.com/hub",
|
||||||
|
"issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
|
||||||
|
"issues_url": "https://api.github.com/issues",
|
||||||
|
"keys_url": "https://api.github.com/user/keys",
|
||||||
|
"label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}",
|
||||||
|
"notifications_url": "https://api.github.com/notifications",
|
||||||
|
"organization_url": "https://api.github.com/orgs/{org}",
|
||||||
|
"organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
|
||||||
|
"organization_teams_url": "https://api.github.com/orgs/{org}/teams",
|
||||||
|
"public_gists_url": "https://api.github.com/gists/public",
|
||||||
|
"rate_limit_url": "https://api.github.com/rate_limit",
|
||||||
|
"repository_url": "https://api.github.com/repos/{owner}/{repo}",
|
||||||
|
"repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
|
||||||
|
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
|
||||||
|
"starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
|
||||||
|
"starred_gists_url": "https://api.github.com/gists/starred",
|
||||||
|
"topic_search_url": "https://api.github.com/search/topics?q={query}{&page,per_page}",
|
||||||
|
"user_url": "https://api.github.com/users/{user}",
|
||||||
|
"user_organizations_url": "https://api.github.com/user/orgs",
|
||||||
|
"user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
|
||||||
|
"user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}",
|
||||||
|
})
|
||||||
|
}
|
||||||
158
internal/controller/copilot/router_register.go
Normal file
158
internal/controller/copilot/router_register.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ClientType string
|
||||||
|
CopilotProxyAll bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig loads the configuration from environment variables.
|
||||||
|
func loadConfig() (*Config, error) {
|
||||||
|
proxyAll, err := strconv.ParseBool(os.Getenv("COPILOT_PROXY_ALL"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid boolean value for COPILOT_PROXY_ALL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
ClientType: os.Getenv("COPILOT_CLIENT_TYPE"),
|
||||||
|
CopilotProxyAll: proxyAll,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinApi 注册路由
|
||||||
|
func GinApi(g *gin.RouterGroup) {
|
||||||
|
config, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础路由
|
||||||
|
setupBasicRoutes(g, config)
|
||||||
|
|
||||||
|
// 用户相关路由
|
||||||
|
setupUserRoutes(g)
|
||||||
|
|
||||||
|
// Copilot相关路由
|
||||||
|
setupCopilotRoutes(g, config)
|
||||||
|
|
||||||
|
// API v3相关路由
|
||||||
|
setupV3Routes(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupBasicRoutes 设置基础路由
|
||||||
|
func setupBasicRoutes(g *gin.RouterGroup, config *Config) {
|
||||||
|
g.GET("/models", createModelsHandler(config))
|
||||||
|
g.GET("/_ping", GetPing)
|
||||||
|
g.POST("/telemetry", PostTelemetry)
|
||||||
|
g.GET("/agents", GetAgents)
|
||||||
|
g.GET("/copilot_internal/user", GetCopilotInternalUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupUserRoutes 设置用户相关路由
|
||||||
|
func setupUserRoutes(g *gin.RouterGroup) {
|
||||||
|
authMiddleware := middleware.AccessTokenCheckAuth()
|
||||||
|
|
||||||
|
userGroup := g.Group("")
|
||||||
|
userGroup.Use(authMiddleware)
|
||||||
|
{
|
||||||
|
userGroup.GET("/user", GetLoginUser)
|
||||||
|
userGroup.GET("/user/orgs", GetUserOrgs)
|
||||||
|
userGroup.GET("/api/v3/user", GetLoginUser)
|
||||||
|
userGroup.GET("/api/v3/user/orgs", GetUserOrgs)
|
||||||
|
userGroup.GET("/teams/:teamID/memberships/:username", GetMembership)
|
||||||
|
userGroup.POST("/chunks", HandleChunks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupCopilotRoutes 设置Copilot相关路由
|
||||||
|
func setupCopilotRoutes(g *gin.RouterGroup, config *Config) {
|
||||||
|
tokenMiddleware := middleware.TokenCheckAuth()
|
||||||
|
|
||||||
|
// Copilot token endpoint
|
||||||
|
g.GET("/copilot_internal/v2/token",
|
||||||
|
middleware.AccessTokenCheckAuth(),
|
||||||
|
createTokenHandler(config))
|
||||||
|
|
||||||
|
// Completions endpoints
|
||||||
|
completionsGroup := g.Group("")
|
||||||
|
completionsGroup.Use(tokenMiddleware)
|
||||||
|
{
|
||||||
|
completionsGroup.POST("/v1/engines/:model-name/completions", createCompletionsHandler(config))
|
||||||
|
completionsGroup.POST("/v1/engines/copilot-codex", createCompletionsHandler(config))
|
||||||
|
completionsGroup.POST("/chat/completions", createChatHandler(config))
|
||||||
|
completionsGroup.POST("/agents/chat", createChatHandler(config))
|
||||||
|
completionsGroup.POST("/v1/chat/completions", createChatHandler(config))
|
||||||
|
completionsGroup.POST("/v1/engines/copilot-centralus-h100/speculation", createChatEditCompletionsHandler(config))
|
||||||
|
completionsGroup.POST("/embeddings", HandleEmbeddings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupV3Routes 设置API v3相关路由
|
||||||
|
func setupV3Routes(g *gin.RouterGroup) {
|
||||||
|
g.GET("/api/v3/meta", V3meta)
|
||||||
|
g.GET("/api/v3/", Cliv3)
|
||||||
|
g.GET("/", Cliv3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理函数生成器
|
||||||
|
func createTokenHandler(config *Config) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if config.ClientType == "github" && !config.CopilotProxyAll {
|
||||||
|
GetCopilotInternalV2Token(c)
|
||||||
|
} else {
|
||||||
|
GetDisguiseCopilotInternalV2Token(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCompletionsHandler 生成代码补全处理函数
|
||||||
|
func createCompletionsHandler(config *Config) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if config.ClientType == "github" && config.CopilotProxyAll {
|
||||||
|
CodexCompletions(c)
|
||||||
|
} else {
|
||||||
|
CodeCompletions(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChatHandler 生成聊天补全处理函数
|
||||||
|
func createChatHandler(config *Config) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if config.ClientType == "github" && config.CopilotProxyAll {
|
||||||
|
ChatsCompletions(c)
|
||||||
|
} else {
|
||||||
|
ChatCompletions(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChatEditCompletionsHandler 生成聊天编辑补全处理函数
|
||||||
|
func createChatEditCompletionsHandler(config *Config) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if config.ClientType == "github" && config.CopilotProxyAll {
|
||||||
|
ChatEditCompletions(c)
|
||||||
|
} else {
|
||||||
|
CodeCompletions(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createModelsHandler 生成模型处理函数
|
||||||
|
func createModelsHandler(config *Config) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if config.ClientType == "github" && config.CopilotProxyAll {
|
||||||
|
GetCopilotModels(c)
|
||||||
|
} else {
|
||||||
|
GetModels(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
internal/controller/copilot/telemetry.go
Normal file
20
internal/controller/copilot/telemetry.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostTelemetry 接收并处理来自GitHub Copilot的遥测数据
|
||||||
|
func PostTelemetry(c *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
c.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"itemsReceived": 0,
|
||||||
|
"itemsAccepted": 0,
|
||||||
|
"appId": nil,
|
||||||
|
"errors": []string{},
|
||||||
|
})
|
||||||
|
}
|
||||||
109
internal/controller/copilot/user.go
Normal file
109
internal/controller/copilot/user.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLoginUser 获取登录用户信息
|
||||||
|
func GetLoginUser(ctx *gin.Context) {
|
||||||
|
userDisplayName := "github"
|
||||||
|
token, _ := jwtpkg.GetJwtProto(ctx, &middleware.UserLoad{})
|
||||||
|
if token != nil && token.UserDisplayName != "" {
|
||||||
|
userDisplayName = token.UserDisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Header("X-OAuth-Scopes", "gist, read:org, repo, user, workflow, write:public_key")
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
ctx.Header("x-github-request-id", requestID)
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"login": userDisplayName,
|
||||||
|
"id": 9919,
|
||||||
|
"node_id": "DEyOk9yZ2FuaXphdGlvbjk5MTk=",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/github",
|
||||||
|
"html_url": "https://github.com/github",
|
||||||
|
"followers_url": "https://api.github.com/users/github/followers",
|
||||||
|
"following_url": "https://api.github.com/users/github/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/github/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/github/repos",
|
||||||
|
"events_url": "https://api.github.com/users/github/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/github/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false,
|
||||||
|
"name": "GitHub",
|
||||||
|
"company": nil,
|
||||||
|
"blog": "",
|
||||||
|
"location": "San Francisco, CA",
|
||||||
|
"email": nil,
|
||||||
|
"hireable": nil,
|
||||||
|
"bio": nil,
|
||||||
|
"twitter_username": nil,
|
||||||
|
"public_repos": 498,
|
||||||
|
"public_gists": 0,
|
||||||
|
"followers": 42848,
|
||||||
|
"following": 0,
|
||||||
|
"created_at": "2008-05-11T04:37:31Z",
|
||||||
|
"updated_at": "2022-11-29T19:44:55Z",
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserOrgs(ctx *gin.Context) {
|
||||||
|
ctx.Header("X-OAuth-Scopes", "gist, read:org, repo, user, workflow, write:public_key")
|
||||||
|
ctx.JSON(http.StatusOK, []interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTrackingID 生成模拟的 analytics_tracking_id
|
||||||
|
func generateTrackingID() string {
|
||||||
|
// 生成一个随机字符串并计算其 MD5
|
||||||
|
randomStr := fmt.Sprintf("%d%d", time.Now().UnixNano(), rand.Int())
|
||||||
|
hash := md5.Sum([]byte(randomStr))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAssignedDate 生成模拟的 assigned_date
|
||||||
|
func generateAssignedDate() string {
|
||||||
|
// 生成最近30天内的随机时间
|
||||||
|
now := time.Now()
|
||||||
|
daysAgo := rand.Intn(30)
|
||||||
|
randomTime := now.AddDate(0, 0, -daysAgo)
|
||||||
|
|
||||||
|
// 随机增加小时和分钟
|
||||||
|
randomHour := rand.Intn(24)
|
||||||
|
randomMinute := rand.Intn(60)
|
||||||
|
randomTime = randomTime.Add(time.Duration(randomHour) * time.Hour)
|
||||||
|
randomTime = randomTime.Add(time.Duration(randomMinute) * time.Minute)
|
||||||
|
|
||||||
|
// 返回格式化的时间字符串
|
||||||
|
return randomTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopilotInternalUser 获取 Copilot 内部用户信息
|
||||||
|
func GetCopilotInternalUser(ctx *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
ctx.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_type_sku": "free_educational",
|
||||||
|
"copilot_plan": "individual",
|
||||||
|
"analytics_tracking_id": generateTrackingID(),
|
||||||
|
"assigned_date": generateAssignedDate(),
|
||||||
|
"can_signup_for_limited": false,
|
||||||
|
"chat_enabled": true,
|
||||||
|
"organization_login_list": []interface{}{},
|
||||||
|
"organization_list": []interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
78
internal/controller/copilot/utils.go
Normal file
78
internal/controller/copilot/utils.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pong struct {
|
||||||
|
Now int `json:"now"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Ns1 string `json:"ns1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPing 模拟ping接口
|
||||||
|
func GetPing(ctx *gin.Context) {
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
ctx.Header("x-github-request-id", requestID)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, Pong{
|
||||||
|
Now: time.Now().Second(),
|
||||||
|
Status: "ok",
|
||||||
|
Ns1: "200 OK",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelsResponse 模型列表响应结构
|
||||||
|
type ModelsResponse struct {
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModels 获取模型列表
|
||||||
|
func GetModels(ctx *gin.Context) {
|
||||||
|
// 从根目录下读取models.json文件
|
||||||
|
jsonFile, err := os.Open(filepath.Join("models.json"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("无法打开models.json文件: %v", err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "无法读取模型列表数据"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer CloseIO(jsonFile)
|
||||||
|
|
||||||
|
// 解析JSON数据
|
||||||
|
jsonData, err := io.ReadAll(jsonFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取models.json内容失败: %v", err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "无法读取模型列表数据"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelsResponse ModelsResponse
|
||||||
|
if err := json.Unmarshal(jsonData, &modelsResponse); err != nil {
|
||||||
|
log.Printf("解析models.json失败: %v", err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "无法解析模型列表数据"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回模型列表数据
|
||||||
|
requestID := uuid.Must(uuid.NewV4()).String()
|
||||||
|
ctx.Header("x-github-request-id", requestID)
|
||||||
|
ctx.JSON(http.StatusOK, modelsResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseIO(c io.Closer) {
|
||||||
|
err := c.Close()
|
||||||
|
if nil != err {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
206
internal/middleware/auth.go
Normal file
206
internal/middleware/auth.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"ripper/internal/app/github_auth"
|
||||||
|
"ripper/internal/response"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OAuthCheck struct {
|
||||||
|
ClientId string `json:"client_id" form:"client_id"`
|
||||||
|
DeviceCode string `json:"device_code" form:"device_code"`
|
||||||
|
GrantType string `json:"grant_type" form:"grant_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceCodeCheckAuth(ctx *gin.Context) {
|
||||||
|
checkInfo := &OAuthCheck{}
|
||||||
|
if err := ctx.ShouldBind(&checkInfo); err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
ctx.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, _ := github_auth.GetClientAuthInfoByDeviceCode(checkInfo.DeviceCode)
|
||||||
|
if info.CardCode == "" {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"error": "authorization_pending",
|
||||||
|
"error_description": "The authorization request is still pending.",
|
||||||
|
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
|
||||||
|
})
|
||||||
|
ctx.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Set("client_auth_info", info)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthCodeFlowCheckAuth(ctx *gin.Context) {
|
||||||
|
checkInfoClient := &github_auth.ClientOAuthInfo{}
|
||||||
|
err := ctx.Bind(&checkInfoClient)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
ctx.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oauthCodeInfo, err := github_auth.GetOAuthCodeInfoByClientIdAndCode(checkInfoClient.ClientId, checkInfoClient.Code)
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(ctx, response.FailStruct{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "Invalid client id.",
|
||||||
|
}, false)
|
||||||
|
ctx.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Set("client_auth_info", oauthCodeInfo)
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AccessTokenCheckAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.Request.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusForbidden, response.NoAccess, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
last := strings.Index(token, " ")
|
||||||
|
if len(token) < last || last == -1 {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusForbidden, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = token[last+1:]
|
||||||
|
chk, jwter, err := jwtpkg.CheckToken(token, &UserLoad{}, "user")
|
||||||
|
if err != nil {
|
||||||
|
errmsg := response.TokenWrongful
|
||||||
|
errmsg.Msg = "令牌验证错误"
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusForbidden, errmsg, true, err.Error())
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !chk {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusForbidden, response.NoAccess, true, "破损令牌")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chs := true
|
||||||
|
issuerStr := ""
|
||||||
|
issuerStr, err = jwter.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
chs = false
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if "user" != issuerStr && issuerStr != "" {
|
||||||
|
chs = false
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !chs {
|
||||||
|
errmsg := response.TokenWrongful
|
||||||
|
errmsg.Msg = "签名错误"
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusForbidden, errmsg, true, err.Error())
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("token", jwter)
|
||||||
|
c.Set("tokenStr", token)
|
||||||
|
c.Set("token.issuer", issuerStr)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TokenCheckAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
clientType := os.Getenv("COPILOT_CLIENT_TYPE")
|
||||||
|
copilotProxyAll, err := strconv.ParseBool(os.Getenv("COPILOT_PROXY_ALL"))
|
||||||
|
if clientType == "github" && !copilotProxyAll {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := c.Request.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusUnauthorized, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
last := strings.Index(token, " ")
|
||||||
|
if len(token) < last || last == -1 {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusUnauthorized, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = token[last+1:]
|
||||||
|
parsedToken := parseAuthorizationToken(token)
|
||||||
|
// 校验exp是否过期
|
||||||
|
expired, err := isExpired(parsedToken["exp"])
|
||||||
|
if err != nil {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusUnauthorized, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if expired {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusUnauthorized, response.TokenOverdue, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawToken := github_auth.JsonMap2Token(map[string]interface{}{
|
||||||
|
"tid": parsedToken["tid"],
|
||||||
|
"exp": parsedToken["exp"],
|
||||||
|
"sku": parsedToken["sku"],
|
||||||
|
"st": parsedToken["st"],
|
||||||
|
"chat": parsedToken["chat"],
|
||||||
|
"u": parsedToken["u"],
|
||||||
|
})
|
||||||
|
sign := "1:" + github_auth.Token2Sign(rawToken)
|
||||||
|
if sign != parsedToken["8kp"] {
|
||||||
|
response.FailJsonAndStatusCode(c, http.StatusUnauthorized, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAuthorizationToken(token string) map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
pairs := strings.Split(token, ";")
|
||||||
|
|
||||||
|
for _, pair := range pairs {
|
||||||
|
kv := strings.SplitN(pair, "=", 2)
|
||||||
|
if len(kv) == 2 {
|
||||||
|
key := kv[0]
|
||||||
|
value := kv[1]
|
||||||
|
|
||||||
|
if key == "tid" || key == "exp" || key == "sku" || key == "st" || key == "8kp" || key == "chat" || key == "u" {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExpired(expStr string) (bool, error) {
|
||||||
|
exp, err := strconv.ParseInt(expStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid exp timestamp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
return now > exp, nil
|
||||||
|
}
|
||||||
30
internal/middleware/cors.go
Normal file
30
internal/middleware/cors.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Cors 跨域中间件
|
||||||
|
func Cors() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.Request.Header.Get("origin") //请求头部
|
||||||
|
if len(origin) == 0 {
|
||||||
|
origin = c.Request.Header.Get("Origin")
|
||||||
|
}
|
||||||
|
//接收客户端发送的origin (重要!)
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
//允许客户端传递校验信息比如 cookie (重要)
|
||||||
|
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", "OPTIONS, GET, POST, PUT, DELETE, UPDATE")
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
// 设置预验请求有效期为 86400 秒
|
||||||
|
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
102
internal/middleware/jwt.go
Normal file
102
internal/middleware/jwt.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"ripper/internal/response"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTCheck 检查是否登陆
|
||||||
|
// 检查完毕会将jwt结构体写入到Context
|
||||||
|
// 适用于同时用于公开与鉴权的路由
|
||||||
|
func JWTCheck(c *gin.Context, model jwtpkg.LoadModel, issure ...string) (bool, error) {
|
||||||
|
token := c.Request.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if len(token) < 8 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
token = token[7:]
|
||||||
|
chk, jwter, err := jwtpkg.CheckToken(token, model, "")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
chs := true
|
||||||
|
for _, v := range issure {
|
||||||
|
jwt, err := jwter.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
chs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if v != jwt {
|
||||||
|
chs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !chs {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !chk {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
c.Set("token", jwter)
|
||||||
|
c.Next()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTAuth 为JWT中间件,客户端下需要在header带上Authorization: Bearer <token>
|
||||||
|
// issure 为可选验证签名,支持多参选择
|
||||||
|
func JWTAuth(model jwtpkg.LoadModel, issure ...string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.Request.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
response.FailJson(c, response.NoAccess, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(token) < 8 {
|
||||||
|
response.FailJson(c, response.TokenWrongful, false)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = token[7:]
|
||||||
|
chk, jwter, err := jwtpkg.CheckToken(token, model, "")
|
||||||
|
if err != nil {
|
||||||
|
errmsg := response.TokenWrongful
|
||||||
|
errmsg.Msg = "令牌验证错误"
|
||||||
|
response.FailJson(c, errmsg, true, err.Error())
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !chk {
|
||||||
|
response.FailJson(c, response.NoAccess, true, "破损令牌")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chs := true
|
||||||
|
issuerStr := ""
|
||||||
|
for _, v := range issure {
|
||||||
|
issuerStr, err = jwter.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
chs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if v != issuerStr {
|
||||||
|
chs = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !chs {
|
||||||
|
errmsg := response.TokenWrongful
|
||||||
|
errmsg.Msg = "签名错误"
|
||||||
|
response.FailJson(c, errmsg, true, err.Error())
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("token", jwter)
|
||||||
|
c.Set("tokenStr", token)
|
||||||
|
c.Set("token.issuer", issuerStr)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
internal/middleware/jwt_model.go
Normal file
23
internal/middleware/jwt_model.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
jwtPkg "ripper/pkg/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminLoad struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserLoad struct {
|
||||||
|
UserDisplayName string `json:"userDisplayName,omitempty"`
|
||||||
|
CardCode string `json:"token"`
|
||||||
|
Client string `json:"client"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserLoad(ID uint, ExpiresAt int64, Issuer string) *UserLoad {
|
||||||
|
return &UserLoad{
|
||||||
|
RegisteredClaims: jwtPkg.CreateStandardClaims(ExpiresAt, Issuer),
|
||||||
|
}
|
||||||
|
}
|
||||||
1
internal/middleware/logger.go
Normal file
1
internal/middleware/logger.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package middleware
|
||||||
72
internal/response/model.go
Normal file
72
internal/response/model.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
/*
|
||||||
|
code约定:
|
||||||
|
code代表错误,无错误始终为0
|
||||||
|
200 OK - [GET]:服务器成功返回用户请求的数据;
|
||||||
|
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功;
|
||||||
|
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务);
|
||||||
|
204 NO CONTENT - [DELETE]:用户删除数据成功;
|
||||||
|
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作;
|
||||||
|
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误);
|
||||||
|
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的;
|
||||||
|
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作;
|
||||||
|
406 Not Acceptable - [GET]:用户请求的格式不可得;
|
||||||
|
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的;
|
||||||
|
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误;
|
||||||
|
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
NoAccess = FailStruct{
|
||||||
|
Code: 401,
|
||||||
|
Msg: "无权访问",
|
||||||
|
}
|
||||||
|
TokenWrongful = FailStruct{
|
||||||
|
Code: 401,
|
||||||
|
Msg: "Token不合法",
|
||||||
|
}
|
||||||
|
TokenOverdue = FailStruct{
|
||||||
|
Code: 401,
|
||||||
|
Msg: "Token过期",
|
||||||
|
}
|
||||||
|
NoIntactParameters = FailStruct{
|
||||||
|
Code: -10001,
|
||||||
|
Msg: "参数提交不完整,请重试",
|
||||||
|
}
|
||||||
|
UserError = FailStruct{
|
||||||
|
Code: 10001,
|
||||||
|
Msg: "帐号或密码错误",
|
||||||
|
}
|
||||||
|
SignError = FailStruct{
|
||||||
|
Code: 10002,
|
||||||
|
Msg: "",
|
||||||
|
}
|
||||||
|
CaptchaError = FailStruct{
|
||||||
|
Code: 10003,
|
||||||
|
Msg: "生成验证码错误",
|
||||||
|
}
|
||||||
|
CaptchaVefError = FailStruct{
|
||||||
|
Code: 10004,
|
||||||
|
Msg: "验证码错误",
|
||||||
|
}
|
||||||
|
WechatLoginError = FailStruct{
|
||||||
|
Code: 10005,
|
||||||
|
Msg: "微信登陆错误",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type FailStruct struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ErrCode int `json:"error"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
Msg string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
62
internal/response/reply.go
Normal file
62
internal/response/reply.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
HTTP状态码约定:
|
||||||
|
服务器访问正常始终200,错误交给code
|
||||||
|
*/
|
||||||
|
|
||||||
|
func BindStruct(c *gin.Context, bind interface{}) error {
|
||||||
|
if err := c.ShouldBindJSON(bind); err != nil {
|
||||||
|
FailJson(c, NoIntactParameters, false, "结构体绑定错误")
|
||||||
|
return errors.New("BindError")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuccessJson(c *gin.Context, msg string, data ...interface{}) {
|
||||||
|
var tmps interface{}
|
||||||
|
if len(data) > 0 {
|
||||||
|
tmps = data[0]
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, Message{
|
||||||
|
ErrCode: 0,
|
||||||
|
Data: tmps,
|
||||||
|
Msg: msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func FailJson(c *gin.Context, load FailStruct, WriteLog bool, logMsh ...string) {
|
||||||
|
if WriteLog {
|
||||||
|
var werrmsg string
|
||||||
|
for _, v := range logMsh {
|
||||||
|
werrmsg += v + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, Message{
|
||||||
|
ErrCode: load.Code,
|
||||||
|
Msg: load.Msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func FailJsonAndStatusCode(c *gin.Context, code int, load FailStruct, WriteLog bool, logMsh ...string) {
|
||||||
|
if WriteLog {
|
||||||
|
var werrmsg string
|
||||||
|
for _, v := range logMsh {
|
||||||
|
werrmsg += v + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(code, Message{
|
||||||
|
ErrCode: load.Code,
|
||||||
|
Msg: load.Msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuccessByte(c *gin.Context, data []byte) {
|
||||||
|
c.Writer.Write(data)
|
||||||
|
}
|
||||||
25
internal/router/register_router.go
Normal file
25
internal/router/register_router.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"html/template"
|
||||||
|
authApi "ripper/internal/controller/auth"
|
||||||
|
"ripper/internal/controller/copilot"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"ripper/static"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHTTPRouter(r *gin.Engine) {
|
||||||
|
rootRouter := r.Group("/")
|
||||||
|
tmpl := template.Must(template.New("").ParseFS(static.Public, "public/*.html"))
|
||||||
|
r.SetHTMLTemplate(tmpl)
|
||||||
|
|
||||||
|
apiRouter := r.Group("/api")
|
||||||
|
|
||||||
|
rootRouter.Use(middleware.Cors())
|
||||||
|
apiRouter.Use(middleware.Cors())
|
||||||
|
|
||||||
|
authApi.GinApi(rootRouter)
|
||||||
|
copilot.GinApi(rootRouter)
|
||||||
|
|
||||||
|
}
|
||||||
242
main.go
Normal file
242
main.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"ripper/pkg/certificate"
|
||||||
|
"ripper/pkg/message"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"ripper/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 检查端口是否被占用,如果被占用则退出程序
|
||||||
|
func checkPortAndExit(host string, port int) {
|
||||||
|
addr := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
conn, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("端口: %d 已被占用, 运行结束!", port)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 设置日志输出
|
||||||
|
setupLogging()
|
||||||
|
|
||||||
|
// 在非生产环境中加载 .env 文件
|
||||||
|
if os.Getenv("ENV") != "production" {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Printf("Warning: Error loading .env file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Current Environment: ", os.Getenv("ENV"))
|
||||||
|
|
||||||
|
// 设置默认环境变量
|
||||||
|
initDefaultEnv()
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
// 添加 HSTS 中间件
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Header("Strict-Transport-Security", "max-age=0")
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
//初始化router
|
||||||
|
router.NewHTTPRouter(r)
|
||||||
|
|
||||||
|
//获取配置
|
||||||
|
httpPort, _ := strconv.Atoi(os.Getenv("PORT"))
|
||||||
|
httpsPort, _ := strconv.Atoi(os.Getenv("HTTPS_PORT"))
|
||||||
|
host := os.Getenv("HOST")
|
||||||
|
|
||||||
|
// 初始化证书
|
||||||
|
certFile, keyFile, reloadChan, err := certificate.InitCertificates()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize certificates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查端口是否被占用
|
||||||
|
checkPortAndExit(host, httpPort)
|
||||||
|
checkPortAndExit(host, httpsPort)
|
||||||
|
|
||||||
|
// 创建一个带取消功能的上下文
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 创建一个错误组
|
||||||
|
g, groupCtx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// 创建一个通道来表示服务器已经启动
|
||||||
|
serverStarted := make(chan struct{}, 2)
|
||||||
|
|
||||||
|
// 启动HTTP服务器
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", host, httpPort),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
g.Go(func() error {
|
||||||
|
log.Printf("Starting HTTP server on %s\n", httpServer.Addr)
|
||||||
|
serverStarted <- struct{}{}
|
||||||
|
return httpServer.ListenAndServe()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建一个函数来启动HTTPS服务器
|
||||||
|
var httpsServer *http.Server
|
||||||
|
startHTTPSServer := func() *http.Server {
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", host, httpsPort),
|
||||||
|
Handler: r,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS10,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
},
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
log.Printf("Starting HTTPS server on %s\n", server.Addr)
|
||||||
|
if httpsServer == nil { // 仅在首次启动时发送信号
|
||||||
|
serverStarted <- struct{}{}
|
||||||
|
}
|
||||||
|
return server.ListenAndServeTLS(certFile, keyFile)
|
||||||
|
})
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动初始HTTPS服务器
|
||||||
|
httpsServer = startHTTPSServer()
|
||||||
|
|
||||||
|
// 等待两个服务器都启动
|
||||||
|
<-serverStarted
|
||||||
|
<-serverStarted
|
||||||
|
|
||||||
|
// 显示消息或消息框
|
||||||
|
message.ShowAppLaunchMessage()
|
||||||
|
|
||||||
|
// 监听证书更新和关闭信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// 处理证书更新和服务器关闭
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-reloadChan:
|
||||||
|
log.Println("Certificate update detected, reloading HTTPS server...")
|
||||||
|
|
||||||
|
// 创建关闭超时上下文
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
|
||||||
|
// 关闭当前的HTTPS服务器
|
||||||
|
if err := httpsServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("Error shutting down HTTPS server: %v", err)
|
||||||
|
}
|
||||||
|
shutdownCancel()
|
||||||
|
|
||||||
|
// 启动新的HTTPS服务器
|
||||||
|
httpsServer = startHTTPSServer()
|
||||||
|
|
||||||
|
case <-quit:
|
||||||
|
log.Println("Shutdown signal received, exiting...")
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-groupCtx.Done():
|
||||||
|
log.Println("Unexpected exit, trying to shutdown gracefully...")
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待取消信号
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
// 给服务器一些时间来完成正在处理的请求
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
// 优雅地关闭服务器
|
||||||
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("HTTP server Shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := httpsServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("HTTPS server Shutdown: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有 goroutine 完成
|
||||||
|
if err := g.Wait(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("Error during server operations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLogging() {
|
||||||
|
// 创建日志目录
|
||||||
|
logDir := "logs"
|
||||||
|
err := os.MkdirAll(logDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("无法创建日志目录:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志文件,使用当前日期作为文件名
|
||||||
|
currentTime := time.Now()
|
||||||
|
logFileName := filepath.Join(logDir, fmt.Sprintf("%s.log", currentTime.Format("2006-01-02")))
|
||||||
|
logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("无法创建日志文件:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 gin 的日志输出到文件和控制台
|
||||||
|
gin.DefaultWriter = io.MultiWriter(logFile, os.Stdout)
|
||||||
|
|
||||||
|
// 设置标准日志输出到文件和控制台
|
||||||
|
log.SetOutput(io.MultiWriter(logFile, os.Stdout))
|
||||||
|
log.SetPrefix("[Copilot Proxies] ")
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDefaultEnv 初始化默认环境变量
|
||||||
|
func initDefaultEnv() {
|
||||||
|
if os.Getenv("COPILOT_PROXY_ALL") == "" {
|
||||||
|
os.Setenv("COPILOT_PROXY_ALL", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("COPILOT_CLIENT_TYPE") == "" {
|
||||||
|
os.Setenv("COPILOT_CLIENT_TYPE", "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("DISGUISE_COPILOT_TOKEN_EXPIRES_AT") == "" {
|
||||||
|
os.Setenv("DISGUISE_COPILOT_TOKEN_EXPIRES_AT", "1800")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("HTTP_CLIENT_TIMEOUT") == "" {
|
||||||
|
os.Setenv("DISGUISE_COPILOT_TOKEN_EXPIRES_AT", "60")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("COPILOT_ACCOUNT_TYPE") == "" {
|
||||||
|
os.Setenv("COPILOT_ACCOUNT_TYPE", "individual")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("LIGHTWEIGHT_MODEL") == "" {
|
||||||
|
os.Setenv("LIGHTWEIGHT_MODEL", "gpt-4o-mini")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1012
models.json
Normal file
1012
models.json
Normal file
File diff suppressed because it is too large
Load Diff
153
pkg/certificate/certificate.go
Normal file
153
pkg/certificate/certificate.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package certificate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certURL = "https://data-1251486259.cos.ap-beijing.myqcloud.com/copilot-ssl/ssl.pem"
|
||||||
|
keyURL = "https://data-1251486259.cos.ap-beijing.myqcloud.com/copilot-ssl/ssl.key"
|
||||||
|
certPath = "cert/ssl.pem"
|
||||||
|
keyPath = "cert/ssl.key"
|
||||||
|
checkInterval = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mutex sync.Mutex
|
||||||
|
stopChan chan struct{}
|
||||||
|
httpsServerReload chan struct{} // 用于通知需要重载服务器
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitCertificates 初始化证书管理
|
||||||
|
func InitCertificates() (string, string, chan struct{}, error) {
|
||||||
|
// 确保证书目录存在
|
||||||
|
if err := os.MkdirAll(filepath.Dir(certPath), 0755); err != nil {
|
||||||
|
return "", "", nil, fmt.Errorf("failed to create cert directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpsServerReload = make(chan struct{}, 1)
|
||||||
|
|
||||||
|
// 首次检查和更新证书
|
||||||
|
if err := checkAndUpdateCertificates(); err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动定时检查
|
||||||
|
startPeriodicCheck()
|
||||||
|
|
||||||
|
return certPath, keyPath, httpsServerReload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopPeriodicCheck 停止定时检查
|
||||||
|
func StopPeriodicCheck() {
|
||||||
|
if stopChan != nil {
|
||||||
|
close(stopChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startPeriodicCheck() {
|
||||||
|
stopChan = make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(checkInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := checkAndUpdateCertificates(); err != nil {
|
||||||
|
log.Printf("Error checking certificates: %v", err)
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAndUpdateCertificates() error {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
needsUpdate, err := certificateNeedsUpdate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsUpdate {
|
||||||
|
if err := downloadFile(certURL, certPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to download certificate: %v", err)
|
||||||
|
}
|
||||||
|
if err := downloadFile(keyURL, keyPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to download key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知需要重载服务器
|
||||||
|
select {
|
||||||
|
case httpsServerReload <- struct{}{}:
|
||||||
|
log.Println("Certificates updated, triggering server reload")
|
||||||
|
default:
|
||||||
|
// 如果通道已满,说明已经有一个重载信号在等待处理
|
||||||
|
log.Println("Server reload already pending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func certificateNeedsUpdate() (bool, error) {
|
||||||
|
if !fileExists(certPath) || !fileExists(keyPath) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
certData, err := os.ReadFile(certPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to read certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(certData)
|
||||||
|
if block == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to parse certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Add(24 * time.Hour).After(cert.NotAfter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadFile(url string, filepath string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(filename string) bool {
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
96
pkg/crypto/AES.go
Normal file
96
pkg/crypto/AES.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//高级加密标准(Adevanced Encryption Standard ,AES)
|
||||||
|
|
||||||
|
//key不能泄露
|
||||||
|
|
||||||
|
//PKCS7Padding PKCS7 填充模式
|
||||||
|
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(ciphertext)%blockSize
|
||||||
|
//Repeat()函数的功能是把切片[]byte{byte(padding)}复制padding个,然后合并成新的字节切片返回
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
//PKCS7UnPadding 填充的反向操作,删除填充字符串
|
||||||
|
func PKCS7UnPadding(origData []byte) ([]byte, error) {
|
||||||
|
//获取数据长度
|
||||||
|
length := len(origData)
|
||||||
|
if length == 0 {
|
||||||
|
return nil, errors.New("加密字符串错误!")
|
||||||
|
} else {
|
||||||
|
//获取填充字符串长度
|
||||||
|
unpadding := int(origData[length-1])
|
||||||
|
//截取切片,删除填充字节,并且返回明文
|
||||||
|
return origData[:(length - unpadding)], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//AesEcrypt 实现加密
|
||||||
|
func AesEcrypt(origData []byte, key []byte) ([]byte, error) {
|
||||||
|
//创建加密算法实例
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//获取块的大小
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
//对数据进行填充,让数据长度满足需求
|
||||||
|
origData = PKCS7Padding(origData, blockSize)
|
||||||
|
//采用AES加密方法中CBC加密模式
|
||||||
|
blocMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||||
|
crypted := make([]byte, len(origData))
|
||||||
|
//执行加密
|
||||||
|
blocMode.CryptBlocks(crypted, origData)
|
||||||
|
return crypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//AesDeCrypt 实现解密
|
||||||
|
func AesDeCrypt(cypted []byte, key []byte) ([]byte, error) {
|
||||||
|
//创建加密算法实例
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//获取块大小
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
//创建加密客户端实例
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||||
|
origData := make([]byte, len(cypted))
|
||||||
|
//这个函数也可以用来解密
|
||||||
|
blockMode.CryptBlocks(origData, cypted)
|
||||||
|
//去除填充字符串
|
||||||
|
origData, err = PKCS7UnPadding(origData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return origData, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//EnPwdCode 加密base64
|
||||||
|
func EnPwdCode(pwd, PwdKey []byte) (string, error) {
|
||||||
|
result, err := AesEcrypt(pwd, PwdKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(result), err
|
||||||
|
}
|
||||||
|
|
||||||
|
//DePwdCode 解密
|
||||||
|
func DePwdCode(pwd string, PwdKey []byte) ([]byte, error) {
|
||||||
|
//解密base64字符串
|
||||||
|
pwdByte, err := base64.StdEncoding.DecodeString(pwd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//执行AES解密
|
||||||
|
return AesDeCrypt(pwdByte, PwdKey)
|
||||||
|
}
|
||||||
16
pkg/crypto/md5.go
Normal file
16
pkg/crypto/md5.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
//GetMd5 生成32位md5字串
|
||||||
|
func GetMd5(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
h := md5.New()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
104
pkg/crypto/sing.go
Normal file
104
pkg/crypto/sing.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSign get the sign info
|
||||||
|
func GetSign(data interface{}, appSecret string) string {
|
||||||
|
md5ctx := md5.New()
|
||||||
|
switch v := reflect.ValueOf(data); v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
md5ctx.Write([]byte(v.String() + appSecret))
|
||||||
|
return hex.EncodeToString(md5ctx.Sum(nil))
|
||||||
|
case reflect.Struct:
|
||||||
|
orderStr := StructToMapSing(v.Interface(), appSecret)
|
||||||
|
md5ctx.Write([]byte(orderStr))
|
||||||
|
return hex.EncodeToString(md5ctx.Sum(nil))
|
||||||
|
case reflect.Ptr:
|
||||||
|
originType := v.Elem().Type()
|
||||||
|
if originType.Kind() != reflect.Struct {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dataType := reflect.TypeOf(data).Elem()
|
||||||
|
dataVal := v.Elem()
|
||||||
|
orderStr := buildOrderStr(dataType, dataVal, appSecret)
|
||||||
|
md5ctx.Write([]byte(orderStr))
|
||||||
|
return hex.EncodeToString(md5ctx.Sum(nil))
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOrderStr(t reflect.Type, v reflect.Value, appSecret string) (returnStr string) {
|
||||||
|
keys := make([]string, 0, t.NumField())
|
||||||
|
var data = make(map[string]interface{})
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
if t.Field(i).Tag.Get("json") == "sign" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[t.Field(i).Tag.Get("json")] = v.Field(i).Interface()
|
||||||
|
|
||||||
|
keys = append(keys, t.Field(i).Tag.Get("json"))
|
||||||
|
}
|
||||||
|
sort.Sort(sort.StringSlice(keys))
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, k := range keys {
|
||||||
|
if data[k] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteByte('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
switch vv := data[k].(type) {
|
||||||
|
case string:
|
||||||
|
buf.WriteString(vv)
|
||||||
|
case int:
|
||||||
|
case int8:
|
||||||
|
case int16:
|
||||||
|
case int32:
|
||||||
|
case int64:
|
||||||
|
buf.WriteString(strconv.FormatInt(int64(vv), 10))
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("&secret=" + appSecret)
|
||||||
|
returnStr = buf.String()
|
||||||
|
return returnStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func StructToMapSing(content interface{}, appSecret string) (returnStr string) {
|
||||||
|
t := reflect.TypeOf(content)
|
||||||
|
v := reflect.ValueOf(content)
|
||||||
|
returnStr = buildOrderStr(t, v, appSecret)
|
||||||
|
return returnStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnSign(query, body, key string) string {
|
||||||
|
//加密算法
|
||||||
|
//r:随机数
|
||||||
|
//t:时间戳
|
||||||
|
//q:query参数md5
|
||||||
|
//b:Body参数md5
|
||||||
|
//k:密钥
|
||||||
|
//组合成 k=%s&r=%d&t=%d&q=%s&b=%s
|
||||||
|
//进行md5
|
||||||
|
//最后组合成 r,t,md5
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
r := rand.Intn(800000) + 100000
|
||||||
|
t := time.Now().Unix()
|
||||||
|
str := fmt.Sprintf("k=%s&r=%d&t=%d&q=%s&b=%s", key, r, t, GetMd5(query), GetMd5(body))
|
||||||
|
return fmt.Sprintf("%d,%d,%s", r, t, GetSign(str, key))
|
||||||
|
}
|
||||||
15
pkg/integral/file.go
Normal file
15
pkg/integral/file.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package integral
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
//PathExists 路径是否存在
|
||||||
|
func PathExists(path string) (bool, error) {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
179
pkg/jwt/core.go
Normal file
179
pkg/jwt/core.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadModel 成员中套上jwt.RegisteredClaims
|
||||||
|
type LoadModel interface {
|
||||||
|
GetExpirationTime() (*jwt.NumericDate, error)
|
||||||
|
GetIssuedAt() (*jwt.NumericDate, error)
|
||||||
|
GetNotBefore() (*jwt.NumericDate, error)
|
||||||
|
GetIssuer() (string, error)
|
||||||
|
GetSubject() (string, error)
|
||||||
|
GetAudience() (jwt.ClaimStrings, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT jwt对象
|
||||||
|
type JWT struct {
|
||||||
|
// 声明签名信息
|
||||||
|
SigningKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJWT 初始化jwt对象
|
||||||
|
func NewJWT() *JWT {
|
||||||
|
return &JWT{
|
||||||
|
[]byte(os.Getenv("TOKEN_SALT")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateToken 调用jwt-go库生成token,编码的算法为jwt.SigningMethodHS256
|
||||||
|
func (j *JWT) CreateToken(payload jwt.Claims) (string, error) {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||||
|
return token.SignedString(j.SigningKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParserToken 将token解码并验证
|
||||||
|
func (j *JWT) ParserToken(tokenString string, model LoadModel) (jwt.Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, model, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return j.SigningKey, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = parserToken(token, model)
|
||||||
|
if token.Valid {
|
||||||
|
return token.Claims, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("token无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parserToken[T LoadModel](token *jwt.Token, model T) (jwt.Claims, error) {
|
||||||
|
if claims, ok := token.Claims.(T); !ok {
|
||||||
|
return nil, fmt.Errorf("token结构错误")
|
||||||
|
} else {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateToken 用于快速生成一个Token
|
||||||
|
// UserLoad 用户负载
|
||||||
|
// ExpiresAt 多少秒之后过期,单位:秒
|
||||||
|
// Issuer 签名颁发着
|
||||||
|
func CreateToken(JWTLoad jwt.Claims) (Token string, err error) {
|
||||||
|
// 构造SignKey: 签名和解签名需要使用一个值
|
||||||
|
j := NewJWT()
|
||||||
|
// 构造用户claims信息(负荷)
|
||||||
|
token, err := j.CreateToken(JWTLoad)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStandardClaims 快速创建签名数据
|
||||||
|
func CreateStandardClaims(ExpiresAt int64, Issuer string) jwt.RegisteredClaims {
|
||||||
|
return jwt.RegisteredClaims{
|
||||||
|
Issuer: Issuer, // 签名颁发者
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(ExpiresAt))), // 签名过期时间
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()), //
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()), // 签名生效时间
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckToken 用于检查Token是否有效
|
||||||
|
// issuer为可选参数
|
||||||
|
func CheckToken[T LoadModel](token string, model T, issuer string) (bool, T, error) {
|
||||||
|
j := NewJWT()
|
||||||
|
load, err := j.ParserToken(token, model)
|
||||||
|
if err != nil {
|
||||||
|
return false, model, err
|
||||||
|
}
|
||||||
|
expTime, err := load.GetExpirationTime()
|
||||||
|
if expTime.Unix() < time.Now().Unix() {
|
||||||
|
return false, model, errors.New("token已过期,请重新登录")
|
||||||
|
}
|
||||||
|
loadIssuer, err := load.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
return false, model, err
|
||||||
|
}
|
||||||
|
if issuer == loadIssuer {
|
||||||
|
return true, load.(T), nil
|
||||||
|
}
|
||||||
|
return false, model, errors.New("token签名错误,请重新登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJwtProto 从Gin中获取Jwt原型体
|
||||||
|
// model为业务内jwt模型
|
||||||
|
func GetJwtProto[T any](c *gin.Context, model T) (T, error) {
|
||||||
|
tokener, _ := c.Get("token")
|
||||||
|
if tokener == nil {
|
||||||
|
return model, errors.New("JWT Is NULL")
|
||||||
|
}
|
||||||
|
token, ok := tokener.(T)
|
||||||
|
if !ok {
|
||||||
|
return model, errors.New("JWTLoad Error")
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenLoad 用于从GinContext中取回JWTUsermapm
|
||||||
|
func GetTokenLoad(c *gin.Context) (*JWTLoad, map[string]interface{}) {
|
||||||
|
token, _ := c.Get("token")
|
||||||
|
if token == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
load := token.(*JWTLoad)
|
||||||
|
if load.UserLoad == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return load, load.UserLoad.(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShouldBindTokenLoad(c *gin.Context, obj any) error {
|
||||||
|
var e error
|
||||||
|
token, _ := c.Get("token")
|
||||||
|
if token == nil {
|
||||||
|
e = errors.New("token is invalid")
|
||||||
|
}
|
||||||
|
load := token.(*JWTLoad)
|
||||||
|
if load.UserLoad == nil {
|
||||||
|
e = errors.New("token is illegal")
|
||||||
|
}
|
||||||
|
for k, v := range load.UserLoad.(map[string]interface{}) {
|
||||||
|
fmt.Println(v, k)
|
||||||
|
e = SetField(obj, k, v)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func SetField(obj interface{}, name string, value interface{}) error {
|
||||||
|
structValue := reflect.ValueOf(obj).Elem()
|
||||||
|
structFieldValue := structValue.FieldByName(name)
|
||||||
|
fmt.Println(structFieldValue)
|
||||||
|
|
||||||
|
if !structFieldValue.IsValid() {
|
||||||
|
return fmt.Errorf("no such field: %s in obj", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !structFieldValue.CanSet() {
|
||||||
|
return fmt.Errorf("cannot set %s field value", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
structFieldType := structFieldValue.Type()
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
if structFieldType != val.Type() {
|
||||||
|
return errors.New("provided value type didn't match obj field type")
|
||||||
|
}
|
||||||
|
|
||||||
|
structFieldValue.Set(val)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
pkg/jwt/examples_test.go
Normal file
46
pkg/jwt/examples_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package jwt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"ripper/internal/middleware"
|
||||||
|
"ripper/internal/response"
|
||||||
|
jwtpkg "ripper/pkg/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务模型
|
||||||
|
type UserLoad struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速创建接口
|
||||||
|
func newUserLoad(ID uint, ExpiresAt int64, Issuer string) *UserLoad {
|
||||||
|
return &UserLoad{
|
||||||
|
ID: ID,
|
||||||
|
RegisteredClaims: jwtpkg.CreateStandardClaims(ExpiresAt, Issuer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandler(c *gin.Context) {
|
||||||
|
// 获取jwt数据并处理错误
|
||||||
|
// 可以在中间件完成这一步,根据业务自行扩展
|
||||||
|
token, err := jwtpkg.GetJwtProto(c, &UserLoad{})
|
||||||
|
if err != nil {
|
||||||
|
response.FailJson(c, response.FailStruct{
|
||||||
|
Code: 401,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
fmt.Println(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
//使用中间件时绑定业务jwt模型,并且可以设置验证的签发人
|
||||||
|
r.Use(middleware.JWTAuth(&UserLoad{}, "is"))
|
||||||
|
r.GET("/test", testHandler)
|
||||||
|
r.Run()
|
||||||
|
}
|
||||||
9
pkg/jwt/model.go
Normal file
9
pkg/jwt/model.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import "github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
|
type JWTLoad struct {
|
||||||
|
UserLoad interface{} `json:"user_load"`
|
||||||
|
Version int64
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
5
pkg/logger/error.go
Normal file
5
pkg/logger/error.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
func Error(err error) {
|
||||||
|
|
||||||
|
}
|
||||||
11
pkg/message/message_other.go
Normal file
11
pkg/message/message_other.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowAppLaunchMessage() {
|
||||||
|
log.Printf("%s: %s\n", "运行成功", "服务已经启动.")
|
||||||
|
}
|
||||||
24
pkg/message/message_windows.go
Normal file
24
pkg/message/message_windows.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MB_OK = 0x00000000
|
||||||
|
MB_ICONINFORMATION = 0x00000040
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
|
messageBoxW = user32.NewProc("MessageBoxW")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowAppLaunchMessage() {
|
||||||
|
titlePtr, _ := syscall.UTF16PtrFromString("运行成功")
|
||||||
|
textPtr, _ := syscall.UTF16PtrFromString("服务已经启动, GUI的程序将会以后台方式运行, 如需关闭请手动结束进程.")
|
||||||
|
messageBoxW.Call(0, uintptr(unsafe.Pointer(textPtr)), uintptr(unsafe.Pointer(titlePtr)), MB_OK|MB_ICONINFORMATION)
|
||||||
|
}
|
||||||
19
pkg/util/crypto.go
Normal file
19
pkg/util/crypto.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"ripper/pkg/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckPassword 用于将用户输入的密码与数据库取出的密码进行比对
|
||||||
|
func CheckPassword(PlainText, SecretKey, CipherText string) bool {
|
||||||
|
chK, _ := crypto.AesEcrypt([]byte(PlainText), []byte(SecretKey))
|
||||||
|
return fmt.Sprintf("%x", md5.Sum(chK)) == CipherText
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePassword 用于将用户输入的密码进行加密
|
||||||
|
func CreatePassword(SecretKey, PlainText string) string {
|
||||||
|
chK, _ := crypto.AesEcrypt([]byte(PlainText), []byte(SecretKey))
|
||||||
|
return fmt.Sprintf("%x", md5.Sum(chK))
|
||||||
|
}
|
||||||
63
pkg/util/encrypt.go
Normal file
63
pkg/util/encrypt.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmojiEncode Emoji表情转码
|
||||||
|
func EmojiDecode(s string) string {
|
||||||
|
//emoji表情的数据表达式
|
||||||
|
var re *regexp.Regexp
|
||||||
|
var reg *regexp.Regexp
|
||||||
|
var src []string
|
||||||
|
re = regexp.MustCompile("\\[[\\\\u0-9a-zA-Z]")
|
||||||
|
//提取emoji数据表达式
|
||||||
|
reg = regexp.MustCompile("\\[\\\\u|]")
|
||||||
|
src = re.FindAllString(s, -1)
|
||||||
|
for i := 0; i < len(src); i++ {
|
||||||
|
var e = reg.ReplaceAllString(src[i], "")
|
||||||
|
var p, err = strconv.ParseInt(e, 16, 32)
|
||||||
|
if err == nil {
|
||||||
|
s = strings.Replace(s, src[i], string(rune(p)), -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表情转换
|
||||||
|
func EmojiCode(s string) string {
|
||||||
|
var ret string
|
||||||
|
var rs []rune
|
||||||
|
rs = []rune(s)
|
||||||
|
|
||||||
|
for i := 0; i < len(rs); i++ {
|
||||||
|
if len(string(rs[i])) == 4 {
|
||||||
|
ret += `[\u` + strconv.FormatInt(int64(rs[i]), 16) + `]`
|
||||||
|
} else {
|
||||||
|
ret += string(rs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseEncode Base64编码
|
||||||
|
func BaseEncode(s string) string {
|
||||||
|
data := []byte(s)
|
||||||
|
dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.StdEncoding.Encode(dst, data)
|
||||||
|
return string(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseDecode Base64解码
|
||||||
|
func BaseDecode(s string) string {
|
||||||
|
data := []byte(s)
|
||||||
|
dst := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
||||||
|
n, err := base64.StdEncoding.Decode(dst, data)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(dst[:n])
|
||||||
|
}
|
||||||
9
pkg/util/memory.go
Normal file
9
pkg/util/memory.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func DeepCoyp(s string) string {
|
||||||
|
b := make([]byte, len(s))
|
||||||
|
copy(b, s)
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
50
pkg/util/misc.go
Normal file
50
pkg/util/misc.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ifs 三目运算的函数
|
||||||
|
func Ifs[T any](a bool, b, c T) T {
|
||||||
|
if a {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUUID() (string, error) {
|
||||||
|
u2, err := uuid.NewV4()
|
||||||
|
return u2.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
var src = rand.NewSource(time.Now().UnixNano())
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 6 bits to represent a letter index
|
||||||
|
letterIdBits = 6
|
||||||
|
// All 1-bits as many as letterIdBits
|
||||||
|
letterIdMask = 1<<letterIdBits - 1
|
||||||
|
letterIdMax = 63 / letterIdBits
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomStr(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
|
||||||
|
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
|
||||||
|
if remain == 0 {
|
||||||
|
cache, remain = src.Int63(), letterIdMax
|
||||||
|
}
|
||||||
|
if idx := int(cache & letterIdMask); idx < len(letters) {
|
||||||
|
b[i] = letters[idx]
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
cache >>= letterIdBits
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
236
static/public/code.html
Normal file
236
static/public/code.html
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>登录 Github Copilot</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #0066CC;
|
||||||
|
--hover-color: #0256A8;
|
||||||
|
--background-light: #ffffff;
|
||||||
|
--text-color: #1d1d1f;
|
||||||
|
--input-background: #fbfbfd;
|
||||||
|
--input-border: #d2d2d7;
|
||||||
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
|
||||||
|
background-color: var(--background-light);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background-color: var(--background-light);
|
||||||
|
padding: 3em 4em;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 440px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 6px 0;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--input-background);
|
||||||
|
font-size: 15px;
|
||||||
|
transition: var(--transition);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-light: #1d1d1f;
|
||||||
|
--text-color: #f5f5f7;
|
||||||
|
--input-background: #2c2c2e;
|
||||||
|
--input-border: #3a3a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#password {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<h1>登录 Github Copilot</h1>
|
||||||
|
<form onsubmit="submitForm()">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input type="text" id="password"
|
||||||
|
placeholder="请输入访问密码">
|
||||||
|
</div>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input type="text" id="authorization"
|
||||||
|
placeholder="请输入授权码" autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input type="text" id="displayUserName"
|
||||||
|
placeholder="GitHub 用户名(可选)">
|
||||||
|
</div>
|
||||||
|
<button type="submit">登录</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2024 Open Source Contributors
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
async function getLoginConfig() {
|
||||||
|
const result = await fetch('/login/config');
|
||||||
|
const resultJson = await result.json();
|
||||||
|
if (resultJson.is_login_password) {
|
||||||
|
document.getElementById('password').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoginConfig();
|
||||||
|
|
||||||
|
function getQueryParam(param) {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCodeInputValue(){
|
||||||
|
const code = getQueryParam('user_code');
|
||||||
|
if (code !== null) {
|
||||||
|
document.getElementById('authorization').value = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCodeInputValue();
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
event.preventDefault();
|
||||||
|
const authorization = document.getElementById('authorization').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const displayUserName = document.getElementById('displayUserName').value;
|
||||||
|
const code = getQueryParam('user_code');
|
||||||
|
|
||||||
|
if (code === null) {
|
||||||
|
alert('链接打开方式不正确,请重新打开');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorization === '') {
|
||||||
|
alert('请输入授权码');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/login/device', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
authorization,
|
||||||
|
password,
|
||||||
|
displayUserName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
alert('HTTP error! status: ' + response.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.error !== 0) {
|
||||||
|
alert(result.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
alert('登录成功, 请返回IDE查看并使用');
|
||||||
|
window.close();
|
||||||
|
} catch (e) {
|
||||||
|
alert('提交表单时出错,请稍后再试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
278
static/public/help.html
Normal file
278
static/public/help.html
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Copilot 配置指南</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #0066CC;
|
||||||
|
--background-light: #ffffff;
|
||||||
|
--text-color: #1d1d1f;
|
||||||
|
--code-background: #f5f7fa;
|
||||||
|
--border-color: #d2d2d7;
|
||||||
|
--warning-background: #fff7ed;
|
||||||
|
--warning-border: #fec589;
|
||||||
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: var(--background-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.5rem;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: var(--code-background);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
background-color: var(--code-background);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: var(--warning-background);
|
||||||
|
border: 1px solid var(--warning-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning::before {
|
||||||
|
content: "🚨";
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
background-color: var(--code-background);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note::before {
|
||||||
|
content: "🚧";
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-light: #1d1d1f;
|
||||||
|
--text-color: #f5f5f7;
|
||||||
|
--code-background: #2c2c2e;
|
||||||
|
--border-color: #3a3a3c;
|
||||||
|
--warning-background: #3a3123;
|
||||||
|
--warning-border: #8b5e34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-domain {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#show-hb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hidden-hb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Copilot 配置指南</h1>
|
||||||
|
|
||||||
|
<h2>VSCode</h2>
|
||||||
|
<div class="step">安装插件: <code>GitHub Copilot</code></div>
|
||||||
|
<div class="step">修改 VSCode 的 settings.json 文件, 添加以下配置:</div>
|
||||||
|
<div class="config-wrapper">
|
||||||
|
<pre id="configCode">{
|
||||||
|
"github.copilot.advanced": {
|
||||||
|
"authProvider": "github-enterprise",
|
||||||
|
"debug.overrideCAPIUrl": "https://api.<span class="dynamic-domain">loading...</span>",
|
||||||
|
"debug.overrideProxyUrl": "https://copilot-proxy.<span class="dynamic-domain">loading...</span>",
|
||||||
|
"debug.chatOverrideProxyUrl": "https://api.<span class="dynamic-domain">loading...</span>/chat/completions",
|
||||||
|
"debug.overrideFastRewriteEngine": "v1/engines/copilot-centralus-h100",
|
||||||
|
"debug.overrideFastRewriteUrl": "https://api.<span class="dynamic-domain">loading...</span>"
|
||||||
|
},
|
||||||
|
"github-enterprise.uri": "https://<span class="dynamic-domain">loading...</span>"
|
||||||
|
}</pre>
|
||||||
|
<button id="copyBtn" class="copy-button">复制配置</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Jetbrains IDE系列</h2>
|
||||||
|
<div class="step">找到<code>设置</code> > <code>语言与框架</code> > <code>GitHub Copilot</code> > <code>Authentication
|
||||||
|
Provider</code></div>
|
||||||
|
<div class="step">填写的值为: <code><span class="dynamic-domain">loading...</span></code></div>
|
||||||
|
|
||||||
|
<h2>Visual Studio 2022</h2>
|
||||||
|
<div class="step">更新到最新版本(内置 Copilot 版本)至少是 <code>17.10.x</code> 以上</div>
|
||||||
|
<div class="step">首先, 开启 Github Enterprise 账户支持:工具->环境->账户->勾选 <code>包含 Github Enterprise 服务器账户</code></div>
|
||||||
|
<div class="step">然后, 重启你的 <code>Visual Studio 2022</code> 编辑器</div>
|
||||||
|
<div class="step">最后, 点击添加 Github 账户,切换到 Github Enterprise 选项卡,输入 <code>https://<span
|
||||||
|
class="dynamic-domain">loading...</span></code> 即可。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>HBuilderX</h2>
|
||||||
|
<div id="show-hb">
|
||||||
|
<div class="step">点击下载 <code><a href="https://pan.quark.cn/s/70e6849970e5" target="_blank">copilot-for-hbuilderx-v1.zip</a></code>
|
||||||
|
插件到本地
|
||||||
|
</div>
|
||||||
|
<div class="step">将插件安装到 plugin目录下, 具体教程参考: <code><a
|
||||||
|
href="https://hx.dcloud.net.cn/Tutorial/OfflineInstall" target="_blank">离线插件安装指南</a></code></div>
|
||||||
|
<div class="step">重启 Hbuilder X 后点击登录 <code>GitHub Copilot</code> 即可.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="hidden-hb" class="warning">
|
||||||
|
当前部署方式不支持 HBuilderX
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2024 Open Source Contributors
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// 获取当前域名和端口
|
||||||
|
const currentHost = window.location.host; // 这将获取 "domain:port" 格式
|
||||||
|
|
||||||
|
// 只要是默认本地部署的域名,就显示 HBuilderX 的安装步骤
|
||||||
|
if (currentHost.includes('copilot.supercopilot.top')) {
|
||||||
|
document.getElementById('show-hb').style.display = 'block';
|
||||||
|
document.getElementById('hidden-hb').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
document.getElementById('show-hb').style.display = 'none';
|
||||||
|
document.getElementById('hidden-hb').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有需要替换的地方
|
||||||
|
const domainElements = document.querySelectorAll('.dynamic-domain');
|
||||||
|
domainElements.forEach(element => {
|
||||||
|
element.textContent = currentHost;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('copyBtn').addEventListener('click', function () {
|
||||||
|
const configText = document.getElementById('configCode').textContent;
|
||||||
|
navigator.clipboard.writeText(configText).then(() => {
|
||||||
|
const originalText = this.textContent;
|
||||||
|
this.textContent = '已复制!';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
265
static/public/login.html
Normal file
265
static/public/login.html
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>获取 GitHub GHU</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #0066CC;
|
||||||
|
--hover-color: #0256A8;
|
||||||
|
--background-light: #ffffff;
|
||||||
|
--text-color: #1d1d1f;
|
||||||
|
--input-background: #fbfbfd;
|
||||||
|
--input-border: #d2d2d7;
|
||||||
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', sans-serif;
|
||||||
|
background-color: var(--background-light);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background-color: var(--background-light);
|
||||||
|
padding: 3.5em 4em;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 440px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 6px 0;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--input-background);
|
||||||
|
font-size: 15px;
|
||||||
|
transition: var(--transition);
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 90%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#code {
|
||||||
|
font-size: 32px;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-family: SF Mono, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.measure-time {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#token-box {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-light: #1d1d1f;
|
||||||
|
--text-color: #f5f5f7;
|
||||||
|
--input-background: #2c2c2e;
|
||||||
|
--input-border: #3a3a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #86868b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<h1>获取 GitHub GHU</h1>
|
||||||
|
<div class="form-box">
|
||||||
|
<div id="token-box">
|
||||||
|
<div class="code-content">
|
||||||
|
<div id="code"></div>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="token" placeholder="正在获取ghu_" readonly/>
|
||||||
|
<div id="timeing" class="submit-button measure-time">剩余时间: 900 秒</div>
|
||||||
|
</div>
|
||||||
|
<div id="submit-btn" class="submit-button" onclick="onSubmit()">登录 GitHub 获取授权码</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2024 Open Source Contributors
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
let device_code = null;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
function countDown(time = 900) {
|
||||||
|
const timeing = document.getElementById('timeing');
|
||||||
|
timer = setInterval(() => {
|
||||||
|
time -= 1;
|
||||||
|
timeing.innerText = `剩余时间: ${time} 秒`;
|
||||||
|
if (time <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
document.getElementById('submit-btn').style.display = 'block';
|
||||||
|
timeing.style.display = 'none';
|
||||||
|
alert("授权码已过期,请重新获取");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
const response = await fetch('/github/login/device/code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
alert('获取授权码失败')
|
||||||
|
window.location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const resultJson = await response.json()
|
||||||
|
confirm(`打开浏览器访问: ${resultJson.verification_uri}, 并输入授权码: ${resultJson.user_code}`)
|
||||||
|
copyCode(resultJson.user_code)
|
||||||
|
device_code = resultJson.device_code
|
||||||
|
document.getElementById('submit-btn').style.display = 'none';
|
||||||
|
document.getElementById('token-box').style.display = 'block';
|
||||||
|
document.getElementById("code").innerText = resultJson.user_code;
|
||||||
|
countDown(resultJson.expires_in);
|
||||||
|
window.open(resultJson.verification_uri);
|
||||||
|
// 根据user_code获取ghu_token
|
||||||
|
await getGhuToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCode(user_code) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.value = user_code;
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGhuToken() {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||||
|
const response = await fetch('/github/login/ghu-token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({device_code})
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
alert('获取ghu_token失败')
|
||||||
|
window.location.reload()
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const resultJson = await response.json()
|
||||||
|
if (resultJson.error === "slow_down" && resultJson.error_description === "Too many requests have been made in the same timeframe.") {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, resultJson.interval * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = resultJson?.access_token || null
|
||||||
|
if (access_token !== null) {
|
||||||
|
document.getElementById('token').value = access_token
|
||||||
|
clearInterval(timer);
|
||||||
|
document.getElementById("timeing").style.display = 'none';
|
||||||
|
document.getElementById("code").style.display = 'none';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await getGhuToken()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
static/static.go
Normal file
8
static/static.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package static
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed public
|
||||||
|
Public embed.FS
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user