Compare commits

...

12 Commits

Author SHA1 Message Date
topfive 770c3d55aa Update Cerobot.py 2025-07-18 00:48:41 +08:00
topfive 4c131df5be Update Cerobot.py 2025-07-18 00:38:47 +08:00
topfive 10dcf0331a Update Cerobot.py 2025-07-18 00:38:13 +08:00
topfive 0ab45e54b8 Update Cerobot.py 2025-07-18 00:24:29 +08:00
topfive cf10bf8832 Update Cerobot.py 2025-07-18 00:05:23 +08:00
topfive 5bf5117e02 Update Cerobot.py 2025-07-17 23:38:24 +08:00
topfive fe498addcd Update config.py 2025-07-17 22:37:19 +08:00
topfive d60763d6ae refactor: .devops/未命名项目.yml 2025-07-17 21:55:48 +08:00
topfive 0bd34d8831 最终运维 2025-07-17 20:44:24 +08:00
topfive d36b6b1bef Update Dockerfile 2025-07-17 11:17:07 +08:00
topfive 06d6a6d22f refactor: .devops/未命名项目.yml 2025-07-17 11:15:01 +08:00
topfive 5bc893b262 refactor: .devops/未命名项目.yml 2025-07-17 11:14:32 +08:00
4461 changed files with 106760 additions and 970512 deletions

60
.devops/repo同步.yml Normal file
View File

@ -0,0 +1,60 @@
version: 2
name: repo同步
description: ""
global:
concurrent: 1
workflow:
- ref: start
name: 开始
task: start
- ref: end
name: 结束
task: end
needs:
- ssh_cmd_0
- ref: docker_image_build_0
name: docker镜像构建
task: docker_image_build@1.6.0
input:
docker_username: ((repo.dockeruser))
docker_password: ((repo.dockerpass))
image_name: '"crpi-nmcmhgtru1ytuxul.cn-hangzhou.personal.cr.aliyuncs.com/gitlink_gonggong123zzz/repo"'
image_tag: '"latest"'
registry_address: '"crpi-nmcmhgtru1ytuxul.cn-hangzhou.personal.cr.aliyuncs.com"'
docker_file: '"Dockerfile"'
docker_build_path: '"."'
workspace: git_clone_0.git_path
image_push: true
build_args: '""'
needs:
- git_clone_0
- ref: git_clone_0
name: git clone
task: git_clone@1.2.9
input:
username: ((repo.user))
password: ((repo.userpass))
remote_url: '"https://www.gitlink.org.cn/gonggong123zzz/reposync.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1
needs:
- start
- ref: ssh_cmd_0
name: ssh执行命令
task: ssh_cmd@1.1.1
input:
ssh_pass: ((repo.sshpass))
ssh_ip: '"118.31.168.130"'
ssh_port: '"22"'
ssh_user: '"root"'
ssh_cmd: >-
"docker stop ghc_group1_01 || true && docker rm ghc_group1_01 || true
&& docker pull
crpi-nmcmhgtru1ytuxul.cn-hangzhou.personal.cr.aliyuncs.com/gitlink_gonggong123zzz/repo:latest
&& docker run -d -p 8000:8000 --name ghc_group1_01 \
-e BOOT_MODE=app \
crpi-nmcmhgtru1ytuxul.cn-hangzhou.personal.cr.aliyuncs.com/gitlink_gonggong123zzz/repo:latest"
needs:
- docker_image_build_0

View File

@ -18,7 +18,7 @@ workflow:
input:
username: ((yjh.gitlink_user))
password: ((yjh.gitlink_pwd))
remote_url: '"https://gitlink.org.cn/jkcl/reposync.git"'
remote_url: '"https://gitlink.org.cn/topfive/reposync.git"'
ref: '"refs/heads/master"'
commit_id: '""'
depth: 1

14
.gitignore vendored
View File

@ -1,8 +1,8 @@
logs/
.git
__pycache__
*.pyc
*.pyo
build
dist
logs/
.git
__pycache__
*.pyc
*.pyo
build
dist
.vscode

View File

@ -1,42 +0,0 @@
stages:
- build
- deploy
variables:
DOCKER_IMAGE: reg.docker.alibaba-inc.com/ob-robot/reposyncer
DOCKER_TAG: $CI_COMMIT_SHORT_SHA
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE:$DOCKER_TAG .
- docker tag $DOCKER_IMAGE:$DOCKER_TAG $DOCKER_IMAGE:latest
- docker push $DOCKER_IMAGE:$DOCKER_TAG
- docker push $DOCKER_IMAGE:latest
only:
- default
- main
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEPLOY_HOST >> ~/.ssh/known_hosts
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/reposync && docker-compose pull && docker-compose up -d"
environment:
name: production
url: http://114.55.237.214
only:
- default
- main

206
API.md
View File

@ -1,206 +0,0 @@
## 环境变量
```python
# 同步任务执行完成后,是否删除同步目录
DELETE_SYNC_DIR = getenv('DELETE_SYNC_DIR', False)
# 是否在日志中详细记录git执行错误时的信息
LOG_DETAIL = getenv('LOG_DETAIL', True)
# 同步目录环境变量
SYNC_DIR = os.getenv("SYNC_DIR", "/tmp/sync_dir/")
```
## 仓库绑定
允许用户通过此接口绑定仓库信息。
- **URL**`/cerobot/sync/repo`
- **Method**`POST`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| enable | bool | true/false | yes | 同步状态 |
| internal_repo_address | string | | yes | 内部仓库地址 |
| external_repo_address | string | | yes | 外部仓库地址 |
| sync_granularity | enum('all', 'one') | 1 为仓库粒度的同步<br />2 为分支粒度的同步 | yes | 同步粒度 |
| sync_direction | enum('to_outer', 'to_inter') | 1 表示内部仓库同步到外部<br />2 表示外部仓库同步到内部 | yes | 同步方向 |
### 请求示例
```json
{
"enable": true,
"repo_name": "ob-robot-test",
"internal_repo_address": "",
"external_repo_address": "",
"sync_granularity": 2,
"sync_direction": 1
}
```
## 分支绑定
允许用户通过此接口在对应仓库上绑定分支。
- **URL**`/cerobot/sync/{repo_name}/branch`
- **Method**`POST`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| enable | bool | true/false | yes | 同步状态 |
| internal_branch_name | string | | yes | 内部分支名称 |
| external_branch_name | string | | yes | 外部分支名称 |
### 请求示例
```json
"repo_name": "ob-robot-test"
{
"enable": true,
"internal_branch_name": "test",
"external_branch_name": "test"
}
```
## 仓库粒度同步
允许用户通过此接口执行单个仓库同步(或强制同步)。
- **URL**`/cerobot/sync/repo/{repo_name}`
- **Method**`POST`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- |--------| --- |------|--------|
| repo_name | string | | yes | 仓库名称 |
| force_flag | bool | | no | 是否强制同步 |
### 成功响应
**条件**:同步执行成功。<br />**状态码:**`0 操作成功`<br />**响应示例**
```json
{
"code_status": 0,
"data": null,
"msg": "操作成功"
}
```
### 错误响应
**条件**:同步执行未成功。<br />**状态码:**`2xxxx 表示git异常错误`<br />**响应示例**
```json
{
"code_status": 20009,
"data": null,
"msg": "分支不存在"
}
```
## 分支粒度同步
允许用户通过此接口执行单个分支同步(或强制同步)。
- **URL**`/cerobot/sync/{repo_name}/branch/{branch_name}`
- **Method**`POST`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
|-------------| --- | --- | --- |--------------------------------------------|
| repo_name | string | | yes | 仓库名称 |
| sync_direct | int | 1/2 | yes | 同步方向:<br/>1 表示内部仓库同步到外部<br />2 表示外部仓库同步到内部 |
| branch_name | string | | yes | 分支名称 |
| force_flag | bool | | no | 是否强制同步 |
注: 仓库由内到外同步时,分支输入内部仓库分支名;仓库由外到内同步时,分支输入外部仓库分支名;
### 成功响应
**条件**:同步执行成功。<br />**状态码:**`0 操作成功`<br />**响应示例**
```json
{
"code_status": 0,
"data": null,
"msg": "操作成功"
}
```
### 错误响应
**条件**:同步执行未成功。<br />**状态码:**`2xxxx 表示git异常错误`<br />**响应示例**
```json
{
"code_status": 20009,
"data": null,
"msg": "分支不存在"
}
```
## 获取仓库信息
允许用户通过此接口分页获取仓库信息。
- **URL**`/cerobot/sync/repo`
- **Method**`GET`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| page_num | int | | no | 页数 |
| page_size | int | | no | 条数 |
| create_sort | bool | | no | 创建时间排序, 默认倒序 |
## 获取分支信息
允许用户通过此接口分页获取仓库信息。
- **URL**`/cerobot/sync/{repo_name}/branch`
- **Method**`GET`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| page_num | int | | no | 页数 |
| page_size | int | | no | 条数 |
| create_sort | bool | | no | 创建时间排序, 默认倒序 |
## 仓库解绑
允许用户通过此接口解绑对应仓库信息,该仓库下的分支也全部解绑。
- **URL**`/cerobot/sync/repo/{repo_name}`
- **Method**`DELETE`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
## 分支解绑
允许用户通过此接口解绑对应仓库的分支信息。
- **URL**`/cerobot/sync/{repo_name}/branch/{branch_name}`
- **Method**`DELETE`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| branch_name | string | <br /> | yes | 分支名称 |
注: 仓库由内到外同步时,分支输入内部仓库分支名;仓库由外到内同步时,分支输入外部仓库分支名;
## 仓库同步状态更新
允许用户通过此接口更新仓库的同步状态。
- **URL**`/cerobot/sync/repo/{repo_name}`
- **Method**`PUT`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| enable | bool | true/false | yes | 分支名称 |
## 分支同步状态更新
允许用户通过此接口更新对应仓库的分支同步状态。
- **URL**`/cerobot/sync/{repo_name}/branch/{branch_name}`
- **Method**`PUT`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
| --- | --- | --- | --- | --- |
| repo_name | string | | yes | 仓库名称 |
| branch_name | string | | yes | 分支名称 |
| enable | bool | true/false | yes | 分支名称 |
注: 仓库由内到外同步时,分支输入内部仓库分支名;仓库由外到内同步时,分支输入外部仓库分支名;
## 日志信息获取
允许用户通过此接口使用多个分支ID或多个仓库名称分页获取仓库/分支的同步日志。
- **URL**`/cerobot/sync/repo/{repo_name}/logs`
- **Method**`GET`
### 请求参数body
| 参数 | 类型 | 示例输入 | 是否必须 | 说明 |
|--------------|--------|---------|------| --- |
| repo_name | string | | yes | 仓库名称 |
| branch_id | string | 1,2,3 | no | 分支id |
| page_num | int | 默认1 | no | 页数 |
| page_size | int | 默认10 | no | 条数 |
| create_sort | bool | 默认False | no |创建时间排序, 默认倒序|
注: 获取仓库粒度的同步日志时无需输入分支id

View File

@ -1,232 +0,0 @@
# RepoSync 云端部署指南
## 概述
本指南将帮助您将 RepoSync 项目部署到云端服务器IP: 114.55.237.214)。
## 部署架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GitLink │ │ 云端服务器 │ │ 本地开发环境 │
│ CI/CD │───▶│ 114.55.237.214 │◀───│ 代码仓库 │
│ 流水线 │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ Docker │
│ Containers │
│ │
│ ┌─────────────┐ │
│ │ MySQL │ │
│ │ Database │ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ RepoSync │ │
│ │ Backend │ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ Nginx │ │
│ │ Frontend │ │
│ └─────────────┘ │
└─────────────────┘
```
## 部署步骤
### 第一步:配置环境变量
1. **配置数据库环境变量**
```bash
# 编辑 env.db 文件
vim env.db
# 修改以下配置:
MYSQL_ROOT_PASSWORD=your_strong_root_password_here
MYSQL_USER=reposync_user
MYSQL_PASSWORD=your_strong_user_password_here
BUC_KEY=your_encryption_key_here
```
2. **配置应用环境变量**
```bash
# 编辑 env.production 文件
vim env.production
# 修改数据库连接配置:
CEROBOT_MYSQL_HOST=114.55.237.214
CEROBOT_MYSQL_PORT=3306
CEROBOT_MYSQL_USER=reposync_user
CEROBOT_MYSQL_PWD=your_strong_user_password_here
CEROBOT_MYSQL_DB=reposync
```
### 第二步:部署数据库
1. **执行数据库部署脚本**
```bash
# 给脚本执行权限
chmod +x deploy-db.sh
# 执行数据库部署
./deploy-db.sh
```
2. **验证数据库部署**
- 访问 phpMyAdmin: http://114.55.237.214:8080
- 用户名: reposync_user
- 密码: 您在 env.db 中设置的密码
### 第三步:推送代码到 GitLink
1. **提交本地更改**
```bash
git add .
git commit -m "feat: 添加云端部署配置和数据库初始化"
git push origin default
```
2. **配置 GitLink CI/CD 变量**
在 GitLink 项目设置中配置以下 CI/CD 变量:
- `CI_REGISTRY`: Docker 镜像仓库地址
- `CI_REGISTRY_USER`: Docker 仓库用户名
- `CI_REGISTRY_PASSWORD`: Docker 仓库密码
- `SSH_PRIVATE_KEY`: 服务器 SSH 私钥
- `DEPLOY_HOST`: 114.55.237.214
- `DEPLOY_USER`: root
### 第四步:部署应用
1. **自动部署(推荐)**
- 推送代码到 GitLink 后CI/CD 流水线会自动触发
- 流水线会自动构建 Docker 镜像并部署到服务器
2. **手动部署**
```bash
# 给脚本执行权限
chmod +x deploy.sh
# 执行应用部署
./deploy.sh
```
### 第五步:验证部署
1. **检查服务状态**
```bash
# 连接到服务器
ssh root@114.55.237.214
# 检查容器状态
docker ps
# 检查应用日志
docker logs reposync-backend
```
2. **访问应用**
- 前端界面: http://114.55.237.214
- API 文档: http://114.55.237.214/docs
- 健康检查: http://114.55.237.214/health
## 数据库表结构
部署完成后,数据库中会包含以下表:
### Issue 同步相关表
- `issue`: Issue 信息表
- `issue_sync_job`: Issue 同步任务表
- `issue_sync_log`: Issue 同步日志表
### 仓库同步相关表
- `sync_repo_mapping`: 同步仓库映射表
- `sync_branch_mapping`: 同步分支映射表
- `repo_sync_log`: 仓库同步日志表
### 配置和映射表
- `sync_config`: 同步配置表
- `issue_mapping`: Issue 映射表
- `pr_mapping`: PR 映射表
- `pr_comment_mapping`: PR 评论映射表
- `sync_log`: 同步日志表
- `system_config`: 系统配置表
## 监控和维护
### 日志查看
```bash
# 查看应用日志
docker logs -f reposync-backend
# 查看数据库日志
docker logs -f reposync-mysql
# 查看 Nginx 日志
docker logs -f reposync-nginx
```
### 数据备份
```bash
# 备份数据库
docker exec reposync-mysql mysqldump -u root -p reposync > backup.sql
# 恢复数据库
docker exec -i reposync-mysql mysql -u root -p reposync < backup.sql
```
### 服务重启
```bash
# 重启所有服务
docker-compose restart
# 重启特定服务
docker-compose restart reposync-backend
```
## 故障排除
### 常见问题
1. **数据库连接失败**
- 检查 env.production 中的数据库配置
- 确认数据库容器已启动
- 检查防火墙设置
2. **应用启动失败**
- 查看应用日志: `docker logs reposync-backend`
- 检查环境变量配置
- 确认数据库连接正常
3. **端口访问失败**
- 检查服务器防火墙设置
- 确认端口映射正确
- 检查容器状态
### 联系支持
如果遇到问题,请:
1. 查看相关日志文件
2. 检查配置文件
3. 联系技术支持团队
## 安全建议
1. **修改默认密码**
- 数据库 root 密码
- 应用用户密码
- 加密密钥
2. **配置防火墙**
- 只开放必要端口
- 限制访问来源
3. **定期备份**
- 数据库备份
- 配置文件备份
4. **监控访问**
- 查看访问日志
- 监控异常访问

View File

@ -1,27 +1,27 @@
FROM centos:7
RUN yum update -y && \
yum install -y wget gcc make openssl-devel bzip2-devel libffi-devel zlib-devel
RUN wget -P /data/ob-tool https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz
RUN cd /data/ob-tool && tar xzf Python-3.9.6.tgz
RUN cd /data/ob-tool/Python-3.9.6 && ./configure --enable-optimizations && make altinstall
ADD ./ /data/ob-robot/
RUN cd /data/ob-robot/ && \
pip3.9 install -r /data/ob-robot/requirement.txt
RUN yum install -y git openssh-server
ENV GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa'
RUN yum install -y autoconf gettext && \
wget http://github.com/git/git/archive/v2.32.0.tar.gz && \
tar -xvf v2.32.0.tar.gz && \
rm -f v2.32.0.tar.gz && \
cd git-* && \
make configure && \
./configure --prefix=/usr && \
make -j16 && \
make install
WORKDIR /data/ob-robot
CMD if [ "$BOOT_MODE" = "app" ] ; then python3.9 main.py; fi
FROM centos:7
# 配置yum源为阿里云镜像
RUN mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak && \
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && \
yum clean all && \
yum makecache
RUN yum install -y wget gcc make openssl-devel bzip2-devel libffi-devel zlib-devel
RUN wget -P /data/ob-tool https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz
RUN cd /data/ob-tool && tar xzf Python-3.9.6.tgz
RUN cd /data/ob-tool/Python-3.9.6 && ./configure --enable-optimizations && make altinstall
ADD ./ /data/ob-robot/
RUN cd /data/ob-robot/ && \
pip3.9 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r /data/ob-robot/requirement.txt
# Install OpenSSH and a modern version of Git from the IUS repository.
# This avoids manual compilation, making the build faster and more reliable.
RUN yum install -y openssh-server && \
yum install -y https://repo.ius.io/ius-release-el7.rpm && \
yum install -y git236
ENV GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa'
WORKDIR /data/ob-robot
CMD if [ "$BOOT_MODE" = "app" ] ; then python3.9 main.py; fi

View File

@ -1,57 +0,0 @@
### 依赖
name|version|necessity
--|:--:|--:
python|3.9|True
uvicorn|0.14.0|True
SQLAlchemy|1.4.21|True
fastapi|0.66.0|True
aiohttp|3.7.4|True
pydantic|1.8.2|True
starlette|0.14.2|True
aiomysql|0.0.21|True
requests|2.25.1|True
loguru|0.6.0|True
typing-extensions|4.1.1|True
aiofiles|0.8.0|True
### 如何安装
> [!NOTE]
> 运行代码必须在python 3.9环境下面
`pip3 install -r requirement.txt`
### 部署数据库
- 创建一个自己的database
- 仓库目录下的 sql/20240408.sql 文件已列出需要在数据库中创建的表结构
- 设置自己的数据库连接串在src/base/config.py文件内
DB 变量的 test_env配置数据库参数
`'host': 数据库服务器的主机名或IP地址。可以通过环境变量 'CEROBOT_MYSQL_HOST' 获取其值,也可以自己设置。`
`'port': 数据库服务器的端口号。可以通过环境变量 'CEROBOT_MYSQL_PORT' 获取其值, 默认端口号2883。`
`'user': 连接数据库的用户名。可以通过环境变量 'CEROBOT_MYSQL_USER' 获取其值,也可以自己设置。`
`'passwd': 连接数据库的密码。可以通过环境变量 'CEROBOT_MYSQL_PWD' 获取其值,也可以自己设置。`
`'dbname': 要连接的数据库的名称。可以通过环境变量 'CEROBOT_MYSQL_DB' 获取其值,也可以自己设置。`
## 启动服务
- python3 main.py
- 服务启动成功后查看API文档 [http://0.0.0.0:8000/docs](http://0.0.0.0:8000/docs)
- 历史日志文件记录在本地的 logs 目录下
## 环境变量说明
```python
# 同步任务执行完成后是否删除同步目录的环境变量
DELETE_SYNC_DIR = ('DELETE_SYNC_DIR', False)
# 是否在日志中详细记录git执行错误信息的环境变量
LOG_DETAIL = ('LOG_DETAIL', True)
# 设置同步目录的环境变量
SYNC_DIR = ("SYNC_DIR", "/tmp/sync_dir/")
```

102
LICENSE
View File

@ -1,51 +1,51 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1,37 +1,37 @@
VERSION := $(shell git rev-parse --short HEAD)
SHELL=/bin/bash
CONDA_ACTIVATE=source $$(conda info --base)/etc/profile.d/conda.sh ; conda activate
# Image URL to use all building/pushing image targets
export IMAGE=reg.docker.alibaba-inc.com/ob-robot/reposyncer:v0.0.1
all: backend-docker frontend-docker
##@ docker
extra-download: ## git clone the extra reference
git submodule update --init
frontend-build:
cd web && $(MAKE) static
docker-build: ## Build docker image
docker build -t ${IMAGE} .
docker-push: ## Push docker image
docker push ${IMAGE}
backend-docker: extra-download frontend-build docker-build docker-push
##@ run
py39:
$(CONDA_ACTIVATE) py39
run: py39
python main.py
static:
cd web && $(MAKE) static
backrun:
nohup python main.py > /tmp/robot.log 2>&1 &
VERSION := $(shell git rev-parse --short HEAD)
SHELL=/bin/bash
CONDA_ACTIVATE=source $$(conda info --base)/etc/profile.d/conda.sh ; conda activate
# Image URL to use all building/pushing image targets
export IMAGE=reg.docker.alibaba-inc.com/ob-robot/reposyncer:v0.0.1
all: backend-docker frontend-docker
##@ docker
extra-download: ## git clone the extra reference
git submodule update --init
frontend-build:
cd web && $(MAKE) static
docker-build: ## Build docker image
docker build -t ${IMAGE} .
docker-push: ## Push docker image
docker push ${IMAGE}
backend-docker: extra-download frontend-build docker-build docker-push
##@ run
py39:
$(CONDA_ACTIVATE) py39
run: py39
python main.py
static:
cd web && $(MAKE) static
backrun:
nohup python main.py > /tmp/robot.log 2>&1 &

View File

@ -1,399 +0,0 @@
# RepoSyncer 插件系统使用指南
## 概述
RepoSyncer 插件系统是一个可扩展的代码质量检测和安全扫描框架,支持多语言代码分析,提供智能修复建议。
## 快速开始
### 1. 启动服务
```bash
# 启动 RepoSyncer 服务
python main.py
```
### 2. 检查插件系统状态
```bash
# 检查健康状态
curl http://localhost:8000/health
# 查看插件系统状态
curl http://localhost:8000/plugins/status
```
### 3. 执行代码质量检测
```bash
# 对指定仓库进行代码质量分析
curl -X POST "http://localhost:8000/cerobot/plugins/quality/analyze" \
-H "Content-Type: application/json" \
-d '{
"repo_path": "/path/to/your/repository",
"languages": ["python", "javascript"],
"include_patterns": ["*.py", "*.js"],
"exclude_patterns": ["test_*", "*_test.py"]
}'
```
### 4. 执行安全扫描
```bash
# 对指定仓库进行安全扫描
curl -X POST "http://localhost:8000/cerobot/plugins/execute" \
-H "Content-Type: application/json" \
-d '{
"plugin_name": "SecurityScanner",
"context": {"repo_path": "/path/to/your/repository"}
}'
```
## 插件功能
### 代码质量检测插件 (CodeQualityGuard)
**功能特性:**
- 支持 10 种编程语言
- 检测代码风格问题
- 识别性能问题
- 提供修复建议
- 生成质量评分
**支持的语言:**
- Python
- JavaScript
- TypeScript
- Java
- Go
- C++
- C
- C#
- PHP
- Ruby
- Rust
**检测项目:**
- 函数过长
- 参数过多
- 类过大
- 行长度超限
- 硬编码密码
- SQL注入风险
- 导入私有模块
### 安全扫描插件 (SecurityScanner)
**功能特性:**
- 检测常见安全漏洞
- 风险等级分级
- 详细修复建议
- 安全评分
**检测漏洞类型:**
- SQL注入漏洞
- XSS攻击漏洞
- 硬编码凭据
- 不安全的随机数生成
- 文件路径遍历漏洞
## API 接口
### 插件管理接口
| 接口 | 方法 | 描述 |
|------|------|------|
| `/cerobot/plugins/list` | GET | 获取插件列表 |
| `/cerobot/plugins/{plugin_name}/info` | GET | 获取插件信息 |
| `/cerobot/plugins/execute` | POST | 执行指定插件 |
| `/cerobot/plugins/{plugin_name}/enable` | POST | 启用插件 |
| `/cerobot/plugins/{plugin_name}/disable` | POST | 禁用插件 |
### 代码质量接口
| 接口 | 方法 | 描述 |
|------|------|------|
| `/cerobot/plugins/quality/analyze` | POST | 代码质量分析 |
| `/cerobot/plugins/quality/analyze-by-language` | POST | 按语言分析 |
### 系统接口
| 接口 | 方法 | 描述 |
|------|------|------|
| `/cerobot/plugins/history` | GET | 获取执行历史 |
| `/cerobot/plugins/export-report` | POST | 导出执行报告 |
| `/plugins/status` | GET | 获取插件系统状态 |
| `/plugins/quality/quick-check` | POST | 快速质量检查 |
## 使用示例
### Python 代码示例
```python
import asyncio
import aiohttp
import json
async def analyze_code_quality():
"""代码质量分析示例"""
url = "http://localhost:8000/cerobot/plugins/quality/analyze"
data = {
"repo_path": "/path/to/repository",
"languages": ["python", "javascript"],
"include_patterns": ["*.py", "*.js"],
"exclude_patterns": ["test_*", "*_test.py"]
}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data) as response:
result = await response.json()
if result.get("success"):
report = result.get("data", {}).get("report", {})
print(f"质量评分: {report.get('quality_score', 0):.1f}/100")
print(f"发现问题: {report.get('total_issues', 0)} 个")
print(f"摘要: {report.get('summary', '')}")
else:
print(f"分析失败: {result.get('error', '未知错误')}")
# 运行示例
asyncio.run(analyze_code_quality())
```
### JavaScript 代码示例
```javascript
// 代码质量分析
async function analyzeCodeQuality() {
const response = await fetch('http://localhost:8000/cerobot/plugins/quality/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
repo_path: '/path/to/repository',
languages: ['python', 'javascript'],
include_patterns: ['*.py', '*.js'],
exclude_patterns: ['test_*', '*_test.py']
})
});
const result = await response.json();
if (result.success) {
const report = result.data.report;
console.log(`质量评分: ${report.quality_score.toFixed(1)}/100`);
console.log(`发现问题: ${report.total_issues} 个`);
console.log(`摘要: ${report.summary}`);
} else {
console.error(`分析失败: ${result.error}`);
}
}
// 执行分析
analyzeCodeQuality();
```
## 集成到 CI/CD
### GitLab CI 配置
```yaml
stages:
- code_quality
code_quality_check:
stage: code_quality
script:
- curl -X POST "http://reposync-server:8000/cerobot/plugins/quality/analyze" \
-H "Content-Type: application/json" \
-d "{\"repo_path\": \"$CI_PROJECT_DIR\"}"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
### GitHub Actions 配置
```yaml
name: Code Quality Check
on:
pull_request:
branches: [ main ]
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Code Quality Analysis
run: |
curl -X POST "http://reposync-server:8000/cerobot/plugins/quality/analyze" \
-H "Content-Type: application/json" \
-d '{"repo_path": "${{ github.workspace }}"}'
```
## 自定义插件开发
### 创建自定义插件
```python
from src.plugins.plugin_manager import BasePlugin, PluginConfig
from typing import Dict, List, Any
class CustomPlugin(BasePlugin):
def __init__(self, config: PluginConfig):
super().__init__(config)
async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""执行插件逻辑"""
repo_path = context.get('repo_path')
# 实现自定义逻辑
result = {
"success": True,
"data": "custom analysis result",
"custom_metric": 95.5
}
return result
def get_supported_languages(self) -> List[str]:
"""获取支持的语言列表"""
return ["python", "javascript"]
# 注册插件
from src.plugins.plugin_manager import plugin_manager
config = PluginConfig(
name="CustomPlugin",
version="1.0.0",
description="自定义分析插件",
enabled=True
)
plugin = CustomPlugin(config)
plugin_manager.register_plugin(plugin)
```
## 配置选项
### 环境变量
```bash
# 插件系统配置
PLUGIN_ENABLED=true
PLUGIN_TIMEOUT=300
PLUGIN_MAX_WORKERS=4
# 代码质量检测配置
QUALITY_CHECK_ENABLED=true
QUALITY_SCORE_THRESHOLD=80
QUALITY_MAX_ISSUES=100
# 安全扫描配置
SECURITY_SCAN_ENABLED=true
SECURITY_SCORE_THRESHOLD=85
SECURITY_MAX_VULNERABILITIES=50
```
### 配置文件
```json
{
"plugins": {
"CodeQualityGuard": {
"enabled": true,
"config": {
"max_line_length": 120,
"max_function_length": 50,
"max_class_methods": 20
}
},
"SecurityScanner": {
"enabled": true,
"config": {
"risk_threshold": "medium",
"scan_patterns": ["*.py", "*.js", "*.java"]
}
}
}
}
```
## 故障排除
### 常见问题
1. **插件加载失败**
- 检查插件文件是否存在
- 验证插件类是否正确继承 BasePlugin
- 查看日志文件获取详细错误信息
2. **代码质量检测失败**
- 确认仓库路径是否正确
- 检查文件权限
- 验证支持的文件类型
3. **API 接口无响应**
- 确认服务是否正常启动
- 检查端口是否被占用
- 验证防火墙设置
### 日志查看
```bash
# 查看应用日志
tail -f logs/app.log
# 查看插件执行日志
tail -f logs/plugin.log
# 查看错误日志
tail -f logs/error.log
```
## 性能优化
### 优化建议
1. **并发处理**:使用异步执行提高性能
2. **缓存结果**:对相同代码的检测结果进行缓存
3. **增量检测**:只检测修改的文件
4. **资源限制**:设置合理的超时时间和内存限制
### 性能监控
```bash
# 查看插件执行时间
curl http://localhost:8000/cerobot/plugins/history
# 查看系统资源使用
curl http://localhost:8000/system/info
# 查看统计信息
curl http://localhost:8000/stats
```
## 更新日志
### v1.0.0 (2024-12-19)
- 初始版本发布
- 支持代码质量检测
- 支持安全漏洞扫描
- 提供插件管理功能
- 集成 API 接口
## 贡献指南
欢迎贡献代码和提出建议!
1. Fork 项目
2. 创建功能分支
3. 提交更改
4. 推送到分支
5. 创建 Pull Request
## 许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。

View File

@ -1,431 +0,0 @@
# RepoSyncer 创新插件系统解决方案报告
## 1. 项目概述
### 1.1 项目背景
RepoSyncer 是一个多平台代码同步工具,支持 GitHub、Gitee、GitLink 等平台之间的代码同步。为了提升项目的实用性和扩展性,我们设计并实现了一个创新的插件系统,能够在代码同步过程中自动检测代码质量问题并提供修复建议。
### 1.2 创新点
- **智能代码质量检测**:支持多语言代码质量分析
- **安全漏洞扫描**:自动检测常见安全风险
- **可扩展插件架构**:支持第三方插件开发和集成
- **自动化修复建议**:提供具体的代码修复方案
- **集成同步流程**:在代码同步前后自动触发检测
## 2. 技术架构设计
### 2.1 整体架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ RepoSyncer │ │ 插件管理器 │ │ 插件系统 │
│ 主应用 │◄──►│ PluginManager │◄──►│ CodeQuality │
│ │ │ │ │ SecurityScan │
│ - 代码同步 │ │ - 插件注册 │ │ CustomPlugin │
│ - API接口 │ │ - 插件执行 │ │ ... │
│ - 配置管理 │ │ - 状态监控 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 2.2 核心组件
#### 2.2.1 插件管理器 (PluginManager)
- **功能**:负责插件的注册、加载、配置和执行
- **特性**
- 支持动态插件加载
- 提供插件生命周期管理
- 记录插件执行历史
- 支持批量插件执行
#### 2.2.2 代码质量检测插件 (CodeQualityGuard)
- **功能**:多语言代码质量检测和修复建议
- **支持语言**Python、JavaScript、TypeScript、Java、Go、C++、C#、PHP、Ruby、Rust
- **检测项目**
- 代码风格问题
- 性能问题
- 最佳实践违反
- 安全风险
#### 2.2.3 安全扫描插件 (SecurityScanner)
- **功能**:代码安全漏洞检测
- **检测项目**
- SQL注入漏洞
- XSS攻击漏洞
- 硬编码凭据
- 不安全的随机数生成
- 文件路径遍历漏洞
## 3. 实现思路
### 3.1 插件系统设计原则
1. **可扩展性**:支持第三方插件开发和集成
2. **松耦合**:插件与主系统解耦,独立开发和部署
3. **标准化**:统一的插件接口和配置规范
4. **高性能**:异步执行,支持并发处理
5. **可观测性**:完整的执行日志和状态监控
### 3.2 技术选型
- **编程语言**Python 3.9
- **Web框架**FastAPI
- **异步处理**asyncio
- **代码解析**ast (Python)、正则表达式
- **数据存储**JSON文件、数据库
- **API文档**OpenAPI/Swagger
### 3.3 核心算法
#### 3.3.1 代码质量评分算法
```python
def calculate_quality_score(total_files, issues):
if total_files == 0:
return 100.0
severity_weights = {
'error': 10,
'warning': 3,
'info': 1
}
total_weight = sum(severity_weights[issue.severity] for issue in issues)
penalty = min(total_weight * 2, 100)
return max(0.0, 100.0 - penalty)
```
#### 3.3.2 安全风险评分算法
```python
def calculate_security_score(vulnerabilities):
if not vulnerabilities:
return 100.0
risk_weights = {'high': 10, 'medium': 5, 'low': 2}
total_weight = sum(risk_weights[vuln['risk_level']] for vuln in vulnerabilities)
penalty = min(total_weight * 3, 100)
return max(0.0, 100.0 - penalty)
```
## 4. 技术实现
### 4.1 插件基类设计
```python
class BasePlugin(ABC):
def __init__(self, config: PluginConfig):
self.config = config
self.name = config.name
self.version = config.version
self.description = config.description
self.enabled = config.enabled
@abstractmethod
async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""执行插件逻辑"""
pass
@abstractmethod
def get_supported_languages(self) -> List[str]:
"""获取支持的语言列表"""
pass
```
### 4.2 插件管理器实现
```python
class PluginManager:
def __init__(self):
self.plugins: Dict[str, BasePlugin] = {}
self.execution_history: List[Dict[str, Any]] = []
async def execute_plugin(self, plugin_name: str, context: Dict[str, Any]) -> Dict[str, Any]:
"""执行指定插件"""
plugin = self.get_plugin(plugin_name)
if not plugin or not plugin.enabled:
return {"success": False, "error": "Plugin not found or disabled"}
start_time = datetime.now()
result = await plugin.execute(context)
end_time = datetime.now()
# 记录执行历史
execution_record = {
"plugin_name": plugin_name,
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"duration": (end_time - start_time).total_seconds(),
"success": result.get("success", False),
"result": result
}
self.execution_history.append(execution_record)
return result
```
### 4.3 API接口设计
```python
@router.post("/quality/analyze", response_model=SYNCResponse)
async def analyze_code_quality(
self,
request: Request,
user: str = Depends(user),
analysis_request: QualityAnalysisRequest = Body(...)
):
"""执行代码质量分析"""
context = {
"repo_path": analysis_request.repo_path,
"languages": analysis_request.languages,
"include_patterns": analysis_request.include_patterns,
"exclude_patterns": analysis_request.exclude_patterns
}
result = await plugin_manager.execute_plugin("CodeQualityGuard", context)
return SYNCResponse(code_status=Status.SUCCESS.code, data=result)
```
## 5. 功能特性
### 5.1 代码质量检测功能
- **多语言支持**支持10种主流编程语言
- **智能检测**基于AST和正则表达式的代码分析
- **分类统计**:按严重程度和类别统计问题
- **修复建议**:提供具体的修复方案和最佳实践
- **质量评分**0-100分的质量评分系统
### 5.2 安全扫描功能
- **漏洞检测**检测5大类常见安全漏洞
- **风险分级**:高、中、低三个风险等级
- **详细报告**:包含漏洞位置、描述和修复建议
- **安全评分**:基于漏洞数量和严重程度的安全评分
### 5.3 插件管理功能
- **插件注册**:支持动态插件注册和注销
- **状态管理**:插件启用/禁用状态控制
- **执行监控**:实时监控插件执行状态
- **历史记录**:完整的插件执行历史
- **报告导出**:支持执行报告导出
## 6. 验证效果
### 6.1 功能验证
通过测试脚本验证了以下功能:
1. **插件注册和加载**:✓ 成功
2. **代码质量检测**:✓ 成功检测到代码风格、性能等问题
3. **安全漏洞扫描**:✓ 成功检测到SQL注入、硬编码凭据等漏洞
4. **批量插件执行**:✓ 成功
5. **执行历史记录**:✓ 成功
6. **报告导出**:✓ 成功
### 6.2 性能测试
- **单文件检测**< 1秒
- **中等项目检测**1000行代码< 5秒
- **大型项目检测**10000行代码< 30秒
- **并发处理**:支持多个插件同时执行
### 6.3 准确性测试
- **代码质量检测准确率**85%+
- **安全漏洞检测准确率**90%+
- **误报率**< 10%
## 7. 使用示例
### 7.1 API调用示例
```bash
# 代码质量分析
curl -X POST "http://localhost:8000/cerobot/plugins/quality/analyze" \
-H "Content-Type: application/json" \
-d '{
"repo_path": "/path/to/repository",
"languages": ["python", "javascript"],
"include_patterns": ["*.py", "*.js"],
"exclude_patterns": ["test_*", "*_test.py"]
}'
# 获取插件列表
curl -X GET "http://localhost:8000/cerobot/plugins/list"
# 执行特定插件
curl -X POST "http://localhost:8000/cerobot/plugins/execute" \
-H "Content-Type: application/json" \
-d '{
"plugin_name": "CodeQualityGuard",
"context": {"repo_path": "/path/to/repository"}
}'
```
### 7.2 Python代码示例
```python
import asyncio
from src.plugins.plugin_manager import plugin_manager, PluginConfig
from src.plugins.code_quality_guard import CodeQualityGuard
async def main():
# 注册插件
config = PluginConfig(
name="CodeQualityGuard",
version="1.0.0",
description="代码质量检测插件",
enabled=True
)
plugin = CodeQualityGuard(config)
plugin_manager.register_plugin(plugin)
# 执行代码质量检测
context = {"repo_path": "/path/to/repository"}
result = await plugin_manager.execute_plugin("CodeQualityGuard", context)
if result.get("success"):
report = result.get("report", {})
print(f"质量评分: {report.get('quality_score', 0):.1f}/100")
print(f"发现问题: {report.get('total_issues', 0)} 个")
asyncio.run(main())
```
## 8. 部署和集成
### 8.1 部署步骤
1. **安装依赖**确保Python 3.9环境
2. **配置数据库**:初始化数据库表结构
3. **启动服务**:运行 `python main.py`
4. **验证功能**:访问 `/health` 接口检查服务状态
### 8.2 集成到CI/CD
```yaml
# GitLab CI配置示例
stages:
- code_quality
code_quality_check:
stage: code_quality
script:
- curl -X POST "http://reposync-server:8000/cerobot/plugins/quality/analyze" \
-H "Content-Type: application/json" \
-d '{"repo_path": "$CI_PROJECT_DIR"}'
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
### 8.3 监控和告警
- **健康检查**:定期检查插件系统状态
- **性能监控**:监控插件执行时间和资源使用
- **错误告警**:插件执行失败时发送告警
- **质量趋势**:跟踪代码质量变化趋势
## 9. 扩展性设计
### 9.1 自定义插件开发
```python
from src.plugins.plugin_manager import BasePlugin, PluginConfig
class CustomPlugin(BasePlugin):
def __init__(self, config: PluginConfig):
super().__init__(config)
async def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
# 实现自定义逻辑
return {"success": True, "data": "custom result"}
def get_supported_languages(self) -> List[str]:
return ["python", "javascript"]
```
### 9.2 插件配置管理
- **环境变量**:支持通过环境变量配置插件
- **配置文件**支持JSON/YAML配置文件
- **数据库存储**:支持插件配置持久化
- **动态配置**:支持运行时配置更新
## 10. 总结
### 10.1 创新成果
1. **首创性**:在代码同步工具中集成智能代码质量检测
2. **实用性**:提供具体的修复建议,提升代码质量
3. **扩展性**:可扩展的插件架构,支持第三方开发
4. **自动化**与CI/CD流程无缝集成实现自动化检测
### 10.2 技术价值
1. **架构设计**:可扩展的插件系统架构
2. **算法创新**:智能代码质量评分算法
3. **工程实践**:完整的测试和文档体系
4. **开源贡献**:为开源社区提供有价值的工具
### 10.3 应用前景
1. **企业应用**:提升企业代码质量和安全性
2. **开源项目**:为开源项目提供质量保障
3. **教育培训**:作为代码质量教育的工具
4. **研究价值**:为代码质量研究提供数据支持
## 11. 附录
### 11.1 文件结构
```
src/plugins/
├── __init__.py # 插件模块初始化
├── plugin_manager.py # 插件管理器
├── code_quality_guard.py # 代码质量检测插件
└── security_scanner.py # 安全扫描插件
src/api/
└── Plugin.py # 插件API接口
test_plugin_system.py # 测试脚本
PLUGIN_SOLUTION_REPORT.md # 解决方案报告
```
### 11.2 API接口列表
- `GET /cerobot/plugins/list` - 获取插件列表
- `GET /cerobot/plugins/{plugin_name}/info` - 获取插件信息
- `POST /cerobot/plugins/execute` - 执行指定插件
- `POST /cerobot/plugins/quality/analyze` - 代码质量分析
- `POST /cerobot/plugins/quality/analyze-by-language` - 按语言分析
- `GET /cerobot/plugins/history` - 获取执行历史
- `POST /cerobot/plugins/export-report` - 导出执行报告
- `POST /cerobot/plugins/{plugin_name}/enable` - 启用插件
- `POST /cerobot/plugins/{plugin_name}/disable` - 禁用插件
### 11.3 测试结果
```
RepoSyncer 插件系统测试
============================================================
1. 初始化插件管理器...
✓ 已注册 2 个插件
2. 插件信息:
- CodeQualityGuard v1.0.0: 智能代码质量检测与自动修复插件
支持语言: python, javascript, typescript, java, go, cpp, c, csharp, php, ruby, rust
状态: 启用
- SecurityScanner v1.0.0: 代码安全漏洞扫描插件
支持语言: python, javascript, typescript, java, go, php, ruby
状态: 启用
3. 测试代码质量检测...
测试目录: /path/to/reposync
✓ 代码质量检测完成
检查文件数: 45
发现问题数: 12
质量评分: 78.5/100
摘要: 代码质量分析完成。共检查 45 个文件,发现 12 个问题。质量评分: 78.5/100。 警告: 8 个, 建议: 4 个。 代码质量一般,建议优化。
4. 测试安全扫描...
✓ 安全扫描完成
发现漏洞数: 3
安全评分: 85.2/100
摘要: 安全扫描完成。发现 3 个安全漏洞,安全评分: 85.2/100。 中危漏洞: 2 个, 低危漏洞: 1 个。 代码安全性良好。
5. 测试批量执行插件...
✓ 批量执行完成,执行了 2 个插件
✓ CodeQualityGuard: 成功
✓ SecurityScanner: 成功
6. 插件执行历史:
✓ CodeQualityGuard: 2.34s
✓ SecurityScanner: 1.87s
7. 导出执行报告...
✓ 执行报告已导出到: plugin_execution_report.json
============================================================
插件系统测试完成
============================================================
```
这个创新插件系统为 RepoSyncer 项目增加了重要的价值,不仅提升了代码质量,还为项目的长期发展奠定了坚实的基础。

View File

@ -1,76 +0,0 @@
# ob-repository-synchronize
## 描述
ob-repository-synchronize是一个帮助工程师进行多平台代码同步的小工具平台包括GitHubGiteeCodeChinaGitlink和内部仓库平台等等平台部分功能待完善。
## 原理
### 基于git rebase的多方向同步方案
<img src="doc/rebase.png" width="500" height="400">
### 基于git diff的单方向同步方案
<img src="doc/diff.png" width="500" height="400">
## 后端
### 依赖
name|version|necessity
--|:--:|--:
python|3.9|True
uvicorn|0.14.0|True
SQLAlchemy|1.4.21|True
fastapi|0.66.0|True
aiohttp|3.7.4|True
pydantic|1.8.2|True
starlette|0.14.2|True
aiomysql|0.0.21|True
requests|2.25.1|True
loguru|0.6.0|True
typing-extensions|4.1.1|True
aiofiles|0.8.0|True
### 如何安装
> [!NOTE]
> 运行代码必须在python 3.9环境下面
`pip3 install -r requirement.txt`
`python3 main.py`
### 在本地跑同步脚本
`python3 sync.py`
## 前端
[参考web下的readme](web/README.md)
## docker
`docker pull XXX:latest`
`docker run -p 8000:8000 -d XXX bash start.sh -s backend`
## 如何使用
1. 部署数据库
- 创建一个自己的database跑在sql文件夹下的table.sql文件
- 设置自己的数据库连接串在src/base/config.py文件内
2. 通过网页设置自己仓库地址同步分支和平台token待完善
<img src="doc/website.png" width="500" height="400">
3. 自适应配置自己的同步脚本请参考example下的两个例子然后运行自己的脚本在一个定时任务下面
应该考虑的一些内容:
- 仓库使用http链接还是ssh链接(如何把你自己的ssh key送入进来)
- 选择rebase还是diff逻辑
- 选择什么定时任务(或许是k8s cronjob或者是linux操作系统的crontab)

View File

@ -1,75 +0,0 @@
# ob-repository-synchronize
## Description
ob-repository-synchronize is a small tool which can help engineer to master their open source production's code synchronization between GitHub, Gitee, CodeChina, internal repository and so on.
## Principle
### Base on git rebase
<img src="doc/rebase.png" width="500" height="400">
### Base on git diff
<img src="doc/diff.png" width="500" height="400">
## backend
### requirement
name|version|necessity
--|:--:|--:
python|3.9|True
uvicorn|0.14.0|True
SQLAlchemy|1.4.21|True
fastapi|0.66.0|True
aiohttp|3.7.4|True
pydantic|1.8.2|True
starlette|0.14.2|True
aiomysql|0.0.21|True
requests|2.25.1|True
loguru|0.6.0|True
typing-extensions|4.1.1|True
aiofiles|0.8.0|True
### how to install
> [!NOTE]
> Run the code in python 3.9
`pip3 install -r requirement.txt`
`python3 main.py`
### run the sync script locally
`python3 sync.py`
## frontend
[Refer the web readme](web/README.md)
## docker
`docker pull XXX:latest`
`docker run -p 8000:8000 -d XXX bash start.sh -s backend`
## How to use it
1. Config your database
- Run the table.sql script in sql folder
- Config the database connection string in src/base/config.py
2. Config your repo address, branch, (todo token) by website
<img src="doc/website.png" width="500" height="400">
3. DIY yourself sync script (Refer the two example in sync folder) and run the sync script under a cronjob
you should consider:
- http address or ssh address (how to add your ssh key)
- rebase logic or diff logic
- which cronjob (maybe the k8s cronjob or linux system crontab)

View File

@ -1,363 +0,0 @@
# RepoSyncer Issue/PR 同步功能使用说明
## 功能概述
RepoSyncer 支持 GitHub、Gitee、GitLink 三个平台之间的 Issue、Pull Request 和 PR评论的双向同步提供完整的同步配置管理、日志记录和状态监控功能。
## 主要特性
- **多平台支持**: GitHub、Gitee、GitLink
- **双向同步**: 支持单向和双向同步
- **智能映射**: 自动维护 Issue/PR/PR评论 的跨平台映射关系
- **完整日志**: 详细的同步操作日志和错误记录
- **配置管理**: 灵活的同步配置管理
- **自动同步**: 支持定时自动同步
- **状态监控**: 实时同步状态和统计信息
## 快速开始
### 1. 数据库初始化
首先需要创建同步相关的数据库表:
```sql
-- 执行 SQL 文件
mysql -u your_username -p your_database < sql/sync_tables.sql
```
### 2. 配置同步
#### 通过 API 创建同步配置
```bash
# 创建 GitHub 到 Gitee 的 Issue 同步配置
curl -X POST "http://localhost:8000/sync/configs" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub-Gitee Issue同步",
"source_platform": "github",
"source_owner": "your-github-username",
"source_repo": "your-github-repo",
"source_token": "your-github-token",
"target_platform": "gitee",
"target_owner": "your-gitee-username",
"target_repo": "your-gitee-repo",
"target_token": "your-gitee-token",
"sync_type": "issue",
"sync_direction": "bidirectional",
"enabled": true,
"auto_sync": true,
"sync_interval": 300
}'
```
#### 创建 GitLink 到 GitHub 的 PR 同步配置
```bash
curl -X POST "http://localhost:8000/sync/configs" \
-H "Content-Type: application/json" \
-d '{
"name": "GitLink-GitHub PR同步",
"source_platform": "gitlink",
"source_owner": "your-gitlink-username",
"source_repo": "your-gitlink-repo",
"source_token": "your-gitlink-token",
"target_platform": "github",
"target_owner": "your-github-username",
"target_repo": "your-github-repo",
"target_token": "your-github-token",
"sync_type": "pull_request",
"sync_direction": "bidirectional",
"enabled": true,
"auto_sync": true,
"sync_interval": 600
}'
```
#### 创建 GitHub 到 Gitee 的 PR评论 同步配置
```bash
curl -X POST "http://localhost:8000/sync/configs" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub-Gitee PR评论同步",
"source_platform": "github",
"source_owner": "your-github-username",
"source_repo": "your-github-repo",
"source_token": "your-github-token",
"target_platform": "gitee",
"target_owner": "your-gitee-username",
"target_repo": "your-gitee-repo",
"target_token": "your-gitee-token",
"sync_type": "pr_comment",
"sync_direction": "bidirectional",
"enabled": true,
"auto_sync": true,
"sync_interval": 300
}'
```
### 3. 启动同步
#### 手动启动单次同步
```bash
# 启动指定配置的 Issue 同步
curl -X POST "http://localhost:8000/sync/start" \
-H "Content-Type: application/json" \
-d '{
"config_id": 1,
"sync_type": "issue"
}'
# 启动指定配置的 PR 同步
curl -X POST "http://localhost:8000/sync/start" \
-H "Content-Type: application/json" \
-d '{
"config_id": 2,
"sync_type": "pull_request"
}'
# 启动指定配置的 PR评论 同步
curl -X POST "http://localhost:8000/sync/start" \
-H "Content-Type: application/json" \
-d '{
"config_id": 3,
"sync_type": "pr_comment"
}'
```
#### 使用命令行工具
```bash
# 运行所有启用的同步配置
python sync/sync_runner.py --mode all
# 运行指定配置的同步
python sync/sync_runner.py --mode single --config-id 1 --sync-type issue
# 启动自动同步循环 (每5分钟执行一次)
python sync/sync_runner.py --mode auto --interval 300
# 运行PR评论同步
python sync/pr_comment_sync_runner.py
# 启动PR评论自动同步
python sync/pr_comment_sync_runner.py --auto --interval 300
```
## API 接口说明
### 同步配置管理
#### 获取同步配置列表
```bash
GET /sync/configs?enabled_only=true
```
#### 获取同步配置详情
```bash
GET /sync/configs/{config_id}
```
#### 创建同步配置
```bash
POST /sync/configs
```
#### 更新同步配置
```bash
PUT /sync/configs/{config_id}
```
#### 删除同步配置
```bash
DELETE /sync/configs/{config_id}
```
### 同步操作
#### 启动同步
```bash
POST /sync/start
```
#### 获取同步日志
```bash
GET /sync/logs?config_id=1&limit=100
```
#### 获取同步状态
```bash
GET /sync/status
```
#### 测试平台连接
```bash
POST /sync/test-connection
```
## 配置参数说明
### 同步配置字段
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | 配置名称 |
| source_platform | string | 是 | 源平台 (github/gitee/gitlink) |
| source_owner | string | 是 | 源仓库所有者 |
| source_repo | string | 是 | 源仓库名称 |
| source_token | string | 是 | 源平台访问令牌 |
| target_platform | string | 是 | 目标平台 |
| target_owner | string | 是 | 目标仓库所有者 |
| target_repo | string | 是 | 目标仓库名称 |
| target_token | string | 是 | 目标平台访问令牌 |
| sync_type | string | 否 | 同步类型 (issue/pull_request/pr_comment) |
| sync_direction | string | 否 | 同步方向 (bidirectional/source_to_target/target_to_source) |
| enabled | boolean | 否 | 是否启用 |
| auto_sync | boolean | 否 | 是否自动同步 |
| sync_interval | integer | 否 | 同步间隔(秒) |
### 同步方向说明
- **bidirectional**: 双向同步,源平台和目标平台的数据会相互同步
- **source_to_target**: 单向同步,只从源平台同步到目标平台
- **target_to_source**: 单向同步,只从目标平台同步到源平台
## 平台令牌获取
### GitHub Token
1. 访问 GitHub Settings > Developer settings > Personal access tokens
2. 点击 "Generate new token"
3. 选择权限: `repo`, `issues`, `pull_requests`
4. 复制生成的令牌
### Gitee Token
1. 访问 Gitee 个人设置 > 私人令牌
2. 点击 "生成新令牌"
3. 选择权限: `issues`, `pull_requests`
4. 复制生成的令牌
### GitLink Token
1. 访问 GitLink 个人设置 > 访问令牌
2. 点击 "生成新令牌"
3. 选择权限: `issues`, `pull_requests`
4. 复制生成的令牌
## 同步逻辑说明
### Issue 同步
- 自动同步 Issue 的标题、内容、状态、标签、指派人
- 维护跨平台的 Issue 编号映射关系
- 支持 Issue 的创建、更新、关闭操作
### Pull Request 同步
- 自动同步 PR 的标题、内容、状态、分支信息
- 维护跨平台的 PR 编号映射关系
- 支持 PR 的创建、更新、合并、关闭操作
### PR评论同步
- 自动同步 PR 的评论内容、位置信息
- 支持代码行级别的评论同步
- 维护跨平台的 PR评论 映射关系
- 智能处理不同平台的评论格式差异
### 冲突处理
- 如果目标平台已存在相同内容的 Issue/PR/评论,会进行更新而不是创建新的
- 通过映射表避免重复同步
- 详细的错误日志记录,便于问题排查
## 监控和日志
### 同步日志
- 记录每次同步操作的详细信息
- 包含操作类型、状态、错误信息等
- 支持按配置ID筛选日志
### 同步状态
- 实时显示同步配置的统计信息
- 记录最后同步时间和状态
- 统计成功和失败的同步次数
### 错误处理
- 网络异常自动重试
- API 限流处理
- 详细的错误日志记录
## 最佳实践
### 1. 令牌安全
- 使用最小权限原则配置平台令牌
- 定期轮换访问令牌
- 不要在代码中硬编码令牌
### 2. 同步频率
- 根据项目活跃度设置合适的同步间隔
- 避免过于频繁的同步请求
- 考虑平台的 API 限流
### 3. 配置管理
- 为不同的同步需求创建独立的配置
- 定期检查和更新同步配置
- 及时禁用不需要的同步配置
### 4. 监控告警
- 定期检查同步日志
- 设置同步失败的告警机制
- 监控同步性能和成功率
## 故障排除
### 常见问题
1. **同步失败**
- 检查平台令牌是否有效
- 确认仓库权限是否正确
- 查看详细的错误日志
2. **重复同步**
- 检查映射表是否正确
- 确认同步方向配置
- 验证平台数据一致性
3. **API 限流**
- 增加同步间隔时间
- 检查平台 API 使用情况
- 考虑使用企业版令牌
### 日志分析
```bash
# 查看最近的同步日志
curl "http://localhost:8000/sync/logs?limit=50"
# 查看特定配置的日志
curl "http://localhost:8000/sync/logs?config_id=1&limit=100"
# 查看同步状态
curl "http://localhost:8000/sync/status"
```
## 扩展功能
### 自定义同步规则
- 支持按标签筛选同步内容
- 支持按时间范围同步
- 支持自定义同步字段映射
### Webhook 集成
- 支持平台 Webhook 触发同步
- 实时响应 Issue/PR 变更
- 减少轮询频率
### 批量操作
- 支持批量创建同步配置
- 支持批量启动同步
- 支持批量导入导出配置
## 技术支持
如有问题或建议,请通过以下方式联系:
- 提交 Issue 到项目仓库
- 查看项目文档和示例
- 参考 API 文档和日志信息

80
boot
View File

@ -1,41 +1,41 @@
#!/bin/bash
# usage:
# docker run -d --net=host -v /path/to/env.ini:/data/ob-robot/env.ini obrobot:1.0.0 ./start.sh -s backend
# docker run -d --net=host -v /path/to/env.ini:/data/ob-robot/env.ini obrobot:1.0.0 ./start.sh -s crontab
# init env
if [[ ! -f env.ini ]]; then
echo "env.ini missing"
exit 1
fi
source env.ini
usage()
{
echo "Usage:"
echo " start.sh -s <service>"
echo "Supported service: backend crontab "
echo "Default service is: backend"
exit 0
}
TEMP=`getopt -o s:h -- "$@"`
eval set -- "$TEMP"
while true ; do
case "$1" in
-h) usage; shift ;;
-s) service=$2; shift 2 ;;
--) shift; break;;
*) echo "Usupported option"; exit 1;;
esac
done
if [[ x"$service" == x"backend" ]]; then
# 启动后端服务
python3 main.py
else
echo "Unsupported service"
exit 1
#!/bin/bash
# usage:
# docker run -d --net=host -v /path/to/env.ini:/data/ob-robot/env.ini obrobot:1.0.0 ./start.sh -s backend
# docker run -d --net=host -v /path/to/env.ini:/data/ob-robot/env.ini obrobot:1.0.0 ./start.sh -s crontab
# init env
if [[ ! -f env.ini ]]; then
echo "env.ini missing"
exit 1
fi
source env.ini
usage()
{
echo "Usage:"
echo " start.sh -s <service>"
echo "Supported service: backend crontab "
echo "Default service is: backend"
exit 0
}
TEMP=`getopt -o s:h -- "$@"`
eval set -- "$TEMP"
while true ; do
case "$1" in
-h) usage; shift ;;
-s) service=$2; shift 2 ;;
--) shift; break;;
*) echo "Usupported option"; exit 1;;
esac
done
if [[ x"$service" == x"backend" ]]; then
# 启动后端服务
python3 main.py
else
echo "Unsupported service"
exit 1
fi

239
debug_gitlink_api.py Normal file
View File

@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Gitlink API 详细调试脚本
测试不同的API端点和参数组合
"""
import requests
import json
import urllib3
# 禁用SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def test_api_endpoints():
"""测试不同的API端点"""
print("=== 测试Gitlink API端点 ===")
base_url = "https://www.gitlink.org.cn"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# 测试不同的API端点
endpoints = [
"/api/v1/user",
"/api/v1/projects",
"/api/v1/repos",
"/api/v1/repositories",
"/api/v1/user/projects",
"/api/v1/user/repos",
"/api/v1/user/repositories"
]
for endpoint in endpoints:
url = base_url + endpoint
print(f"\n测试端点: {endpoint}")
try:
response = requests.get(url, headers=headers, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
print(f"内容类型: {response.headers.get('Content-Type', 'N/A')}")
print(f"响应内容: {response.text[:200]}...")
if response.status_code == 200:
try:
data = response.json()
print(f"JSON格式: {type(data)}")
if isinstance(data, dict):
print(f"键: {list(data.keys())}")
except:
print("非JSON格式")
except Exception as e:
print(f"异常: {e}")
def test_project_search():
"""测试项目搜索的不同方法"""
print("\n=== 测试项目搜索方法 ===")
base_url = "https://www.gitlink.org.cn"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
owner = "gonggong123zzz"
repo_name = "repo2"
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# 方法1: 通过查询参数搜索
print("\n方法1: 通过查询参数搜索")
url1 = f"{base_url}/api/v1/projects"
params1 = {'owner': owner, 'name': repo_name}
try:
response = requests.get(url1, params=params1, headers=headers, timeout=30, verify=False)
print(f"URL: {response.url}")
print(f"状态码: {response.status_code}")
print(f"响应: {response.text}")
except Exception as e:
print(f"异常: {e}")
# 方法2: 通过路径参数搜索
print("\n方法2: 通过路径参数搜索")
endpoints = [
f"{base_url}/api/v1/projects/{owner}/{repo_name}",
f"{base_url}/api/v1/repos/{owner}/{repo_name}",
f"{base_url}/api/v1/repositories/{owner}/{repo_name}"
]
for endpoint in endpoints:
print(f"\n尝试: {endpoint}")
try:
response = requests.get(endpoint, headers=headers, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
print(f"内容类型: {response.headers.get('Content-Type', 'N/A')}")
if response.status_code == 200:
print(f"成功: {response.text[:200]}...")
else:
print(f"失败: {response.text[:100]}...")
except Exception as e:
print(f"异常: {e}")
def test_user_projects():
"""测试获取用户项目列表"""
print("\n=== 测试用户项目列表 ===")
base_url = "https://www.gitlink.org.cn"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
endpoints = [
"/api/v1/user/projects",
"/api/v1/user/repos",
"/api/v1/user/repositories"
]
for endpoint in endpoints:
url = base_url + endpoint
print(f"\n测试: {endpoint}")
try:
response = requests.get(url, headers=headers, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
try:
data = response.json()
if isinstance(data, list):
print(f"找到 {len(data)} 个项目")
if data:
print(f"第一个项目: {json.dumps(data[0], indent=2, ensure_ascii=False)}")
else:
print(f"响应格式: {type(data)}")
print(f"内容: {json.dumps(data, indent=2, ensure_ascii=False)}")
except:
print(f"非JSON格式: {response.text[:200]}...")
else:
print(f"失败: {response.text}")
except Exception as e:
print(f"异常: {e}")
def test_auth_methods():
"""测试不同的认证方法"""
print("\n=== 测试认证方法 ===")
base_url = "https://www.gitlink.org.cn"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
# 方法1: 只使用Cookie
print("\n方法1: 只使用Cookie")
headers1 = {'Cookie': cookie}
try:
response = requests.get(f"{base_url}/api/v1/user", headers=headers1, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
print(f"响应: {response.text}")
except Exception as e:
print(f"异常: {e}")
# 方法2: 使用Cookie + JSON头
print("\n方法2: 使用Cookie + JSON头")
headers2 = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
try:
response = requests.get(f"{base_url}/api/v1/user", headers=headers2, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
print(f"响应: {response.text}")
except Exception as e:
print(f"异常: {e}")
def test_web_interface():
"""测试Web界面访问"""
print("\n=== 测试Web界面访问 ===")
base_url = "https://www.gitlink.org.cn"
owner = "gonggong123zzz"
repo_name = "repo2"
# 测试项目页面
project_url = f"{base_url}/{owner}/{repo_name}"
print(f"项目页面: {project_url}")
try:
response = requests.get(project_url, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
print("✓ 项目页面存在")
# 检查页面内容是否包含项目信息
if repo_name.lower() in response.text.lower():
print("✓ 页面包含项目名称")
else:
print("✗ 页面不包含项目名称")
else:
print("✗ 项目页面不存在")
except Exception as e:
print(f"异常: {e}")
def main():
"""主函数"""
print("Gitlink API 详细调试工具")
print("=" * 60)
# 1. 测试API端点
test_api_endpoints()
# 2. 测试项目搜索
test_project_search()
# 3. 测试用户项目列表
test_user_projects()
# 4. 测试认证方法
test_auth_methods()
# 5. 测试Web界面
test_web_interface()
print("\n" + "=" * 60)
print("调试完成")
print("\n分析结果:")
print("1. 检查认证是否有效")
print("2. 确认项目是否存在")
print("3. 验证API端点是否正确")
print("4. 检查网络连接")
if __name__ == "__main__":
main()

49
debug_gitlink_branches.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
import aiohttp
import ssl
import json
from src.base import config
def create_ssl_connector():
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
return aiohttp.TCPConnector(ssl=ssl_context)
async def debug_gitlink_branches():
"""调试Gitlink PR的原始分支信息"""
gitlink_token = config.ACCOUNT.get('gitlink_cookie', '')
print("=== 调试Gitlink PR的原始分支信息 ===")
headers = {
'Cookie': f'autologin_trustie={gitlink_token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
url = "https://www.gitlink.org.cn/api/v1/gonggong123zzz/repo1/pulls.json"
params = {"per_page": 100}
async with aiohttp.ClientSession(connector=create_ssl_connector()) as session:
async with session.get(url, headers=headers, params=params) as response:
if response.status == 200:
result = await response.json()
pulls = result.get('pulls', [])
print(f"原始PR数量: {len(pulls)}")
for pr in pulls:
print(f"\nPR #{pr.get('id')}: {pr.get('title')}")
print(f" 原始head: {pr.get('head')}")
print(f" 原始base: {pr.get('base')}")
print(f" 状态: {pr.get('status')}")
print(f" 完整原始数据:")
print(f" {json.dumps(pr, indent=2, ensure_ascii=False)}")
if __name__ == "__main__":
asyncio.run(debug_gitlink_branches())
print("\n调试完成!")

View File

@ -1,71 +0,0 @@
#!/bin/bash
# 数据库部署脚本
set -e
echo "开始部署 RepoSync 数据库到生产环境..."
# 检查环境变量文件
if [ ! -f "env.db" ]; then
echo "错误: 未找到 env.db 文件,请先配置数据库环境变量"
exit 1
fi
# 加载环境变量
source env.db
# 检查必要的环境变量
if [ -z "$DEPLOY_HOST" ]; then
echo "错误: 未设置 DEPLOY_HOST 环境变量"
exit 1
fi
if [ -z "$DEPLOY_USER" ]; then
echo "错误: 未设置 DEPLOY_USER 环境变量"
exit 1
fi
# 检查密码是否已修改
if [ "$MYSQL_ROOT_PASSWORD" = "your_strong_root_password_here" ]; then
echo "错误: 请修改 env.db 文件中的数据库密码"
exit 1
fi
if [ "$MYSQL_PASSWORD" = "your_strong_user_password_here" ]; then
echo "错误: 请修改 env.db 文件中的用户密码"
exit 1
fi
# 创建远程目录
echo "创建远程数据库部署目录..."
ssh $DEPLOY_USER@$DEPLOY_HOST "mkdir -p /opt/reposync-db"
# 复制数据库配置文件
echo "复制数据库配置文件..."
scp docker-compose.db.yml $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync-db/docker-compose.yml
scp mysql.cnf $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync-db/
scp -r sql $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync-db/
scp env.db $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync-db/.env
# 在远程服务器上执行数据库部署
echo "执行数据库部署..."
ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/reposync-db && \
docker-compose down && \
docker-compose pull && \
docker-compose up -d && \
echo '等待数据库启动...' && \
sleep 30 && \
docker-compose ps"
# 验证数据库连接
echo "验证数据库连接..."
ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/reposync-db && \
docker-compose exec mysql mysql -u$MYSQL_USER -p$MYSQL_PASSWORD -e 'SHOW DATABASES;'"
echo "数据库部署完成!"
echo "数据库访问信息:"
echo " - 主机: $DEPLOY_HOST"
echo " - 端口: 3306"
echo " - 数据库: $MYSQL_DATABASE"
echo " - 用户: $MYSQL_USER"
echo " - phpMyAdmin: http://$DEPLOY_HOST:8080"

View File

@ -1,38 +0,0 @@
#!/bin/bash
# 部署脚本
set -e
echo "开始部署 RepoSync 到生产环境..."
# 检查环境变量
if [ -z "$DEPLOY_HOST" ]; then
echo "错误: 未设置 DEPLOY_HOST 环境变量"
exit 1
fi
if [ -z "$DEPLOY_USER" ]; then
echo "错误: 未设置 DEPLOY_USER 环境变量"
exit 1
fi
# 创建远程目录
echo "创建远程部署目录..."
ssh $DEPLOY_USER@$DEPLOY_HOST "mkdir -p /opt/reposync"
# 复制配置文件
echo "复制配置文件..."
scp docker-compose.yml $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync/
scp env.production $DEPLOY_USER@$DEPLOY_HOST:/opt/reposync/.env
# 在远程服务器上执行部署
echo "执行部署..."
ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/reposync && \
docker-compose pull && \
docker-compose down && \
docker-compose up -d && \
docker-compose ps"
echo "部署完成!"
echo "应用访问地址: http://$DEPLOY_HOST"
echo "API 文档地址: http://$DEPLOY_HOST/docs"

View File

@ -1,46 +0,0 @@
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: reposync-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
- ./mysql.cnf:/etc/mysql/conf.d/mysql.cnf
command: --default-authentication-plugin=mysql_native_password
networks:
- reposync-network
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
container_name: reposync-phpmyadmin
restart: unless-stopped
environment:
PMA_HOST: mysql
PMA_PORT: 3306
PMA_USER: ${MYSQL_USER}
PMA_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- "8080:80"
depends_on:
- mysql
networks:
- reposync-network
volumes:
mysql_data:
driver: local
networks:
reposync-network:
driver: bridge

View File

@ -1,42 +0,0 @@
version: '3.8'
services:
reposync-backend:
image: reg.docker.alibaba-inc.com/ob-robot/reposyncer:latest
container_name: reposync-backend
restart: unless-stopped
ports:
- "8000:8000"
environment:
- BOOT_MODE=app
- WEB_CONCURRENCY=4
- SYS_ENV=PROD
- CEROBOT_MYSQL_HOST=${CEROBOT_MYSQL_HOST}
- CEROBOT_MYSQL_PORT=${CEROBOT_MYSQL_PORT}
- CEROBOT_MYSQL_USER=${CEROBOT_MYSQL_USER}
- CEROBOT_MYSQL_PWD=${CEROBOT_MYSQL_PWD}
- CEROBOT_MYSQL_DB=${CEROBOT_MYSQL_DB}
- BUC_KEY=${BUC_KEY}
volumes:
- ./logs:/data/ob-robot/logs
networks:
- reposync-network
nginx:
image: nginx:alpine
container_name: reposync-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./web/nginx.conf:/etc/nginx/nginx.conf
- ./web/public:/usr/share/nginx/html
depends_on:
- reposync-backend
networks:
- reposync-network
networks:
reposync-network:
driver: bridge

23
env.db
View File

@ -1,23 +0,0 @@
# 数据库环境变量配置
# 生产环境数据库配置
# MySQL 数据库配置
MYSQL_ROOT_PASSWORD=your_strong_root_password_here
MYSQL_DATABASE=reposync
MYSQL_USER=reposync_user
MYSQL_PASSWORD=your_strong_user_password_here
# 应用连接数据库配置
CEROBOT_MYSQL_HOST=mysql
CEROBOT_MYSQL_PORT=3306
CEROBOT_MYSQL_USER=reposync_user
CEROBOT_MYSQL_PWD=your_strong_user_password_here
CEROBOT_MYSQL_DB=reposync
# 系统配置
SYS_ENV=PROD
BUC_KEY=your_encryption_key_here
# 部署配置
DEPLOY_HOST=114.55.237.214
DEPLOY_USER=root

19
env.ini
View File

@ -1,19 +0,0 @@
export SYS_ENV=DEV
export LOG_PATH=./logs
export LOG_LV=DEBUG
# 后端数据库配置
export CEROBOT_MYSQL_HOST=127.0.0.1
export CEROBOT_MYSQL_PORT=3306
export CEROBOT_MYSQL_USER=root
export CEROBOT_MYSQL_PWD=123456789LY@
export CEROBOT_MYSQL_DB=reposync
# 缓存数据库配置
# 运行构建任务容器名
export EL8_DOCKER_IMAGE=''
export EL7_DOCKER_IMAGE=''
# GitLink 配置
export GITLINK_COOKIE=autologin_trustie=4e25b144d7a870842f3063bbdb6f54fd3cc9b914

View File

@ -1,16 +1,16 @@
export SYS_ENV=DEV
export LOG_PATH=
export LOG_LV=DEBUG
# 后端数据库配置
export CEROBOT_MYSQL_HOST=127.0.0.1
export CEROBOT_MYSQL_PORT=
export CEROBOT_MYSQL_USER=""
export CEROBOT_MYSQL_PWD=""
export CEROBOT_MYSQL_DB=
# 缓存数据库配置
# 运行构建任务容器名
export EL8_DOCKER_IMAGE=''
export SYS_ENV=DEV
export LOG_PATH=
export LOG_LV=DEBUG
# 后端数据库配置
export CEROBOT_MYSQL_HOST=localhost
export CEROBOT_MYSQL_PORT=3306
export CEROBOT_MYSQL_USER=root
export CEROBOT_MYSQL_PWD=225231
export CEROBOT_MYSQL_DB=ob_sync
# 缓存数据库配置
# 运行构建任务容器名
export EL8_DOCKER_IMAGE=''
export EL7_DOCKER_IMAGE=''

View File

@ -1,18 +0,0 @@
# 生产环境配置
SYS_ENV=PROD
LOG_PATH=/data/ob-robot/logs
LOG_LV=INFO
# 后端数据库配置
CEROBOT_MYSQL_HOST=your_mysql_host
CEROBOT_MYSQL_PORT=3306
CEROBOT_MYSQL_USER=your_mysql_user
CEROBOT_MYSQL_PWD=your_mysql_password
CEROBOT_MYSQL_DB=your_mysql_database
# 加密密钥
BUC_KEY=your_encryption_key
# 部署配置
DEPLOY_HOST=114.55.237.214
DEPLOY_USER=root

View File

@ -1,9 +1,9 @@
nohup.out
*.pyc
*.pyo
build
dist
.vscode
.git
__pycache__
.idea/workspace.xml
nohup.out
*.pyc
*.pyo
build
dist
.vscode
.git
__pycache__
.idea/workspace.xml

View File

@ -1,4 +1,4 @@
FROM reg.docker.alibaba-inc.com/obvos/python3:3.9.2
COPY ./requirement.txt /tmp/requirement.txt
FROM reg.docker.alibaba-inc.com/obvos/python3:3.9.2
COPY ./requirement.txt /tmp/requirement.txt
RUN /usr/local/bin/pip3.9 install -r /tmp/requirement.txt; rm -f /tmp/requirement.txt

View File

@ -1,168 +1,168 @@
from typing import Union, Dict, Any
from urllib import parse
__all__ = ["ConfigsUtil", "MysqlConfig"]
class ConfigsError(Exception):
pass
class Config:
def __hash__(self):
check_sum = 0
config = self.__dict__
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda:0)()
return check_sum
def __eq__(self, value):
if isinstance(value, self.__class__):
return value.__hash__() == self.__hash__()
return False
class MysqlConfig(Config):
def __init__(self, host: str, port: int, dbname: str, user: str, passwd: str=None):
self.host = host
self.port = port
self.dbname = dbname
self.user = user
self.passwd = passwd
def get_url(self, drive: str="aiomysql", charset='utf8'):
user = parse.quote_plus(self.user)
if self.passwd:
user = '%s:%s' % (user, parse.quote_plus(self.passwd))
url = "mysql+%s://%s@%s:%s/%s" % (drive, user, self.host, self.port, self.dbname)
if charset:
url += "?charset=%s" % charset
return url
class RedisConfig(Config):
def __init__(
self,
host: str,
*,
port: int = 6379,
db: Union[str, int] = 0,
password: str = None,
socket_timeout: float = None,
socket_connect_timeout: float = None,
socket_keepalive: bool = None,
socket_keepalive_options: Dict[str, Any] = None,
unix_socket_path: str = None,
encoding: str = "utf-8",
encoding_errors: str = "strict",
decode_responses: bool = False,
retry_on_timeout: bool = False,
ssl: bool = False,
ssl_keyfile: str = None,
ssl_certfile: str = None,
ssl_cert_reqs: str = "required",
ssl_ca_certs: str = None,
ssl_check_hostname: bool = False,
max_connections: int = 0,
single_connection_client: bool = False,
health_check_interval: int = 0,
client_name: str = None,
username: str = None
):
self.host: str = host
self.port: int = port
self.db: Union[str, int] = db
self.password: str = password
self.socket_timeout: float = socket_timeout
self.socket_connect_timeout: float = socket_connect_timeout
self.socket_keepalive: bool = socket_keepalive
self.socket_keepalive_options: Dict[str, Any] = socket_keepalive_options
self.unix_socket_path: str = unix_socket_path
self.encoding: str = encoding
self.encoding_errors: str = encoding_errors
self.decode_responses: bool = decode_responses
self.retry_on_timeout: bool = retry_on_timeout
self.ssl: bool = ssl
self.ssl_keyfile: str = ssl_keyfile
self.ssl_certfile: str = ssl_certfile
self.ssl_cert_reqs: str = ssl_cert_reqs
self.ssl_ca_certs: str = ssl_ca_certs
self.ssl_check_hostname: bool = ssl_check_hostname
self.max_connections: int = max_connections
self.single_connection_client: bool = single_connection_client
self.health_check_interval: int = health_check_interval
self.client_name: str = client_name
self.username: str = username
@property
def config(self) -> Dict:
return self.__dict__
def __hash__(self):
check_sum = 0
config = self.config
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda:0)()
return check_sum
class ObFastApi(Config):
def __init__(self, buc_key: str = "OBVOS_USER_SIGN", log_name: str = 'obfastapi', log_path: str = None, log_level: str = "INFO", log_interval: int = 1, log_count: int = 7):
self.buc_key = buc_key
self.log_name = log_name
self.log_path = log_path
self.log_level = log_level
self.log_interval = log_interval
self.log_count = log_count
class ConfigsUtil:
MYSQL: Dict[str, MysqlConfig] = {}
REDIS: Dict[str, RedisConfig] = {}
OB_FAST_API = ObFastApi()
@staticmethod
def get_config(configs: Dict[str, Config], key:str) -> Config:
config = configs.get(key)
if not config:
raise ConfigsError('Nu such config %s' % key)
return config
@staticmethod
def set_config(configs: Dict[str, Config], key:str, config: Config):
configs[key] = config
@classmethod
def get_mysql_config(cls, key: str) -> MysqlConfig:
return cls.get_config(cls.MYSQL, key)
@classmethod
def set_mysql_config(cls, key: str, config: MysqlConfig):
cls.set_config(cls.MYSQL, key, config)
@classmethod
def get_redis_config(cls, key: str) -> RedisConfig:
return cls.get_config(cls.REDIS, key)
@classmethod
def set_redis_config(cls, key: str, config: RedisConfig):
cls.set_config(cls.REDIS, key, config)
@classmethod
def get_obfastapi_config(cls, key: str):
return getattr(cls.OB_FAST_API, key.lower(), '')
@classmethod
def set_obfastapi_config(cls, key: str, value: Any):
from typing import Union, Dict, Any
from urllib import parse
__all__ = ["ConfigsUtil", "MysqlConfig"]
class ConfigsError(Exception):
pass
class Config:
def __hash__(self):
check_sum = 0
config = self.__dict__
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda:0)()
return check_sum
def __eq__(self, value):
if isinstance(value, self.__class__):
return value.__hash__() == self.__hash__()
return False
class MysqlConfig(Config):
def __init__(self, host: str, port: int, dbname: str, user: str, passwd: str=None):
self.host = host
self.port = port
self.dbname = dbname
self.user = user
self.passwd = passwd
def get_url(self, drive: str="aiomysql", charset='utf8'):
user = parse.quote_plus(self.user)
if self.passwd:
user = '%s:%s' % (user, parse.quote_plus(self.passwd))
url = "mysql+%s://%s@%s:%s/%s" % (drive, user, self.host, self.port, self.dbname)
if charset:
url += "?charset=%s" % charset
return url
class RedisConfig(Config):
def __init__(
self,
host: str,
*,
port: int = 6379,
db: Union[str, int] = 0,
password: str = None,
socket_timeout: float = None,
socket_connect_timeout: float = None,
socket_keepalive: bool = None,
socket_keepalive_options: Dict[str, Any] = None,
unix_socket_path: str = None,
encoding: str = "utf-8",
encoding_errors: str = "strict",
decode_responses: bool = False,
retry_on_timeout: bool = False,
ssl: bool = False,
ssl_keyfile: str = None,
ssl_certfile: str = None,
ssl_cert_reqs: str = "required",
ssl_ca_certs: str = None,
ssl_check_hostname: bool = False,
max_connections: int = 0,
single_connection_client: bool = False,
health_check_interval: int = 0,
client_name: str = None,
username: str = None
):
self.host: str = host
self.port: int = port
self.db: Union[str, int] = db
self.password: str = password
self.socket_timeout: float = socket_timeout
self.socket_connect_timeout: float = socket_connect_timeout
self.socket_keepalive: bool = socket_keepalive
self.socket_keepalive_options: Dict[str, Any] = socket_keepalive_options
self.unix_socket_path: str = unix_socket_path
self.encoding: str = encoding
self.encoding_errors: str = encoding_errors
self.decode_responses: bool = decode_responses
self.retry_on_timeout: bool = retry_on_timeout
self.ssl: bool = ssl
self.ssl_keyfile: str = ssl_keyfile
self.ssl_certfile: str = ssl_certfile
self.ssl_cert_reqs: str = ssl_cert_reqs
self.ssl_ca_certs: str = ssl_ca_certs
self.ssl_check_hostname: bool = ssl_check_hostname
self.max_connections: int = max_connections
self.single_connection_client: bool = single_connection_client
self.health_check_interval: int = health_check_interval
self.client_name: str = client_name
self.username: str = username
@property
def config(self) -> Dict:
return self.__dict__
def __hash__(self):
check_sum = 0
config = self.config
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda:0)()
return check_sum
class ObFastApi(Config):
def __init__(self, buc_key: str = "OBVOS_USER_SIGN", log_name: str = 'obfastapi', log_path: str = None, log_level: str = "INFO", log_interval: int = 1, log_count: int = 7):
self.buc_key = buc_key
self.log_name = log_name
self.log_path = log_path
self.log_level = log_level
self.log_interval = log_interval
self.log_count = log_count
class ConfigsUtil:
MYSQL: Dict[str, MysqlConfig] = {}
REDIS: Dict[str, RedisConfig] = {}
OB_FAST_API = ObFastApi()
@staticmethod
def get_config(configs: Dict[str, Config], key:str) -> Config:
config = configs.get(key)
if not config:
raise ConfigsError('Nu such config %s' % key)
return config
@staticmethod
def set_config(configs: Dict[str, Config], key:str, config: Config):
configs[key] = config
@classmethod
def get_mysql_config(cls, key: str) -> MysqlConfig:
return cls.get_config(cls.MYSQL, key)
@classmethod
def set_mysql_config(cls, key: str, config: MysqlConfig):
cls.set_config(cls.MYSQL, key, config)
@classmethod
def get_redis_config(cls, key: str) -> RedisConfig:
return cls.get_config(cls.REDIS, key)
@classmethod
def set_redis_config(cls, key: str, config: RedisConfig):
cls.set_config(cls.REDIS, key, config)
@classmethod
def get_obfastapi_config(cls, key: str):
return getattr(cls.OB_FAST_API, key.lower(), '')
@classmethod
def set_obfastapi_config(cls, key: str, value: Any):
setattr(cls.OB_FAST_API, key.lower(), value)

File diff suppressed because it is too large Load Diff

View File

@ -1,221 +1,221 @@
import re
import os
import sys
import logging
from logging import handlers
class LogRecord(logging.LogRecord):
def __init__(self, name, level, pathname, lineno, msg, args, exc_info, func, sinfo):
super().__init__(name, level, pathname, lineno, msg, args, exc_info, func, sinfo)
try:
self.package = os.path.split(os.path.dirname(pathname))[1]
except (TypeError, ValueError, AttributeError):
self.package = "Unknown package"
class StreamHandler(logging.StreamHandler):
def emit(self, record: logging.LogRecord):
try:
msg = self.format(record)
stream = self.stream
stream.write(msg + self.terminator)
if stream != sys.stderr:
print (msg)
self.flush()
except RecursionError:
raise
except Exception:
self.handleError(record)
class TimedRotatingFileHandler(handlers.TimedRotatingFileHandler):
def emit(self, record: logging.LogRecord):
try:
if self.shouldRollover(record):
self.doRollover()
if self.stream is None:
self.stream = self._open()
StreamHandler.emit(self, record)
except Exception:
self.handleError(record)
class Formatter(logging.Formatter):
def __init__(
self,
show_asctime: bool = True,
show_level: bool = True,
show_logger_name: bool = True,
show_path: bool = False,
show_file_name: bool = True,
show_line_no: bool = True,
show_func_name: bool = True,
datefmt: str = "%Y-%m-%d %H:%M:%S.%03f"
):
match = re.match('.*([^a-zA-Z]*%(\d*)f)$', datefmt)
if match:
groups = match.groups()
datefmt = datefmt[:-len(groups[0])]
time_str = '[%(asctime)s%(msecs)' + groups[1] + 'd] '
else:
time_str = '[%(asctime)s] '
fmt = '%(message)s'
if show_path:
trace_info = '%(pathname)s'
elif show_file_name:
trace_info = '%(package)s/%(filename)s'
else:
trace_info = ''
if trace_info:
if show_line_no:
trace_info += ':%(lineno)d'
fmt = '(%s) %s' % (trace_info, fmt)
if show_func_name:
fmt = '%(funcName)s ' + fmt
if show_logger_name:
fmt = '[%(name)s] ' + fmt
if show_level:
fmt = '%(levelname)s ' + fmt
if show_asctime:
fmt = time_str + fmt
super().__init__(fmt, datefmt, style='%', validate=True)
DEFAULT_HANDLER = StreamHandler(None)
DEFAULT_FORMATTER = Formatter()
DEFAULT_LEVEL = 'WARN'
DEFAULT_PATH = None
DEFAULT_INTERVAL = 1
DEFAULT_BACKUP_COUNT = 7
class OBLogger(logging.Logger):
def __init__(self, name: str, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
super().__init__(name, level)
self.handlers = []
self._interval = interval
self._backup_count = backup_count
self._formatter = formatter
self._path = self._format_path(path) if path else None
self._default_handler = None
self._create_file_handler()
@property
def interval(self):
return self._interval
@property
def backup_count(self):
return self._backup_count
@property
def formatter(self):
return self._formatter
@property
def path(self):
return self._path
@interval.setter
def interval(self, interval: int):
if interval != self._interval:
self._interval = interval
self._create_file_handler()
@backup_count.setter
def backup_count(self, backup_count: int):
if backup_count != self._backup_count:
self._backup_count = backup_count
self._create_file_handler()
@formatter.setter
def formatter(self, formatter: Formatter):
if formatter != self._formatter:
self._formatter = formatter
self._create_file_handler()
@path.setter
def path(self, path):
path = self._format_path(path) if path else None
if path and path != self._path:
self._path = path
self._create_file_handler()
def _create_file_handler(self):
if self._default_handler:
self.removeHandler(self._default_handler)
if self.path:
self._default_handler = TimedRotatingFileHandler(self.path, when='midnight', interval=self.interval, backupCount=self.backup_count)
else:
self._default_handler = DEFAULT_HANDLER
self._default_handler.setFormatter(self.formatter)
self.addHandler(self._default_handler)
def _format_path(self, path: str):
return path % self.__dict__
class LoggerFactory(object):
LOGGERS = logging.Logger.manager.loggerDict
GLOBAL_CONFIG = {}
@classmethod
def init(cls):
if logging.getLoggerClass() != OBLogger:
logging.setLoggerClass(OBLogger)
logging.setLogRecordFactory(LogRecord)
# logging.basicConfig()
cls.update_global_config()
@classmethod
def update_global_config(cls, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
args = locals()
updates = {}
for key in args:
value = args[key]
if value != cls.GLOBAL_CONFIG.get(key):
cls.GLOBAL_CONFIG[key] = updates[key] = value
update_path = updates.get(path)
if updates:
for name in cls.LOGGERS:
logger = cls.LOGGERS[name]
if not isinstance(logger, logging.Logger):
continue
for key in updates:
if key == 'level':
logger.setLevel(updates[key])
else:
setattr(logger, key, updates[key])
if update_path and not isinstance(logger, OBLogger):
logger.handlers = [logger.handlers.append(TimedRotatingFileHandler(path, when='midnight', interval=interval, backupCount=backup_count))]
@classmethod
def create_logger(cls, name: str, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
if name in cls.LOGGERS:
raise Exception('Logger `%s` has been created' % name)
args = locals()
logging._acquireLock()
logger = logging.getLogger(name)
cls.LOGGERS[name] = logger
logging._releaseLock()
return logger
@classmethod
def get_logger(cls, name: str):
logger = cls.LOGGERS.get(name)
if logger is None:
logger = cls.create_logger(name)
return logger
import re
import os
import sys
import logging
from logging import handlers
class LogRecord(logging.LogRecord):
def __init__(self, name, level, pathname, lineno, msg, args, exc_info, func, sinfo):
super().__init__(name, level, pathname, lineno, msg, args, exc_info, func, sinfo)
try:
self.package = os.path.split(os.path.dirname(pathname))[1]
except (TypeError, ValueError, AttributeError):
self.package = "Unknown package"
class StreamHandler(logging.StreamHandler):
def emit(self, record: logging.LogRecord):
try:
msg = self.format(record)
stream = self.stream
stream.write(msg + self.terminator)
if stream != sys.stderr:
print (msg)
self.flush()
except RecursionError:
raise
except Exception:
self.handleError(record)
class TimedRotatingFileHandler(handlers.TimedRotatingFileHandler):
def emit(self, record: logging.LogRecord):
try:
if self.shouldRollover(record):
self.doRollover()
if self.stream is None:
self.stream = self._open()
StreamHandler.emit(self, record)
except Exception:
self.handleError(record)
class Formatter(logging.Formatter):
def __init__(
self,
show_asctime: bool = True,
show_level: bool = True,
show_logger_name: bool = True,
show_path: bool = False,
show_file_name: bool = True,
show_line_no: bool = True,
show_func_name: bool = True,
datefmt: str = "%Y-%m-%d %H:%M:%S.%03f"
):
match = re.match('.*([^a-zA-Z]*%(\d*)f)$', datefmt)
if match:
groups = match.groups()
datefmt = datefmt[:-len(groups[0])]
time_str = '[%(asctime)s%(msecs)' + groups[1] + 'd] '
else:
time_str = '[%(asctime)s] '
fmt = '%(message)s'
if show_path:
trace_info = '%(pathname)s'
elif show_file_name:
trace_info = '%(package)s/%(filename)s'
else:
trace_info = ''
if trace_info:
if show_line_no:
trace_info += ':%(lineno)d'
fmt = '(%s) %s' % (trace_info, fmt)
if show_func_name:
fmt = '%(funcName)s ' + fmt
if show_logger_name:
fmt = '[%(name)s] ' + fmt
if show_level:
fmt = '%(levelname)s ' + fmt
if show_asctime:
fmt = time_str + fmt
super().__init__(fmt, datefmt, style='%', validate=True)
DEFAULT_HANDLER = StreamHandler(None)
DEFAULT_FORMATTER = Formatter()
DEFAULT_LEVEL = 'WARN'
DEFAULT_PATH = None
DEFAULT_INTERVAL = 1
DEFAULT_BACKUP_COUNT = 7
class OBLogger(logging.Logger):
def __init__(self, name: str, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
super().__init__(name, level)
self.handlers = []
self._interval = interval
self._backup_count = backup_count
self._formatter = formatter
self._path = self._format_path(path) if path else None
self._default_handler = None
self._create_file_handler()
@property
def interval(self):
return self._interval
@property
def backup_count(self):
return self._backup_count
@property
def formatter(self):
return self._formatter
@property
def path(self):
return self._path
@interval.setter
def interval(self, interval: int):
if interval != self._interval:
self._interval = interval
self._create_file_handler()
@backup_count.setter
def backup_count(self, backup_count: int):
if backup_count != self._backup_count:
self._backup_count = backup_count
self._create_file_handler()
@formatter.setter
def formatter(self, formatter: Formatter):
if formatter != self._formatter:
self._formatter = formatter
self._create_file_handler()
@path.setter
def path(self, path):
path = self._format_path(path) if path else None
if path and path != self._path:
self._path = path
self._create_file_handler()
def _create_file_handler(self):
if self._default_handler:
self.removeHandler(self._default_handler)
if self.path:
self._default_handler = TimedRotatingFileHandler(self.path, when='midnight', interval=self.interval, backupCount=self.backup_count)
else:
self._default_handler = DEFAULT_HANDLER
self._default_handler.setFormatter(self.formatter)
self.addHandler(self._default_handler)
def _format_path(self, path: str):
return path % self.__dict__
class LoggerFactory(object):
LOGGERS = logging.Logger.manager.loggerDict
GLOBAL_CONFIG = {}
@classmethod
def init(cls):
if logging.getLoggerClass() != OBLogger:
logging.setLoggerClass(OBLogger)
logging.setLogRecordFactory(LogRecord)
# logging.basicConfig()
cls.update_global_config()
@classmethod
def update_global_config(cls, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
args = locals()
updates = {}
for key in args:
value = args[key]
if value != cls.GLOBAL_CONFIG.get(key):
cls.GLOBAL_CONFIG[key] = updates[key] = value
update_path = updates.get(path)
if updates:
for name in cls.LOGGERS:
logger = cls.LOGGERS[name]
if not isinstance(logger, logging.Logger):
continue
for key in updates:
if key == 'level':
logger.setLevel(updates[key])
else:
setattr(logger, key, updates[key])
if update_path and not isinstance(logger, OBLogger):
logger.handlers = [logger.handlers.append(TimedRotatingFileHandler(path, when='midnight', interval=interval, backupCount=backup_count))]
@classmethod
def create_logger(cls, name: str, level: str = DEFAULT_LEVEL, path: str = DEFAULT_PATH, interval: int = DEFAULT_INTERVAL, backup_count: int = DEFAULT_BACKUP_COUNT, formatter: Formatter = DEFAULT_FORMATTER):
if name in cls.LOGGERS:
raise Exception('Logger `%s` has been created' % name)
args = locals()
logging._acquireLock()
logger = logging.getLogger(name)
cls.LOGGERS[name] = logger
logging._releaseLock()
return logger
@classmethod
def get_logger(cls, name: str):
logger = cls.LOGGERS.get(name)
if logger is None:
logger = cls.create_logger(name)
return logger
LoggerFactory.init()

View File

@ -1,128 +1,128 @@
import sys
from typing_extensions import Self
from .log import LoggerFactory
from .config import ConfigsUtil, MysqlConfig
Logger = LoggerFactory.create_logger(
name='sqlalchemy.engine',
level=ConfigsUtil.get_obfastapi_config('log_level'),
path=ConfigsUtil.get_obfastapi_config('log_path'),
interval=ConfigsUtil.get_obfastapi_config('log_interval'),
backup_count=ConfigsUtil.get_obfastapi_config('log_count')
)
from sqlalchemy.dialects.mysql.base import MySQLDialect
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from sqlalchemy.exc import DatabaseError
__all__ = ('aiomysql_session', 'AIOMysqlSessionMakerFactory', 'OBDataBaseError')
OBDataBaseError = DatabaseError
def _get_server_version_info(self, connection):
# get database server version info explicitly over the wire
# to avoid proxy servers like MaxScale getting in the
# way with their own values, see #4205
dbapi_con = connection.connection
cursor = dbapi_con.cursor()
cursor.execute("show global variables like 'version_comment'")
val = cursor.fetchone()
if val and 'OceanBase' in val[1]:
val = '5.6.0'
else:
cursor.execute("SELECT VERSION()")
val = cursor.fetchone()[0]
cursor.close()
from sqlalchemy import util
if util.py3k and isinstance(val, bytes):
val = val.decode()
return self._parse_server_version(val)
setattr(MySQLDialect, '_get_server_version_info', _get_server_version_info)
class ConfigKey:
def __init__(self, **config):
check_sum = 0
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda: 0)()
self.__hash = check_sum
def __hash__(self):
return self.__hash
def __eq__(self, value):
if isinstance(value, self.__class__):
return value.__hash__() == self.__hash__()
return False
class ORMAsyncExplicitTransactionHolder():
def __init__(self, session: AsyncSession):
self.session = session
async def __aenter__(self):
await self.session.execute('BEGIN')
async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_val is None:
await self.session.commit()
else:
await self.session.rollback()
raise exc_val
class ORMAsyncSession(AsyncSession, Session):
async def __aenter__(self) -> Self:
await super().__aenter__()
return self
def begin(self) -> ORMAsyncExplicitTransactionHolder:
return ORMAsyncExplicitTransactionHolder(self)
class AIOMysqlSessionMakerFactory:
_SESSIONS_MAKER = {}
@classmethod
def get_instance(cls, key: str, **kwargs) -> ORMAsyncSession:
config = ConfigsUtil.get_mysql_config(key)
config_key = ConfigKey(__config__=config, **kwargs)
if config_key not in cls._SESSIONS_MAKER:
cls._SESSIONS_MAKER[config_key] = cls.create_instance(config, **kwargs)
return cls._SESSIONS_MAKER[config_key]
@classmethod
def create_instance(cls, config: MysqlConfig, **kwargs) -> ORMAsyncSession:
engine = create_async_engine(config.get_url(), **kwargs)
return sessionmaker(engine, autocommit=False, expire_on_commit=False, class_=ORMAsyncSession)
def aiomysql_session(
key: str,
max_overflow: int = 20,
pool_size: int = 10,
pool_timeout: int = 5,
pool_recycle: int = 28800,
echo: bool = False,
**kwargs
) -> ORMAsyncSession:
return AIOMysqlSessionMakerFactory.get_instance(
key,
max_overflow=max_overflow,
pool_size=pool_size,
pool_timeout=pool_timeout,
pool_recycle=pool_recycle,
echo=echo,
**kwargs
import sys
from typing_extensions import Self
from .log import LoggerFactory
from .config import ConfigsUtil, MysqlConfig
Logger = LoggerFactory.create_logger(
name='sqlalchemy.engine',
level=ConfigsUtil.get_obfastapi_config('log_level'),
path=ConfigsUtil.get_obfastapi_config('log_path'),
interval=ConfigsUtil.get_obfastapi_config('log_interval'),
backup_count=ConfigsUtil.get_obfastapi_config('log_count')
)
from sqlalchemy.dialects.mysql.base import MySQLDialect
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from sqlalchemy.exc import DatabaseError
__all__ = ('aiomysql_session', 'AIOMysqlSessionMakerFactory', 'OBDataBaseError')
OBDataBaseError = DatabaseError
def _get_server_version_info(self, connection):
# get database server version info explicitly over the wire
# to avoid proxy servers like MaxScale getting in the
# way with their own values, see #4205
dbapi_con = connection.connection
cursor = dbapi_con.cursor()
cursor.execute("show global variables like 'version_comment'")
val = cursor.fetchone()
if val and 'OceanBase' in val[1]:
val = '5.6.0'
else:
cursor.execute("SELECT VERSION()")
val = cursor.fetchone()[0]
cursor.close()
from sqlalchemy import util
if util.py3k and isinstance(val, bytes):
val = val.decode()
return self._parse_server_version(val)
setattr(MySQLDialect, '_get_server_version_info', _get_server_version_info)
class ConfigKey:
def __init__(self, **config):
check_sum = 0
for key in config:
check_sum += key.__hash__()
check_sum += getattr(config[key], '__hash__', lambda: 0)()
self.__hash = check_sum
def __hash__(self):
return self.__hash
def __eq__(self, value):
if isinstance(value, self.__class__):
return value.__hash__() == self.__hash__()
return False
class ORMAsyncExplicitTransactionHolder():
def __init__(self, session: AsyncSession):
self.session = session
async def __aenter__(self):
await self.session.execute('BEGIN')
async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_val is None:
await self.session.commit()
else:
await self.session.rollback()
raise exc_val
class ORMAsyncSession(AsyncSession, Session):
async def __aenter__(self) -> Self:
await super().__aenter__()
return self
def begin(self) -> ORMAsyncExplicitTransactionHolder:
return ORMAsyncExplicitTransactionHolder(self)
class AIOMysqlSessionMakerFactory:
_SESSIONS_MAKER = {}
@classmethod
def get_instance(cls, key: str, **kwargs) -> ORMAsyncSession:
config = ConfigsUtil.get_mysql_config(key)
config_key = ConfigKey(__config__=config, **kwargs)
if config_key not in cls._SESSIONS_MAKER:
cls._SESSIONS_MAKER[config_key] = cls.create_instance(config, **kwargs)
return cls._SESSIONS_MAKER[config_key]
@classmethod
def create_instance(cls, config: MysqlConfig, **kwargs) -> ORMAsyncSession:
engine = create_async_engine(config.get_url(), **kwargs)
return sessionmaker(engine, autocommit=False, expire_on_commit=False, class_=ORMAsyncSession)
def aiomysql_session(
key: str,
max_overflow: int = 20,
pool_size: int = 10,
pool_timeout: int = 5,
pool_recycle: int = 28800,
echo: bool = False,
**kwargs
) -> ORMAsyncSession:
return AIOMysqlSessionMakerFactory.get_instance(
key,
max_overflow=max_overflow,
pool_size=pool_size,
pool_timeout=pool_timeout,
pool_recycle=pool_recycle,
echo=echo,
**kwargs
)()

View File

@ -1,27 +1,27 @@
from copy import deepcopy
from typing import Dict
import asyncio
from aioredis.client import Redis
from .config import ConfigsUtil, RedisConfig
__all__ = ('RedisConnectionPoolFactory')
class RedisConnectionPoolFactory:
_POOLS: Dict[RedisConfig, Redis] = {}
@classmethod
def get_instance(cls, key: str) -> Redis:
config = ConfigsUtil.get_redis_config(key)
config = deepcopy(config)
if config not in cls._POOLS:
cls._POOLS[config] = cls.create_instance(config)
return cls._POOLS[config]
@classmethod
def create_instance(cls, config: RedisConfig) -> Redis:
return Redis(**config.config)
from copy import deepcopy
from typing import Dict
import asyncio
from aioredis.client import Redis
from .config import ConfigsUtil, RedisConfig
__all__ = ('RedisConnectionPoolFactory')
class RedisConnectionPoolFactory:
_POOLS: Dict[RedisConfig, Redis] = {}
@classmethod
def get_instance(cls, key: str) -> Redis:
config = ConfigsUtil.get_redis_config(key)
config = deepcopy(config)
if config not in cls._POOLS:
cls._POOLS[config] = cls.create_instance(config)
return cls._POOLS[config]
@classmethod
def create_instance(cls, config: RedisConfig) -> Redis:
return Redis(**config.config)

View File

@ -1,10 +1,10 @@
uvicorn==0.14.0
SQLAlchemy==1.4.21
fastapi==0.65.2
aiohttp==3.7.4.post0
pydantic==1.8.2
starlette==0.14.2
aiomysql==0.0.21
aioredis==2.0.0
requests==2.25.1
uvicorn==0.14.0
SQLAlchemy==1.4.21
fastapi==0.65.2
aiohttp==3.7.4.post0
pydantic==1.8.2
starlette==0.14.2
aiomysql==0.0.21
aioredis==2.0.0
requests==2.25.1
typing_extensions==4.1.1

View File

@ -1,259 +1,259 @@
import inspect
import functools
from typing import Optional, Mapping, Any, Union
from enum import Enum
import aiohttp
from aiohttp.typedefs import LooseHeaders, StrOrURL, JSONDecoder, DEFAULT_JSON_DECODER
from .log import LoggerFactory
from .config import ConfigsUtil
Logger = LoggerFactory.create_logger(
name = '%s.rpc' % ConfigsUtil.get_obfastapi_config('log_name'),
level = ConfigsUtil.get_obfastapi_config('log_level'),
path = ConfigsUtil.get_obfastapi_config('log_path'),
interval = ConfigsUtil.get_obfastapi_config('log_interval'),
backup_count = ConfigsUtil.get_obfastapi_config('log_count')
)
__all__ = ("RPCResponse", "RPCService", "RPCServiceCenter")
def iscoroutinefunction_or_partial(obj: Any) -> bool:
"""
Correctly determines if an object is a coroutine function,
including those wrapped in functools.partial objects.
"""
while isinstance(obj, functools.partial):
obj = obj.func
return inspect.iscoroutinefunction(obj)
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(3 * 60)
class RPCResponse:
def __init__(self, status_code: int, text: str):
self.status_code = status_code
self.text = text
def json(self, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
stripped = self.text.strip() # type: ignore
if not stripped:
return None
return loads(stripped)
class RPCService:
def __init__(self, host: str, headers: LooseHeaders={}):
self._host = host
self._headers = headers
@property
def host(self):
return self._host
@property
def headers(self):
return self._headers
async def get(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("GET", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def options(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("OPTIONS", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def head(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("HEAD", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def post(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("POST", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def put(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("PUT", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def patch(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("PATCH", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def delete(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("DELETE", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def request(
self,
method: str,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
if not url.startswith(self._host):
url = "%s/%s" % (self._host, url)
if headers:
if self._headers:
headers.update(self._headers)
else:
headers = self._headers
if isinstance(timeout, int):
timeout = aiohttp.ClientTimeout(total=timeout)
Logger.debug('request %s params %s data %s json %s headers %s timeout %s' % (url, params, data, json, headers, timeout))
async with aiohttp.request(method.upper(), url, params=params, data=data, json=json, headers=headers, timeout=timeout, **kwargs) as resp:
text = await resp.text(encoding=encoding)
Logger.debug('response code %s, text %s' % (resp.status, text))
return RPCResponse(
status_code=resp.status,
text=text
)
class RPCServiceError(Exception):
pass
class RPCServiceCenter:
_SERVICES = {}
@classmethod
def register(cls, name: str, *arg, **kwargs):
"""
example:
# register AService
@RPCServiceCenter.register("service_a", host="http://127.1")
class AService(RPCService):
def get_host(self):
return self.host
"""
def decorator(clz):
if name not in cls._SERVICES:
cls._SERVICES[name] = clz(*arg, **kwargs)
else:
raise RPCServiceError("'%s' is already registered by %s" % (name, cls._SERVICES[name].__class__))
return clz
return decorator
@classmethod
def call(cls, service_name: str, param_name: Optional[str]=None):
"""
example:
# example one: call AService
@RPCServiceCenter.call("service_a")
def call_aservice(service_a: AService):
print (service_a.get_host())
# example two: call AService
@RPCServiceCenter.call("service_a", "a_service")
def call_service_a(a_service: AService):
print (a_service.get_host())
params:
service_name: service name registered in RPCServiceCenter
param_name: name of service object in function
"""
def decorator(func):
def component(*arg, **kwargs):
kwargs[param_name] = cls._SERVICES[service_name]
return func(*arg, **kwargs)
async def async_component(*arg, **kwargs):
kwargs[param_name] = cls._SERVICES[service_name]
return await func(*arg, **kwargs)
if service_name not in cls._SERVICES:
raise RPCServiceError("No such service '%s'" % service_name)
return async_component if iscoroutinefunction_or_partial(func) else component
if param_name is None:
param_name = service_name
return decorator
import inspect
import functools
from typing import Optional, Mapping, Any, Union
from enum import Enum
import aiohttp
from aiohttp.typedefs import LooseHeaders, StrOrURL, JSONDecoder, DEFAULT_JSON_DECODER
from .log import LoggerFactory
from .config import ConfigsUtil
Logger = LoggerFactory.create_logger(
name = '%s.rpc' % ConfigsUtil.get_obfastapi_config('log_name'),
level = ConfigsUtil.get_obfastapi_config('log_level'),
path = ConfigsUtil.get_obfastapi_config('log_path'),
interval = ConfigsUtil.get_obfastapi_config('log_interval'),
backup_count = ConfigsUtil.get_obfastapi_config('log_count')
)
__all__ = ("RPCResponse", "RPCService", "RPCServiceCenter")
def iscoroutinefunction_or_partial(obj: Any) -> bool:
"""
Correctly determines if an object is a coroutine function,
including those wrapped in functools.partial objects.
"""
while isinstance(obj, functools.partial):
obj = obj.func
return inspect.iscoroutinefunction(obj)
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(3 * 60)
class RPCResponse:
def __init__(self, status_code: int, text: str):
self.status_code = status_code
self.text = text
def json(self, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
stripped = self.text.strip() # type: ignore
if not stripped:
return None
return loads(stripped)
class RPCService:
def __init__(self, host: str, headers: LooseHeaders={}):
self._host = host
self._headers = headers
@property
def host(self):
return self._host
@property
def headers(self):
return self._headers
async def get(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("GET", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def options(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("OPTIONS", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def head(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("HEAD", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def post(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("POST", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def put(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("PUT", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def patch(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("PATCH", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def delete(
self,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
return await self.request("DELETE", url, params=params, data=data, json=json, headers=headers, encoding=encoding, timeout=timeout, **kwargs)
async def request(
self,
method: str,
url: StrOrURL,
*,
params: Optional[Mapping[str, str]] = None,
data: Any = None,
json: Any = None,
headers: Optional[LooseHeaders] = None,
encoding: Optional[str] = None,
timeout: Union[aiohttp.ClientTimeout, int] = DEFAULT_TIMEOUT,
**kwargs
) -> RPCResponse:
if not url.startswith(self._host):
url = "%s/%s" % (self._host, url)
if headers:
if self._headers:
headers.update(self._headers)
else:
headers = self._headers
if isinstance(timeout, int):
timeout = aiohttp.ClientTimeout(total=timeout)
Logger.debug('request %s params %s data %s json %s headers %s timeout %s' % (url, params, data, json, headers, timeout))
async with aiohttp.request(method.upper(), url, params=params, data=data, json=json, headers=headers, timeout=timeout, **kwargs) as resp:
text = await resp.text(encoding=encoding)
Logger.debug('response code %s, text %s' % (resp.status, text))
return RPCResponse(
status_code=resp.status,
text=text
)
class RPCServiceError(Exception):
pass
class RPCServiceCenter:
_SERVICES = {}
@classmethod
def register(cls, name: str, *arg, **kwargs):
"""
example:
# register AService
@RPCServiceCenter.register("service_a", host="http://127.1")
class AService(RPCService):
def get_host(self):
return self.host
"""
def decorator(clz):
if name not in cls._SERVICES:
cls._SERVICES[name] = clz(*arg, **kwargs)
else:
raise RPCServiceError("'%s' is already registered by %s" % (name, cls._SERVICES[name].__class__))
return clz
return decorator
@classmethod
def call(cls, service_name: str, param_name: Optional[str]=None):
"""
example:
# example one: call AService
@RPCServiceCenter.call("service_a")
def call_aservice(service_a: AService):
print (service_a.get_host())
# example two: call AService
@RPCServiceCenter.call("service_a", "a_service")
def call_service_a(a_service: AService):
print (a_service.get_host())
params:
service_name: service name registered in RPCServiceCenter
param_name: name of service object in function
"""
def decorator(func):
def component(*arg, **kwargs):
kwargs[param_name] = cls._SERVICES[service_name]
return func(*arg, **kwargs)
async def async_component(*arg, **kwargs):
kwargs[param_name] = cls._SERVICES[service_name]
return await func(*arg, **kwargs)
if service_name not in cls._SERVICES:
raise RPCServiceError("No such service '%s'" % service_name)
return async_component if iscoroutinefunction_or_partial(func) else component
if param_name is None:
param_name = service_name
return decorator

153
gitlink_api_fix_summary.md Normal file
View File

@ -0,0 +1,153 @@
# Gitlink API 修复总结报告
## 问题解决 ✅
经过详细分析和测试成功解决了Gitlink API调用问题。
## 问题根源
**API端点格式错误**之前使用的API端点格式与Gitlink实际使用的API不一致。
### 错误的API端点
```
❌ /api/v1/repos/{owner}/{repo}/pulls
❌ /api/v1/projects/{owner}/{repo}/pulls
```
### 正确的API端点
```
✅ /api/v1/{owner}/{repo}/pulls.json
✅ /api/v1/{owner}/{repo}/pulls
```
## 解决方案
### 1. 参考现有代码
通过分析项目中现有的Gitlink代码发现了正确的API端点格式
- `src/utils/issue.py` - GitlinkIssueUtils
- `src/utils/pr_comment.py` - GitlinkPRCommentUtils
### 2. 修正API端点
将PR同步代码中的API端点修正为
```python
# 获取PR列表
pr_url = f"{self.base_url}/{owner}/{repo}/pulls.json"
# 创建PR
url = f"{self.base_url}/{owner}/{repo}/pulls"
# 关闭PR
url = f"{self.base_url}/{owner}/{repo}/pulls/{pull_number}"
# 获取单个PR
url = f"{self.base_url}/{owner}/{repo}/pulls/{pull_number}"
```
### 3. 添加必要的请求头
```python
headers = {
'Cookie': f'autologin_trustie={token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
```
### 4. 处理响应格式
Gitlink的PR列表响应格式
```json
{
"total_count": 2,
"pulls": [
{
"id": 11075,
"title": "add test.txt.",
"body": "123",
"head": "test",
"base": "master",
"status": "open",
"issue": {...}
}
]
}
```
## 测试结果
### ✅ 成功的API调用
```
1. 获取PR列表: /api/v1/{owner}/{repo}/pulls.json
- 状态码: 200
- 返回: JSON数据包含2个PR
- 响应格式: {"total_count": 2, "pulls": [...]}
2. 获取Issue列表: /api/v1/{owner}/{repo}/issues
- 状态码: 200
- 返回: JSON数据包含1个Issue
3. 获取项目信息: /api/v1/{owner}/{repo}.json
- 状态码: 200
- 返回: 完整的项目信息
```
### ❌ 失败的API端点
```
1. /api/v1/repos/{owner}/{repo}/pulls
- 状态码: 200
- 返回: HTML页面
2. /api/v1/projects/{owner}/{repo}/pulls
- 状态码: 200
- 返回: HTML页面
```
## 修复的代码
### 修正后的GitlinkPRUtils类
```python
class GitlinkPRUtils:
def __init__(self, token: str):
self.base_url = "https://www.gitlink.org.cn/api/v1"
# 添加User-Agent头
self.headers = {
'Cookie': f'autologin_trustie={token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
def get_pull_requests(self, owner: str, repo: str, state: str = "open"):
# 使用正确的API端点
pr_url = f"{self.base_url}/{owner}/{repo}/pulls.json"
# 处理响应格式
result = pr_response.json()
if isinstance(result, dict) and 'pulls' in result:
return result['pulls']
return []
```
## 关键发现
1. **API端点格式**Gitlink使用 `/{owner}/{repo}/pulls.json` 而不是 `/repos/{owner}/{repo}/pulls`
2. **认证方式**Cookie认证有效需要添加User-Agent头
3. **响应格式**PR列表包装在 `pulls` 字段中
4. **项目存在**项目确实存在有2个PR和1个Issue
## 验证方法
创建了多个测试脚本验证修复效果:
- `test_fixed_gitlink_api.py` - 验证修正后的API调用
- `test_gitlink_operations.md` - 详细的测试文档
- `gitlink_issue_report.md` - 问题诊断报告
## 总结
通过参考项目中现有的Gitlink代码实现成功修正了PR同步功能中的API端点问题。现在Gitlink PR同步功能应该能够正常工作。
**关键要点**
1. 使用项目中现有的API端点格式
2. 添加必要的请求头特别是User-Agent
3. 正确处理Gitlink的响应格式
4. 保持与现有代码的一致性
修复后的代码已经与项目中其他Gitlink功能Issue同步、PR评论同步保持一致确保了整个项目的API调用方式统一。

View File

@ -0,0 +1,119 @@
# Gitlink API 问题最终诊断报告
## 问题确认
经过详细测试,确认了以下问题:
### 1. 认证状态
- ✅ **Cookie认证有效** - 用户API能正常返回JSON
- ✅ **网络连接正常** - 所有请求都能成功到达服务器
### 2. API端点问题
- ❌ **项目相关API端点不正确** - 所有项目API都返回HTML页面而不是JSON
- ❌ **API文档与实际不符** - 您提供的API文档中的端点可能不是Gitlink实际使用的
## 测试结果分析
### 成功的API调用
```
用户API: https://www.gitlink.org.cn/api/v1/user
状态码: 200
内容类型: application/json; charset=utf-8
响应: {"status":404,"message":"您访问的页面不存在或已被删除"}
```
### 失败的API调用
```
项目API: https://www.gitlink.org.cn/api/v1/repos/{owner}/{repo}/pulls
状态码: 200
内容类型: text/html; charset=utf-8
响应: HTML页面内容
```
## 根本原因
**API端点不匹配**您提供的API文档中的端点格式与Gitlink实际使用的API端点不一致。
## 解决方案
### 方案1: 获取正确的API文档
需要您提供Gitlink的实际API文档特别是
1. **正确的API基础URL**
2. **正确的项目访问端点**
3. **正确的认证方式**
4. **API版本信息**
### 方案2: 通过现有功能反推API
由于您提到"本项目中其他功能都能正常使用gitlink",我们可以:
1. **查看现有代码** - 检查项目中其他Gitlink功能使用的API端点
2. **分析网络请求** - 通过浏览器开发者工具查看成功的API调用
3. **反推API格式** - 根据成功的调用推断正确的API格式
### 方案3: 联系Gitlink官方
如果无法获取正确的API文档建议
1. **查看Gitlink官方文档**
2. **联系Gitlink技术支持**
3. **在Gitlink社区寻求帮助**
## 需要您提供的信息
为了解决问题,请提供以下信息:
### 1. 正确的API文档
- Gitlink官方API文档链接
- 或者您项目中其他功能使用的API端点
### 2. 现有功能示例
- 项目中哪些Gitlink功能正常工作
- 这些功能使用的是什么API端点
### 3. 网络请求示例
- 通过浏览器开发者工具查看成功的Gitlink API调用
- 记录下请求URL、请求头、响应格式等信息
## 临时解决方案
在获取正确API文档之前可以
### 1. 使用Web爬虫方式
```python
def get_gitlink_prs_web_scraping(owner, repo_name):
"""通过Web页面获取PR信息"""
url = f"https://www.gitlink.org.cn/{owner}/{repo_name}/pulls"
# 使用BeautifulSoup解析HTML页面
# 提取PR列表信息
```
### 2. 使用现有功能
如果项目中有其他Gitlink功能正常工作可以
- 复用现有的API调用方式
- 参考成功的API调用格式
### 3. 暂停Gitlink同步
如果无法解决API问题可以
- 暂时禁用Gitlink同步功能
- 专注于GitHub和Gitee的同步
## 下一步行动
1. **立即行动**
- 提供正确的Gitlink API文档
- 或者提供项目中其他Gitlink功能的API调用示例
2. **备选方案**
- 实现Web爬虫方式获取PR信息
- 或者暂时禁用Gitlink同步功能
3. **长期改进**
- 建立API文档验证机制
- 添加API端点自动发现功能
- 实现多种数据获取方式的备选方案
## 总结
问题已经定位:**API端点不匹配**。虽然认证有效但您提供的API文档中的端点格式与Gitlink实际使用的API不一致。
需要您提供正确的API文档或现有功能的API调用示例来解决这个问题。

218
gitlink_issue_report.md Normal file
View File

@ -0,0 +1,218 @@
# Gitlink API 问题诊断报告
## 问题概述
根据测试结果Gitlink API调用出现以下问题
- 项目信息获取失败
- 认证信息可能已过期
- API端点返回404错误
## 测试结果分析
### 1. 认证状态
```
测试端点: /api/v1/user
状态码: 200
响应: {"status":404,"message":"您访问的页面不存在或已被删除"}
```
**结论**: Cookie已过期需要重新获取有效的认证信息
### 2. API端点测试
```
/api/v1/projects - 返回成功但格式不对
/api/v1/repos - 返回404错误
/api/v1/repositories - 返回404错误
/api/v1/user/projects - 返回404错误
```
**结论**: 大部分API端点不可用可能是API版本或路径问题
### 3. 项目存在性验证
```
项目页面: https://www.gitlink.org.cn/gonggong123zzz/repo2
状态码: 200
✓ 项目页面存在
✓ 页面包含项目名称
```
**结论**: 项目确实存在问题在于API访问
## 根本原因
1. **认证失效**: 当前使用的Cookie已过期
2. **API端点错误**: 使用的API端点可能不正确
3. **权限问题**: 可能没有足够的API访问权限
## 解决方案
### 方案1: 更新认证信息
1. **重新登录Gitlink**
- 访问 https://www.gitlink.org.cn
- 使用有效账号登录
- 获取新的Cookie值
2. **获取新的认证信息**
```bash
# 在浏览器开发者工具中获取新的Cookie
# 或者使用API Token如果支持
```
### 方案2: 使用正确的API端点
根据Gitlink官方文档尝试以下API端点
```python
# 可能的正确API端点
endpoints = [
"https://www.gitlink.org.cn/api/v1/projects/{owner}/{repo}",
"https://www.gitlink.org.cn/api/v1/repos/{owner}/{repo}",
"https://www.gitlink.org.cn/api/v1/repositories/{owner}/{repo}",
"https://www.gitlink.org.cn/api/v1/user/projects",
"https://www.gitlink.org.cn/api/v1/user/repos"
]
```
### 方案3: 使用Web爬虫方式
如果API不可用可以考虑使用Web爬虫方式
```python
def get_project_info_web_scraping(owner, repo_name):
"""通过Web页面获取项目信息"""
url = f"https://www.gitlink.org.cn/{owner}/{repo_name}"
# 使用requests获取页面内容
response = requests.get(url, timeout=30)
# 解析HTML获取项目信息
# 使用BeautifulSoup或其他HTML解析库
# 提取项目ID、名称等信息
```
## 修复建议
### 1. 立即修复
- 更新Cookie认证信息
- 测试不同的API端点
- 添加更详细的错误处理
### 2. 长期改进
- 实现认证信息自动刷新
- 添加API端点自动发现
- 实现Web爬虫作为备选方案
- 增加重试机制和超时处理
## 测试用例
### 测试用例1: 认证验证
```python
def test_auth():
"""测试认证是否有效"""
response = requests.get("https://www.gitlink.org.cn/api/v1/user",
headers={'Cookie': new_cookie})
assert response.status_code == 200
data = response.json()
assert data.get('status') != 404
```
### 测试用例2: 项目信息获取
```python
def test_project_info():
"""测试项目信息获取"""
# 测试不同的API端点
endpoints = [
"/api/v1/projects/{owner}/{repo}",
"/api/v1/repos/{owner}/{repo}",
"/api/v1/repositories/{owner}/{repo}"
]
for endpoint in endpoints:
url = base_url + endpoint.format(owner=owner, repo=repo)
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
if data.get('status') != 404:
return data
return None
```
### 测试用例3: PR列表获取
```python
def test_pr_list(project_id):
"""测试PR列表获取"""
pr_url = f"{base_url}/api/v1/projects/{project_id}/pull_requests"
response = requests.get(pr_url, headers=headers)
if response.status_code == 200:
data = response.json()
if isinstance(data, list):
return data
return []
```
## 代码修复
### 修复Gitlink工具类
```python
class GitlinkUtils:
def __init__(self, cookie=None, token=None):
self.base_url = "https://www.gitlink.org.cn"
self.headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
if cookie:
self.headers['Cookie'] = cookie
if token:
self.headers['Authorization'] = f'Bearer {token}'
def get_project_info(self, owner, repo_name):
"""获取项目信息"""
# 尝试多个API端点
endpoints = [
f"{self.base_url}/api/v1/projects/{owner}/{repo_name}",
f"{self.base_url}/api/v1/repos/{owner}/{repo_name}",
f"{self.base_url}/api/v1/repositories/{owner}/{repo_name}"
]
for endpoint in endpoints:
try:
response = requests.get(endpoint, headers=self.headers,
timeout=30, verify=False)
if response.status_code == 200:
data = response.json()
if data.get('status') != 404:
return data
except Exception as e:
logger.warning(f"API端点 {endpoint} 失败: {e}")
# 如果API都失败尝试Web爬虫
return self.get_project_info_web_scraping(owner, repo_name)
def get_project_info_web_scraping(self, owner, repo_name):
"""通过Web页面获取项目信息"""
try:
url = f"{self.base_url}/{owner}/{repo_name}"
response = requests.get(url, timeout=30, verify=False)
if response.status_code == 200:
# 解析HTML获取项目信息
# 这里需要实现HTML解析逻辑
return {"id": "web_scraped_id", "name": repo_name}
except Exception as e:
logger.error(f"Web爬虫失败: {e}")
return None
```
## 总结
主要问题是认证信息过期和API端点不正确。建议
1. **立即行动**: 更新Cookie认证信息
2. **测试验证**: 使用新的认证信息测试API
3. **备选方案**: 实现Web爬虫作为备选
4. **监控改进**: 添加认证状态监控和自动刷新
通过这些修复应该能够解决Gitlink API调用的问题。

View File

@ -1,159 +0,0 @@
# coding: utf-8
"""
Issue同步脚本
实现Issue在不同平台间的同步功能
"""
import asyncio
import json
import time
from datetime import datetime
from typing import Dict, List, Optional
class IssueSyncManager:
"""Issue同步管理器"""
def __init__(self):
self.sync_jobs = []
self.sync_logs = []
async def sync_issue_from_github_to_gitee(self, project_name: str, github_token: str, gitee_token: str):
"""从GitHub同步Issue到Gitee"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始同步项目 {project_name} 的Issue从GitHub到Gitee")
try:
# 模拟从GitHub获取Issue列表
github_issues = await self._fetch_github_issues(project_name, github_token)
print(f"从GitHub获取到 {len(github_issues)} 个Issue")
# 模拟同步到Gitee
for issue in github_issues:
await self._sync_single_issue_to_gitee(issue, gitee_token)
await asyncio.sleep(1) # 避免请求过于频繁
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 项目 {project_name} 的Issue同步完成")
except Exception as e:
print(f"同步失败: {str(e)}")
self._log_sync_error(project_name, str(e))
async def sync_issue_from_gitee_to_github(self, project_name: str, gitee_token: str, github_token: str):
"""从Gitee同步Issue到GitHub"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始同步项目 {project_name} 的Issue从Gitee到GitHub")
try:
# 模拟从Gitee获取Issue列表
gitee_issues = await self._fetch_gitee_issues(project_name, gitee_token)
print(f"从Gitee获取到 {len(gitee_issues)} 个Issue")
# 模拟同步到GitHub
for issue in gitee_issues:
await self._sync_single_issue_to_github(issue, github_token)
await asyncio.sleep(1) # 避免请求过于频繁
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 项目 {project_name} 的Issue同步完成")
except Exception as e:
print(f"同步失败: {str(e)}")
self._log_sync_error(project_name, str(e))
async def _fetch_github_issues(self, project_name: str, token: str) -> List[Dict]:
"""模拟从GitHub获取Issue列表"""
# 这里应该实现真实的GitHub API调用
# 目前返回模拟数据
return [
{
"id": 1,
"title": "Bug: 登录功能异常",
"description": "用户登录时出现500错误",
"state": "open",
"labels": ["bug", "high-priority"],
"assignee": "developer1",
"author": "user1"
},
{
"id": 2,
"title": "Feature: 添加用户管理功能",
"description": "需要添加用户增删改查功能",
"state": "open",
"labels": ["enhancement"],
"assignee": "developer2",
"author": "user2"
}
]
async def _fetch_gitee_issues(self, project_name: str, token: str) -> List[Dict]:
"""模拟从Gitee获取Issue列表"""
# 这里应该实现真实的Gitee API调用
# 目前返回模拟数据
return [
{
"id": 101,
"title": "Bug: 数据导出功能异常",
"description": "导出Excel时格式错误",
"state": "open",
"labels": ["bug"],
"assignee": "developer3",
"author": "user3"
}
]
async def _sync_single_issue_to_gitee(self, issue: Dict, token: str):
"""同步单个Issue到Gitee"""
print(f"正在同步Issue '{issue['title']}' 到Gitee...")
# 这里应该实现真实的Gitee API调用
await asyncio.sleep(0.5) # 模拟API调用时间
print(f"Issue '{issue['title']}' 同步到Gitee成功")
async def _sync_single_issue_to_github(self, issue: Dict, token: str):
"""同步单个Issue到GitHub"""
print(f"正在同步Issue '{issue['title']}' 到GitHub...")
# 这里应该实现真实的GitHub API调用
await asyncio.sleep(0.5) # 模拟API调用时间
print(f"Issue '{issue['title']}' 同步到GitHub成功")
def _log_sync_error(self, project_name: str, error_message: str):
"""记录同步错误日志"""
log_entry = {
"timestamp": datetime.now().isoformat(),
"project": project_name,
"type": "error",
"message": error_message
}
self.sync_logs.append(log_entry)
print(f"错误日志已记录: {log_entry}")
def get_sync_logs(self) -> List[Dict]:
"""获取同步日志"""
return self.sync_logs
async def main():
"""主函数"""
print("=== Issue同步工具启动 ===")
# 创建同步管理器
sync_manager = IssueSyncManager()
# 配置同步参数
project_name = "test-project"
github_token = "your_github_token"
gitee_token = "your_gitee_token"
# 执行同步任务
print("1. 从GitHub同步到Gitee")
await sync_manager.sync_issue_from_github_to_gitee(project_name, github_token, gitee_token)
print("\n2. 从Gitee同步到GitHub")
await sync_manager.sync_issue_from_gitee_to_github(project_name, gitee_token, github_token)
# 显示同步日志
print("\n=== 同步日志 ===")
logs = sync_manager.get_sync_logs()
for log in logs:
print(f"[{log['timestamp']}] {log['type'].upper()}: {log['message']}")
print("\n=== Issue同步工具运行完成 ===")
if __name__ == "__main__":
# 运行异步主函数
asyncio.run(main())

View File

@ -1,339 +0,0 @@
# GitLink-Gitee PR同步功能
## 概述
GitLink-Gitee PR同步功能是一个专门用于在GitLink和Gitee平台之间同步Pull Request的工具。它支持PR的创建、更新、评论同步等功能并提供灵活的配置选项。
## 功能特性
- ✅ **双向同步**: 支持GitLink到Gitee、Gitee到GitLink、以及双向同步
- ✅ **PR评论同步**: 支持同步PR下的评论包括普通评论和代码行评论
- ✅ **智能去重**: 避免重复同步已存在的PR和评论
- ✅ **错误处理**: 完善的错误处理和日志记录
- ✅ **API接口**: 提供RESTful API接口进行配置管理
- ✅ **定时任务**: 支持定时自动同步
- ✅ **状态监控**: 实时监控同步状态和统计信息
## 系统架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GitLink API │ │ Sync Service │ │ Gitee API │
│ │◄──►│ │◄──►│ │
│ - 获取PR列表 │ │ - 同步逻辑 │ │ - 获取PR列表 │
│ - 创建PR │ │ - 映射管理 │ │ - 创建PR │
│ - 获取评论 │ │ - 错误处理 │ │ - 获取评论 │
│ - 创建评论 │ │ - 日志记录 │ │ - 创建评论 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ Database │
│ │
│ - 配置表 │
│ - 映射表 │
│ - 日志表 │
│ - 统计表 │
└─────────────────┘
```
## 安装和配置
### 1. 环境要求
- Python 3.7+
- MySQL 5.7+
- GitLink账号和Cookie
- Gitee账号和访问令牌
### 2. 安装依赖
```bash
pip install fastapi uvicorn pymysql requests schedule
```
### 3. 数据库初始化
```sql
-- 执行数据库表创建脚本
mysql -u root -p your_database < issue_sync/sql/gitlink_gitee_pr_tables.sql
```
### 4. 环境配置
`env.ini` 文件中配置数据库连接信息:
```ini
export CEROBOT_MYSQL_HOST=localhost
export CEROBOT_MYSQL_PORT=3306
export CEROBOT_MYSQL_USER=root
export CEROBOT_MYSQL_PWD=your_password
export CEROBOT_MYSQL_DB=issue_sync
```
## 使用方法
### 1. 基本使用
```python
from service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
# 配置信息
config = {
'gitlink_owner': 'your_gitlink_owner',
'gitlink_repo': 'your_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_cookie_here',
'gitee_owner': 'your_gitee_owner',
'gitee_repo': 'your_gitee_repo',
'gitee_token': 'your_gitee_token_here',
'sync_direction': 'bidirectional', # 双向同步
'sync_comments': True # 同步评论
}
# 创建同步服务
sync_service = GitLinkGiteePRSyncService(config)
# 执行同步
sync_service.sync_pull_requests()
```
### 2. 单向同步
```python
# 只从GitLink同步到Gitee
config = {
# ... 其他配置
'sync_direction': 'gitlink_to_gitee',
'sync_comments': True
}
# 只从Gitee同步到GitLink
config = {
# ... 其他配置
'sync_direction': 'gitee_to_gitlink',
'sync_comments': False # 不同步评论
}
```
### 3. 使用Runner
```bash
# 运行所有启用的同步配置
python issue_sync/sync/gitlink_gitee_pr_sync_runner.py
# 手动运行同步
python issue_sync/sync/gitlink_gitee_pr_sync_runner.py --manual
# 指定配置ID运行
python issue_sync/sync/gitlink_gitee_pr_sync_runner.py --manual --config-id 1
# 启动定时任务调度器
python issue_sync/sync/gitlink_gitee_pr_sync_runner.py --scheduler
```
## API接口
### 1. 配置管理
#### 获取所有配置
```http
GET /gitlink-gitee-pr/configs
```
#### 添加配置
```http
POST /gitlink-gitee-pr/configs
Content-Type: application/json
{
"gitlink_owner": "your_owner",
"gitlink_repo": "your_repo",
"gitlink_cookie": "autologin_trustie=your_cookie",
"gitee_owner": "your_owner",
"gitee_repo": "your_repo",
"gitee_token": "your_token",
"sync_direction": "bidirectional",
"sync_comments": true,
"enabled": true,
"auto_sync": false,
"sync_interval": 300
}
```
#### 修改配置
```http
PUT /gitlink-gitee-pr/configs/{config_id}
```
#### 删除配置
```http
DELETE /gitlink-gitee-pr/configs/{config_id}
```
### 2. 同步操作
#### 手动触发同步
```http
POST /gitlink-gitee-pr/sync/{config_id}
```
#### 获取同步状态
```http
GET /gitlink-gitee-pr/status/{config_id}
```
#### 测试连接
```http
POST /gitlink-gitee-pr/test-connection/{config_id}
```
### 3. 监控和日志
#### 获取同步日志
```http
GET /gitlink-gitee-pr/logs/{config_id}?limit=50
```
#### 获取PR映射关系
```http
GET /gitlink-gitee-pr/mappings/{config_id}
```
## 配置说明
### 配置参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| gitlink_owner | string | 是 | GitLink仓库拥有者 |
| gitlink_repo | string | 是 | GitLink仓库名 |
| gitlink_cookie | string | 是 | GitLink认证Cookie |
| gitee_owner | string | 是 | Gitee仓库拥有者 |
| gitee_repo | string | 是 | Gitee仓库名 |
| gitee_token | string | 是 | Gitee访问令牌 |
| sync_direction | string | 否 | 同步方向gitlink_to_gitee/gitee_to_gitlink/bidirectional |
| sync_comments | boolean | 否 | 是否同步评论默认true |
| enabled | boolean | 否 | 是否启用默认true |
| auto_sync | boolean | 否 | 是否自动同步默认false |
| sync_interval | integer | 否 | 同步间隔(秒)默认300 |
### 同步方向说明
- `gitlink_to_gitee`: 只从GitLink同步到Gitee
- `gitee_to_gitlink`: 只从Gitee同步到GitLink
- `bidirectional`: 双向同步(推荐)
## 获取认证信息
### GitLink Cookie获取
1. 登录GitLink网站
2. 打开浏览器开发者工具
3. 在Network标签页中找到任意请求
4. 复制Cookie中的`autologin_trustie`值
### Gitee Token获取
1. 登录Gitee
2. 进入设置 -> 私人令牌
3. 创建新的私人令牌
4. 复制生成的token
## 数据库表结构
### 主要表说明
1. **gitlink_gitee_pr_config**: 同步配置表
2. **gitlink_gitee_pr_mapping**: PR映射关系表
3. **gitlink_gitee_pr_comment_mapping**: PR评论映射表
4. **gitlink_gitee_pr_sync_log**: 同步日志表
5. **gitlink_gitee_pr_sync_stats**: 同步统计表
## 错误处理
### 常见错误及解决方案
1. **认证失败**
- 检查GitLink Cookie是否有效
- 检查Gitee Token是否有效
- 确认账号权限
2. **仓库不存在**
- 确认仓库名称正确
- 确认仓库拥有者正确
- 确认有访问权限
3. **网络连接问题**
- 检查网络连接
- 检查防火墙设置
- 确认API地址可访问
## 监控和维护
### 日志查看
```bash
# 查看同步日志
mysql -u root -p issue_sync -e "SELECT * FROM gitlink_gitee_pr_sync_log ORDER BY created_at DESC LIMIT 10;"
# 查看同步统计
mysql -u root -p issue_sync -e "SELECT * FROM gitlink_gitee_pr_sync_stats ORDER BY sync_date DESC LIMIT 10;"
```
### 性能优化
1. 合理设置同步间隔
2. 避免同时运行多个同步任务
3. 定期清理历史日志数据
## 示例和测试
### 运行示例
```bash
python issue_sync/examples/gitlink_gitee_pr_sync_example.py
```
### 测试连接
```python
from service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
config = {
# ... 你的配置
}
sync_service = GitLinkGiteePRSyncService(config)
# 测试GitLink连接
gitlink_prs = sync_service.gitlink_api.fetch_pull_requests()
print(f"GitLink PR数量: {len(gitlink_prs) if gitlink_prs else 0}")
# 测试Gitee连接
gitee_prs = sync_service.gitee_api.fetch_pull_requests()
print(f"Gitee PR数量: {len(gitee_prs) if gitee_prs else 0}")
```
## 注意事项
1. **API限制**: 注意GitLink和Gitee的API调用频率限制
2. **数据一致性**: 建议在低峰期进行同步操作
3. **备份重要数据**: 定期备份同步配置和映射数据
4. **监控同步状态**: 定期检查同步日志,及时发现问题
## 技术支持
如果遇到问题,请:
1. 查看同步日志获取详细错误信息
2. 检查配置参数是否正确
3. 确认网络连接和认证信息
4. 参考示例代码进行测试
## 更新日志
### v1.0.0
- 初始版本发布
- 支持基本的PR同步功能
- 支持评论同步
- 提供API接口和定时任务

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1,267 +0,0 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import pymysql
import os
from datetime import datetime
# 假设你的 service 目录下有 IssueSyncService、PRSyncService
from issue_sync.service.issue_sync_service import IssueSyncService
from issue_sync.service.pr_sync_service import PRSyncService
router = APIRouter()
# 配置模型
class SyncConfig(BaseModel):
id: Optional[int] = None
source_platform: str
source_owner: str
source_repo: str
source_token: str
target_platform: str
target_owner: str
target_repo: str
target_token: str
sync_type: str
sync_direction: str
enabled: bool = True
auto_sync: bool = False
sync_interval: Optional[int] = 300
def get_db():
# 读取项目根目录的 env.ini 文件
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),"env.ini")
db_config = {}
try:
with open(config_path, "r", encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[len('export '):]
parts = line.split('=', 1)
if len(parts) == 2:
key, value = parts[0].strip(), parts[1].strip()
# 去除值可能存在的引号
if (value.startswith('"') and value.endswith('"')) or \
(value.startswith("'") and value.endswith("'")):
value = value[1:-1]
db_config[key] = value
except FileNotFoundError:
print(f"配置文件未找到: {config_path}")
except Exception as e:
print(f"读取配置文件时出错: {e}")
return pymysql.connect(
host=db_config.get('CEROBOT_MYSQL_HOST', 'localhost'),
user=db_config.get('CEROBOT_MYSQL_USER', 'root'),
password=db_config.get('CEROBOT_MYSQL_PWD', ''),
database='issue_sync',
charset="utf8mb4"
)
# 查询所有同步配置
@router.get("/configs", response_model=List[SyncConfig])
def list_configs():
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM sync_config")
configs = cursor.fetchall()
db.close()
return configs
# 新增同步配置
@router.post("/configs", response_model=SyncConfig)
def add_config(config: SyncConfig):
db = get_db()
cursor = db.cursor()
sql = """
INSERT INTO sync_config
(source_platform, source_owner, source_repo, source_token,
target_platform, target_owner, target_repo, target_token,
sync_type, sync_direction, enabled, auto_sync, sync_interval)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
cursor.execute(sql, (
config.source_platform, config.source_owner, config.source_repo, config.source_token,
config.target_platform, config.target_owner, config.target_repo, config.target_token,
config.sync_type, config.sync_direction, config.enabled, config.auto_sync, config.sync_interval
))
db.commit()
config.id = cursor.lastrowid
db.close()
return config
# 修改同步配置
@router.put("/configs/{config_id}", response_model=SyncConfig)
def update_config(config_id: int, config: SyncConfig):
db = get_db()
cursor = db.cursor()
sql = """
UPDATE sync_config SET
source_platform=%s, source_owner=%s, source_repo=%s, source_token=%s,
target_platform=%s, target_owner=%s, target_repo=%s, target_token=%s,
sync_type=%s, sync_direction=%s, enabled=%s, auto_sync=%s, sync_interval=%s
WHERE id=%s
"""
cursor.execute(sql, (
config.source_platform, config.source_owner, config.source_repo, config.source_token,
config.target_platform, config.target_owner, config.target_repo, config.target_token,
config.sync_type, config.sync_direction, config.enabled, config.auto_sync, config.sync_interval,
config_id
))
db.commit()
db.close()
config.id = config_id
return config
# 删除同步配置
@router.delete("/configs/{config_id}")
def delete_config(config_id: int):
db = get_db()
cursor = db.cursor()
cursor.execute("DELETE FROM sync_config WHERE id=%s", (config_id,))
db.commit()
db.close()
return {"msg": "deleted"}
# 查询单个配置
@router.get("/configs/{config_id}", response_model=SyncConfig)
def get_config(config_id: int):
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM sync_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return config
# 手动触发同步
@router.post("/sync/{config_id}")
def manual_sync(config_id: str):
print(f"收到同步请求config_id: {config_id}")
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM sync_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
print(f"数据库查询结果: {config}")
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
if config["sync_type"] == "issue":
service = IssueSyncService(config)
service.sync()
elif config["sync_type"] == "pull_request":
service = PRSyncService(config)
service.sync()
else:
raise HTTPException(status_code=400, detail="Unknown sync_type")
return {"msg": "sync started"}
# 运行所有 Issue 同步
@router.post("/run-all-issue-sync")
def run_all_issue_sync():
try:
print("开始运行所有 Issue 同步...")
# 导入并运行 issue_sync_runner
import sys
import os
# 获取 issue_sync_runner.py 的路径
runner_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "sync", "issue_sync_runner.py")
print(f"运行脚本路径: {runner_path}")
# 检查文件是否存在
if not os.path.exists(runner_path):
raise HTTPException(status_code=404, detail=f"Runner file not found: {runner_path}")
# 导入并执行
sys.path.insert(0, os.path.dirname(runner_path))
# 导入 issue_sync_runner 模块
from issue_sync.sync.issue_sync_runner import run_all_sync
# 执行同步
run_all_sync()
return {"msg": "所有 Issue 同步已启动", "status": "success", "timestamp": datetime.now().isoformat()}
except Exception as e:
print(f"运行 Issue 同步时出错: {str(e)}")
raise HTTPException(status_code=500, detail=f"同步执行失败: {str(e)}")
# 运行单个 Issue 同步配置
@router.post("/run-single-issue-sync/{config_id}")
def run_single_issue_sync(config_id: str):
try:
print(f"开始运行单个 Issue 同步config_id: {config_id}")
# 获取配置
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM sync_config WHERE id=%s AND sync_type='issue'", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Issue sync config not found")
# 创建同步服务并执行
service = IssueSyncService(config)
# 根据配置决定同步方向
if config.get('sync_direction') == 'bidirectional':
print("执行双向同步...")
service.bidirectional_sync()
else:
print("执行单向同步...")
service.sync()
return {"msg": f"Issue 同步已启动 (config_id: {config_id})", "status": "success", "config": {
"id": config['id'],
"name": f"{config['source_platform']} -> {config['target_platform']}",
"source": f"{config['source_platform']}:{config['source_repo']}",
"target": f"{config['target_platform']}:{config['target_repo']}",
"direction": config.get('sync_direction', 'source_to_target')
}, "timestamp": datetime.now().isoformat()}
except Exception as e:
print(f"运行单个 Issue 同步时出错: {str(e)}")
raise HTTPException(status_code=500, detail=f"同步执行失败: {str(e)}")
# 获取 Issue 同步状态
@router.get("/issue-sync-status")
def get_issue_sync_status():
try:
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM sync_config WHERE sync_type='issue'")
configs = cursor.fetchall()
db.close()
status_list = []
for config in configs:
status_list.append({
"id": config['id'],
"name": f"{config['source_platform']} -> {config['target_platform']}",
"source": f"{config['source_platform']}:{config['source_repo']}",
"target": f"{config['target_platform']}:{config['target_repo']}",
"direction": config.get('sync_direction', 'source_to_target'),
"enabled": config.get('enabled'),
"auto_sync": config.get('auto_sync', False)
})
return {
"total_configs": len(status_list),
"enabled_configs": len([c for c in status_list if c['enabled']]),
"configs": status_list
}
except Exception as e:
print(f"获取 Issue 同步状态时出错: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取状态失败: {str(e)}")

View File

@ -1,287 +0,0 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict
import pymysql
import os
from datetime import datetime
from issue_sync.service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
router = APIRouter()
# 配置模型
class GitLinkGiteePRConfig(BaseModel):
id: Optional[int] = None
gitlink_owner: str
gitlink_repo: str
gitlink_cookie: str
gitee_owner: str
gitee_repo: str
gitee_token: str
sync_direction: str = "bidirectional" # gitlink_to_gitee, gitee_to_gitlink, bidirectional
sync_comments: bool = True
enabled: bool = True
auto_sync: bool = False
sync_interval: Optional[int] = 300
# 同步状态模型
class SyncStatus(BaseModel):
last_sync_time: str
sync_direction: str
sync_comments: bool
gitlink_repo: str
gitee_repo: str
status: str
message: str
def get_db():
return pymysql.connect(
host=os.getenv("DB_HOST", "localhost"),
user=os.getenv("DB_USER", "root"),
password=os.getenv("DB_PASS", "yourpassword"),
database=os.getenv("DB_NAME", "issue_sync"),
charset="utf8mb4"
)
# 查询所有GitLink-Gitee PR同步配置
@router.get("/gitlink-gitee-pr/configs", response_model=List[GitLinkGiteePRConfig])
def list_gitlink_gitee_pr_configs():
"""获取所有GitLink-Gitee PR同步配置"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config")
configs = cursor.fetchall()
db.close()
return configs
# 新增GitLink-Gitee PR同步配置
@router.post("/gitlink-gitee-pr/configs", response_model=GitLinkGiteePRConfig)
def add_gitlink_gitee_pr_config(config: GitLinkGiteePRConfig):
"""添加GitLink-Gitee PR同步配置"""
db = get_db()
cursor = db.cursor()
sql = """
INSERT INTO gitlink_gitee_pr_config
(gitlink_owner, gitlink_repo, gitlink_cookie,
gitee_owner, gitee_repo, gitee_token,
sync_direction, sync_comments, enabled, auto_sync, sync_interval)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
cursor.execute(sql, (
config.gitlink_owner, config.gitlink_repo, config.gitlink_cookie,
config.gitee_owner, config.gitee_repo, config.gitee_token,
config.sync_direction, config.sync_comments, config.enabled, config.auto_sync, config.sync_interval
))
db.commit()
config.id = cursor.lastrowid
db.close()
return config
# 修改GitLink-Gitee PR同步配置
@router.put("/gitlink-gitee-pr/configs/{config_id}", response_model=GitLinkGiteePRConfig)
def update_gitlink_gitee_pr_config(config_id: int, config: GitLinkGiteePRConfig):
"""修改GitLink-Gitee PR同步配置"""
db = get_db()
cursor = db.cursor()
sql = """
UPDATE gitlink_gitee_pr_config SET
gitlink_owner=%s, gitlink_repo=%s, gitlink_cookie=%s,
gitee_owner=%s, gitee_repo=%s, gitee_token=%s,
sync_direction=%s, sync_comments=%s, enabled=%s, auto_sync=%s, sync_interval=%s
WHERE id=%s
"""
cursor.execute(sql, (
config.gitlink_owner, config.gitlink_repo, config.gitlink_cookie,
config.gitee_owner, config.gitee_repo, config.gitee_token,
config.sync_direction, config.sync_comments, config.enabled, config.auto_sync, config.sync_interval,
config_id
))
db.commit()
db.close()
config.id = config_id
return config
# 删除GitLink-Gitee PR同步配置
@router.delete("/gitlink-gitee-pr/configs/{config_id}")
def delete_gitlink_gitee_pr_config(config_id: int):
"""删除GitLink-Gitee PR同步配置"""
db = get_db()
cursor = db.cursor()
cursor.execute("DELETE FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
db.commit()
db.close()
return {"msg": "deleted"}
# 查询单个GitLink-Gitee PR同步配置
@router.get("/gitlink-gitee-pr/configs/{config_id}", response_model=GitLinkGiteePRConfig)
def get_gitlink_gitee_pr_config(config_id: int):
"""获取单个GitLink-Gitee PR同步配置"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return config
# 手动触发GitLink-Gitee PR同步
@router.post("/gitlink-gitee-pr/sync/{config_id}")
def manual_gitlink_gitee_pr_sync(config_id: int):
"""手动触发GitLink-Gitee PR同步"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
try:
# 创建同步服务
sync_service = GitLinkGiteePRSyncService(config)
# 执行同步
sync_service.sync_pull_requests()
return {
"msg": "sync started",
"config_id": config_id,
"sync_time": datetime.now().isoformat()
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Sync failed: {str(e)}")
# 获取同步状态
@router.get("/gitlink-gitee-pr/status/{config_id}", response_model=SyncStatus)
def get_gitlink_gitee_pr_sync_status(config_id: int):
"""获取GitLink-Gitee PR同步状态"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
try:
sync_service = GitLinkGiteePRSyncService(config)
status = sync_service.get_sync_status()
status['status'] = 'ready'
status['message'] = '同步服务已准备就绪'
return status
except Exception as e:
return SyncStatus(
last_sync_time=datetime.now().isoformat(),
sync_direction=config.get('sync_direction', 'bidirectional'),
sync_comments=config.get('sync_comments', True),
gitlink_repo=f"{config['gitlink_owner']}/{config['gitlink_repo']}",
gitee_repo=f"{config['gitee_owner']}/{config['gitee_repo']}",
status='error',
message=f'获取状态失败: {str(e)}'
)
# 测试连接
@router.post("/gitlink-gitee-pr/test-connection/{config_id}")
def test_gitlink_gitee_pr_connection(config_id: int):
"""测试GitLink和Gitee连接"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
try:
sync_service = GitLinkGiteePRSyncService(config)
# 测试GitLink连接
gitlink_prs = sync_service.gitlink_api.fetch_pull_requests()
gitlink_status = "connected" if gitlink_prs is not None else "failed"
# 测试Gitee连接
gitee_prs = sync_service.gitee_api.fetch_pull_requests()
gitee_status = "connected" if gitee_prs is not None else "failed"
return {
"gitlink_status": gitlink_status,
"gitee_status": gitee_status,
"gitlink_pr_count": len(gitlink_prs) if gitlink_prs else 0,
"gitee_pr_count": len(gitee_prs) if gitee_prs else 0,
"test_time": datetime.now().isoformat()
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Connection test failed: {str(e)}")
# 获取同步日志
@router.get("/gitlink-gitee-pr/logs/{config_id}")
def get_gitlink_gitee_pr_sync_logs(config_id: int, limit: int = 50):
"""获取GitLink-Gitee PR同步日志"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
# 获取配置信息
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
if not config:
db.close()
raise HTTPException(status_code=404, detail="Config not found")
# 获取同步日志
cursor.execute("""
SELECT * FROM sync_log
WHERE (source LIKE %s OR target LIKE %s)
ORDER BY timestamp DESC
LIMIT %s
""", (
f"%{config['gitlink_repo']}%",
f"%{config['gitee_repo']}%",
limit
))
logs = cursor.fetchall()
db.close()
return {
"config_id": config_id,
"logs": logs,
"total": len(logs)
}
# 获取PR映射关系
@router.get("/gitlink-gitee-pr/mappings/{config_id}")
def get_gitlink_gitee_pr_mappings(config_id: int):
"""获取GitLink-Gitee PR映射关系"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
# 获取配置信息
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
if not config:
db.close()
raise HTTPException(status_code=404, detail="Config not found")
# 获取PR映射
cursor.execute("""
SELECT * FROM pr_mapping
WHERE (source_platform='gitlink' AND source_repo=%s AND target_platform='gitee' AND target_repo=%s)
OR (source_platform='gitee' AND source_repo=%s AND target_platform='gitlink' AND target_repo=%s)
ORDER BY last_sync_time DESC
""", (
config['gitlink_repo'], config['gitee_repo'],
config['gitee_repo'], config['gitlink_repo']
))
mappings = cursor.fetchall()
db.close()
return {
"config_id": config_id,
"mappings": mappings,
"total": len(mappings)
}

View File

@ -1,72 +0,0 @@
import requests
class GiteeAPI:
def __init__(self, owner, repo, token=""):
self.owner = owner
self.repo = repo
self.token = token
def fetch_issues(self):
url = f"https://gitee.com/api/v5/repos/{self.owner}/{self.repo}/issues"
params = {'access_token': self.token}
resp = requests.get(url, params=params)
return resp.json() if resp.status_code == 200 else []
def create_issue(self, title, body):
url = f"https://gitee.com/api/v5/repos/{self.owner}/issues"
payload = {
"access_token": self.token,
"repo": self.repo,
"title": title,
"body": body
}
print(f"准备创建Gitee issueurl: {url}, owner: {self.owner}, repo: {self.repo}, title: {title!r}, body: {body!r}")
resp = requests.post(url, json=payload)
print("Gitee创建issue返回状态码:", resp.status_code)
print("Gitee创建issue返回内容:", resp.text)
return resp.json() if resp.status_code in (200, 201) else None
def fetch_pull_requests(self):
url = f"https://gitee.com/api/v5/repos/{self.owner}/{self.repo}/pulls"
params = {'access_token': self.token}
resp = requests.get(url, params=params)
return resp.json() if resp.status_code == 200 else []
def create_pull_request(self, title, head, base, body=""):
url = f"https://gitee.com/api/v5/repos/{self.owner}/{self.repo}/pulls"
params = {'access_token': self.token}
data = {
"title": title,
"head": head, # 源分支
"base": base, # 目标分支
"body": body
}
resp = requests.post(url, params=params, json=data)
return resp.json() if resp.status_code in (200, 201) else None
def fetch_pr_comments(self, pr_number):
"""获取PR的评论列表"""
url = f"https://gitee.com/api/v5/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
params = {'access_token': self.token}
resp = requests.get(url, params=params)
print(f"Gitee获取PR评论返回状态码: {resp.status_code}")
return resp.json() if resp.status_code == 200 else []
def create_pr_comment(self, pr_number, body, commit_id=None, path=None, position=None):
"""创建PR评论支持代码行评论"""
url = f"https://gitee.com/api/v5/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
params = {'access_token': self.token}
data = {"body": body}
# 如果提供了代码行信息,则创建代码行评论
if commit_id and path and position is not None:
data.update({
"commit_id": commit_id,
"path": path,
"position": position
})
print(f"Gitee创建PR评论PR: {pr_number}, 内容: {body[:30]}...")
resp = requests.post(url, params=params, json=data)
print(f"Gitee创建PR评论返回状态码: {resp.status_code}")
return resp.json() if resp.status_code in (200, 201) else None

View File

@ -1,65 +0,0 @@
import requests
class GithubAPI:
def __init__(self, owner, repo, token):
self.owner = owner
self.repo = repo
self.token = token
def fetch_issues(self):
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/issues"
headers = {'Authorization': f'token {self.token}'}
resp = requests.get(url, headers=headers)
return resp.json() if resp.status_code == 200 else []
def create_issue(self, title, body):
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/issues"
headers = {'Authorization': f'token {self.token}'}
data = {"title": title, "body": body}
resp = requests.post(url, headers=headers, json=data)
return resp.json() if resp.status_code in (200, 201) else None
def fetch_pull_requests(self):
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/pulls"
headers = {'Authorization': f'token {self.token}'}
resp = requests.get(url, headers=headers)
return resp.json() if resp.status_code == 200 else []
def create_pull_request(self, title, head, base, body=""):
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/pulls"
headers = {'Authorization': f'token {self.token}'}
data = {
"title": title,
"head": head, # 源分支
"base": base, # 目标分支
"body": body
}
resp = requests.post(url, headers=headers, json=data)
return resp.json() if resp.status_code in (200, 201) else None
# 新增PR评论相关方法
def fetch_pr_comments(self, pr_number):
"""获取PR的评论列表"""
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
headers = {'Authorization': f'token {self.token}'}
resp = requests.get(url, headers=headers)
return resp.json() if resp.status_code == 200 else []
def create_pr_comment(self, pr_number, body, commit_id=None, path=None, position=None):
"""创建PR评论支持代码行评论"""
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
headers = {'Authorization': f'token {self.token}'}
data = {"body": body}
# 如果提供了代码行信息,则创建代码行评论
if commit_id and path and position is not None:
data.update({
"commit_id": commit_id,
"path": path,
"position": position
})
resp = requests.post(url, headers=headers, json=data)
return resp.json() if resp.status_code in (200, 201) else None
# PR相关方法同理

View File

@ -1,206 +0,0 @@
import requests
import json
class GitlinkAPI:
"""
GitLink Issue API 封装支持Bearer Token和Cookie认证
"""
def __init__(self, owner, repo, cookie_str=None, token=None):
"""
:param owner: 仓库拥有者
:param repo: 仓库名
:param cookie_str: 浏览器抓包获得的完整Cookie字符串 autologin_trustie=xxx
:param token: Bearer Token用于API认证
"""
self.owner = owner
self.repo = repo
# 如果传入的 cookie_str 不包含 '=',则假定它是 autologin_trustie 的值
if cookie_str and '=' not in cookie_str:
cookie_str = f'autologin_trustie={cookie_str}'
self.cookies = self._parse_cookie(cookie_str) if cookie_str else {}
self.token = token
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# 如果提供了token添加到Authorization header
if self.token:
self.headers['Authorization'] = f'Bearer {self.token}'
def _parse_cookie(self, cookie_str):
cookies = {}
if not cookie_str:
return cookies
for item in cookie_str.split(';'):
if '=' in item:
k, v = item.strip().split('=', 1)
cookies[k] = v
return cookies
def fetch_issues(self):
url = f"https://gitlink.org.cn/api/v1/{self.owner}/{self.repo}/issues.json"
print(f"[GitLink] 请求Issues: {url}")
print(f"[GitLink] 使用Cookie: {json.dumps(self.cookies, ensure_ascii=False)}")
if self.token:
print(f"[GitLink] 使用Bearer Token: {self.token[:10]}...")
resp = requests.get(url, headers=self.headers, cookies=self.cookies)
print(f"[GitLink] 状态码: {resp.status_code}")
print(f"[GitLink] 响应: {resp.text[:200]}")
try:
data = resp.json()
if isinstance(data, dict) and 'issues' in data:
return data['issues']
if isinstance(data, list):
return data
return []
except Exception as e:
print(f"[GitLink] 解析JSON失败: {e}")
return []
def create_issue(self, title, body, status_id=1, priority_id=2):
"""
在GitLink仓库创建issue支持Bearer Token和Cookie认证
:param title: issue标题
:param body: issue内容
:param status_id: 状态1新增 2正在解决 3已解决
:param priority_id: 优先级1 2正常 3
"""
url = f"https://gitlink.org.cn/api/v1/{self.owner}/{self.repo}/issues.json"
data = {
"status_id": status_id,
"priority_id": priority_id,
"subject": title,
"description": body or ""
}
print(f"[GitLink] 创建Issue: {url}")
print(f"[GitLink] Data: {json.dumps(data, ensure_ascii=False)}")
print(f"[GitLink] 使用Cookie: {json.dumps(self.cookies, ensure_ascii=False)}")
if self.token:
print(f"[GitLink] 使用Bearer Token: {self.token[:10]}...")
print(f"[GitLink] description字段内容: {data['description']}")
resp = requests.post(url, headers=self.headers, cookies=self.cookies, json=data)
print(f"[GitLink] 状态码: {resp.status_code}")
print(f"[GitLink] 响应: {resp.text[:200]}")
try:
return resp.json()
except Exception as e:
print(f"[GitLink] 解析JSON失败: {e}")
return None
def fetch_milestones(self):
url = f"https://gitlink.org.cn/api/v1/{self.owner}/{self.repo}/milestones.json"
print(f"[GitLink] 请求Milestones: URL={url}")
print(f"[GitLink] Headers: {json.dumps(self.headers, ensure_ascii=False)}")
print(f"[GitLink] Cookies: {json.dumps(self.cookies, ensure_ascii=False)}")
resp = requests.get(url, cookies=self.cookies, headers=self.headers)
print("[GitLink] Milestones接口返回状态码:", resp.status_code)
print("[GitLink] Milestones接口返回内容:", resp.text[:200])
try:
data = resp.json() if resp.status_code == 200 else []
return data.get('milestones', []) if isinstance(data, dict) else []
except Exception as e:
print(f"[GitLink] Milestones接口返回内容不是JSON无法解析。异常: {e}")
return []
def fetch_pull_requests(self):
url = f"https://gitlink.org.cn/api/v1/repos/{self.owner}/{self.repo}/pulls"
print(f"[GitLink] 请求PR: URL={url}")
print(f"[GitLink] Headers: {json.dumps(self.headers, ensure_ascii=False)}")
print(f"[GitLink] Cookies: {json.dumps(self.cookies, ensure_ascii=False)}")
resp = requests.get(url, cookies=self.cookies, headers=self.headers)
print("[GitLink] 获取PR返回状态码:", resp.status_code)
print("[GitLink] 获取PR返回内容:", resp.text[:200])
try:
return resp.json() if resp.status_code == 200 else []
except Exception as e:
print(f"[GitLink] 获取PR返回内容不是JSON无法解析。异常: {e}")
return []
def create_pull_request(self, title, head, base, body=""):
url = f"https://gitlink.org.cn/api/v1/repos/{self.owner}/{self.repo}/pulls"
data = {
"title": title,
"head": head,
"base": base,
"body": body
}
print(f"[GitLink] 创建PR: URL={url}")
print(f"[GitLink] Headers: {json.dumps(self.headers, ensure_ascii=False)}")
print(f"[GitLink] Cookies: {json.dumps(self.cookies, ensure_ascii=False)}")
print(f"[GitLink] Data: {json.dumps(data, ensure_ascii=False)}")
resp = requests.post(url, cookies=self.cookies, headers=self.headers, json=data)
print("[GitLink] 创建PR返回状态码:", resp.status_code)
print("[GitLink] 创建PR返回内容:", resp.text[:200])
try:
return resp.json() if resp.status_code in (200, 201) else None
except Exception as e:
print(f"[GitLink] 创建PR返回内容不是JSON无法解析。异常: {e}")
return None
# 新增PR评论相关方法
def fetch_pr_comments(self, pr_number):
"""获取PR的评论列表"""
url = f"https://gitlink.org.cn/api/v1/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
print(f"[GitLink] 请求PR评论: URL={url}")
print(f"[GitLink] Headers: {json.dumps(self.headers, ensure_ascii=False)}")
print(f"[GitLink] Cookies: {json.dumps(self.cookies, ensure_ascii=False)}")
resp = requests.get(url, cookies=self.cookies, headers=self.headers)
print(f"[GitLink] 获取PR评论返回状态码: {resp.status_code}")
try:
return resp.json() if resp.status_code == 200 else []
except Exception as e:
print(f"[GitLink] 获取PR评论返回内容不是JSON无法解析。异常: {e}")
return []
def create_pr_comment(self, pr_number, body, commit_id=None, path=None, position=None):
"""创建PR评论支持代码行评论"""
url = f"https://gitlink.org.cn/api/v1/repos/{self.owner}/{self.repo}/pulls/{pr_number}/comments"
data = {"body": body}
# 如果提供了代码行信息,则创建代码行评论
if commit_id and path and position is not None:
data.update({
"commit_id": commit_id,
"path": path,
"position": position
})
print(f"[GitLink] 创建PR评论: URL={url}")
print(f"[GitLink] Headers: {json.dumps(self.headers, ensure_ascii=False)}")
print(f"[GitLink] Cookies: {json.dumps(self.cookies, ensure_ascii=False)}")
print(f"[GitLink] Data: {json.dumps(data, ensure_ascii=False)}")
resp = requests.post(url, cookies=self.cookies, headers=self.headers, json=data)
print(f"[GitLink] 创建PR评论返回状态码: {resp.status_code}")
try:
return resp.json() if resp.status_code in (200, 201) else None
except Exception as e:
print(f"[GitLink] 创建PR评论返回内容不是JSON无法解析。异常: {e}")
return None
# 示例用法
if __name__ == '__main__':
# 配置区
# 使用已知可访问的仓库进行测试
GITLINK_OWNER = 'jkcl'
GITLINK_REPO = 'reposync'
# 请替换为你的有效 Cookie用于创建 issue
GITLINK_COOKIE = 'autologin_trustie=0d5cc2e383cb03ee76ecf712c98d0b8b63f72aae'
api = GitlinkAPI(GITLINK_OWNER, GITLINK_REPO, cookie_str=GITLINK_COOKIE)
print("--- 正在获取 issues ---")
issues = api.fetch_issues()
if issues:
print(f"成功获取到 {len(issues)} 个 issue。")
else:
print("获取 issue 失败或仓库中没有 issue。")
print("\n--- 正在尝试创建 issue ---")
# 创建 issue 仍可能因权限不足而失败
api.create_issue("这是一个API测试标题", "这是通过API脚本创建的测试内容。")

View File

@ -1,23 +0,0 @@
import pymysql
import os
def get_db():
return pymysql.connect(
host='127.0.0.1',
user='root',
password='123456789LY@',
database='issue_sync',
charset="utf8mb4"
)
def write_sync_log(sync_type, source, target, status, message):
db = get_db()
cursor = db.cursor()
sql = """
INSERT INTO sync_log
(sync_type, source, target, status, message, timestamp)
VALUES (%s,%s,%s,%s,%s,NOW())
"""
cursor.execute(sql, (sync_type, source, target, status, message))
db.commit()
db.close()

View File

@ -1,107 +0,0 @@
import pymysql
import os
from datetime import datetime
def get_db():
return pymysql.connect(
host=os.getenv("DB_HOST", "localhost"),
user=os.getenv("DB_USER", "root"),
password=os.getenv("DB_PASS", "123456789LY@"),
database=os.getenv("DB_NAME", "issue_sync"),
charset="utf8mb4"
)
def get_issue_mapping(source_platform, source_repo, source_issue_id, target_platform, target_repo):
"""获取Issue映射"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM issue_mapping
WHERE source_platform=%s AND source_repo=%s AND source_issue_id=%s
AND target_platform=%s AND target_repo=%s
""", (source_platform, source_repo, source_issue_id, target_platform, target_repo))
result = cursor.fetchone()
db.close()
return result
def save_issue_mapping(source_platform, source_repo, source_issue_id, target_platform, target_repo, target_issue_id):
"""保存Issue映射"""
db = get_db()
cursor = db.cursor()
cursor.execute("""
INSERT INTO issue_mapping
(source_platform, source_repo, source_issue_id, target_platform, target_repo, target_issue_id, last_sync_time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (source_platform, source_repo, source_issue_id, target_platform, target_repo, target_issue_id, datetime.now()))
db.commit()
db.close()
def get_pr_mapping(source_platform, source_repo, source_pr_id, target_platform, target_repo):
"""获取PR映射"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM pr_mapping
WHERE source_platform=%s AND source_repo=%s AND source_pr_id=%s
AND target_platform=%s AND target_repo=%s
""", (source_platform, source_repo, source_pr_id, target_platform, target_repo))
result = cursor.fetchone()
db.close()
return result
def save_pr_mapping(source_platform, source_repo, source_pr_id, target_platform, target_repo, target_pr_id):
"""保存PR映射"""
db = get_db()
cursor = db.cursor()
cursor.execute("""
INSERT INTO pr_mapping
(source_platform, source_repo, source_pr_id, target_platform, target_repo, target_pr_id, last_sync_time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (source_platform, source_repo, source_pr_id, target_platform, target_repo, target_pr_id, datetime.now()))
db.commit()
db.close()
# 新增PR评论映射相关方法
def get_pr_comment_mapping(source_platform, source_repo, source_pr_id, source_comment_id, target_platform, target_repo):
"""获取PR评论映射"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM pr_comment_mapping
WHERE source_platform=%s AND source_repo=%s AND source_pr_id=%s AND source_comment_id=%s
AND target_platform=%s AND target_repo=%s
""", (source_platform, source_repo, source_pr_id, source_comment_id, target_platform, target_repo))
result = cursor.fetchone()
db.close()
return result
def save_pr_comment_mapping(source_platform, source_repo, source_pr_id, source_comment_id,
target_platform, target_repo, target_pr_id, target_comment_id,
comment_body, commit_id=None, path=None, position=None):
"""保存PR评论映射"""
db = get_db()
cursor = db.cursor()
cursor.execute("""
INSERT INTO pr_comment_mapping
(source_platform, source_repo, source_pr_id, source_comment_id,
target_platform, target_repo, target_pr_id, target_comment_id,
comment_body, commit_id, path, position, last_sync_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (source_platform, source_repo, source_pr_id, source_comment_id,
target_platform, target_repo, target_pr_id, target_comment_id,
comment_body, commit_id, path, position, datetime.now()))
db.commit()
db.close()
def get_pr_comments_by_pr(source_platform, source_repo, source_pr_id, target_platform, target_repo):
"""获取特定PR下的所有评论映射"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM pr_comment_mapping
WHERE source_platform=%s AND source_repo=%s AND source_pr_id=%s
AND target_platform=%s AND target_repo=%s
""", (source_platform, source_repo, source_pr_id, target_platform, target_repo))
results = cursor.fetchall()
db.close()
return results

View File

@ -1,223 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitLink-Gitee PR同步使用示例
演示如何使用GitLink-Gitee PR同步功能
"""
import sys
import os
import json
from datetime import datetime
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from issue_sync.service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
def example_basic_sync():
"""
基本同步示例
"""
print("=== GitLink-Gitee PR同步基本示例 ===")
# 配置信息
config = {
'gitlink_owner': 'your_gitlink_owner',
'gitlink_repo': 'your_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_cookie_here',
'gitee_owner': 'your_gitee_owner',
'gitee_repo': 'your_gitee_repo',
'gitee_token': 'your_gitee_token_here',
'sync_direction': 'bidirectional', # 双向同步
'sync_comments': True # 同步评论
}
try:
# 创建同步服务
sync_service = GitLinkGiteePRSyncService(config)
# 执行同步
sync_service.sync_pull_requests()
# 获取同步状态
status = sync_service.get_sync_status()
print(f"同步状态: {json.dumps(status, ensure_ascii=False, indent=2)}")
except Exception as e:
print(f"同步失败: {str(e)}")
def example_unidirectional_sync():
"""
单向同步示例
"""
print("\n=== GitLink-Gitee PR单向同步示例 ===")
# 从GitLink同步到Gitee
config_gitlink_to_gitee = {
'gitlink_owner': 'your_gitlink_owner',
'gitlink_repo': 'your_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_cookie_here',
'gitee_owner': 'your_gitee_owner',
'gitee_repo': 'your_gitee_repo',
'gitee_token': 'your_gitee_token_here',
'sync_direction': 'gitlink_to_gitee', # 只从GitLink同步到Gitee
'sync_comments': True
}
try:
sync_service = GitLinkGiteePRSyncService(config_gitlink_to_gitee)
sync_service.sync_pull_requests()
print("GitLink到Gitee同步完成")
except Exception as e:
print(f"GitLink到Gitee同步失败: {str(e)}")
# 从Gitee同步到GitLink
config_gitee_to_gitlink = {
'gitlink_owner': 'your_gitlink_owner',
'gitlink_repo': 'your_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_cookie_here',
'gitee_owner': 'your_gitee_owner',
'gitee_repo': 'your_gitee_repo',
'gitee_token': 'your_gitee_token_here',
'sync_direction': 'gitee_to_gitlink', # 只从Gitee同步到GitLink
'sync_comments': False # 不同步评论
}
try:
sync_service = GitLinkGiteePRSyncService(config_gitee_to_gitlink)
sync_service.sync_pull_requests()
print("Gitee到GitLink同步完成")
except Exception as e:
print(f"Gitee到GitLink同步失败: {str(e)}")
def example_with_error_handling():
"""
带错误处理的同步示例
"""
print("\n=== GitLink-Gitee PR同步错误处理示例 ===")
config = {
'gitlink_owner': 'invalid_owner',
'gitlink_repo': 'invalid_repo',
'gitlink_cookie': 'invalid_cookie',
'gitee_owner': 'invalid_owner',
'gitee_repo': 'invalid_repo',
'gitee_token': 'invalid_token',
'sync_direction': 'bidirectional',
'sync_comments': True
}
try:
sync_service = GitLinkGiteePRSyncService(config)
sync_service.sync_pull_requests()
except Exception as e:
print(f"预期的错误: {str(e)}")
print("错误处理正常工作")
def example_test_connection():
"""
测试连接示例
"""
print("\n=== GitLink-Gitee PR连接测试示例 ===")
config = {
'gitlink_owner': 'your_gitlink_owner',
'gitlink_repo': 'your_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_cookie_here',
'gitee_owner': 'your_gitee_owner',
'gitee_repo': 'your_gitee_repo',
'gitee_token': 'your_gitee_token_here',
'sync_direction': 'bidirectional',
'sync_comments': True
}
try:
sync_service = GitLinkGiteePRSyncService(config)
# 测试GitLink连接
print("测试GitLink连接...")
gitlink_prs = sync_service.gitlink_api.fetch_pull_requests()
if gitlink_prs is not None:
print(f"GitLink连接成功找到 {len(gitlink_prs)} 个PR")
else:
print("GitLink连接失败")
# 测试Gitee连接
print("测试Gitee连接...")
gitee_prs = sync_service.gitee_api.fetch_pull_requests()
if gitee_prs is not None:
print(f"Gitee连接成功找到 {len(gitee_prs)} 个PR")
else:
print("Gitee连接失败")
except Exception as e:
print(f"连接测试失败: {str(e)}")
def example_custom_config():
"""
自定义配置示例
"""
print("\n=== GitLink-Gitee PR自定义配置示例 ===")
# 不同的同步配置
configs = [
{
'name': '开发环境同步',
'config': {
'gitlink_owner': 'dev_owner',
'gitlink_repo': 'dev_repo',
'gitlink_cookie': 'autologin_trustie=dev_cookie',
'gitee_owner': 'dev_owner',
'gitee_repo': 'dev_repo',
'gitee_token': 'dev_token',
'sync_direction': 'bidirectional',
'sync_comments': True
}
},
{
'name': '生产环境同步',
'config': {
'gitlink_owner': 'prod_owner',
'gitlink_repo': 'prod_repo',
'gitlink_cookie': 'autologin_trustie=prod_cookie',
'gitee_owner': 'prod_owner',
'gitee_repo': 'prod_repo',
'gitee_token': 'prod_token',
'sync_direction': 'gitlink_to_gitee', # 只从GitLink同步到Gitee
'sync_comments': False # 生产环境不同步评论
}
}
]
for config_info in configs:
print(f"\n执行 {config_info['name']} 同步...")
try:
sync_service = GitLinkGiteePRSyncService(config_info['config'])
sync_service.sync_pull_requests()
print(f"{config_info['name']} 同步完成")
except Exception as e:
print(f"{config_info['name']} 同步失败: {str(e)}")
def main():
"""
主函数
"""
print("GitLink-Gitee PR同步功能演示")
print("=" * 50)
# 运行各种示例
example_basic_sync()
example_unidirectional_sync()
example_with_error_handling()
example_test_connection()
example_custom_config()
print("\n" + "=" * 50)
print("演示完成")
if __name__ == "__main__":
main()

View File

@ -1,415 +0,0 @@
import requests
import json
import time
from datetime import datetime
from typing import Dict, List, Optional
from issue_sync.common.gitlink_api import GitlinkAPI
from issue_sync.common.gitee_api import GiteeAPI
from issue_sync.dao.mapping_dao import get_pr_mapping, save_pr_mapping, get_pr_comment_mapping, save_pr_comment_mapping
from issue_sync.dao.log_dao import write_sync_log
class GitLinkGiteePRSyncService:
"""
GitLink和Gitee之间的PR同步服务
支持PR创建更新评论同步等功能
"""
def __init__(self, config: Dict):
"""
初始化同步服务
Args:
config: 配置字典包含以下字段
- gitlink_owner: GitLink仓库拥有者
- gitlink_repo: GitLink仓库名
- gitlink_cookie: GitLink认证Cookie
- gitee_owner: Gitee仓库拥有者
- gitee_repo: Gitee仓库名
- gitee_token: Gitee访问令牌
- sync_direction: 同步方向 ('gitlink_to_gitee', 'gitee_to_gitlink', 'bidirectional')
- sync_comments: 是否同步评论 (True/False)
"""
self.config = config
# 初始化GitLink API
self.gitlink_api = GitlinkAPI(
config['gitlink_owner'],
config['gitlink_repo'],
config['gitlink_cookie']
)
# 初始化Gitee API
self.gitee_api = GiteeAPI(
config['gitee_owner'],
config['gitee_repo'],
config['gitee_token']
)
self.sync_direction = config.get('sync_direction', 'gitlink_to_gitee')
self.sync_comments = config.get('sync_comments', True)
def sync_pull_requests(self):
"""
同步PR的主要方法
"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始PR同步")
print(f"同步方向: {self.sync_direction}")
if self.sync_direction in ['gitlink_to_gitee', 'bidirectional']:
self._sync_gitlink_to_gitee()
if self.sync_direction in ['gitee_to_gitlink', 'bidirectional']:
self._sync_gitee_to_gitlink()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] PR同步完成")
def _sync_gitlink_to_gitee(self):
"""
从GitLink同步PR到Gitee
"""
print("开始从GitLink同步PR到Gitee...")
try:
# 获取GitLink上的所有PR
gitlink_prs = self.gitlink_api.fetch_pull_requests()
print(f"从GitLink获取到 {len(gitlink_prs)} 个PR")
for pr in gitlink_prs:
self._sync_single_gitlink_pr_to_gitee(pr)
time.sleep(1) # 避免请求过于频繁
except Exception as e:
print(f"GitLink到Gitee同步失败: {str(e)}")
write_sync_log(
'pull_request',
f"gitlink:{self.config['gitlink_repo']}",
f"gitee:{self.config['gitee_repo']}",
'fail',
f'GitLink到Gitee同步失败: {str(e)}'
)
def _sync_gitee_to_gitlink(self):
"""
从Gitee同步PR到GitLink
"""
print("开始从Gitee同步PR到GitLink...")
try:
# 获取Gitee上的所有PR
gitee_prs = self.gitee_api.fetch_pull_requests()
print(f"从Gitee获取到 {len(gitee_prs)} 个PR")
for pr in gitee_prs:
self._sync_single_gitee_pr_to_gitlink(pr)
time.sleep(1) # 避免请求过于频繁
except Exception as e:
print(f"Gitee到GitLink同步失败: {str(e)}")
write_sync_log(
'pull_request',
f"gitee:{self.config['gitee_repo']}",
f"gitlink:{self.config['gitlink_repo']}",
'fail',
f'Gitee到GitLink同步失败: {str(e)}'
)
def _sync_single_gitlink_pr_to_gitee(self, gitlink_pr: Dict):
"""
同步单个GitLink PR到Gitee
Args:
gitlink_pr: GitLink PR数据
"""
pr_id = str(gitlink_pr.get('id') or gitlink_pr.get('number'))
title = gitlink_pr.get('title', '')
print(f"处理GitLink PR #{pr_id}: {title}")
# 检查是否已经同步过
mapping = get_pr_mapping(
'gitlink', self.config['gitlink_repo'], pr_id,
'gitee', self.config['gitee_repo']
)
if mapping:
print(f"PR #{pr_id} 已经同步过,跳过")
return
try:
# 准备PR数据
head_branch = self._extract_branch_from_gitlink_pr(gitlink_pr, 'head')
base_branch = self._extract_branch_from_gitlink_pr(gitlink_pr, 'base')
body = gitlink_pr.get('body', '')
# 创建Gitee PR
gitee_pr = self.gitee_api.create_pull_request(
title=title,
head=head_branch,
base=base_branch,
body=body
)
if gitee_pr and 'id' in gitee_pr:
# 保存映射关系
save_pr_mapping(
'gitlink', self.config['gitlink_repo'], pr_id,
'gitee', self.config['gitee_repo'], str(gitee_pr['id'])
)
# 记录同步日志
write_sync_log(
'pull_request',
f"gitlink:{pr_id}",
f"gitee:{gitee_pr['id']}",
'success',
'synced'
)
print(f"成功同步GitLink PR #{pr_id} 到Gitee PR #{gitee_pr['id']}")
# 同步评论
if self.sync_comments:
self._sync_pr_comments_gitlink_to_gitee(pr_id, str(gitee_pr['id']))
else:
print(f"创建Gitee PR失败: {title}")
write_sync_log(
'pull_request',
f"gitlink:{pr_id}",
f"gitee:{self.config['gitee_repo']}",
'fail',
'create failed'
)
except Exception as e:
print(f"同步GitLink PR #{pr_id} 失败: {str(e)}")
write_sync_log(
'pull_request',
f"gitlink:{pr_id}",
f"gitee:{self.config['gitee_repo']}",
'fail',
f'sync failed: {str(e)}'
)
def _sync_single_gitee_pr_to_gitlink(self, gitee_pr: Dict):
"""
同步单个Gitee PR到GitLink
Args:
gitee_pr: Gitee PR数据
"""
pr_id = str(gitee_pr.get('id') or gitee_pr.get('number'))
title = gitee_pr.get('title', '')
print(f"处理Gitee PR #{pr_id}: {title}")
# 检查是否已经同步过
mapping = get_pr_mapping(
'gitee', self.config['gitee_repo'], pr_id,
'gitlink', self.config['gitlink_repo']
)
if mapping:
print(f"PR #{pr_id} 已经同步过,跳过")
return
try:
# 准备PR数据
head_branch = self._extract_branch_from_gitee_pr(gitee_pr, 'head')
base_branch = self._extract_branch_from_gitee_pr(gitee_pr, 'base')
body = gitee_pr.get('body', '')
# 创建GitLink PR
gitlink_pr = self.gitlink_api.create_pull_request(
title=title,
head=head_branch,
base=base_branch,
body=body
)
if gitlink_pr and 'id' in gitlink_pr:
# 保存映射关系
save_pr_mapping(
'gitee', self.config['gitee_repo'], pr_id,
'gitlink', self.config['gitlink_repo'], str(gitlink_pr['id'])
)
# 记录同步日志
write_sync_log(
'pull_request',
f"gitee:{pr_id}",
f"gitlink:{gitlink_pr['id']}",
'success',
'synced'
)
print(f"成功同步Gitee PR #{pr_id} 到GitLink PR #{gitlink_pr['id']}")
# 同步评论
if self.sync_comments:
self._sync_pr_comments_gitee_to_gitlink(pr_id, str(gitlink_pr['id']))
else:
print(f"创建GitLink PR失败: {title}")
write_sync_log(
'pull_request',
f"gitee:{pr_id}",
f"gitlink:{self.config['gitlink_repo']}",
'fail',
'create failed'
)
except Exception as e:
print(f"同步Gitee PR #{pr_id} 失败: {str(e)}")
write_sync_log(
'pull_request',
f"gitee:{pr_id}",
f"gitlink:{self.config['gitlink_repo']}",
'fail',
f'sync failed: {str(e)}'
)
def _sync_pr_comments_gitlink_to_gitee(self, gitlink_pr_id: str, gitee_pr_id: str):
"""
同步GitLink PR评论到Gitee
Args:
gitlink_pr_id: GitLink PR ID
gitee_pr_id: Gitee PR ID
"""
try:
# 获取GitLink PR评论
gitlink_comments = self.gitlink_api.fetch_pr_comments(gitlink_pr_id)
print(f"GitLink PR #{gitlink_pr_id}{len(gitlink_comments)} 条评论")
for comment in gitlink_comments:
comment_id = str(comment.get('id'))
# 检查评论是否已同步
mapping = get_pr_comment_mapping(
'gitlink', self.config['gitlink_repo'], gitlink_pr_id, comment_id,
'gitee', self.config['gitee_repo']
)
if mapping:
continue
# 创建Gitee评论
body = comment.get('body', '')
gitee_comment = self.gitee_api.create_pr_comment(
gitee_pr_id, body
)
if gitee_comment and 'id' in gitee_comment:
# 保存评论映射
save_pr_comment_mapping(
'gitlink', self.config['gitlink_repo'], gitlink_pr_id, comment_id,
'gitee', self.config['gitee_repo'], gitee_pr_id, str(gitee_comment['id']),
body
)
print(f"同步评论: GitLink #{comment_id} -> Gitee #{gitee_comment['id']}")
except Exception as e:
print(f"同步GitLink PR #{gitlink_pr_id} 评论失败: {str(e)}")
def _sync_pr_comments_gitee_to_gitlink(self, gitee_pr_id: str, gitlink_pr_id: str):
"""
同步Gitee PR评论到GitLink
Args:
gitee_pr_id: Gitee PR ID
gitlink_pr_id: GitLink PR ID
"""
try:
# 获取Gitee PR评论
gitee_comments = self.gitee_api.fetch_pr_comments(gitee_pr_id)
print(f"Gitee PR #{gitee_pr_id}{len(gitee_comments)} 条评论")
for comment in gitee_comments:
comment_id = str(comment.get('id'))
# 检查评论是否已同步
mapping = get_pr_comment_mapping(
'gitee', self.config['gitee_repo'], gitee_pr_id, comment_id,
'gitlink', self.config['gitlink_repo']
)
if mapping:
continue
# 创建GitLink评论
body = comment.get('body', '')
gitlink_comment = self.gitlink_api.create_pr_comment(
gitlink_pr_id, body
)
if gitlink_comment and 'id' in gitlink_comment:
# 保存评论映射
save_pr_comment_mapping(
'gitee', self.config['gitee_repo'], gitee_pr_id, comment_id,
'gitlink', self.config['gitlink_repo'], gitlink_pr_id, str(gitlink_comment['id']),
body
)
print(f"同步评论: Gitee #{comment_id} -> GitLink #{gitlink_comment['id']}")
except Exception as e:
print(f"同步Gitee PR #{gitee_pr_id} 评论失败: {str(e)}")
def _extract_branch_from_gitlink_pr(self, pr: Dict, branch_type: str) -> str:
"""
从GitLink PR数据中提取分支信息
Args:
pr: PR数据
branch_type: 分支类型 ('head' 'base')
Returns:
分支名
"""
branch_data = pr.get(branch_type, {})
if isinstance(branch_data, dict):
return branch_data.get('ref', 'main')
elif isinstance(branch_data, str):
return branch_data
else:
return 'main'
def _extract_branch_from_gitee_pr(self, pr: Dict, branch_type: str) -> str:
"""
从Gitee PR数据中提取分支信息
Args:
pr: PR数据
branch_type: 分支类型 ('head' 'base')
Returns:
分支名
"""
branch_data = pr.get(branch_type, {})
if isinstance(branch_data, dict):
return branch_data.get('ref', 'master')
elif isinstance(branch_data, str):
return branch_data
else:
return 'master'
def update_existing_prs(self):
"""
更新已存在的PR状态标题等
"""
print("开始更新已存在的PR...")
# TODO: 实现PR更新逻辑
pass
def get_sync_status(self) -> Dict:
"""
获取同步状态信息
Returns:
同步状态字典
"""
return {
'last_sync_time': datetime.now().isoformat(),
'sync_direction': self.sync_direction,
'sync_comments': self.sync_comments,
'gitlink_repo': f"{self.config['gitlink_owner']}/{self.config['gitlink_repo']}",
'gitee_repo': f"{self.config['gitee_owner']}/{self.config['gitee_repo']}"
}

View File

@ -1,115 +0,0 @@
import time
from issue_sync.common.github_api import GithubAPI
from issue_sync.common.gitee_api import GiteeAPI
from issue_sync.common.gitlink_api import GitlinkAPI
from issue_sync.dao.mapping_dao import get_issue_mapping, save_issue_mapping
from issue_sync.dao.log_dao import write_sync_log
class IssueSyncService:
def __init__(self, config):
"""
config: dict, 包含如下字段
{
'source_platform': 'github'/'gitee'/'gitlink',
'source_owner': 'xxx',
'source_repo': 'xxx',
'source_token': 'xxx',
'target_platform': 'github'/'gitee'/'gitlink',
'target_owner': 'xxx',
'target_repo': 'xxx',
'target_token': 'xxx'
}
"""
self.source_api = self.get_api(
config['source_platform'],
config['source_owner'],
config['source_repo'],
config['source_token']
)
self.target_api = self.get_api(
config['target_platform'],
config['target_owner'],
config['target_repo'],
config['target_token']
)
self.config = config
def get_api(self, platform, owner, repo, token):
if platform == 'github':
return GithubAPI(owner, repo, token)
elif platform == 'gitee':
return GiteeAPI(owner, repo, token)
elif platform == 'gitlink':
# GitLink使用cookie认证token参数实际上是cookie值
# 如果token以'Bearer '开头则作为Bearer Token使用
if token and token.startswith('Bearer '):
return GitlinkAPI(owner, repo, token=token[7:]) # 去掉'Bearer '前缀
else:
return GitlinkAPI(owner, repo, cookie_str=token)
else:
raise Exception(f"Unknown platform: {platform}")
def sync(self):
"""
主同步流程 source 平台的 issue 同步到 target 平台
"""
source_issues = self.source_api.fetch_issues()
for issue in source_issues:
# 以 issue['id'] 作为唯一标识
mapped = get_issue_mapping(
self.config['source_platform'],
self.config['source_repo'],
str(issue['id']),
self.config['target_platform'],
self.config['target_repo']
)
if not mapped:
# 创建到目标平台
# 兼容GitLink字段 subject -> titledescription -> body
title = issue.get('title') or issue.get('subject', '')
body = issue.get('body') or issue.get('description', '')
print(f"[同步到{self.config['target_platform']}] title: {title!r}, body: {body!r}")
new_issue = self.target_api.create_issue(title, body)
if new_issue and 'id' in new_issue:
save_issue_mapping(
self.config['source_platform'], self.config['source_repo'], str(issue['id']),
self.config['target_platform'], self.config['target_repo'], str(new_issue['id'])
)
write_sync_log(
'issue',
f"{self.config['source_platform']}:{issue['id']}",
f"{self.config['target_platform']}:{new_issue['id']}",
'success',
'synced'
)
else:
write_sync_log(
'issue',
f"{self.config['source_platform']}:{issue['id']}",
f"{self.config['target_platform']}",
'fail',
'create failed'
)
# 防止频率过快被服务器断开连接
time.sleep(5)
# 已同步的 issue 可根据需要做更新(可选)
def bidirectional_sync(self):
"""
双向同步A->B, B->A 各跑一遍
"""
# 正向
self.sync()
# 反向
reverse_config = {
'source_platform': self.config['target_platform'],
'source_owner': self.config['target_owner'],
'source_repo': self.config['target_repo'],
'source_token': self.config['target_token'],
'target_platform': self.config['source_platform'],
'target_owner': self.config['source_owner'],
'target_repo': self.config['source_repo'],
'target_token': self.config['source_token'],
}
reverse_service = IssueSyncService(reverse_config)
reverse_service.sync()

View File

@ -1,177 +0,0 @@
import time
from issue_sync.common.github_api import GithubAPI
from issue_sync.common.gitee_api import GiteeAPI
from issue_sync.common.gitlink_api import GitlinkAPI
from issue_sync.dao.mapping_dao import get_pr_mapping, get_pr_comment_mapping, save_pr_comment_mapping, get_pr_comments_by_pr
from issue_sync.dao.log_dao import write_sync_log
from datetime import datetime
class PRCommentSyncService:
def __init__(self, config):
"""
config: dict, 包含如下字段
{
'source_platform': 'github'/'gitee'/'gitlink',
'source_owner': 'xxx',
'source_repo': 'xxx',
'source_token': 'xxx',
'target_platform': 'github'/'gitee'/'gitlink',
'target_owner': 'xxx',
'target_repo': 'xxx',
'target_token': 'xxx'
}
"""
self.source_api = self.get_api(
config['source_platform'],
config['source_owner'],
config['source_repo'],
config['source_token']
)
self.target_api = self.get_api(
config['target_platform'],
config['target_owner'],
config['target_repo'],
config['target_token']
)
self.config = config
def get_api(self, platform, owner, repo, token):
if platform == 'github':
return GithubAPI(owner, repo, token)
elif platform == 'gitee':
return GiteeAPI(owner, repo, token)
elif platform == 'gitlink':
return GitlinkAPI(owner, repo, token)
else:
raise Exception(f"Unknown platform: {platform}")
def sync(self):
"""
主同步流程 source 平台的 PR 评论同步到 target 平台
"""
print(f"开始同步PR评论: {self.config['source_platform']} -> {self.config['target_platform']}")
# 1. 获取源平台上的所有PR
source_prs = self.source_api.fetch_pull_requests()
print(f"{self.config['source_platform']}获取到 {len(source_prs)} 个PR")
# 2. 遍历每个PR
for pr in source_prs:
source_pr_id = str(pr['id'] if 'id' in pr else pr['number'])
print(f"处理PR #{source_pr_id}")
# 3. 查找PR映射关系
pr_mapping = get_pr_mapping(
self.config['source_platform'],
self.config['source_repo'],
source_pr_id,
self.config['target_platform'],
self.config['target_repo']
)
if not pr_mapping:
print(f"PR #{source_pr_id} 在目标平台没有映射,跳过评论同步")
continue
target_pr_id = pr_mapping['target_pr_id']
print(f"找到目标平台PR映射: {target_pr_id}")
# 4. 获取源PR的所有评论
source_comments = self.source_api.fetch_pr_comments(source_pr_id)
print(f"PR #{source_pr_id}{len(source_comments)} 条评论")
# 5. 同步每条评论
for comment in source_comments:
source_comment_id = str(comment['id'])
# 检查评论是否已同步
comment_mapping = get_pr_comment_mapping(
self.config['source_platform'],
self.config['source_repo'],
source_pr_id,
source_comment_id,
self.config['target_platform'],
self.config['target_repo']
)
if comment_mapping:
print(f"评论 #{source_comment_id} 已同步,跳过")
continue
# 提取评论内容和位置信息
body = comment.get('body', '')
commit_id = comment.get('commit_id')
path = comment.get('path')
position = comment.get('position')
# 创建评论到目标平台
print(f"正在同步评论 #{source_comment_id} 到目标平台")
new_comment = self.target_api.create_pr_comment(
target_pr_id,
body,
commit_id=commit_id,
path=path,
position=position
)
if new_comment and ('id' in new_comment or 'number' in new_comment):
target_comment_id = str(new_comment.get('id') or new_comment.get('number'))
# 保存评论映射
save_pr_comment_mapping(
self.config['source_platform'],
self.config['source_repo'],
source_pr_id,
source_comment_id,
self.config['target_platform'],
self.config['target_repo'],
target_pr_id,
target_comment_id,
body,
commit_id,
path,
position
)
write_sync_log(
'pr_comment',
f"{self.config['source_platform']}:{source_pr_id}:{source_comment_id}",
f"{self.config['target_platform']}:{target_pr_id}:{target_comment_id}",
'success',
'synced'
)
print(f"评论同步成功: {source_comment_id} -> {target_comment_id}")
else:
write_sync_log(
'pr_comment',
f"{self.config['source_platform']}:{source_pr_id}:{source_comment_id}",
f"{self.config['target_platform']}:{target_pr_id}",
'fail',
'create failed'
)
print(f"评论同步失败: {source_comment_id}")
# 防止频率过快被服务器断开连接
time.sleep(3)
def bidirectional_sync(self):
"""
双向同步A->B, B->A 各跑一遍
"""
# 正向
self.sync()
# 反向
reverse_config = {
'source_platform': self.config['target_platform'],
'source_owner': self.config['target_owner'],
'source_repo': self.config['target_repo'],
'source_token': self.config['target_token'],
'target_platform': self.config['source_platform'],
'target_owner': self.config['source_owner'],
'target_repo': self.config['source_repo'],
'target_token': self.config['source_token'],
}
reverse_service = PRCommentSyncService(reverse_config)
reverse_service.sync()

View File

@ -1,109 +0,0 @@
from issue_sync.common.github_api import GithubAPI
from issue_sync.common.gitee_api import GiteeAPI
from issue_sync.common.gitlink_api import GitlinkAPI
from issue_sync.dao.mapping_dao import get_pr_mapping, save_pr_mapping
from issue_sync.dao.log_dao import write_sync_log
class PRSyncService:
def __init__(self, config):
"""
config: dict, 包含如下字段
{
'source_platform': 'github'/'gitee'/'gitlink',
'source_owner': 'xxx',
'source_repo': 'xxx',
'source_token': 'xxx',
'target_platform': 'github'/'gitee'/'gitlink',
'target_owner': 'xxx',
'target_repo': 'xxx',
'target_token': 'xxx'
}
"""
self.source_api = self.get_api(
config['source_platform'],
config['source_owner'],
config['source_repo'],
config['source_token']
)
self.target_api = self.get_api(
config['target_platform'],
config['target_owner'],
config['target_repo'],
config['target_token']
)
self.config = config
def get_api(self, platform, owner, repo, token):
if platform == 'github':
return GithubAPI(owner, repo, token)
elif platform == 'gitee':
return GiteeAPI(owner, repo, token)
elif platform == 'gitlink':
return GitlinkAPI(owner, repo, token)
else:
raise Exception(f"Unknown platform: {platform}")
def sync(self):
"""
主同步流程 source 平台的 PR 同步到 target 平台
"""
source_prs = self.source_api.fetch_pull_requests()
for pr in source_prs:
# 以 pr['id'] 作为唯一标识
mapped = get_pr_mapping(
self.config['source_platform'],
self.config['source_repo'],
str(pr['id']),
self.config['target_platform'],
self.config['target_repo']
)
if not mapped:
# 创建到目标平台
# head: 源分支名base: 目标分支名
new_pr = self.target_api.create_pull_request(
pr['title'],
pr['head']['ref'] if 'head' in pr and 'ref' in pr['head'] else pr.get('head', ''),
pr['base']['ref'] if 'base' in pr and 'ref' in pr['base'] else pr.get('base', ''),
pr.get('body', '')
)
if new_pr and 'id' in new_pr:
save_pr_mapping(
self.config['source_platform'], self.config['source_repo'], str(pr['id']),
self.config['target_platform'], self.config['target_repo'], str(new_pr['id'])
)
write_sync_log(
'pull_request',
f"{self.config['source_platform']}:{pr['id']}",
f"{self.config['target_platform']}:{new_pr['id']}",
'success',
'synced'
)
else:
write_sync_log(
'pull_request',
f"{self.config['source_platform']}:{pr['id']}",
f"{self.config['target_platform']}",
'fail',
'create failed'
)
# 已同步的 PR 可根据需要做更新(可选)
def bidirectional_sync(self):
"""
双向同步A->B, B->A 各跑一遍
"""
# 正向
self.sync()
# 反向
reverse_config = {
'source_platform': self.config['target_platform'],
'source_owner': self.config['target_owner'],
'source_repo': self.config['target_repo'],
'source_token': self.config['target_token'],
'target_platform': self.config['source_platform'],
'target_owner': self.config['source_owner'],
'target_repo': self.config['source_repo'],
'target_token': self.config['source_token'],
}
reverse_service = PRSyncService(reverse_config)
reverse_service.sync()

View File

@ -1,121 +0,0 @@
-- GitLink-Gitee PR同步相关表
-- GitLink-Gitee PR同步配置表
CREATE TABLE IF NOT EXISTS gitlink_gitee_pr_config (
id INT AUTO_INCREMENT PRIMARY KEY,
gitlink_owner VARCHAR(100) NOT NULL COMMENT 'GitLink仓库拥有者',
gitlink_repo VARCHAR(100) NOT NULL COMMENT 'GitLink仓库名',
gitlink_cookie TEXT NOT NULL COMMENT 'GitLink认证Cookie',
gitee_owner VARCHAR(100) NOT NULL COMMENT 'Gitee仓库拥有者',
gitee_repo VARCHAR(100) NOT NULL COMMENT 'Gitee仓库名',
gitee_token VARCHAR(255) NOT NULL COMMENT 'Gitee访问令牌',
sync_direction ENUM('gitlink_to_gitee', 'gitee_to_gitlink', 'bidirectional') DEFAULT 'bidirectional' COMMENT '同步方向',
sync_comments BOOLEAN DEFAULT TRUE COMMENT '是否同步评论',
enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
auto_sync BOOLEAN DEFAULT FALSE COMMENT '是否自动同步',
sync_interval INT DEFAULT 300 COMMENT '同步间隔(秒)',
last_sync_time DATETIME DEFAULT NULL COMMENT '最后同步时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_enabled (enabled),
INDEX idx_auto_sync (auto_sync),
INDEX idx_repos (gitlink_owner, gitlink_repo, gitee_owner, gitee_repo)
) COMMENT='GitLink-Gitee PR同步配置表';
-- GitLink-Gitee PR映射表
CREATE TABLE IF NOT EXISTS gitlink_gitee_pr_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
source_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '源平台',
source_pr_id VARCHAR(50) NOT NULL COMMENT '源平台PR ID',
target_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '目标平台',
target_pr_id VARCHAR(50) NOT NULL COMMENT '目标平台PR ID',
pr_title VARCHAR(500) NOT NULL COMMENT 'PR标题',
pr_state VARCHAR(20) DEFAULT 'open' COMMENT 'PR状态',
sync_direction ENUM('gitlink_to_gitee', 'gitee_to_gitlink') NOT NULL COMMENT '同步方向',
last_sync_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '最后同步时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_source_target (source_platform, source_pr_id, target_platform, target_pr_id),
INDEX idx_source (source_platform, source_pr_id),
INDEX idx_target (target_platform, target_pr_id),
INDEX idx_sync_time (last_sync_time)
) COMMENT='GitLink-Gitee PR映射表';
-- GitLink-Gitee PR评论映射表
CREATE TABLE IF NOT EXISTS gitlink_gitee_pr_comment_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
source_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '源平台',
source_pr_id VARCHAR(50) NOT NULL COMMENT '源平台PR ID',
source_comment_id VARCHAR(50) NOT NULL COMMENT '源平台评论ID',
target_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '目标平台',
target_pr_id VARCHAR(50) NOT NULL COMMENT '目标平台PR ID',
target_comment_id VARCHAR(50) NOT NULL COMMENT '目标平台评论ID',
comment_body TEXT COMMENT '评论内容',
comment_author VARCHAR(100) COMMENT '评论作者',
comment_type ENUM('general', 'line', 'review') DEFAULT 'general' COMMENT '评论类型',
commit_id VARCHAR(100) DEFAULT NULL COMMENT '提交ID(用于行评论)',
file_path VARCHAR(500) DEFAULT NULL COMMENT '文件路径(用于行评论)',
line_number INT DEFAULT NULL COMMENT '行号(用于行评论)',
sync_direction ENUM('gitlink_to_gitee', 'gitee_to_gitlink') NOT NULL COMMENT '同步方向',
last_sync_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '最后同步时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_source_comment_target (source_platform, source_pr_id, source_comment_id, target_platform, target_pr_id),
INDEX idx_source_comment (source_platform, source_pr_id, source_comment_id),
INDEX idx_target_comment (target_platform, target_pr_id, target_comment_id),
INDEX idx_sync_time (last_sync_time)
) COMMENT='GitLink-Gitee PR评论映射表';
-- GitLink-Gitee PR同步日志表
CREATE TABLE IF NOT EXISTS gitlink_gitee_pr_sync_log (
id INT AUTO_INCREMENT PRIMARY KEY,
config_id INT NOT NULL COMMENT '配置ID',
sync_type ENUM('pr', 'comment', 'status') NOT NULL COMMENT '同步类型',
source_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '源平台',
source_pr_id VARCHAR(50) COMMENT '源平台PR ID',
target_platform ENUM('gitlink', 'gitee') NOT NULL COMMENT '目标平台',
target_pr_id VARCHAR(50) COMMENT '目标平台PR ID',
sync_direction ENUM('gitlink_to_gitee', 'gitee_to_gitlink') NOT NULL COMMENT '同步方向',
status ENUM('success', 'failed', 'skipped') NOT NULL COMMENT '同步状态',
message TEXT COMMENT '同步消息',
error_details TEXT COMMENT '错误详情',
sync_duration_ms INT DEFAULT NULL COMMENT '同步耗时(毫秒)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_config_id (config_id),
INDEX idx_sync_type (sync_type),
INDEX idx_status (status),
INDEX idx_created_at (created_at),
INDEX idx_source_target (source_platform, target_platform),
FOREIGN KEY (config_id) REFERENCES gitlink_gitee_pr_config(id) ON DELETE CASCADE
) COMMENT='GitLink-Gitee PR同步日志表';
-- GitLink-Gitee PR同步统计表
CREATE TABLE IF NOT EXISTS gitlink_gitee_pr_sync_stats (
id INT AUTO_INCREMENT PRIMARY KEY,
config_id INT NOT NULL COMMENT '配置ID',
sync_date DATE NOT NULL COMMENT '同步日期',
sync_direction ENUM('gitlink_to_gitee', 'gitee_to_gitlink', 'bidirectional') NOT NULL COMMENT '同步方向',
total_prs_synced INT DEFAULT 0 COMMENT '同步的PR总数',
total_comments_synced INT DEFAULT 0 COMMENT '同步的评论总数',
successful_syncs INT DEFAULT 0 COMMENT '成功同步次数',
failed_syncs INT DEFAULT 0 COMMENT '失败同步次数',
skipped_syncs INT DEFAULT 0 COMMENT '跳过同步次数',
avg_sync_duration_ms INT DEFAULT NULL COMMENT '平均同步耗时(毫秒)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_config_date_direction (config_id, sync_date, sync_direction),
INDEX idx_config_id (config_id),
INDEX idx_sync_date (sync_date),
FOREIGN KEY (config_id) REFERENCES gitlink_gitee_pr_config(id) ON DELETE CASCADE
) COMMENT='GitLink-Gitee PR同步统计表';
-- 插入示例数据
INSERT INTO gitlink_gitee_pr_config (
gitlink_owner, gitlink_repo, gitlink_cookie,
gitee_owner, gitee_repo, gitee_token,
sync_direction, sync_comments, enabled, auto_sync, sync_interval
) VALUES (
'example_owner', 'example_repo', 'autologin_trustie=your_cookie_here',
'example_owner', 'example_repo', 'your_gitee_token_here',
'bidirectional', TRUE, TRUE, FALSE, 300
) ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;

View File

@ -1,68 +0,0 @@
CREATE TABLE IF NOT EXISTS sync_config (
id VARCHAR(36) PRIMARY KEY,
source_platform VARCHAR(20),
source_owner VARCHAR(100),
source_repo VARCHAR(100),
source_token VARCHAR(100),
target_platform VARCHAR(20),
target_owner VARCHAR(100),
target_repo VARCHAR(100),
target_token VARCHAR(100),
sync_type VARCHAR(20),
sync_direction VARCHAR(20),
enabled BOOLEAN,
auto_sync BOOLEAN,
sync_interval INT
);
CREATE TABLE IF NOT EXISTS issue_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
source_platform VARCHAR(20),
source_repo VARCHAR(100),
source_issue_id VARCHAR(50),
target_platform VARCHAR(20),
target_repo VARCHAR(100),
target_issue_id VARCHAR(50),
last_sync_time DATETIME
);
CREATE TABLE IF NOT EXISTS pr_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
source_platform VARCHAR(20),
source_repo VARCHAR(100),
source_pr_id VARCHAR(50),
target_platform VARCHAR(20),
target_repo VARCHAR(100),
target_pr_id VARCHAR(50),
last_sync_time DATETIME
);
CREATE TABLE IF NOT EXISTS sync_log (
id INT AUTO_INCREMENT PRIMARY KEY,
sync_type VARCHAR(20),
source VARCHAR(100),
target VARCHAR(100),
status VARCHAR(20),
message TEXT,
timestamp DATETIME
);
-- 新增PR评论映射表
CREATE TABLE IF NOT EXISTS pr_comment_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
source_platform VARCHAR(20),
source_repo VARCHAR(100),
source_pr_id VARCHAR(50),
source_comment_id VARCHAR(50),
target_platform VARCHAR(20),
target_repo VARCHAR(100),
target_pr_id VARCHAR(50),
target_comment_id VARCHAR(50),
comment_body TEXT,
commit_id VARCHAR(100),
path VARCHAR(255),
position INT,
last_sync_time DATETIME,
INDEX(source_platform, source_repo, source_pr_id),
INDEX(target_platform, target_repo, target_pr_id)
);

View File

@ -1,297 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitLink-Gitee PR同步运行器
用于定时执行GitLink和Gitee之间的PR同步任务
"""
import sys
import os
import time
import schedule
import threading
from datetime import datetime
from typing import List, Dict
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from issue_sync.service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
import pymysql
def load_env_file(path):
"""从 .env 或 .ini 文件加载环境变量, 支持 'export KEY=VALUE' 格式"""
env_vars = {}
try:
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[len('export '):]
parts = line.split('=', 1)
if len(parts) == 2:
key, value = parts[0].strip(), parts[1].strip()
# 去除值可能存在的引号
if (value.startswith("'") and value.endswith("'")) or \
(value.startswith('"') and value.endswith('"')):
value = value[1:-1]
env_vars[key] = value
except FileNotFoundError:
print(f"配置文件未找到: {path}")
except Exception as e:
print(f"读取配置文件时出错: {e}")
return env_vars
def get_db():
"""获取数据库连接"""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'env.ini')
db_config = load_env_file(config_path)
return pymysql.connect(
host=db_config.get('CEROBOT_MYSQL_HOST', 'localhost'),
user=db_config.get('CEROBOT_MYSQL_USER', 'root'),
password=db_config.get('CEROBOT_MYSQL_PWD', ''),
database=db_config.get('CEROBOT_MYSQL_DB', 'issue_sync'),
charset="utf8mb4"
)
def get_enabled_gitlink_gitee_pr_configs() -> List[Dict]:
"""
从数据库读取所有启用的 GitLink-Gitee PR 同步配置
"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM gitlink_gitee_pr_config
WHERE enabled=1
""")
configs = cursor.fetchall()
db.close()
return configs
def update_last_sync_time(config_id: int):
"""更新配置的最后同步时间"""
db = get_db()
cursor = db.cursor()
cursor.execute("""
UPDATE gitlink_gitee_pr_config
SET last_sync_time = NOW()
WHERE id = %s
""", (config_id,))
db.commit()
db.close()
def log_sync_result(config_id: int, sync_type: str, source_platform: str,
target_platform: str, status: str, message: str,
source_pr_id: str = None, target_pr_id: str = None,
sync_duration_ms: int = None):
"""记录同步结果到日志表"""
db = get_db()
cursor = db.cursor()
cursor.execute("""
INSERT INTO gitlink_gitee_pr_sync_log
(config_id, sync_type, source_platform, source_pr_id, target_platform, target_pr_id,
sync_direction, status, message, sync_duration_ms, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
""", (
config_id, sync_type, source_platform, source_pr_id, target_platform, target_pr_id,
f"{source_platform}_to_{target_platform}", status, message, sync_duration_ms
))
db.commit()
db.close()
def run_single_config_sync(config: Dict):
"""
运行单个配置的同步任务
Args:
config: 同步配置字典
"""
config_id = config['id']
gitlink_repo = f"{config['gitlink_owner']}/{config['gitlink_repo']}"
gitee_repo = f"{config['gitee_owner']}/{config['gitee_repo']}"
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始同步配置 {config_id}")
print(f"GitLink仓库: {gitlink_repo}")
print(f"Gitee仓库: {gitee_repo}")
print(f"同步方向: {config['sync_direction']}")
start_time = time.time()
try:
# 创建同步服务
sync_service = GitLinkGiteePRSyncService(config)
# 执行同步
sync_service.sync_pull_requests()
# 计算同步耗时
sync_duration_ms = int((time.time() - start_time) * 1000)
# 更新最后同步时间
update_last_sync_time(config_id)
# 记录成功日志
log_sync_result(
config_id, 'pr', 'gitlink', 'gitee', 'success',
f'同步完成,耗时{sync_duration_ms}ms',
sync_duration_ms=sync_duration_ms
)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 配置 {config_id} 同步完成,耗时 {sync_duration_ms}ms")
except Exception as e:
# 计算同步耗时
sync_duration_ms = int((time.time() - start_time) * 1000)
# 记录失败日志
log_sync_result(
config_id, 'pr', 'gitlink', 'gitee', 'failed',
f'同步失败: {str(e)}',
sync_duration_ms=sync_duration_ms
)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 配置 {config_id} 同步失败: {str(e)}")
def run_all_sync():
"""
运行所有启用的同步配置
"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始执行GitLink-Gitee PR同步任务")
try:
configs = get_enabled_gitlink_gitee_pr_configs()
print(f"找到 {len(configs)} 个启用的同步配置")
if not configs:
print("没有找到启用的同步配置")
return
for config in configs:
# 检查是否需要自动同步
if config.get('auto_sync', False):
run_single_config_sync(config)
else:
print(f"配置 {config['id']} 未启用自动同步,跳过")
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] GitLink-Gitee PR同步任务执行完成")
except Exception as e:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 执行同步任务时出错: {str(e)}")
def run_manual_sync(config_id: int = None):
"""
手动运行同步任务
Args:
config_id: 指定配置ID如果为None则运行所有配置
"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始手动执行GitLink-Gitee PR同步")
try:
if config_id:
# 运行指定配置
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT * FROM gitlink_gitee_pr_config WHERE id=%s", (config_id,))
config = cursor.fetchone()
db.close()
if not config:
print(f"配置 {config_id} 不存在")
return
if not config.get('enabled', False):
print(f"配置 {config_id} 未启用")
return
run_single_config_sync(config)
else:
# 运行所有启用的配置
run_all_sync()
except Exception as e:
print(f"手动同步失败: {str(e)}")
def setup_scheduler():
"""
设置定时任务调度器
"""
print("设置定时任务调度器...")
def get_configs_with_auto_sync():
"""获取所有启用自动同步的配置"""
configs = get_enabled_gitlink_gitee_pr_configs()
return [config for config in configs if config.get('auto_sync', False)]
def schedule_config_sync(config):
"""为单个配置设置定时任务"""
interval = config.get('sync_interval', 300) # 默认5分钟
# 设置定时任务
schedule.every(interval).seconds.do(run_single_config_sync, config)
print(f"配置 {config['id']} 已设置定时任务,间隔 {interval}")
# 获取所有启用自动同步的配置
auto_sync_configs = get_configs_with_auto_sync()
if not auto_sync_configs:
print("没有找到启用自动同步的配置")
return
# 为每个配置设置定时任务
for config in auto_sync_configs:
schedule_config_sync(config)
print(f"已设置 {len(auto_sync_configs)} 个定时任务")
def run_scheduler():
"""
运行调度器
"""
print("启动定时任务调度器...")
while True:
try:
schedule.run_pending()
time.sleep(1)
except KeyboardInterrupt:
print("收到中断信号,正在停止调度器...")
break
except Exception as e:
print(f"调度器运行出错: {str(e)}")
time.sleep(5) # 出错后等待5秒再继续
def main():
"""
主函数
"""
import argparse
parser = argparse.ArgumentParser(description='GitLink-Gitee PR同步工具')
parser.add_argument('--manual', action='store_true', help='手动运行同步')
parser.add_argument('--config-id', type=int, help='指定配置ID进行同步')
parser.add_argument('--scheduler', action='store_true', help='启动定时任务调度器')
parser.add_argument('--setup-scheduler', action='store_true', help='设置定时任务')
args = parser.parse_args()
if args.manual:
# 手动运行同步
run_manual_sync(args.config_id)
elif args.scheduler:
# 启动调度器
setup_scheduler()
run_scheduler()
elif args.setup_scheduler:
# 只设置调度器,不运行
setup_scheduler()
else:
# 默认运行一次所有同步
run_all_sync()
if __name__ == "__main__":
main()

View File

@ -1,78 +0,0 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from service.issue_sync_service import IssueSyncService
import pymysql
def load_env_file(path):
"""从 .env 或 .ini 文件加载环境变量, 支持 'export KEY=VALUE' 格式"""
env_vars = {}
try:
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[len('export '):]
parts = line.split('=', 1)
if len(parts) == 2:
key, value = parts[0].strip(), parts[1].strip()
# 去除值可能存在的引号
if (value.startswith("'") and value.endswith("'")) or \
(value.startswith('"') and value.endswith('"')):
value = value[1:-1]
env_vars[key] = value
except FileNotFoundError:
print(f"配置文件未找到: {path}")
except Exception as e:
print(f"读取配置文件时出错: {e}")
return env_vars
# 加载配置
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'env.ini')
db_config = load_env_file(config_path)
def get_db():
return pymysql.connect(
host=db_config.get('CEROBOT_MYSQL_HOST', 'localhost'),
user=db_config.get('CEROBOT_MYSQL_USER', 'root'),
password=db_config.get('CEROBOT_MYSQL_PWD', ''),
database='issue_sync',
charset="utf8mb4"
)
def get_all_enabled_sync_configs():
"""
从数据库读取所有启用的 issue 同步配置
"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM sync_config
WHERE enabled=1 AND sync_type='issue'
""")
configs = cursor.fetchall()
db.close()
return configs
def run_all_sync():
configs = get_all_enabled_sync_configs()
for config in configs:
print(f"开始同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
service = IssueSyncService(config)
# 根据配置决定同步方向
if config.get('sync_direction') == 'bidirectional':
print("执行双向同步...")
service.bidirectional_sync()
else:
print("执行单向同步...")
service.sync()
print(f"完成同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
if __name__ == "__main__":
run_all_sync()

View File

@ -1,111 +0,0 @@
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from service.pr_comment_sync_service import PRCommentSyncService
import pymysql
import time
from datetime import datetime
def load_env_file(path):
"""从 .env 或 .ini 文件加载环境变量, 支持 'export KEY=VALUE' 格式"""
env_vars = {}
try:
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[len('export '):]
parts = line.split('=', 1)
if len(parts) == 2:
key, value = parts[0].strip(), parts[1].strip()
# 去除值可能存在的引号
if (value.startswith("'") and value.endswith("'")) or \
(value.startswith('"') and value.endswith('"')):
value = value[1:-1]
env_vars[key] = value
except FileNotFoundError:
print(f"配置文件未找到: {path}")
except Exception as e:
print(f"读取配置文件时出错: {e}")
return env_vars
# 加载配置
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'env.ini')
db_config = load_env_file(config_path)
def get_db():
return pymysql.connect(
host=db_config.get('CEROBOT_MYSQL_HOST', 'localhost'),
user=db_config.get('CEROBOT_MYSQL_USER', 'root'),
password=db_config.get('CEROBOT_MYSQL_PWD', '123456789LY@'),
database='issue_sync',
charset="utf8mb4"
)
def get_all_enabled_sync_configs():
"""
从数据库读取所有启用的 PR评论 同步配置
"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM sync_config
WHERE enabled=1 AND sync_type='pr_comment'
""")
configs = cursor.fetchall()
db.close()
return configs
def run_all_sync():
"""执行所有启用的PR评论同步配置"""
configs = get_all_enabled_sync_configs()
print(f"找到 {len(configs)} 个启用的PR评论同步配置")
for config in configs:
print(f"开始同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
service = PRCommentSyncService(config)
# 根据配置决定同步方向
if config.get('sync_direction') == 'bidirectional':
print("执行双向同步...")
service.bidirectional_sync()
else:
print("执行单向同步...")
service.sync()
print(f"完成同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
def run_auto_sync(interval=300):
"""
自动定期执行同步
:param interval: 同步间隔()
"""
print(f"启动自动同步,间隔: {interval}")
while True:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 执行定时同步...")
try:
run_all_sync()
except Exception as e:
print(f"同步过程中发生错误: {e}")
print(f"同步完成,等待{interval}秒后再次执行...")
time.sleep(interval)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='PR评论同步工具')
parser.add_argument('--auto', action='store_true', help='启用自动同步模式')
parser.add_argument('--interval', type=int, default=300, help='自动同步间隔(秒)')
args = parser.parse_args()
if args.auto:
run_auto_sync(args.interval)
else:
run_all_sync()

View File

@ -1,40 +0,0 @@
from issue_sync.service.pr_sync_service import PRSyncService
import pymysql
import os
def get_db():
return pymysql.connect(
host=os.getenv("DB_HOST", "localhost"),
user=os.getenv("DB_USER", "root"),
password=os.getenv("DB_PASS", "yourpassword"),
database=os.getenv("DB_NAME", "yourdb"),
charset="utf8mb4"
)
def get_all_enabled_sync_configs():
"""
从数据库读取所有启用的 PR 同步配置
"""
db = get_db()
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("""
SELECT * FROM sync_config
WHERE enabled=1 AND sync_type='pull_request'
""")
configs = cursor.fetchall()
db.close()
return configs
def run_all_sync():
configs = get_all_enabled_sync_configs()
for config in configs:
print(f"开始同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
service = PRSyncService(config)
# 单向同步
service.sync()
# 如果需要双向同步,取消下一行注释
# service.bidirectional_sync()
print(f"完成同步: {config['source_platform']}->{config['target_platform']} {config['source_repo']}->{config['target_repo']}")
if __name__ == "__main__":
run_all_sync()

View File

@ -1,223 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitLink-Gitee PR同步功能测试脚本
用于测试同步功能是否正常工作
"""
import sys
import os
import json
from datetime import datetime
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from issue_sync.service.gitlink_gitee_pr_sync_service import GitLinkGiteePRSyncService
from issue_sync.common.gitlink_api import GitlinkAPI
from issue_sync.common.gitee_api import GiteeAPI
def test_gitlink_api():
"""测试GitLink API连接"""
print("=== 测试GitLink API ===")
# 使用测试配置
gitlink_api = GitlinkAPI(
owner='test_owner',
repo='test_repo',
cookie_str='test_cookie'
)
try:
# 测试获取PR列表
prs = gitlink_api.fetch_pull_requests()
print(f"GitLink API测试结果: {'成功' if prs is not None else '失败'}")
return prs is not None
except Exception as e:
print(f"GitLink API测试失败: {str(e)}")
return False
def test_gitee_api():
"""测试Gitee API连接"""
print("\n=== 测试Gitee API ===")
# 使用测试配置
gitee_api = GiteeAPI(
owner='test_owner',
repo='test_repo',
token='test_token'
)
try:
# 测试获取PR列表
prs = gitee_api.fetch_pull_requests()
print(f"Gitee API测试结果: {'成功' if prs is not None else '失败'}")
return prs is not None
except Exception as e:
print(f"Gitee API测试失败: {str(e)}")
return False
def test_sync_service():
"""测试同步服务"""
print("\n=== 测试同步服务 ===")
# 测试配置
config = {
'gitlink_owner': 'test_owner',
'gitlink_repo': 'test_repo',
'gitlink_cookie': 'test_cookie',
'gitee_owner': 'test_owner',
'gitee_repo': 'test_repo',
'gitee_token': 'test_token',
'sync_direction': 'bidirectional',
'sync_comments': True
}
try:
# 创建同步服务
sync_service = GitLinkGiteePRSyncService(config)
# 测试获取同步状态
status = sync_service.get_sync_status()
print(f"同步服务状态: {json.dumps(status, ensure_ascii=False, indent=2)}")
print("同步服务测试成功")
return True
except Exception as e:
print(f"同步服务测试失败: {str(e)}")
return False
def test_with_real_config():
"""使用真实配置进行测试(需要用户提供配置)"""
print("\n=== 真实配置测试 ===")
# 这里需要用户提供真实的配置信息
print("请提供真实的配置信息进行测试:")
print("1. GitLink仓库信息")
print("2. Gitee仓库信息")
print("3. 认证信息")
# 示例配置(需要用户修改)
real_config = {
'gitlink_owner': 'your_real_gitlink_owner',
'gitlink_repo': 'your_real_gitlink_repo',
'gitlink_cookie': 'autologin_trustie=your_real_cookie',
'gitee_owner': 'your_real_gitee_owner',
'gitee_repo': 'your_real_gitee_repo',
'gitee_token': 'your_real_gitee_token',
'sync_direction': 'bidirectional',
'sync_comments': True
}
# 检查是否提供了真实配置
if (real_config['gitlink_owner'] == 'your_real_gitlink_owner' or
real_config['gitee_owner'] == 'your_real_gitee_owner'):
print("请修改脚本中的配置信息,然后重新运行测试")
return False
try:
sync_service = GitLinkGiteePRSyncService(real_config)
# 测试连接
print("测试GitLink连接...")
gitlink_prs = sync_service.gitlink_api.fetch_pull_requests()
print(f"GitLink PR数量: {len(gitlink_prs) if gitlink_prs else 0}")
print("测试Gitee连接...")
gitee_prs = sync_service.gitee_api.fetch_pull_requests()
print(f"Gitee PR数量: {len(gitee_prs) if gitee_prs else 0}")
print("真实配置测试成功")
return True
except Exception as e:
print(f"真实配置测试失败: {str(e)}")
return False
def test_database_connection():
"""测试数据库连接"""
print("\n=== 测试数据库连接 ===")
try:
import pymysql
import os
# 从环境变量或配置文件读取数据库配置
db_config = {
'host': os.getenv('CEROBOT_MYSQL_HOST', 'localhost'),
'user': os.getenv('CEROBOT_MYSQL_USER', 'root'),
'password': os.getenv('CEROBOT_MYSQL_PWD', ''),
'database': os.getenv('CEROBOT_MYSQL_DB', 'issue_sync'),
'charset': 'utf8mb4'
}
# 尝试连接数据库
conn = pymysql.connect(**db_config)
cursor = conn.cursor()
# 测试查询
cursor.execute("SELECT 1")
result = cursor.fetchone()
cursor.close()
conn.close()
print("数据库连接测试成功")
return True
except Exception as e:
print(f"数据库连接测试失败: {str(e)}")
return False
def run_all_tests():
"""运行所有测试"""
print("开始运行GitLink-Gitee PR同步功能测试")
print("=" * 50)
test_results = []
# 运行各项测试
test_results.append(("GitLink API", test_gitlink_api()))
test_results.append(("Gitee API", test_gitee_api()))
test_results.append(("同步服务", test_sync_service()))
test_results.append(("数据库连接", test_database_connection()))
# 显示测试结果
print("\n" + "=" * 50)
print("测试结果汇总:")
print("=" * 50)
passed = 0
total = len(test_results)
for test_name, result in test_results:
status = "✅ 通过" if result else "❌ 失败"
print(f"{test_name}: {status}")
if result:
passed += 1
print(f"\n总计: {passed}/{total} 项测试通过")
if passed == total:
print("🎉 所有测试通过GitLink-Gitee PR同步功能可以正常使用。")
else:
print("⚠️ 部分测试失败,请检查配置和依赖。")
return passed == total
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='GitLink-Gitee PR同步功能测试')
parser.add_argument('--real-config', action='store_true', help='使用真实配置进行测试')
args = parser.parse_args()
if args.real_config:
# 只运行真实配置测试
test_with_real_config()
else:
# 运行所有测试
run_all_tests()
if __name__ == "__main__":
main()

View File

302
main.py
View File

@ -1,251 +1,51 @@
# coding: utf-8
import uvicorn
import platform
import hashlib
import json
import os
from datetime import datetime
from fastapi import Request, Body
from fastapi.responses import JSONResponse
import time
from pydantic import BaseModel
import src.api.Cerobot
import src.api.Sync
import src.api.Account
import src.api.PullRequest
import src.api.User
import src.api.Log
import src.api.Auth
import src.api.Sync_config
import src.api.Issue
import src.api.Plugin
import issue_sync.api.config_api
import issue_sync.api.gitlink_gitee_pr_api
from extras.obfastapi.frame import OBFastAPI
from src.router import CE_ROBOT, PROJECT, JOB, ACCOUNT, PULL_REQUEST, USER, LOG, AUTH, SYNC_CONFIG, ISSUE, PLUGIN
from fastapi.staticfiles import StaticFiles
from src.plugins.plugin_manager import plugin_manager
from src.plugins.code_quality_guard import CodeQualityGuard, PluginConfig
app = OBFastAPI()
# 请求计数器
request_count = 0
# 初始化插件系统
def init_plugin_system():
"""初始化插件系统"""
try:
# 注册代码质量检测插件
config = PluginConfig(
name="CodeQualityGuard",
version="1.0.0",
description="智能代码质量检测与自动修复插件",
enabled=True
)
quality_plugin = CodeQualityGuard(config)
plugin_manager.register_plugin(quality_plugin)
# 从插件目录加载其他插件
plugin_dir = os.path.join(os.path.dirname(__file__), 'src', 'plugins')
loaded_count = plugin_manager.load_plugins_from_directory(plugin_dir)
print(f"插件系统初始化完成,加载了 {loaded_count} 个插件")
except Exception as e:
print(f"插件系统初始化失败: {str(e)}")
# 添加请求日志中间件
@app.middleware("http")
async def log_requests(request: Request, call_next):
global request_count
request_count += 1
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
# 简单的控制台日志
print(f"[{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC] "
f"{request.method} {request.url.path} - "
f"Status: {response.status_code} - "
f"Time: {process_time:.3f}s")
return response
# 添加健康检查接口
@app.get("/health")
async def health_check():
"""健康检查接口"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"service": "RepoSyncer API",
"plugins": {
"total": len(plugin_manager.get_all_plugins()),
"enabled": len(plugin_manager.get_enabled_plugins())
}
}
# 添加系统信息接口
@app.get("/system/info")
async def system_info():
"""系统信息接口"""
return {
"platform": platform.system(),
"platform_version": platform.version(),
"python_version": platform.python_version(),
"cpu_count": os.cpu_count(),
"current_working_directory": os.getcwd(),
"environment": os.environ.get('SYS_ENV', 'unknown'),
"startup_time": datetime.now().isoformat(),
"plugin_system": {
"total_plugins": len(plugin_manager.get_all_plugins()),
"enabled_plugins": len(plugin_manager.get_enabled_plugins())
}
}
# 添加简单的统计接口
@app.get("/stats")
async def get_stats():
"""获取系统统计信息"""
return {
"total_projects": 0, # 这里可以后续连接数据库获取真实数据
"active_jobs": 0,
"total_requests": request_count,
"last_sync": None,
"plugins": {
"total": len(plugin_manager.get_all_plugins()),
"enabled": len(plugin_manager.get_enabled_plugins()),
"execution_history_count": len(plugin_manager.get_execution_history())
}
}
# 添加工具接口
@app.get("/tools/hash/{text}")
async def generate_hash(text: str, algorithm: str = "md5"):
"""生成文本的哈希值"""
algorithms = {
"md5": hashlib.md5,
"sha1": hashlib.sha1,
"sha256": hashlib.sha256,
"sha512": hashlib.sha512
}
if algorithm not in algorithms:
return {"error": f"不支持的算法: {algorithm}", "supported": list(algorithms.keys())}
hash_obj = algorithms[algorithm]()
hash_obj.update(text.encode('utf-8'))
return {
"text": text,
"algorithm": algorithm,
"hash": hash_obj.hexdigest()
}
@app.get("/tools/timestamp")
async def get_timestamp():
"""获取当前时间戳"""
now = datetime.now()
return {
"timestamp": int(time.time()),
"datetime": now.isoformat(),
"formatted": now.strftime("%Y-%m-%d %H:%M:%S"),
"timezone": "UTC"
}
class JSONStringModel(BaseModel):
json_string: str
@app.get("/tools/validate/json")
async def validate_json(json_string: str):
"""验证JSON字符串 (GET)"""
try:
parsed = json.loads(json_string)
return {
"valid": True,
"parsed": parsed,
"type": type(parsed).__name__
}
except json.JSONDecodeError as e:
return {
"valid": False,
"error": str(e)
}
@app.post("/tools/validate/json")
async def validate_json_post(data: JSONStringModel = Body(...)):
"""验证JSON字符串 (POST)"""
try:
parsed = json.loads(data.json_string)
return {
"valid": True,
"parsed": parsed,
"type": type(parsed).__name__
}
except json.JSONDecodeError as e:
return {
"valid": False,
"error": str(e)
}
# 添加插件系统相关接口
@app.get("/plugins/status")
async def get_plugin_status():
"""获取插件系统状态"""
plugins = plugin_manager.get_all_plugins()
plugin_status = {}
for name, plugin in plugins.items():
plugin_status[name] = {
"name": plugin.name,
"version": plugin.version,
"description": plugin.description,
"enabled": plugin.enabled,
"supported_languages": plugin.get_supported_languages()
}
return {
"total_plugins": len(plugins),
"enabled_plugins": len(plugin_manager.get_enabled_plugins()),
"plugins": plugin_status,
"execution_history_count": len(plugin_manager.get_execution_history())
}
@app.post("/plugins/quality/quick-check")
async def quick_quality_check(repo_path: str = Body(..., embed=True)):
"""快速代码质量检查"""
try:
context = {"repo_path": repo_path}
result = await plugin_manager.execute_plugin("CodeQualityGuard", context)
return result
except Exception as e:
return {"success": False, "error": str(e)}
app.include_router(CE_ROBOT)
app.include_router(PROJECT)
app.include_router(JOB)
app.include_router(ACCOUNT)
app.include_router(PULL_REQUEST)
app.include_router(USER)
app.include_router(LOG)
app.include_router(AUTH)
app.include_router(SYNC_CONFIG)
app.include_router(ISSUE)
app.include_router(PLUGIN)
# 注册 issue_sync 模块的 API 路由
app.include_router(issue_sync.api.config_api.router, tags=["Issue Sync"])
app.include_router(issue_sync.api.gitlink_gitee_pr_api.router, prefix="/gitlink-gitee-pr", tags=["GitLink Gitee PR Sync"])
# app.mount("/", StaticFiles(directory="web/dist"), name="static")
if __name__ == '__main__':
# 初始化插件系统
init_plugin_system()
# workers 参数仅在命令行使用uvicorn启动时有效 或使用环境变量 WEB_CONCURRENCY
uvicorn.run(app='main:app', host='0.0.0.0', port=8000,
reload=True, debug=True, workers=2)
# coding: utf-8
import uvicorn
from fastapi.middleware.cors import CORSMiddleware
import src.api.Cerobot
import src.api.Sync
import src.api.Account
import src.api.PullRequest
import src.api.User
import src.api.Log
import src.api.Auth
import src.api.Sync_config
import src.api.Health
import src.api.Issue
import src.api.Comment
import src.api.PRComment
from extras.obfastapi.frame import OBFastAPI
from src.router import CE_ROBOT, PROJECT, JOB, ACCOUNT, PULL_REQUEST, USER, LOG, AUTH, SYNC_CONFIG, HEALTH, ISSUE, COMMENT, PR_COMMENT
from fastapi.staticfiles import StaticFiles
app = OBFastAPI()
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源,生产环境应该设置具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(CE_ROBOT)
app.include_router(PROJECT)
app.include_router(JOB)
app.include_router(ACCOUNT)
app.include_router(PULL_REQUEST)
app.include_router(USER)
app.include_router(LOG)
app.include_router(AUTH)
app.include_router(SYNC_CONFIG)
app.include_router(HEALTH)
app.include_router(ISSUE)
app.include_router(COMMENT)
app.include_router(PR_COMMENT)
# app.mount("/", StaticFiles(directory="web/dist"), name="static")
if __name__ == '__main__':
# workers 参数仅在命令行使用uvicorn启动时有效 或使用环境变量 WEB_CONCURRENCY
uvicorn.run(app='main:app', host='0.0.0.0', port=8000,
reload=True, debug=True, workers=2)

View File

@ -1,32 +0,0 @@
[mysqld]
# 字符集配置
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# 连接配置
max_connections = 200
max_connect_errors = 1000
# 缓存配置
innodb_buffer_pool_size = 256M
query_cache_size = 32M
query_cache_type = 1
# 日志配置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 性能优化
innodb_flush_log_at_trx_commit = 2
innodb_log_file_size = 64M
innodb_log_buffer_size = 16M
# 时区配置
default-time-zone = '+8:00'
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4

187
quick_test_gitlink.py Normal file
View File

@ -0,0 +1,187 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快速Gitlink问题诊断脚本
基于日志中的具体错误进行针对性测试
"""
import requests
import json
import urllib3
# 禁用SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def test_exact_log_issue():
"""测试日志中显示的具体问题"""
print("=== 测试日志中的具体问题 ===")
# 从日志中提取的信息
owner = "gonggong123zzz"
repo_name = "repo2"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
base_url = "https://www.gitlink.org.cn"
project_url = f"{base_url}/api/v1/projects"
params = {
'owner': owner,
'name': repo_name
}
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
print(f"测试项目: {owner}/{repo_name}")
print(f"请求URL: {project_url}")
print(f"请求参数: {params}")
print(f"请求头: {headers}")
print("-" * 50)
try:
response = requests.get(project_url, params=params, headers=headers, timeout=30, verify=False)
print(f"响应状态码: {response.status_code}")
print(f"响应头: {dict(response.headers)}")
print(f"响应内容: {response.text}")
if response.status_code == 200:
try:
data = response.json()
print(f"JSON解析成功: {json.dumps(data, indent=2, ensure_ascii=False)}")
if isinstance(data, list):
if data:
print(f"✓ 找到 {len(data)} 个项目")
project = data[0]
project_id = project.get('id')
print(f"项目ID: {project_id}")
# 测试PR列表获取
if project_id:
test_pr_list(project_id, headers, base_url)
else:
print("✗ 项目列表为空")
else:
print(f"✗ 响应格式不是列表: {type(data)}")
except json.JSONDecodeError as e:
print(f"✗ JSON解析失败: {e}")
print(f"响应内容可能是HTML: {response.text[:200]}...")
else:
print(f"✗ 请求失败,状态码: {response.status_code}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_pr_list(project_id, headers, base_url):
"""测试PR列表获取"""
print(f"\n=== 测试PR列表获取 (项目ID: {project_id}) ===")
pr_url = f"{base_url}/api/v1/projects/{project_id}/pull_requests"
try:
response = requests.get(pr_url, headers=headers, timeout=30, verify=False)
print(f"PR列表响应状态码: {response.status_code}")
print(f"PR列表响应内容: {response.text[:500]}...")
if response.status_code == 200:
try:
prs = response.json()
print(f"✓ PR列表获取成功数量: {len(prs) if isinstance(prs, list) else 'N/A'}")
except json.JSONDecodeError as e:
print(f"✗ PR列表JSON解析失败: {e}")
else:
print(f"✗ PR列表获取失败")
except Exception as e:
print(f"✗ PR列表请求异常: {e}")
def test_alternative_approaches():
"""测试替代方法"""
print("\n=== 测试替代方法 ===")
owner = "gonggong123zzz"
repo_name = "repo2"
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
base_url = "https://www.gitlink.org.cn"
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# 方法1: 直接通过路径获取项目
endpoints = [
f"{base_url}/api/v1/repos/{owner}/{repo_name}",
f"{base_url}/api/v1/projects/{owner}/{repo_name}",
f"{base_url}/api/v1/repositories/{owner}/{repo_name}"
]
for endpoint in endpoints:
print(f"\n尝试端点: {endpoint}")
try:
response = requests.get(endpoint, headers=headers, timeout=30, verify=False)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
print(f"✓ 成功: {endpoint}")
print(f"响应: {response.text[:300]}...")
break
else:
print(f"✗ 失败: {endpoint}")
except Exception as e:
print(f"✗ 异常: {endpoint}, 错误: {e}")
def test_auth_status():
"""测试认证状态"""
print("\n=== 测试认证状态 ===")
cookie = "autologin_trustie=4a98f5358aab9fc87f9bdb6332dc87ec337dc8cf"
base_url = "https://www.gitlink.org.cn"
headers = {
'Cookie': cookie,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
try:
response = requests.get(f"{base_url}/api/v1/user", headers=headers, timeout=30, verify=False)
print(f"用户信息响应状态码: {response.status_code}")
print(f"用户信息响应内容: {response.text}")
if response.status_code == 200:
print("✓ 认证成功")
else:
print("✗ 认证失败Cookie可能已过期")
except Exception as e:
print(f"✗ 认证测试异常: {e}")
def main():
"""主函数"""
print("Gitlink 快速问题诊断工具")
print("=" * 60)
# 1. 测试认证状态
test_auth_status()
# 2. 测试日志中的具体问题
test_exact_log_issue()
# 3. 测试替代方法
test_alternative_approaches()
print("\n" + "=" * 60)
print("诊断完成")
print("\n可能的问题:")
print("1. Cookie过期 - 需要重新登录获取新的Cookie")
print("2. 项目不存在 - 检查项目名称和所有者")
print("3. API端点错误 - 查看Gitlink官方API文档")
print("4. 权限不足 - 确认用户对项目有访问权限")
if __name__ == "__main__":
main()

1
reposync Submodule

@ -0,0 +1 @@
Subproject commit e2835f7c59f9f12e1351778c388e69f25b0ee1ce

View File

@ -1,11 +1,12 @@
uvicorn==0.14.0
SQLAlchemy==1.4.21
fastapi==0.66.0
aiohttp==3.7.4.post0
pydantic==1.8.2
starlette==0.14.2
aiomysql==0.0.21
requests==2.26.0
loguru==0.6.0
typing-extensions==4.1.1
aiofiles==0.8.0
uvicorn==0.14.0
SQLAlchemy==1.4.21
fastapi==0.66.0
aiohttp==3.7.4.post0
pydantic==1.8.2
starlette==0.14.2
aiomysql==0.0.21
requests==2.26.0
loguru==0.6.0
typing-extensions==4.1.1
aiofiles==0.8.0
psutil==7.0.0

View File

@ -1,28 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: reposyncer-conf
namespace: reposyncer
labels:
name: reposyncer-conf
data:
env.ini: |
export SYS_ENV=DEV
export LOG_PATH=
export LOG_LV=DEBUG
# 后端数据库配置
export CEROBOT_MYSQL_HOST=
export CEROBOT_MYSQL_PORT=
export CEROBOT_MYSQL_USER=""
export CEROBOT_MYSQL_PWD=""
export CEROBOT_MYSQL_DB=""
# 对称加密密钥
export DATA_ENCRYPT_KEY=
# 运行构建任务容器名
export EL8_DOCKER_IMAGE=''
export EL7_DOCKER_IMAGE=''
# authentication
export BUC_KEY=OBRDE_DEV_USER_SIGN
apiVersion: v1
kind: ConfigMap
metadata:
name: reposyncer-conf
namespace: reposyncer
labels:
name: reposyncer-conf
data:
env.ini: |
export SYS_ENV=DEV
export LOG_PATH=
export LOG_LV=DEBUG
# 后端数据库配置
export CEROBOT_MYSQL_HOST=
export CEROBOT_MYSQL_PORT=
export CEROBOT_MYSQL_USER=""
export CEROBOT_MYSQL_PWD=""
export CEROBOT_MYSQL_DB=""
# 对称加密密钥
export DATA_ENCRYPT_KEY=
# 运行构建任务容器名
export EL8_DOCKER_IMAGE=''
export EL7_DOCKER_IMAGE=''
# authentication
export BUC_KEY=OBRDE_DEV_USER_SIGN

View File

@ -1,13 +1,13 @@
apiVersion: v1
kind: Service
metadata:
namespace: reposyncer-test
name: reposyncer-test-backend
labels:
k8s-app: reposyncer-test-backend
spec:
ports:
- port: 80
targetPort: 8000
selector:
k8s-app: reposyncer-test-backend
apiVersion: v1
kind: Service
metadata:
namespace: reposyncer-test
name: reposyncer-test-backend
labels:
k8s-app: reposyncer-test-backend
spec:
ports:
- port: 80
targetPort: 8000
selector:
k8s-app: reposyncer-test-backend

View File

@ -1,40 +1,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: reposyncer-test-backend
namespace: reposyncer-test
spec:
selector:
matchLabels:
k8s-app: reposyncer-test-backend
replicas: 1
template:
metadata:
labels:
k8s-app: reposyncer-test-backend
spec:
containers:
- name: reposyncer
image: #/ob-robot/reposyncer:v0.0.1
imagePullPolicy: Always
ports:
- containerPort: 8000
env:
- name: BOOT_MODE
value: "app"
- name: WEB_CONCURRENCY
value: "4"
- name: SYS_ENV
value: "DEV"
- name: CEROBOT_MYSQL_HOST
value: ""
- name: CEROBOT_MYSQL_PORT
value: ""
- name: CEROBOT_MYSQL_USER
value: ""
- name: CEROBOT_MYSQL_PWD
value: ""
- name: CEROBOT_MYSQL_DB
value: ""
- name: BUC_KEY
value: "OBRDE_DEV_USER_SIGN"
apiVersion: apps/v1
kind: Deployment
metadata:
name: reposyncer-test-backend
namespace: reposyncer-test
spec:
selector:
matchLabels:
k8s-app: reposyncer-test-backend
replicas: 1
template:
metadata:
labels:
k8s-app: reposyncer-test-backend
spec:
containers:
- name: reposyncer
image: #/ob-robot/reposyncer:v0.0.1
imagePullPolicy: Always
ports:
- containerPort: 8000
env:
- name: BOOT_MODE
value: "app"
- name: WEB_CONCURRENCY
value: "4"
- name: SYS_ENV
value: "DEV"
- name: CEROBOT_MYSQL_HOST
value: ""
- name: CEROBOT_MYSQL_PORT
value: ""
- name: CEROBOT_MYSQL_USER
value: ""
- name: CEROBOT_MYSQL_PWD
value: ""
- name: CEROBOT_MYSQL_DB
value: ""
- name: BUC_KEY
value: "OBRDE_DEV_USER_SIGN"

View File

@ -1,13 +1,13 @@
kind: Service
apiVersion: v1
metadata:
name: ob-robot-frontend
namespace: ob-robot
labels:
k8s-app: ob-robot-frontend
spec:
selector:
k8s-app: ob-robot-frontend
ports:
- port: 80
targetPort: 8080
kind: Service
apiVersion: v1
metadata:
name: ob-robot-frontend
namespace: ob-robot
labels:
k8s-app: ob-robot-frontend
spec:
selector:
k8s-app: ob-robot-frontend
ports:
- port: 80
targetPort: 8080

View File

@ -1,21 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ob-robot-frontend
namespace: ob-robot
spec:
replicas: 1
selector:
matchLabels:
k8s-app: ob-robot-frontend
template:
metadata:
labels:
k8s-app: ob-robot-frontend
spec:
containers:
- name: ob-robot-frontend
image: #/ob-robot/frontend:v0.0.14
imagePullPolicy: Always
ports:
- containerPort: 8080
apiVersion: apps/v1
kind: Deployment
metadata:
name: ob-robot-frontend
namespace: ob-robot
spec:
replicas: 1
selector:
matchLabels:
k8s-app: ob-robot-frontend
template:
metadata:
labels:
k8s-app: ob-robot-frontend
spec:
containers:
- name: ob-robot-frontend
image: #/ob-robot/frontend:v0.0.14
imagePullPolicy: Always
ports:
- containerPort: 8080

View File

@ -1,4 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: reposyncer-test
apiVersion: v1
kind: Namespace
metadata:
name: reposyncer-test

View File

@ -1,49 +1,49 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: document-sync
namespace: ob-robot
spec:
concurrencyPolicy: Forbid
schedule: "*/10 * * * *"
jobTemplate:
spec:
template:
spec:
volumes:
- name: ssh-key-volume
secret:
secretName: my-ssh-key
defaultMode: 256
containers:
- name: document-sync
image: #/ob-robot/reposyncer:v0.0.1
imagePullPolicy: IfNotPresent
command:
- python3
- sync.py
env:
- name: BOOT_MODE
value: "sync"
- name: WEB_CONCURRENCY
value: "4"
- name: SYS_ENV
value: "DEV"
- name: CEROBOT_MYSQL_HOST
value: ""
- name: CEROBOT_MYSQL_PORT
value: ""
- name: CEROBOT_MYSQL_USER
value: ""
- name: CEROBOT_MYSQL_PWD
value: ""
- name: CEROBOT_MYSQL_DB
value: ""
- name: BUC_KEY
value: "OBRDE_DEV_USER_SIGN"
- name: SYS_ENV
value: "DEV"
volumeMounts:
- name: ssh-key-volume
mountPath: "/root/.ssh/"
restartPolicy: OnFailure
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: document-sync
namespace: ob-robot
spec:
concurrencyPolicy: Forbid
schedule: "*/10 * * * *"
jobTemplate:
spec:
template:
spec:
volumes:
- name: ssh-key-volume
secret:
secretName: my-ssh-key
defaultMode: 256
containers:
- name: document-sync
image: #/ob-robot/reposyncer:v0.0.1
imagePullPolicy: IfNotPresent
command:
- python3
- sync.py
env:
- name: BOOT_MODE
value: "sync"
- name: WEB_CONCURRENCY
value: "4"
- name: SYS_ENV
value: "DEV"
- name: CEROBOT_MYSQL_HOST
value: ""
- name: CEROBOT_MYSQL_PORT
value: ""
- name: CEROBOT_MYSQL_USER
value: ""
- name: CEROBOT_MYSQL_PWD
value: ""
- name: CEROBOT_MYSQL_DB
value: ""
- name: BUC_KEY
value: "OBRDE_DEV_USER_SIGN"
- name: SYS_ENV
value: "DEV"
volumeMounts:
- name: ssh-key-volume
mountPath: "/root/.ssh/"
restartPolicy: OnFailure

View File

@ -1,37 +1,41 @@
CREATE TABLE IF NOT EXISTS `sync_repo_mapping` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_repo_address` varchar(255) NOT NULL COMMENT '内部仓库地址',
`external_repo_address` varchar(255) NOT NULL COMMENT '外部仓库地址',
`sync_granularity` enum('all', 'one') NOT NULL COMMENT '同步粒度',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '仓库绑定时间',
PRIMARY KEY (`id`),
UNIQUE KEY (`repo_name`)
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '同步仓库映射表';
CREATE TABLE IF NOT EXISTS `sync_branch_mapping`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_id` bigint unsigned NOT NULL COMMENT '仓库ID',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_branch_name` varchar(255) NOT NULL COMMENT '内部仓库分支名称',
`external_branch_name` varchar(255) NOT NULL COMMENT '外部仓库分支名称',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分支绑定时间',
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '同步分支映射表';
CREATE TABLE IF NOT EXISTS `repo_sync_log`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`branch_id` bigint unsigned COMMENT '分支id',
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`commit_id` varchar(255) COMMENT 'commit ID',
`log` longtext COMMENT '同步日志',
`sync_direct` enum('to_outer', 'to_inter') NOT NULL COMMENT '同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
--
CREATE TABLE IF NOT EXISTS `sync_repo_mapping` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_repo_address` varchar(255) NOT NULL COMMENT '内部仓库地址',
`external_repo_address` varchar(255) NOT NULL COMMENT '外部仓库地址',
`sync_granularity` enum('all', 'one') NOT NULL COMMENT '同步粒度',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '仓库绑定时间',
PRIMARY KEY (`id`),
UNIQUE KEY (`repo_name`)
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '同步仓库映射表';
--
CREATE TABLE IF NOT EXISTS `sync_branch_mapping`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_id` bigint unsigned NOT NULL COMMENT '仓库ID',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_branch_name` varchar(255) NOT NULL COMMENT '内部仓库分支名称',
`external_branch_name` varchar(255) NOT NULL COMMENT '外部仓库分支名称',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分支绑定时间',
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '同步分支映射表';
--
CREATE TABLE IF NOT EXISTS `repo_sync_log`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`branch_id` bigint unsigned COMMENT '分支id',
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`commit_id` varchar(255) COMMENT 'commit ID',
`log` longtext COMMENT '同步日志',
`sync_direct` enum('to_outer', 'to_inter') NOT NULL COMMENT '同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '同步日志';

View File

@ -1,71 +1,71 @@
ALTER TABLE `github_account`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `domain` varchar(20) COMMENT '域账号',
MODIFY COLUMN `nickname` varchar(20) COMMENT '花名',
MODIFY COLUMN `account` varchar(50) COMMENT 'GitHub账号',
MODIFY COLUMN `email` varchar(50) COMMENT '邮箱',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `gitee_account`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `domain` varchar(20) COMMENT '域账号',
MODIFY COLUMN `nickname` varchar(20) COMMENT '花名',
MODIFY COLUMN `account` varchar(20) COMMENT 'GitHub账号',
MODIFY COLUMN `email` varchar(20) COMMENT '邮箱',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_project`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `name` varchar(50) COMMENT '名称',
MODIFY COLUMN `github` varchar(100) COMMENT 'GitHub地址',
MODIFY COLUMN `gitee` varchar(100) COMMENT 'Gitee地址',
MODIFY COLUMN `gitlab` varchar(100) COMMENT 'Gitlab地址',
MODIFY COLUMN `code_china` varchar(100) COMMENT 'CodeChina地址',
MODIFY COLUMN `gitlink` varchar(100) COMMENT 'Gitlink地址',
MODIFY COLUMN `github_token` varchar(100) COMMENT 'GitHub token',
MODIFY COLUMN `gitee_token` varchar(100) COMMENT 'Gitee token',
MODIFY COLUMN `code_china_token` varchar(100) COMMENT 'CodeChina token',
MODIFY COLUMN `gitlink_token` varchar(100) COMMENT 'Gitlink token',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_job`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `project` varchar(50) COMMENT '工程名称',
MODIFY COLUMN `type` enum('OneWay','TwoWay') COMMENT '同步类型',
MODIFY COLUMN `status` tinyint(1) COMMENT '同步流状态',
MODIFY COLUMN `github_branch` varchar(50) COMMENT 'GitHub分支',
MODIFY COLUMN `gitee_branch` varchar(50) COMMENT 'Gitee分支',
MODIFY COLUMN `gitlab_branch` varchar(50) COMMENT 'Gitlab分支',
MODIFY COLUMN `code_china_branch` varchar(50) COMMENT 'CodeChina分支',
MODIFY COLUMN `gitlink_branch` varchar(50) COMMENT 'Gitlink分支',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间',
MODIFY COLUMN `commit` varchar(50) COMMENT '最新commit';
ALTER TABLE `pull_request`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `pull_request_id` bigint unsigned COMMENT 'pull request id',
MODIFY COLUMN `title` text COMMENT 'title',
MODIFY COLUMN `project` varchar(20) COMMENT '工程名称',
MODIFY COLUMN `type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') COMMENT '仓库类型',
MODIFY COLUMN `address` varchar(100) COMMENT 'pull request详情页地址',
MODIFY COLUMN `author` varchar(20) COMMENT '作者',
MODIFY COLUMN `email` varchar(50) COMMENT '邮箱',
MODIFY COLUMN `target_branch` varchar(50) COMMENT '目标分支',
MODIFY COLUMN `inline` tinyint(1) COMMENT '是否推送内部',
MODIFY COLUMN `latest_commit` varchar(50) COMMENT '最新的commit',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_log`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `sync_job_id` bigint unsigned COMMENT '同步工程id',
MODIFY COLUMN `log_type` varchar(20) COMMENT '单条日志类型',
MODIFY COLUMN `log` text COMMENT 'title',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间';
ALTER TABLE `sync_log` add INDEX idx_sync_log_job_id(sync_job_id);
ALTER TABLE `github_account`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `domain` varchar(20) COMMENT '域账号',
MODIFY COLUMN `nickname` varchar(20) COMMENT '花名',
MODIFY COLUMN `account` varchar(50) COMMENT 'GitHub账号',
MODIFY COLUMN `email` varchar(50) COMMENT '邮箱',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `gitee_account`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `domain` varchar(20) COMMENT '域账号',
MODIFY COLUMN `nickname` varchar(20) COMMENT '花名',
MODIFY COLUMN `account` varchar(20) COMMENT 'GitHub账号',
MODIFY COLUMN `email` varchar(20) COMMENT '邮箱',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_project`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `name` varchar(50) COMMENT '名称',
MODIFY COLUMN `github` varchar(100) COMMENT 'GitHub地址',
MODIFY COLUMN `gitee` varchar(100) COMMENT 'Gitee地址',
MODIFY COLUMN `gitlab` varchar(100) COMMENT 'Gitlab地址',
MODIFY COLUMN `code_china` varchar(100) COMMENT 'CodeChina地址',
MODIFY COLUMN `gitlink` varchar(100) COMMENT 'Gitlink地址',
MODIFY COLUMN `github_token` varchar(100) COMMENT 'GitHub token',
MODIFY COLUMN `gitee_token` varchar(100) COMMENT 'Gitee token',
MODIFY COLUMN `code_china_token` varchar(100) COMMENT 'CodeChina token',
MODIFY COLUMN `gitlink_token` varchar(100) COMMENT 'Gitlink token',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_job`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `project` varchar(50) COMMENT '工程名称',
MODIFY COLUMN `type` enum('OneWay','TwoWay') COMMENT '同步类型',
MODIFY COLUMN `status` tinyint(1) COMMENT '同步流状态',
MODIFY COLUMN `github_branch` varchar(50) COMMENT 'GitHub分支',
MODIFY COLUMN `gitee_branch` varchar(50) COMMENT 'Gitee分支',
MODIFY COLUMN `gitlab_branch` varchar(50) COMMENT 'Gitlab分支',
MODIFY COLUMN `code_china_branch` varchar(50) COMMENT 'CodeChina分支',
MODIFY COLUMN `gitlink_branch` varchar(50) COMMENT 'Gitlink分支',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间',
MODIFY COLUMN `commit` varchar(50) COMMENT '最新commit';
ALTER TABLE `pull_request`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `pull_request_id` bigint unsigned COMMENT 'pull request id',
MODIFY COLUMN `title` text COMMENT 'title',
MODIFY COLUMN `project` varchar(20) COMMENT '工程名称',
MODIFY COLUMN `type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') COMMENT '仓库类型',
MODIFY COLUMN `address` varchar(100) COMMENT 'pull request详情页地址',
MODIFY COLUMN `author` varchar(20) COMMENT '作者',
MODIFY COLUMN `email` varchar(50) COMMENT '邮箱',
MODIFY COLUMN `target_branch` varchar(50) COMMENT '目标分支',
MODIFY COLUMN `inline` tinyint(1) COMMENT '是否推送内部',
MODIFY COLUMN `latest_commit` varchar(50) COMMENT '最新的commit',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间',
MODIFY COLUMN `update_time` DATETIME COMMENT '更新时间';
ALTER TABLE `sync_log`
MODIFY COLUMN `id` bigint unsigned AUTO_INCREMENT,
MODIFY COLUMN `sync_job_id` bigint unsigned COMMENT '同步工程id',
MODIFY COLUMN `log_type` varchar(20) COMMENT '单条日志类型',
MODIFY COLUMN `log` text COMMENT 'title',
MODIFY COLUMN `create_time` DATETIME COMMENT '创建时间';
ALTER TABLE `sync_log` add INDEX idx_sync_log_job_id(sync_job_id);
ALTER TABLE `sync_job` add INDEX idx_sync_job_project(project);

View File

@ -1,202 +0,0 @@
-- RepoSync 数据库初始化脚本
-- 创建时间: 2024-12-19
-- 设置字符集
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS reposync CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE reposync;
-- ==================== Issue 同步相关表 ====================
-- Issue 信息表
CREATE TABLE IF NOT EXISTS `issue` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`issue_id` bigint unsigned NOT NULL COMMENT 'issue id',
`title` text NOT NULL COMMENT 'issue标题',
`description` longtext COMMENT 'issue描述',
`project` varchar(50) NOT NULL COMMENT '工程名称',
`type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '仓库类型',
`status` enum('open','closed','reopened') NOT NULL DEFAULT 'open' COMMENT 'issue状态',
`priority` enum('low','medium','high','urgent') DEFAULT 'medium' COMMENT '优先级',
`labels` text COMMENT '标签JSON格式存储',
`assignee` varchar(50) DEFAULT NULL COMMENT '负责人',
`author` varchar(50) NOT NULL COMMENT '创建者',
`address` varchar(200) NOT NULL COMMENT 'issue详情页地址',
`external_issue_id` varchar(100) DEFAULT NULL COMMENT '外部平台issue id',
`external_issue_url` varchar(200) DEFAULT NULL COMMENT '外部平台issue url',
`sync_status` enum('pending','syncing','synced','failed') NOT NULL DEFAULT 'pending' COMMENT '同步状态',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_issue_project_type` (`issue_id`, `project`, `type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Issue信息表';
-- Issue 同步任务表
CREATE TABLE IF NOT EXISTS `issue_sync_job` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`project` varchar(50) NOT NULL COMMENT '工程名称',
`source_platform` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '源平台',
`target_platform` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '目标平台',
`sync_type` enum('OneWay','TwoWay') NOT NULL COMMENT '同步类型',
`status` enum('active','inactive','error') NOT NULL DEFAULT 'active' COMMENT '同步状态',
`last_sync_time` DATETIME DEFAULT NULL COMMENT '最后同步时间',
`sync_interval` int DEFAULT 300 COMMENT '同步间隔(秒)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_project_platforms` (`project`, `source_platform`, `target_platform`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Issue同步任务表';
-- Issue 同步日志表
CREATE TABLE IF NOT EXISTS `issue_sync_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`issue_sync_job_id` bigint unsigned NOT NULL COMMENT 'issue同步任务id',
`issue_id` bigint unsigned DEFAULT NULL COMMENT 'issue id',
`log_type` varchar(20) NOT NULL COMMENT '日志类型',
`log` text NOT NULL COMMENT '日志内容',
`sync_direction` enum('source_to_target','target_to_source') NOT NULL COMMENT '同步方向',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX `idx_issue_sync_job_id` (`issue_sync_job_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Issue同步日志表';
-- ==================== 仓库同步相关表 ====================
-- 同步仓库映射表
CREATE TABLE IF NOT EXISTS `sync_repo_mapping` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_repo_address` varchar(255) NOT NULL COMMENT '内部仓库地址',
`external_repo_address` varchar(255) NOT NULL COMMENT '外部仓库地址',
`sync_granularity` enum('all', 'one') NOT NULL COMMENT '同步粒度',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '仓库绑定时间',
PRIMARY KEY (`id`),
UNIQUE KEY (`repo_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步仓库映射表';
-- 同步分支映射表
CREATE TABLE IF NOT EXISTS `sync_branch_mapping`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`repo_id` bigint unsigned NOT NULL COMMENT '仓库ID',
`enable` tinyint(1) NOT NULL COMMENT '同步状态',
`internal_branch_name` varchar(255) NOT NULL COMMENT '内部仓库分支名称',
`external_branch_name` varchar(255) NOT NULL COMMENT '外部仓库分支名称',
`sync_direction` enum('to_outer', 'to_inter') NOT NULL COMMENT '首次同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分支绑定时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步分支映射表';
-- 仓库同步日志表
CREATE TABLE IF NOT EXISTS `repo_sync_log`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`branch_id` bigint unsigned COMMENT '分支id',
`repo_name` varchar(255) NOT NULL COMMENT '仓库名称',
`commit_id` varchar(255) COMMENT 'commit ID',
`log` longtext COMMENT '同步日志',
`sync_direct` enum('to_outer', 'to_inter') NOT NULL COMMENT '同步方向',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步日志';
-- ==================== Issue 同步配置表 ====================
-- 同步配置表
CREATE TABLE IF NOT EXISTS `sync_config` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`source_platform` VARCHAR(20),
`source_owner` VARCHAR(100),
`source_repo` VARCHAR(100),
`source_token` VARCHAR(100),
`target_platform` VARCHAR(20),
`target_owner` VARCHAR(100),
`target_repo` VARCHAR(100),
`target_token` VARCHAR(100),
`sync_type` VARCHAR(20),
`sync_direction` VARCHAR(20),
`enabled` BOOLEAN,
`auto_sync` BOOLEAN,
`sync_interval` INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步配置表';
-- Issue 映射表
CREATE TABLE IF NOT EXISTS `issue_mapping` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`source_platform` VARCHAR(20),
`source_repo` VARCHAR(100),
`source_issue_id` VARCHAR(50),
`target_platform` VARCHAR(20),
`target_repo` VARCHAR(100),
`target_issue_id` VARCHAR(50),
`last_sync_time` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Issue映射表';
-- PR 映射表
CREATE TABLE IF NOT EXISTS `pr_mapping` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`source_platform` VARCHAR(20),
`source_repo` VARCHAR(100),
`source_pr_id` VARCHAR(50),
`target_platform` VARCHAR(20),
`target_repo` VARCHAR(100),
`target_pr_id` VARCHAR(50),
`last_sync_time` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PR映射表';
-- 同步日志表
CREATE TABLE IF NOT EXISTS `sync_log` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`sync_type` VARCHAR(20),
`source` VARCHAR(100),
`target` VARCHAR(100),
`status` VARCHAR(20),
`message` TEXT,
`timestamp` DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步日志表';
-- PR 评论映射表
CREATE TABLE IF NOT EXISTS `pr_comment_mapping` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`source_platform` VARCHAR(20),
`source_repo` VARCHAR(100),
`source_pr_id` VARCHAR(50),
`source_comment_id` VARCHAR(50),
`target_platform` VARCHAR(20),
`target_repo` VARCHAR(100),
`target_pr_id` VARCHAR(50),
`target_comment_id` VARCHAR(50),
`comment_body` TEXT,
`commit_id` VARCHAR(100),
`path` VARCHAR(255),
`position` INT,
`last_sync_time` DATETIME,
INDEX(`source_platform`, `source_repo`, `source_pr_id`),
INDEX(`target_platform`, `target_repo`, `target_pr_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PR评论映射表';
-- ==================== 系统配置表 ====================
-- 系统配置表
CREATE TABLE IF NOT EXISTS `system_config` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`config_key` VARCHAR(100) NOT NULL UNIQUE,
`config_value` TEXT,
`description` VARCHAR(255),
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
-- 插入默认配置
INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES
('encryption_key', 'your_default_encryption_key_here', '系统加密密钥'),
('sync_interval_default', '300', '默认同步间隔(秒)'),
('max_retry_count', '3', '最大重试次数'),
('log_retention_days', '30', '日志保留天数');
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -1,51 +1,104 @@
-- Issue 同步相关表
CREATE TABLE `issue` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`issue_id` bigint unsigned NOT NULL COMMENT 'issue id',
`title` text NOT NULL COMMENT 'issue标题',
`description` longtext COMMENT 'issue描述',
`project` varchar(50) NOT NULL COMMENT '工程名称',
`type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '仓库类型',
`status` enum('open','closed','reopened') NOT NULL DEFAULT 'open' COMMENT 'issue状态',
`priority` enum('low','medium','high','urgent') DEFAULT 'medium' COMMENT '优先级',
`labels` text COMMENT '标签JSON格式存储',
`assignee` varchar(50) DEFAULT NULL COMMENT '负责人',
`author` varchar(50) NOT NULL COMMENT '创建者',
`address` varchar(200) NOT NULL COMMENT 'issue详情页地址',
`external_issue_id` varchar(100) DEFAULT NULL COMMENT '外部平台issue id',
`external_issue_url` varchar(200) DEFAULT NULL COMMENT '外部平台issue url',
`sync_status` enum('pending','syncing','synced','failed') NOT NULL DEFAULT 'pending' COMMENT '同步状态',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_issue_project_type` (`issue_id`, `project`, `type`)
) COMMENT='Issue信息表';
CREATE TABLE `issue_sync_job` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`project` varchar(50) NOT NULL COMMENT '工程名称',
`source_platform` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '源平台',
`target_platform` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '目标平台',
`sync_type` enum('OneWay','TwoWay') NOT NULL COMMENT '同步类型',
`status` enum('active','inactive','error') NOT NULL DEFAULT 'active' COMMENT '同步状态',
`last_sync_time` DATETIME DEFAULT NULL COMMENT '最后同步时间',
`sync_interval` int DEFAULT 300 COMMENT '同步间隔(秒)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_project_platforms` (`project`, `source_platform`, `target_platform`)
) COMMENT='Issue同步任务表';
CREATE TABLE `issue_sync_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`issue_sync_job_id` bigint unsigned NOT NULL COMMENT 'issue同步任务id',
`issue_id` bigint unsigned DEFAULT NULL COMMENT 'issue id',
`log_type` varchar(20) NOT NULL COMMENT '日志类型',
`log` text NOT NULL COMMENT '日志内容',
`sync_direction` enum('source_to_target','target_to_source') NOT NULL COMMENT '同步方向',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
INDEX `idx_issue_sync_job_id` (`issue_sync_job_id`),
INDEX `idx_create_time` (`create_time`)
) COMMENT='Issue同步日志表';
-- Issue相关表结构
-- Issue主表
CREATE TABLE `issue` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`issue_id` varchar(50) NOT NULL COMMENT 'Issue唯一标识',
`project` varchar(50) NOT NULL COMMENT '工程名称',
`type` enum('GitHub','Gitee','Gitlink') NOT NULL COMMENT '仓库类型',
`title` varchar(500) NOT NULL COMMENT 'Issue标题',
`body` text COMMENT 'Issue描述',
`state` varchar(20) NOT NULL DEFAULT 'open' COMMENT 'Issue状态',
`number` varchar(50) COMMENT 'Issue编号',
`html_url` varchar(500) COMMENT 'Issue页面地址',
`api_url` varchar(500) COMMENT 'Issue API地址',
`assignee` varchar(100) COMMENT '负责人',
`labels` text COMMENT '标签(JSON格式)',
`milestone` varchar(100) COMMENT '里程碑',
`priority` int DEFAULT 0 COMMENT '优先级',
`issue_type` varchar(50) COMMENT 'Issue类型',
`security_hole` tinyint(1) DEFAULT FALSE COMMENT '是否为私有issue',
`created_at` datetime COMMENT '创建时间',
`updated_at` datetime COMMENT '更新时间',
`closed_at` datetime COMMENT '关闭时间',
`sync_status` tinyint(1) DEFAULT FALSE COMMENT '同步状态',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_issue_project_type_id` (`issue_id`, `project`, `type`),
KEY `idx_project_type` (`project`, `type`),
KEY `idx_sync_status` (`sync_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Issue信息表';
-- Issue同步映射表
CREATE TABLE `issue_sync_mapping` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`source_issue_id` bigint unsigned NOT NULL COMMENT '源Issue ID',
`target_issue_id` bigint unsigned NOT NULL COMMENT '目标Issue ID',
`source_type` enum('GitHub','Gitee','Gitlink') NOT NULL COMMENT '源仓库类型',
`target_type` enum('GitHub','Gitee','Gitlink') NOT NULL COMMENT '目标仓库类型',
`project` varchar(50) NOT NULL COMMENT '工程名称',
`sync_direction` enum('one_way','two_way') NOT NULL DEFAULT 'one_way' COMMENT '同步方向',
`sync_status` tinyint(1) DEFAULT FALSE COMMENT '同步状态',
`last_sync_time` datetime COMMENT '最后同步时间',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_source_target` (`source_issue_id`, `target_issue_id`),
KEY `idx_project` (`project`),
KEY `idx_sync_status` (`sync_status`),
FOREIGN KEY (`source_issue_id`) REFERENCES `issue`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`target_issue_id`) REFERENCES `issue`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Issue同步映射表';
-- Issue同步配置表
CREATE TABLE `issue_sync_config` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`project` varchar(50) NOT NULL COMMENT '工程名称',
`source_type` enum('GitHub','Gitee','Gitlink') NOT NULL COMMENT '源仓库类型',
`target_type` enum('GitHub','Gitee','Gitlink') NOT NULL COMMENT '目标仓库类型',
`source_repo` varchar(200) NOT NULL COMMENT '源仓库地址',
`target_repo` varchar(200) NOT NULL COMMENT '目标仓库地址',
`sync_direction` enum('one_way','two_way') NOT NULL DEFAULT 'one_way' COMMENT '同步方向',
`enable` tinyint(1) DEFAULT TRUE COMMENT '是否启用',
`auto_sync` tinyint(1) DEFAULT FALSE COMMENT '是否自动同步',
`sync_interval` int DEFAULT 3600 COMMENT '同步间隔(秒)',
`last_sync_time` datetime COMMENT '最后同步时间',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_project_source_target` (`project`, `source_type`, `target_type`),
KEY `idx_enable` (`enable`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Issue同步配置表';
-- Issue同步日志表
CREATE TABLE `issue_sync_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`sync_config_id` bigint unsigned NOT NULL COMMENT '同步配置ID',
`source_issue_id` bigint unsigned COMMENT '源Issue ID',
`target_issue_id` bigint unsigned COMMENT '目标Issue ID',
`operation` varchar(50) NOT NULL COMMENT '操作类型',
`status` varchar(20) NOT NULL COMMENT '操作状态',
`message` text COMMENT '操作消息',
`details` text COMMENT '详细信息',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_sync_config_id` (`sync_config_id`),
KEY `idx_create_time` (`create_time`),
FOREIGN KEY (`sync_config_id`) REFERENCES `issue_sync_config`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`source_issue_id`) REFERENCES `issue`(`id`) ON DELETE SET NULL,
FOREIGN KEY (`target_issue_id`) REFERENCES `issue`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Issue同步日志表';
CREATE TABLE IF NOT EXISTS sync_repo_mapping (
id INT PRIMARY KEY AUTO_INCREMENT,
repo_name VARCHAR(128) UNIQUE NOT NULL COMMENT '仓库名称',
enable BOOLEAN DEFAULT TRUE COMMENT '是否启用同步',
internal_repo_address VARCHAR(255) NOT NULL COMMENT '内部仓库地址',
inter_token VARCHAR(255) COMMENT '内部仓库token',
external_repo_address VARCHAR(255) NOT NULL COMMENT '外部仓库地址',
exter_token VARCHAR(255) COMMENT '外部仓库token',
sync_granularity ENUM('all', 'one') COMMENT '同步类型',
sync_direction ENUM('to_outer', 'to_inter') COMMENT '首次同步方向',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,84 +1,84 @@
CREATE TABLE `github_account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(20) NOT NULL COMMENT '域账号',
`nickname` varchar(20) DEFAULT NULL COMMENT '花名',
`account` varchar(50) DEFAULT NULL COMMENT 'GitHub账号',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `gitee_account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(20) NOT NULL COMMENT '域账号',
`nickname` varchar(20) DEFAULT NULL COMMENT '花名',
`account` varchar(20) DEFAULT NULL COMMENT 'GitHub账号',
`email` varchar(20) DEFAULT NULL COMMENT '邮箱',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_project` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '名称',
`github` varchar(100) DEFAULT NULL COMMENT 'GitHub地址',
`gitlab` varchar(100) DEFAULT NULL COMMENT 'Gitlab地址',
`gitee` varchar(100) DEFAULT NULL COMMENT 'Gitee地址',
`code_china` varchar(100) DEFAULT NULL COMMENT 'CodeChina地址',
`gitlink` varchar(100) DEFAULT NULL COMMENT 'Gitlink地址',
`github_token` varchar(100) DEFAULT NULL COMMENT 'GitHub token',
`gitee_token` varchar(100) DEFAULT NULL COMMENT 'Gitee token',
`code_china_token` varchar(100) DEFAULT NULL COMMENT 'CodeChina token',
`gitlink_token` varchar(100) DEFAULT NULL COMMENT 'Gitlink token',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_job` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`project` varchar(50) NOT NULL COMMENT '工程名称',
`type` enum('OneWay','TwoWay') NOT NULL COMMENT '同步类型',
`status` tinyint(1) NOT NULL DEFAULT FALSE COMMENT '同步流状态',
`github_branch` varchar(50) DEFAULT NULL COMMENT 'GitHub分支',
`gitee_branch` varchar(50) DEFAULT NULL COMMENT 'Gitee分支',
`gitlab_branch` varchar(50) DEFAULT NULL COMMENT 'Gitlab分支',
`code_china_branch` varchar(50) DEFAULT NULL COMMENT 'CodeChina分支',
`gitlink_branch` varchar(50) DEFAULT NULL COMMENT 'Gitlink分支',
`base` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') DEFAULT NULL COMMENT '基础仓库',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`commit` varchar(50) NOT NULL COMMENT '最新commit',
PRIMARY KEY (`id`)
);
CREATE TABLE `pull_request` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`pull_request_id` bigint unsigned NOT NULL COMMENT 'pull request id',
`title` text NOT NULL COMMENT 'title',
`project` varchar(20) NOT NULL COMMENT '工程名称',
`type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '仓库类型',
`address` varchar(100) NOT NULL COMMENT 'pull request详情页地址',
`author` varchar(20) NOT NULL COMMENT '作者',
`email` varchar(50) NOT NULL COMMENT '邮箱',
`target_branch` varchar(50) NOT NULL COMMENT '目标分支',
`inline` tinyint(1) NOT NULL DEFAULT FALSE COMMENT '是否推送内部',
`latest_commit` varchar(50) NOT NULL COMMENT '最新的commit',
-- `code_review_address` varchar(50) COMMENT 'code review地址',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`sync_job_id` bigint unsigned NOT NULL COMMENT '同步工程id',
`commit` varchar(50) DEFAULT NULL COMMENT 'commit',
`pull_request_id` bigint unsigned DEFAULT NULL COMMENT 'pull request id',
`log_type` varchar(20) NOT NULL COMMENT '单条日志类型',
`log` text NOT NULL COMMENT '单条日志',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
CREATE TABLE `github_account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(20) NOT NULL COMMENT '域账号',
`nickname` varchar(20) DEFAULT NULL COMMENT '花名',
`account` varchar(50) DEFAULT NULL COMMENT 'GitHub账号',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `gitee_account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(20) NOT NULL COMMENT '域账号',
`nickname` varchar(20) DEFAULT NULL COMMENT '花名',
`account` varchar(20) DEFAULT NULL COMMENT 'GitHub账号',
`email` varchar(20) DEFAULT NULL COMMENT '邮箱',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_project` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '名称',
`github` varchar(100) DEFAULT NULL COMMENT 'GitHub地址',
`gitlab` varchar(100) DEFAULT NULL COMMENT 'Gitlab地址',
`gitee` varchar(100) DEFAULT NULL COMMENT 'Gitee地址',
`code_china` varchar(100) DEFAULT NULL COMMENT 'CodeChina地址',
`gitlink` varchar(100) DEFAULT NULL COMMENT 'Gitlink地址',
`github_token` varchar(100) DEFAULT NULL COMMENT 'GitHub token',
`gitee_token` varchar(100) DEFAULT NULL COMMENT 'Gitee token',
`code_china_token` varchar(100) DEFAULT NULL COMMENT 'CodeChina token',
`gitlink_token` varchar(100) DEFAULT NULL COMMENT 'Gitlink token',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_job` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`project` varchar(50) NOT NULL COMMENT '工程名称',
`type` enum('OneWay','TwoWay') NOT NULL COMMENT '同步类型',
`status` tinyint(1) NOT NULL DEFAULT FALSE COMMENT '同步流状态',
`github_branch` varchar(50) DEFAULT NULL COMMENT 'GitHub分支',
`gitee_branch` varchar(50) DEFAULT NULL COMMENT 'Gitee分支',
`gitlab_branch` varchar(50) DEFAULT NULL COMMENT 'Gitlab分支',
`code_china_branch` varchar(50) DEFAULT NULL COMMENT 'CodeChina分支',
`gitlink_branch` varchar(50) DEFAULT NULL COMMENT 'Gitlink分支',
`base` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') DEFAULT NULL COMMENT '基础仓库',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`commit` varchar(50) NOT NULL COMMENT '最新commit',
PRIMARY KEY (`id`)
);
CREATE TABLE `pull_request` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`pull_request_id` bigint unsigned NOT NULL COMMENT 'pull request id',
`title` text NOT NULL COMMENT 'title',
`project` varchar(20) NOT NULL COMMENT '工程名称',
`type` enum('GitHub','Gitee','Gitlab','Gitcode','Gitlink') NOT NULL COMMENT '仓库类型',
`address` varchar(100) NOT NULL COMMENT 'pull request详情页地址',
`author` varchar(20) NOT NULL COMMENT '作者',
`email` varchar(50) NOT NULL COMMENT '邮箱',
`target_branch` varchar(50) NOT NULL COMMENT '目标分支',
`inline` tinyint(1) NOT NULL DEFAULT FALSE COMMENT '是否推送内部',
`latest_commit` varchar(50) NOT NULL COMMENT '最新的commit',
-- `code_review_address` varchar(50) COMMENT 'code review地址',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);
CREATE TABLE `sync_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`sync_job_id` bigint unsigned NOT NULL COMMENT '同步工程id',
`commit` varchar(50) DEFAULT NULL COMMENT 'commit',
`pull_request_id` bigint unsigned DEFAULT NULL COMMENT 'pull request id',
`log_type` varchar(20) NOT NULL COMMENT '单条日志类型',
`log` text NOT NULL COMMENT '单条日志',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
);

View File

@ -1,6 +1,6 @@
ALTER TABLE `sync_repo_mapping`
ADD COLUMN `inter_token` VARCHAR(100) COMMENT '内部仓库token',
ADD COLUMN `exter_token` VARCHAR(100) COMMENT '外部仓库token';
ALTER TABLE `sync_branch_mapping`
MODIFY COLUMN `sync_direction` enum('to_outer', 'to_inter') COMMENT '首次同步方向';
ALTER TABLE `sync_repo_mapping`
ADD COLUMN `inter_token` VARCHAR(100) COMMENT '内部仓库token',
ADD COLUMN `exter_token` VARCHAR(100) COMMENT '外部仓库token';
ALTER TABLE `sync_branch_mapping`
MODIFY COLUMN `sync_direction` enum('to_outer', 'to_inter') COMMENT '首次同步方向';

View File

@ -1,3 +1,3 @@
ALTER TABLE `repo_sync_log`
MODIFY COLUMN `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;
ALTER TABLE `repo_sync_log`
MODIFY COLUMN `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;

View File

@ -1,160 +1,160 @@
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from typing import Optional
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.base.code import Code
from src.router import ACCOUNT as account
from src.base.error_code import ErrorTemplate, Errors
from src.api.Controller import APIController as Controller
from src.dto.account import GithubAccount as GithubAccountData
from src.service.account import GithubAccountService, GiteeAccountService
from src.dto.account import CreateAccountItem, UpdateAccountItem
class Account(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@account.get("/github_accounts", response_model=Response[DataList[GithubAccountData]], description='展示GitHub账号信息')
async def list_github_account(
self,
search: Optional[str] = Query(None, description='搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项'),
pageNum: int = Query(1, description="Page number"),
pageSize: int = Query(10, description="Page size")
):
account_service = GithubAccountService()
if search is not None:
search = search.replace(" ", "")
count = await account_service.get_count(search=search)
ans = await account_service.list_github_account(search)
if ans is None:
logger.error("Github accounts fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=ans)
)
@ account.get("/gitee_accounts", response_model=Response[DataList[GithubAccountData]], description='展示Gitee账号信息')
async def list_gitee_account(
self,
search: Optional[str] = Query(False, description='搜索内容'),
orderby: Optional[str] = Query(False, description='排序选项'),
pageNum: int = Query(1, description="Page number"),
pageSize: int = Query(10, description="Page size")
):
account_service = GiteeAccountService()
count = await account_service.get_count()
ans = await account_service.list_gitee_account()
if ans is None:
logger.error("Gitee accounts fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=ans)
)
@ account.post("/github_accounts", response_model=Response, description='增加一条GitHub账号信息')
async def add_github_account(
self,
item: CreateAccountItem = (...)
):
account_service = GithubAccountService()
ans = await account_service.insert_github_account(item)
if ans is None:
logger.error(f"Insert Github accounts {item.domain} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
msg="添加账号成功",
)
@ account.post("/gitee_accounts", response_model=Response, description='增加一条Gitee账号信息')
async def add_gitee_account(
self,
item: CreateAccountItem = (...)
):
account_service = GiteeAccountService()
ans = await account_service.insert_gitee_account(item)
if ans is None:
logger.error(f"Insert Gitee accounts {item.domain} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
msg="添加账号成功",
)
@ account.delete("/github_accounts", response_model=Response, description='删除一条GitHub账号信息')
async def delete_github_account(
self,
id: int = Query(..., description="账号id")
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("删除用户账号")
account_service = GithubAccountService()
ans = await account_service.delete_github_account(id)
if not ans:
logger.error(f"Delete Github accounts failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg='删除成功'
)
@ account.delete("/gitee_accounts", response_model=Response, description='删除一条Gitee账号信息')
async def delete_gitee_account(
self,
id: int = Query(..., description="账号id")
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("删除用户账号")
account_service = GiteeAccountService()
ans = await account_service.delete_gitee_account(id)
if not ans:
logger.error(f"Delete Gitee accounts failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg='删除成功'
)
@ account.put("/github_accounts", response_model=Response, description='更新一条GitHub账号信息')
async def update_github_account(
self,
item: UpdateAccountItem = (...)
):
account_service = GithubAccountService()
ans = await account_service.update_github_account(item)
if not ans:
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg='更新成功'
)
@ account.put("/gitee_accounts", response_model=Response, description='更新一条Gitee账号信息')
async def update_gitee_account(
self,
item: UpdateAccountItem = (...)
):
account_service = GiteeAccountService()
ans = await account_service.update_gitee_account(item)
if not ans:
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg='更新成功'
)
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from typing import Optional
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.base.code import Code
from src.router import ACCOUNT as account
from src.base.error_code import ErrorTemplate, Errors
from src.api.Controller import APIController as Controller
from src.dto.account import GithubAccount as GithubAccountData
from src.service.account import GithubAccountService, GiteeAccountService
from src.dto.account import CreateAccountItem, UpdateAccountItem
class Account(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@account.get("/github_accounts", response_model=Response[DataList[GithubAccountData]], description='展示GitHub账号信息')
async def list_github_account(
self,
search: Optional[str] = Query(None, description='搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项'),
pageNum: int = Query(1, description="Page number"),
pageSize: int = Query(10, description="Page size")
):
account_service = GithubAccountService()
if search is not None:
search = search.replace(" ", "")
count = await account_service.get_count(search=search)
ans = await account_service.list_github_account(search)
if ans is None:
logger.error("Github accounts fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=ans)
)
@ account.get("/gitee_accounts", response_model=Response[DataList[GithubAccountData]], description='展示Gitee账号信息')
async def list_gitee_account(
self,
search: Optional[str] = Query(False, description='搜索内容'),
orderby: Optional[str] = Query(False, description='排序选项'),
pageNum: int = Query(1, description="Page number"),
pageSize: int = Query(10, description="Page size")
):
account_service = GiteeAccountService()
count = await account_service.get_count()
ans = await account_service.list_gitee_account()
if ans is None:
logger.error("Gitee accounts fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=ans)
)
@ account.post("/github_accounts", response_model=Response, description='增加一条GitHub账号信息')
async def add_github_account(
self,
item: CreateAccountItem = (...)
):
account_service = GithubAccountService()
ans = await account_service.insert_github_account(item)
if ans is None:
logger.error(f"Insert Github accounts {item.domain} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
msg="添加账号成功",
)
@ account.post("/gitee_accounts", response_model=Response, description='增加一条Gitee账号信息')
async def add_gitee_account(
self,
item: CreateAccountItem = (...)
):
account_service = GiteeAccountService()
ans = await account_service.insert_gitee_account(item)
if ans is None:
logger.error(f"Insert Gitee accounts {item.domain} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
msg="添加账号成功",
)
@ account.delete("/github_accounts", response_model=Response, description='删除一条GitHub账号信息')
async def delete_github_account(
self,
id: int = Query(..., description="账号id")
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("删除用户账号")
account_service = GithubAccountService()
ans = await account_service.delete_github_account(id)
if not ans:
logger.error(f"Delete Github accounts failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg='删除成功'
)
@ account.delete("/gitee_accounts", response_model=Response, description='删除一条Gitee账号信息')
async def delete_gitee_account(
self,
id: int = Query(..., description="账号id")
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("删除用户账号")
account_service = GiteeAccountService()
ans = await account_service.delete_gitee_account(id)
if not ans:
logger.error(f"Delete Gitee accounts failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg='删除成功'
)
@ account.put("/github_accounts", response_model=Response, description='更新一条GitHub账号信息')
async def update_github_account(
self,
item: UpdateAccountItem = (...)
):
account_service = GithubAccountService()
ans = await account_service.update_github_account(item)
if not ans:
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg='更新成功'
)
@ account.put("/gitee_accounts", response_model=Response, description='更新一条Gitee账号信息')
async def update_gitee_account(
self,
item: UpdateAccountItem = (...)
):
account_service = GiteeAccountService()
ans = await account_service.update_gitee_account(item)
if not ans:
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg='更新成功'
)

View File

@ -1,52 +1,52 @@
from xmlrpc.client import Boolean
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.router import AUTH as auth
from src.base.code import Code
from src.api.Controller import APIController as Controller
from src.dto.auth import AuthItem
from src.base.error_code import ErrorTemplate, Errors
from src.common.repo import RepoType
from src.service.auth import AuthService
class Auth(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@auth.post("/repo/auth", response_model=Response[Boolean], description='认证账号权限')
async def auth(
self,
item: AuthItem = Body(..., description='账号验证属性')
):
if not item:
raise ErrorTemplate.ARGUMENT_LACK("请求体")
if not item.type:
raise ErrorTemplate.ARGUMENT_LACK("账户类型")
if not item.token:
raise ErrorTemplate.ARGUMENT_LACK("账户token")
service = AuthService()
ans = service.auth(item)
if not ans:
return Response(
code=Code.SUCCESS,
data=False,
msg="账户认证失败"
)
return Response(
code=Code.SUCCESS,
data=True,
msg="账户认证成功"
)
from xmlrpc.client import Boolean
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.router import AUTH as auth
from src.base.code import Code
from src.api.Controller import APIController as Controller
from src.dto.auth import AuthItem
from src.base.error_code import ErrorTemplate, Errors
from src.common.repo import RepoType
from src.service.auth import AuthService
class Auth(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@auth.post("/repo/auth", response_model=Response[Boolean], description='认证账号权限')
async def auth(
self,
item: AuthItem = Body(..., description='账号验证属性')
):
if not item:
raise ErrorTemplate.ARGUMENT_LACK("请求体")
if not item.type:
raise ErrorTemplate.ARGUMENT_LACK("账户类型")
if not item.token:
raise ErrorTemplate.ARGUMENT_LACK("账户token")
service = AuthService()
ans = await service.auth(item)
if not ans:
return Response(
code=Code.SUCCESS,
data=False,
msg="账户认证失败"
)
return Response(
code=Code.SUCCESS,
data=True,
msg="账户认证成功"
)

View File

@ -1,32 +1,32 @@
from fastapi import (
Security
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.router import CE_ROBOT as ce_robot
from src.base.code import Code
from src.api.Controller import APIController as Controller
class Answer(BaseModel):
answer: str
class OBRobot(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@ce_robot.get("", response_model=Response[Answer], description='Reposyncer')
async def get_ob_robot(
self
):
answer = Answer(answer="Hello ob-repository-sychronizer")
logger.info(f"Hello ob-repository-sychronizer")
return Response(
code=Code.SUCCESS,
data=answer
)
from fastapi import (
Security
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.router import CE_ROBOT as ce_robot
from src.base.code import Code
from src.api.Controller import APIController as Controller
class Answer(BaseModel):
answer: str
class OBRobot(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@ce_robot.get("", response_model=Response[Answer], description='Reposyncer')
async def get_ob_robot(
self
):
answer = Answer(answer="Hello ob-repository-sychronizer")
logger.info(f"Hello ob-repository-sychronizer")
return Response(
code=Code.SUCCESS,
data=answer
)

388
src/api/Comment.py Normal file
View File

@ -0,0 +1,388 @@
# coding: utf-8
from fastapi import APIRouter, HTTPException, Request
from src.dto.comment import CommentCreateRequest, CommentUpdateRequest, CommentSyncRequest, CommentListRequest
from src.service.comment import CommentService
from typing import Optional, List
from src.utils.issue import IssueUtils
router = APIRouter(prefix="/cerobot/comment", tags=["Comment"])
service = CommentService()
# =================== 评论同步API ===================
@router.post("/sync/oneway")
async def sync_comments_one_way(
source_project: str,
target_project: str,
issue_number: str,
source_type: str,
target_type: str,
comment_ids: Optional[List[str]] = None
):
"""
单向同步issue评论
从source_project的issue评论同步到target_project
"""
try:
result = await service.sync_comments_one_way(
source_project=source_project,
target_project=target_project,
issue_number=issue_number,
source_type=source_type,
target_type=target_type,
comment_ids=comment_ids
)
return {
"code": 0,
"msg": "评论同步完成",
"data": {
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/repo")
async def sync_all_repo_comments(
request: Request,
source_project: str = None,
target_project: str = None,
source_type: str = None,
target_type: str = None
):
"""
同步仓库A中所有issue的评论到仓库B
Args:
source_project: 源仓库URL (: https://github.com/user/repo1)
target_project: 目标仓库URL (: https://gitee.com/user/repo2)
source_type: 源平台类型 (GitHub, Gitee, Gitlink)
target_type: 目标平台类型 (GitHub, Gitee, Gitlink)
Returns:
仓库评论同步结果
"""
try:
# 支持查询参数和请求体两种方式
if request.method == "POST":
# 尝试从查询参数获取
if not source_project:
source_project = request.query_params.get("source_project")
if not target_project:
target_project = request.query_params.get("target_project")
if not source_type:
source_type = request.query_params.get("source_type")
if not target_type:
target_type = request.query_params.get("target_type")
# 参数验证
if not all([source_project, target_project, source_type, target_type]):
missing = []
if not source_project: missing.append("source_project")
if not target_project: missing.append("target_project")
if not source_type: missing.append("source_type")
if not target_type: missing.append("target_type")
raise HTTPException(status_code=400, detail=f"缺少必需参数: {', '.join(missing)}")
print(f"API接收到的参数:")
print(f" source_project: {source_project}")
print(f" target_project: {target_project}")
print(f" source_type: {source_type}")
print(f" target_type: {target_type}")
result = await service.sync_all_repo_comments(
source_project=source_project,
target_project=target_project,
source_type=source_type,
target_type=target_type
)
return {
"code": 0,
"msg": "仓库评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"message": result.message,
"details": result.details
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/repo/bidirectional")
async def sync_all_repo_comments_bidirectional(
request: Request,
source_project: str = None,
target_project: str = None,
source_type: str = None,
target_type: str = None
):
"""
双向同步仓库A和仓库B的所有issue评论
Args:
source_project: 源仓库URL (: https://github.com/user/repo1)
target_project: 目标仓库URL (: https://gitee.com/user/repo2)
source_type: 源平台类型 (GitHub, Gitee, Gitlink)
target_type: 目标平台类型 (GitHub, Gitee, Gitlink)
Returns:
双向仓库评论同步结果
"""
try:
# 支持查询参数和请求体两种方式
if request.method == "POST":
# 尝试从查询参数获取
if not source_project:
source_project = request.query_params.get("source_project")
if not target_project:
target_project = request.query_params.get("target_project")
if not source_type:
source_type = request.query_params.get("source_type")
if not target_type:
target_type = request.query_params.get("target_type")
# 参数验证
if not all([source_project, target_project, source_type, target_type]):
missing = []
if not source_project: missing.append("source_project")
if not target_project: missing.append("target_project")
if not source_type: missing.append("source_type")
if not target_type: missing.append("target_type")
raise HTTPException(status_code=400, detail=f"缺少必需参数: {', '.join(missing)}")
print(f"双向同步API接收到的参数:")
print(f" source_project: {source_project}")
print(f" target_project: {target_project}")
print(f" source_type: {source_type}")
print(f" target_type: {target_type}")
result = await service.sync_all_repo_comments_bidirectional(
source_project=source_project,
target_project=target_project,
source_type=source_type,
target_type=target_type
)
return {
"code": 0,
"msg": "双向仓库评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"message": result.message,
"details": result.details
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== GitHub评论API ===================
@router.get("/github/list")
async def list_github_comments(
project: str,
issue_number: str,
page: int = 1,
per_page: int = 30
):
"""获取GitHub issue评论列表"""
try:
result = await service.get_comments(
project=project,
platform_type="GitHub",
issue_number=issue_number,
page=page,
per_page=per_page
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/github/create")
async def create_github_comment(
project: str,
issue_number: str,
body: str
):
"""创建GitHub issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
github_utils = service._get_platform_utils("GitHub")
result = await github_utils.create_comment(owner, repo, issue_number, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/github/update")
async def update_github_comment(
project: str,
comment_id: str,
body: str
):
"""更新GitHub issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
github_utils = service._get_platform_utils("GitHub")
result = await github_utils.update_comment(owner, repo, comment_id, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/github/delete")
async def delete_github_comment(
project: str,
comment_id: str
):
"""删除GitHub issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
github_utils = service._get_platform_utils("GitHub")
result = await github_utils.delete_comment(owner, repo, comment_id)
return {"code": 0, "msg": "success", "data": {"deleted": result}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== Gitee评论API ===================
@router.get("/gitee/list")
async def list_gitee_comments(
project: str,
issue_number: str,
page: int = 1,
per_page: int = 30
):
"""获取Gitee issue评论列表"""
try:
result = await service.get_comments(
project=project,
platform_type="Gitee",
issue_number=issue_number,
page=page,
per_page=per_page
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/gitee/create")
async def create_gitee_comment(
project: str,
issue_number: str,
body: str
):
"""创建Gitee issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitee_utils = service._get_platform_utils("Gitee")
result = await gitee_utils.create_comment(owner, repo, issue_number, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitee/update")
async def update_gitee_comment(
project: str,
comment_id: str,
body: str
):
"""更新Gitee issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitee_utils = service._get_platform_utils("Gitee")
result = await gitee_utils.update_comment(owner, repo, comment_id, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitee/delete")
async def delete_gitee_comment(
project: str,
comment_id: str
):
"""删除Gitee issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitee_utils = service._get_platform_utils("Gitee")
result = await gitee_utils.delete_comment(owner, repo, comment_id)
return {"code": 0, "msg": "success", "data": {"deleted": result}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== Gitlink评论API ===================
@router.get("/gitlink/list")
async def list_gitlink_comments(
project: str,
issue_number: str,
page: int = 1,
per_page: int = 30
):
"""获取Gitlink issue评论列表"""
try:
result = await service.get_comments(
project=project,
platform_type="Gitlink",
issue_number=issue_number,
page=page,
per_page=per_page
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/gitlink/create")
async def create_gitlink_comment(
project: str,
issue_number: str,
body: str
):
"""创建Gitlink issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitlink_utils = service._get_platform_utils("Gitlink")
result = await gitlink_utils.create_comment(owner, repo, issue_number, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitlink/update")
async def update_gitlink_comment(
project: str,
comment_id: str,
body: str
):
"""更新Gitlink issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitlink_utils = service._get_platform_utils("Gitlink")
result = await gitlink_utils.update_comment(owner, repo, comment_id, body)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitlink/delete")
async def delete_gitlink_comment(
project: str,
comment_id: str
):
"""删除Gitlink issue评论"""
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
gitlink_utils = service._get_platform_utils("Gitlink")
result = await gitlink_utils.delete_comment(owner, repo, comment_id)
return {"code": 0, "msg": "success", "data": {"deleted": result}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,40 +1,40 @@
import json
import base64
from typing import Optional
from fastapi import Security
from src.base.config import TOKEN_KEY
from extras.obfastapi.frame import Controller, User
class APIController(Controller):
def decode_token(self, token: str) -> Optional[dict]:
s = ''
try:
for _s in token:
s += chr((ord(_s) - TOKEN_KEY) % 128)
s = base64.urlsafe_b64decode(s).decode('utf-8')
return json.loads(s)
except:
return None
def get_user(
self,
cookie_key: str = Security(Controller.API_KEY_BUC_COOKIE),
token: Optional[str] = None,
):
if token:
user = self.decode_token(token)
if user:
user = User(**user)
if user.emp_id:
self._user = user
if not self._user:
return super().get_user(cookie_key)
return self._user
def user(self):
user = "robot"
return user
import json
import base64
from typing import Optional
from fastapi import Security
from src.base.config import TOKEN_KEY
from extras.obfastapi.frame import Controller, User
class APIController(Controller):
def decode_token(self, token: str) -> Optional[dict]:
s = ''
try:
for _s in token:
s += chr((ord(_s) - TOKEN_KEY) % 128)
s = base64.urlsafe_b64decode(s).decode('utf-8')
return json.loads(s)
except:
return None
def get_user(
self,
cookie_key: str = Security(Controller.API_KEY_BUC_COOKIE),
token: Optional[str] = None,
):
if token:
user = self.decode_token(token)
if user:
user = User(**user)
if user.emp_id:
self._user = user
if not self._user:
return super().get_user(cookie_key)
return self._user
def user(self):
user = "robot"
return user

60
src/api/Health.py Normal file
View File

@ -0,0 +1,60 @@
from fastapi import Query
from src.base.error_code import ErrorTemplate
from src.utils.logger import logger
from extras.obfastapi.frame import OBResponse as Response
from src.base.code import Code
from src.router import HEALTH as health
from src.api.Controller import APIController as Controller
from src.service.health import HealthService
class Health(Controller):
def get_user(self, cookie_key=None, token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@health.get("/database", response_model=Response, description='检查数据库连接状态')
async def check_database(self):
"""检查数据库连接状态"""
try:
health_service = HealthService()
is_healthy = await health_service.check_database()
if is_healthy:
return Response(
code=Code.SUCCESS,
data={"status": "healthy", "message": "数据库连接正常"},
msg="数据库连接正常"
)
else:
return Response(
code=Code.OPERATION_FAILED,
data={"status": "unhealthy", "message": "数据库连接失败"},
msg="数据库连接失败"
)
except Exception as e:
logger.error(f"Database health check failed: {e}")
return Response(
code=Code.OPERATION_FAILED,
data={"status": "error", "message": str(e)},
msg="数据库健康检查失败"
)
@health.get("/system", response_model=Response, description='检查系统整体状态')
async def check_system(self):
"""检查系统整体状态"""
try:
health_service = HealthService()
system_status = await health_service.check_system()
return Response(
code=Code.SUCCESS,
data=system_status,
msg="系统状态检查完成"
)
except Exception as e:
logger.error(f"System health check failed: {e}")
return Response(
code=Code.OPERATION_FAILED,
data={"status": "error", "message": str(e)},
msg="系统健康检查失败"
)

View File

@ -1,237 +1,160 @@
# coding: utf-8
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query, Body
from pydantic import BaseModel
from datetime import datetime
import subprocess
import sys
import os
# 定义数据模型
class IssueBase(BaseModel):
title: str
description: Optional[str] = None
project: str
type: str
status: str = "open"
priority: str = "medium"
labels: Optional[str] = None
assignee: Optional[str] = None
author: str
address: str
class IssueCreate(IssueBase):
pass
class IssueUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
priority: Optional[str] = None
labels: Optional[str] = None
assignee: Optional[str] = None
class IssueResponse(IssueBase):
id: int
issue_id: int
external_issue_id: Optional[str] = None
external_issue_url: Optional[str] = None
sync_status: str
create_time: datetime
update_time: datetime
class Config:
from_attributes = True
class IssueSyncJobBase(BaseModel):
project: str
source_platform: str
target_platform: str
sync_type: str
status: str = "active"
sync_interval: int = 300
class IssueSyncJobCreate(IssueSyncJobBase):
pass
class IssueSyncJobResponse(IssueSyncJobBase):
id: int
last_sync_time: Optional[datetime] = None
create_time: datetime
update_time: datetime
class Config:
from_attributes = True
# 模拟数据存储(实际项目中应该连接数据库)
issues_db = []
issue_sync_jobs_db = []
issue_id_counter = 1
sync_job_id_counter = 1
# 创建路由器
router = APIRouter()
@router.get("/issues", response_model=List[IssueResponse], tags=["Issues"])
async def get_issues(
project: Optional[str] = Query(None, description="项目名称"),
status: Optional[str] = Query(None, description="Issue状态"),
type: Optional[str] = Query(None, description="平台类型")
):
"""获取Issue列表"""
filtered_issues = issues_db
if project:
filtered_issues = [issue for issue in filtered_issues if issue["project"] == project]
if status:
filtered_issues = [issue for issue in filtered_issues if issue["status"] == status]
if type:
filtered_issues = [issue for issue in filtered_issues if issue["type"] == type]
return filtered_issues
@router.get("/issues/{issue_id}", response_model=IssueResponse, tags=["Issues"])
async def get_issue(issue_id: int):
"""获取单个Issue详情"""
for issue in issues_db:
if issue["issue_id"] == issue_id:
return issue
raise HTTPException(status_code=404, detail="Issue not found")
@router.post("/issues", response_model=IssueResponse, tags=["Issues"])
async def create_issue(issue: IssueCreate):
"""创建新的Issue"""
global issue_id_counter
new_issue = {
"id": len(issues_db) + 1,
"issue_id": issue_id_counter,
"title": issue.title,
"description": issue.description,
"project": issue.project,
"type": issue.type,
"status": issue.status,
"priority": issue.priority,
"labels": issue.labels,
"assignee": issue.assignee,
"author": issue.author,
"address": issue.address,
"external_issue_id": None,
"external_issue_url": None,
"sync_status": "pending",
"create_time": datetime.now(),
"update_time": datetime.now()
}
issues_db.append(new_issue)
issue_id_counter += 1
return new_issue
@router.put("/issues/{issue_id}", response_model=IssueResponse, tags=["Issues"])
async def update_issue(issue_id: int, issue_update: IssueUpdate):
"""更新Issue"""
for issue in issues_db:
if issue["issue_id"] == issue_id:
update_data = issue_update.dict(exclude_unset=True)
for field, value in update_data.items():
issue[field] = value
issue["update_time"] = datetime.now()
return issue
raise HTTPException(status_code=404, detail="Issue not found")
@router.delete("/issues/{issue_id}", tags=["Issues"])
async def delete_issue(issue_id: int):
"""删除Issue"""
for i, issue in enumerate(issues_db):
if issue["issue_id"] == issue_id:
deleted_issue = issues_db.pop(i)
return {"message": f"Issue {issue_id} deleted successfully"}
raise HTTPException(status_code=404, detail="Issue not found")
@router.get("/issue-sync-jobs", response_model=List[IssueSyncJobResponse], tags=["Issue Sync Jobs"])
async def get_issue_sync_jobs(
project: Optional[str] = Query(None, description="项目名称"),
status: Optional[str] = Query(None, description="同步状态")
):
"""获取Issue同步任务列表"""
filtered_jobs = issue_sync_jobs_db
if project:
filtered_jobs = [job for job in filtered_jobs if job["project"] == project]
if status:
filtered_jobs = [job for job in filtered_jobs if job["status"] == status]
return filtered_jobs
@router.post("/issue-sync-jobs", response_model=IssueSyncJobResponse, tags=["Issue Sync Jobs"])
async def create_issue_sync_job(job: IssueSyncJobCreate):
"""创建Issue同步任务"""
global sync_job_id_counter
new_job = {
"id": sync_job_id_counter,
"project": job.project,
"source_platform": job.source_platform,
"target_platform": job.target_platform,
"sync_type": job.sync_type,
"status": job.status,
"last_sync_time": None,
"sync_interval": job.sync_interval,
"create_time": datetime.now(),
"update_time": datetime.now()
}
issue_sync_jobs_db.append(new_job)
sync_job_id_counter += 1
return new_job
@router.post("/issue-sync-jobs/{job_id}/sync", tags=["Issue Sync Jobs"])
async def trigger_issue_sync(job_id: int):
"""触发Issue同步"""
for job in issue_sync_jobs_db:
if job["id"] == job_id:
# 这里应该实现实际的同步逻辑
job["last_sync_time"] = datetime.now()
job["update_time"] = datetime.now()
return {
"message": f"Issue sync triggered for job {job_id}",
"sync_time": job["last_sync_time"]
}
raise HTTPException(status_code=404, detail="Sync job not found")
@router.get("/issue-sync-jobs/{job_id}/logs", tags=["Issue Sync Jobs"])
async def get_issue_sync_logs(job_id: int):
"""获取Issue同步日志"""
# 这里应该从数据库查询实际的日志
return {
"job_id": job_id,
"logs": [
{
"id": 1,
"log_type": "info",
"log": f"Started sync for job {job_id}",
"sync_direction": "source_to_target",
"create_time": datetime.now()
}
]
}
@router.post("/issues/sync", tags=["Issues"])
async def sync_issues():
"""一键同步所有Issue调用issue_sync_runner.py"""
try:
script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'issue_sync', 'sync', 'issue_sync_runner.py')
result = subprocess.run([sys.executable, script_path], capture_output=True, text=True, timeout=600)
return {
"success": result.returncode == 0,
"stdout": result.stdout,
"stderr": result.stderr
}
except Exception as e:
return {"success": False, "error": str(e)}
# coding: utf-8
from fastapi import APIRouter, HTTPException
from src.dto.issue import IssueCreateRequest, IssueUpdateRequest
from src.service.issue import IssueService
from typing import Optional
from src.utils.issue import IssueUtils
router = APIRouter(prefix="/cerobot/issue", tags=["Issue"])
service = IssueService()
@router.post("/gitee/create")
async def create_gitee_issue(req: IssueCreateRequest):
try:
result = await service.create_gitee_issue(req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/gitee/get")
async def get_gitee_issue(project: str, number: str):
try:
result = await service.get_gitee_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitee/update")
async def update_gitee_issue(project: str, number: str, req: IssueUpdateRequest):
try:
result = await service.update_gitee_issue(project, number, req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitee/delete")
async def delete_gitee_issue(project: str, number: str):
try:
result = await service.delete_gitee_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/github/create")
async def create_github_issue(req: IssueCreateRequest):
try:
result = await service.create_github_issue(req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/github/get")
async def get_github_issue(project: str, number: str):
try:
result = await service.get_github_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/github/update")
async def update_github_issue(project: str, number: str, req: IssueUpdateRequest):
try:
result = await service.update_github_issue(project, number, req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/github/delete")
async def delete_github_issue(project: str, number: str):
try:
result = await service.delete_github_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/gitlink/create")
async def create_gitlink_issue(req: IssueCreateRequest):
try:
result = await service.create_gitlink_issue(req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/gitlink/get")
async def get_gitlink_issue(project: str, number: str):
try:
result = await service.get_gitlink_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitlink/update")
async def update_gitlink_issue(project: str, number: str, req: IssueUpdateRequest):
try:
result = await service.update_gitlink_issue(project, number, req)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitlink/delete")
async def delete_gitlink_issue(project: str, number: str):
try:
result = await service.delete_gitlink_issue(project, number)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# TODO: 同步相关API接口骨架
@router.post("/sync/oneway")
async def sync_issue_one_way(source_project: str, target_project: str, source_type: str, target_type: str):
try:
result = await service.sync_issue_one_way(source_project, target_project, source_type, target_type)
return {"code": 0, "msg": "同步完成", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/twoway")
async def sync_issue_two_way(project_a: str, project_b: str, type_a: str, type_b: str):
try:
await service.sync_issue_two_way(project_a, project_b, type_a, type_b)
return {"code": 0, "msg": "双向同步任务已触发"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/all")
async def sync_all_projects_issues():
try:
await service.sync_all_projects_issues()
return {"code": 0, "msg": "全量同步任务已触发"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/gitee/list")
async def list_gitee_issues(project: str, state: str = "open", assignee: Optional[str] = None,
labels: Optional[str] = None, page: int = 1, per_page: int = 30):
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
result = await service.gitee.list_issues(owner, repo, state, assignee, labels, page, per_page)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/github/list")
async def list_github_issues(project: str, state: str = "open", assignee: Optional[str] = None,
labels: Optional[str] = None, page: int = 1, per_page: int = 30):
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
result = await service.github.list_issues(owner, repo, state, assignee, labels, page, per_page)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/gitlink/list")
async def list_gitlink_issues(project: str, state: str = "open", assignee: Optional[str] = None,
labels: Optional[str] = None, page: int = 1, per_page: int = 30):
try:
repo_type, owner, repo = IssueUtils.parse_repo_url(project)
result = await service.gitlink.list_issues(owner, repo, state, assignee, labels, page, per_page)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,27 +1,27 @@
from fastapi import (
Security
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import OBResponse as Response
from src.router import LOG as log
from src.base.code import Code
from src.api.Controller import APIController as Controller
from src.service.log import LogService
class Log(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@log.delete("/log/delete", response_model=Response, description='删除日志')
async def delete_sync_logs(
self
):
service = LogService()
await service.delete_logs()
return Response(
code=Code.SUCCESS
)
from fastapi import (
Security
)
from pydantic.main import BaseModel
from src.utils.logger import logger
from extras.obfastapi.frame import OBResponse as Response
from src.router import LOG as log
from src.base.code import Code
from src.api.Controller import APIController as Controller
from src.service.log import LogService
class Log(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@log.delete("/log/delete", response_model=Response, description='删除日志')
async def delete_sync_logs(
self
):
service = LogService()
await service.delete_logs()
return Response(
code=Code.SUCCESS
)

585
src/api/PRComment.py Normal file
View File

@ -0,0 +1,585 @@
# coding: utf-8
from fastapi import APIRouter, HTTPException, Request
from src.dto.pr_comment import (
PRCommentCreateRequest, PRCommentUpdateRequest,
PRCommentSyncRequest, PRCommentListRequest, PRCommentRepoSyncRequest,
PRCommentSyncByTitleRequest, PRCommentSyncByTitleBidirectionalRequest
)
from src.service.pr_comment import PRCommentService
from src.utils.pr_comment import PRCommentUtils
from typing import Optional, List
router = APIRouter(prefix="/cerobot/pr-comment", tags=["PR Comment"])
service = PRCommentService()
utils = PRCommentUtils()
# =================== PR普通评论同步API ===================
@router.post("/sync/oneway")
async def sync_pr_comments_one_way(
request: PRCommentSyncRequest
):
"""
单向同步PR普通评论
从source_project的PR普通评论同步到target_project
"""
try:
result = await service.sync_pr_comments_one_way(
source_project=request.source_project,
target_project=request.target_project,
pull_number=request.pull_number,
source_type=request.source_type,
target_type=request.target_type,
comment_ids=request.comment_ids,
target_pull_number=None
)
return {
"code": 0,
"msg": "PR普通评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details,
"sync_summary": result.sync_summary
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/bidirectional")
async def sync_pr_comments_bidirectional(
request: PRCommentSyncRequest
):
"""
双向同步PR普通评论
同步两个仓库的PR普通评论
"""
try:
result = await service.sync_pr_comments_bidirectional(
source_project=request.source_project,
target_project=request.target_project,
pull_number=request.pull_number,
source_type=request.source_type,
target_type=request.target_type
)
return {
"code": 0,
"msg": "PR普通评论双向同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details,
"sync_summary": result.sync_summary
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/by-title")
async def sync_pr_comments_by_title(
request: PRCommentSyncByTitleRequest
):
"""
根据PR标题进行单向同步推荐方法
通过PR标题匹配两个仓库中的对应PR然后同步评论
"""
try:
result = await service.sync_pr_comments_by_title(
source_project=request.source_project,
target_project=request.target_project,
pr_title=request.pr_title,
source_type=request.source_type,
target_type=request.target_type,
comment_ids=request.comment_ids
)
return {
"code": 0,
"msg": "基于PR标题的评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details,
"sync_summary": result.sync_summary
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/by-title/bidirectional")
async def sync_pr_comments_by_title_bidirectional(
request: PRCommentSyncByTitleBidirectionalRequest
):
"""
根据PR标题进行双向同步推荐方法
通过PR标题匹配两个仓库中的对应PR然后双向同步评论
"""
try:
result = await service.sync_pr_comments_by_title_bidirectional(
source_project=request.source_project,
target_project=request.target_project,
pr_title=request.pr_title,
source_type=request.source_type,
target_type=request.target_type
)
return {
"code": 0,
"msg": "基于PR标题的双向评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details,
"sync_summary": result.sync_summary
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/matching-prs")
async def get_matching_prs(
source_project: str,
target_project: str,
source_type: str,
target_type: str
):
"""
获取两个仓库中所有匹配的PR列表
通过PR标题匹配显示两个仓库中相同标题的PR对应关系
"""
try:
# 参数验证
if not all([source_project, target_project, source_type, target_type]):
missing = []
if not source_project: missing.append("source_project")
if not target_project: missing.append("target_project")
if not source_type: missing.append("source_type")
if not target_type: missing.append("target_type")
raise HTTPException(status_code=400, detail=f"缺少必需参数: {', '.join(missing)}")
matched_prs = await service.get_matching_prs(
source_project=source_project,
target_project=target_project,
source_type=source_type,
target_type=target_type
)
return {
"code": 0,
"msg": "获取匹配PR列表成功",
"data": {
"total_matched": len(matched_prs),
"matched_prs": matched_prs
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/repo")
async def sync_all_repo_pr_comments(
request: Request,
source_project: str = None,
target_project: str = None,
source_type: str = None,
target_type: str = None,
pull_filter: Optional[List[str]] = None
):
"""
同步仓库所有PR的普通评论
Args:
source_project: 源仓库URL (: https://github.com/user/repo1)
target_project: 目标仓库URL (: https://gitee.com/user/repo2)
source_type: 源平台类型 (GitHub, Gitee, Gitlink)
target_type: 目标平台类型 (GitHub, Gitee, Gitlink)
pull_filter: 可选的PR编号过滤列表为空则同步所有PR的普通评论
Returns:
仓库PR普通评论同步结果
"""
try:
# 支持查询参数和请求体两种方式
if request.method == "POST":
# 尝试从查询参数获取
if not source_project:
source_project = request.query_params.get("source_project")
if not target_project:
target_project = request.query_params.get("target_project")
if not source_type:
source_type = request.query_params.get("source_type")
if not target_type:
target_type = request.query_params.get("target_type")
if not pull_filter:
pull_filter_str = request.query_params.get("pull_filter")
if pull_filter_str:
# 处理逗号分隔的字符串
pull_filter = [x.strip() for x in pull_filter_str.split(",") if x.strip()]
# 参数验证
if not all([source_project, target_project, source_type, target_type]):
missing = []
if not source_project: missing.append("source_project")
if not target_project: missing.append("target_project")
if not source_type: missing.append("source_type")
if not target_type: missing.append("target_type")
raise HTTPException(status_code=400, detail=f"缺少必需参数: {', '.join(missing)}")
print(f"API接收到的参数:")
print(f" source_project: {source_project}")
print(f" target_project: {target_project}")
print(f" source_type: {source_type}")
print(f" target_type: {target_type}")
print(f" pull_filter: {pull_filter}")
result = await service.sync_all_repo_pr_comments(
source_project=source_project,
target_project=target_project,
source_type=source_type,
target_type=target_type,
pull_filter=pull_filter
)
return {
"code": 0,
"msg": "仓库PR普通评论同步完成",
"data": {
"success": result.success,
"created_count": result.created_count,
"updated_count": result.updated_count,
"deleted_count": result.deleted_count,
"total_count": result.total_count,
"failed_count": result.failed_count,
"details": result.details,
"sync_summary": result.sync_summary
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== GitHub PR普通评论API ===================
@router.get("/github/list")
async def list_github_pr_comments(
project: str,
pull_number: str,
page: int = 1,
per_page: int = 30,
since: Optional[str] = None
):
"""获取GitHub PR普通评论列表"""
try:
result = await service.get_pr_comments(
project=project,
platform_type="GitHub",
pull_number=pull_number,
page=page,
per_page=per_page,
since=since
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/github/create")
async def create_github_pr_comment(
project: str,
pull_number: str,
body: str
):
"""创建GitHub PR普通评论"""
try:
result = await service.create_pr_comment(
project=project,
platform_type="GitHub",
pull_number=pull_number,
body=body
)
return {"code": 0, "msg": "GitHub PR普通评论创建成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/github/update")
async def update_github_pr_comment(
project: str,
comment_id: str,
body: str
):
"""更新GitHub PR普通评论"""
try:
result = await service.update_pr_comment(
project=project,
platform_type="GitHub",
comment_id=comment_id,
body=body
)
return {"code": 0, "msg": "GitHub PR普通评论更新成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/github/delete")
async def delete_github_pr_comment(
project: str,
comment_id: str
):
"""删除GitHub PR普通评论"""
try:
result = await service.delete_pr_comment(
project=project,
platform_type="GitHub",
comment_id=comment_id
)
return {"code": 0, "msg": "GitHub PR普通评论删除成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== Gitee PR普通评论API ===================
@router.get("/gitee/list")
async def list_gitee_pr_comments(
project: str,
pull_number: str,
page: int = 1,
per_page: int = 30,
since: Optional[str] = None
):
"""获取Gitee PR普通评论列表"""
try:
result = await service.get_pr_comments(
project=project,
platform_type="Gitee",
pull_number=pull_number,
page=page,
per_page=per_page,
since=since
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/gitee/create")
async def create_gitee_pr_comment(
project: str,
pull_number: str,
body: str
):
"""创建Gitee PR普通评论"""
try:
result = await service.create_pr_comment(
project=project,
platform_type="Gitee",
pull_number=pull_number,
body=body
)
return {"code": 0, "msg": "Gitee PR普通评论创建成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitee/update")
async def update_gitee_pr_comment(
project: str,
comment_id: str,
body: str
):
"""更新Gitee PR普通评论"""
try:
result = await service.update_pr_comment(
project=project,
platform_type="Gitee",
comment_id=comment_id,
body=body
)
return {"code": 0, "msg": "Gitee PR普通评论更新成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitee/delete")
async def delete_gitee_pr_comment(
project: str,
comment_id: str
):
"""删除Gitee PR普通评论"""
try:
result = await service.delete_pr_comment(
project=project,
platform_type="Gitee",
comment_id=comment_id
)
return {"code": 0, "msg": "Gitee PR普通评论删除成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== GitLink PR普通评论API ===================
@router.get("/gitlink/list")
async def list_gitlink_pr_comments(
project: str,
pull_number: str,
page: int = 1,
per_page: int = 30,
since: Optional[str] = None
):
"""获取GitLink PR普通评论列表"""
try:
result = await service.get_pr_comments(
project=project,
platform_type="GitLink",
pull_number=pull_number,
page=page,
per_page=per_page,
since=since
)
return {"code": 0, "msg": "success", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/gitlink/create")
async def create_gitlink_pr_comment(
project: str,
pull_number: str,
body: str
):
"""创建GitLink PR普通评论"""
try:
result = await service.create_pr_comment(
project=project,
platform_type="GitLink",
pull_number=pull_number,
body=body
)
return {"code": 0, "msg": "GitLink PR普通评论创建成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/gitlink/update")
async def update_gitlink_pr_comment(
project: str,
comment_id: str,
body: str
):
"""更新GitLink PR普通评论"""
try:
result = await service.update_pr_comment(
project=project,
platform_type="GitLink",
comment_id=comment_id,
body=body
)
return {"code": 0, "msg": "GitLink PR普通评论更新成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/gitlink/delete")
async def delete_gitlink_pr_comment(
project: str,
comment_id: str
):
"""删除GitLink PR普通评论"""
try:
result = await service.delete_pr_comment(
project=project,
platform_type="GitLink",
comment_id=comment_id
)
return {"code": 0, "msg": "GitLink PR普通评论删除成功", "data": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =================== 批量操作API ===================
@router.post("/batch/create")
async def batch_create_pr_comments(
project: str,
platform_type: str,
pull_number: str,
comments: List[dict]
):
"""批量创建PR普通评论"""
try:
results = []
for comment_data in comments:
body = comment_data.get("body", "")
if not body:
continue
result = await service.create_pr_comment(
project=project,
platform_type=platform_type,
pull_number=pull_number,
body=body
)
results.append(result)
return {"code": 0, "msg": f"批量创建{len(results)}条PR普通评论成功", "data": results}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/stats")
async def get_pr_comment_stats(
project: str,
platform_type: str,
pull_number: str
):
"""获取PR普通评论统计信息"""
try:
comments = await service.get_pr_comments(
project=project,
platform_type=platform_type,
pull_number=pull_number,
page=1,
per_page=1000 # 获取足够多的评论用于统计
)
# 计算统计信息
total_comments = len(comments)
authors = set()
for comment in comments:
if comment.get('author'):
authors.add(comment['author'])
stats = {
"total_comments": total_comments,
"unique_authors": len(authors),
"authors_list": list(authors)
}
return {"code": 0, "msg": "success", "data": stats}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/export")
async def export_pr_comments(
project: str,
platform_type: str,
pull_number: str,
format: str = "json"
):
"""导出PR普通评论数据"""
try:
comments = await service.get_pr_comments(
project=project,
platform_type=platform_type,
pull_number=pull_number,
page=1,
per_page=1000 # 获取足够多的评论用于导出
)
if format.lower() == "json":
return {"code": 0, "msg": "success", "data": comments}
else:
raise HTTPException(status_code=400, detail="仅支持JSON格式导出")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,363 +0,0 @@
"""
插件管理API接口
提供插件的注册执行配置和管理功能
"""
from fastapi import (
Body,
Path,
Depends,
Query,
Security,
HTTPException
)
from typing import Dict, List, Any, Optional
from pydantic import BaseModel
from starlette.requests import Request
from src.utils import base
from src.utils.sync_log import sync_log, LogType, api_log
from src.api.Controller import APIController as Controller
from src.router import PLUGIN as router
from src.plugins.plugin_manager import plugin_manager, PluginConfig, BasePlugin
from src.base.status_code import Status, SYNCResponse, SYNCException
class PluginInfo(BaseModel):
"""插件信息"""
name: str
version: str
description: str
enabled: bool
supported_languages: List[str]
class PluginExecutionRequest(BaseModel):
"""插件执行请求"""
plugin_name: str
context: Dict[str, Any]
class PluginExecutionResult(BaseModel):
"""插件执行结果"""
success: bool
result: Dict[str, Any]
execution_time: float
error: Optional[str] = None
class QualityAnalysisRequest(BaseModel):
"""代码质量分析请求"""
repo_path: str
languages: Optional[List[str]] = None
include_patterns: Optional[List[str]] = None
exclude_patterns: Optional[List[str]] = None
class QualityAnalysisResult(BaseModel):
"""代码质量分析结果"""
total_files: int
total_issues: int
quality_score: float
summary: str
issues_by_severity: Dict[str, int]
issues_by_category: Dict[str, int]
fix_suggestions: Dict[str, Any]
report_file: str
class PluginManagement(Controller):
"""插件管理控制器"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def user(self):
"""获取当前用户信息"""
return super().user()
@router.get("/list", response_model=SYNCResponse, description='获取所有插件列表')
async def get_plugins(
self,
request: Request,
user: str = Depends(user)
):
"""获取所有插件列表"""
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path}", user)
try:
plugins = plugin_manager.get_all_plugins()
plugin_list = []
for name, plugin in plugins.items():
plugin_info = PluginInfo(
name=plugin.name,
version=plugin.version,
description=plugin.description,
enabled=plugin.enabled,
supported_languages=plugin.get_supported_languages()
)
plugin_list.append(plugin_info.dict())
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data={"plugins": plugin_list, "total": len(plugin_list)}
)
except Exception as e:
logger.error(f"Failed to get plugins: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"获取插件列表失败: {str(e)}"
)
@router.get("/{plugin_name}/info", response_model=SYNCResponse, description='获取指定插件信息')
async def get_plugin_info(
self,
request: Request,
user: str = Depends(user),
plugin_name: str = Path(..., description="插件名称")
):
"""获取指定插件信息"""
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path}", user)
try:
plugin = plugin_manager.get_plugin(plugin_name)
if not plugin:
return SYNCResponse(
code_status=Status.NOT_FOUND.code,
msg=f"插件 {plugin_name} 不存在"
)
plugin_info = PluginInfo(
name=plugin.name,
version=plugin.version,
description=plugin.description,
enabled=plugin.enabled,
supported_languages=plugin.get_supported_languages()
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data=plugin_info.dict()
)
except Exception as e:
logger.error(f"Failed to get plugin info: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"获取插件信息失败: {str(e)}"
)
@router.post("/execute", response_model=SYNCResponse, description='执行指定插件')
async def execute_plugin(
self,
request: Request,
user: str = Depends(user),
execution_request: PluginExecutionRequest = Body(..., description="插件执行请求")
):
"""执行指定插件"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
result = await plugin_manager.execute_plugin(
execution_request.plugin_name,
execution_request.context
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data=result
)
except Exception as e:
logger.error(f"Failed to execute plugin: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"执行插件失败: {str(e)}"
)
@router.post("/quality/analyze", response_model=SYNCResponse, description='执行代码质量分析')
async def analyze_code_quality(
self,
request: Request,
user: str = Depends(user),
analysis_request: QualityAnalysisRequest = Body(..., description="代码质量分析请求")
):
"""执行代码质量分析"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
# 构建执行上下文
context = {
"repo_path": analysis_request.repo_path,
"languages": analysis_request.languages,
"include_patterns": analysis_request.include_patterns,
"exclude_patterns": analysis_request.exclude_patterns
}
# 执行代码质量检测插件
result = await plugin_manager.execute_plugin("CodeQualityGuard", context)
if not result.get("success"):
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"代码质量分析失败: {result.get('error', '未知错误')}"
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data=result
)
except Exception as e:
logger.error(f"Failed to analyze code quality: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"代码质量分析失败: {str(e)}"
)
@router.post("/quality/analyze-by-language", response_model=SYNCResponse, description='根据语言执行代码质量分析')
async def analyze_code_quality_by_language(
self,
request: Request,
user: str = Depends(user),
language: str = Query(..., description="编程语言"),
repo_path: str = Query(..., description="仓库路径")
):
"""根据语言执行代码质量分析"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
context = {"repo_path": repo_path}
result = await plugin_manager.execute_plugins_by_language(language, context)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data=result
)
except Exception as e:
logger.error(f"Failed to analyze code quality by language: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"代码质量分析失败: {str(e)}"
)
@router.get("/history", response_model=SYNCResponse, description='获取插件执行历史')
async def get_execution_history(
self,
request: Request,
user: str = Depends(user),
limit: int = Query(100, description="返回记录数量限制")
):
"""获取插件执行历史"""
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path}", user)
try:
history = plugin_manager.get_execution_history(limit)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data={"history": history, "total": len(history)}
)
except Exception as e:
logger.error(f"Failed to get execution history: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"获取执行历史失败: {str(e)}"
)
@router.post("/export-report", response_model=SYNCResponse, description='导出插件执行报告')
async def export_execution_report(
self,
request: Request,
user: str = Depends(user),
output_file: str = Query(..., description="输出文件路径")
):
"""导出插件执行报告"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
success = plugin_manager.export_execution_report(output_file)
if success:
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg,
data={"output_file": output_file}
)
else:
return SYNCResponse(
code_status=Status.FAILED.code,
msg="导出执行报告失败"
)
except Exception as e:
logger.error(f"Failed to export execution report: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"导出执行报告失败: {str(e)}"
)
@router.post("/{plugin_name}/enable", response_model=SYNCResponse, description='启用插件')
async def enable_plugin(
self,
request: Request,
user: str = Depends(user),
plugin_name: str = Path(..., description="插件名称")
):
"""启用插件"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
plugin = plugin_manager.get_plugin(plugin_name)
if not plugin:
return SYNCResponse(
code_status=Status.NOT_FOUND.code,
msg=f"插件 {plugin_name} 不存在"
)
plugin.enabled = True
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=f"插件 {plugin_name} 已启用"
)
except Exception as e:
logger.error(f"Failed to enable plugin: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"启用插件失败: {str(e)}"
)
@router.post("/{plugin_name}/disable", response_model=SYNCResponse, description='禁用插件')
async def disable_plugin(
self,
request: Request,
user: str = Depends(user),
plugin_name: str = Path(..., description="插件名称")
):
"""禁用插件"""
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path}", user)
try:
plugin = plugin_manager.get_plugin(plugin_name)
if not plugin:
return SYNCResponse(
code_status=Status.NOT_FOUND.code,
msg=f"插件 {plugin_name} 不存在"
)
plugin.enabled = False
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=f"插件 {plugin_name} 已禁用"
)
except Exception as e:
logger.error(f"Failed to disable plugin: {str(e)}")
return SYNCResponse(
code_status=Status.FAILED.code,
msg=f"禁用插件失败: {str(e)}"
)

View File

@ -1,181 +1,209 @@
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from typing import Optional
from starlette.exceptions import HTTPException
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.base.code import Code
from src.base.error_code import ErrorTemplate, Errors
from src.router import PULL_REQUEST as pull_request
from src.api.Controller import APIController as Controller
from src.dto.pull_request import PullRequest as PullRequestData
from src.service.pull_request import PullRequestService
from src.service.sync import ProjectService
from src.utils import github
class PullRequest(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@pull_request.get("/projects/{name}/pullrequests", response_model=Response[DataList[PullRequestData]], description='列出pull request')
async def list_pull_request(
self,
name: str = Query(..., description='工程名字'),
search: Optional[str] = Query(None, description='搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项')
):
await self._check_project(name)
pull_request_service = PullRequestService()
count = await pull_request_service.count_pull_request(name)
answer = await pull_request_service.fetch_pull_request(name)
if not answer:
logger.info(f"The project {name} has no pull request")
answer = []
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer)
)
@pull_request.get("/projects/{name}/pullrequests/sync", response_model=Response, description='列出pull request')
async def sync_pull_request(
self,
name: str = Query(..., description='工程名字')
):
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
await pull_request_service.sync_pull_request(name, organization, repo)
else:
logger.error(f"The pull rquest of project {name} sync failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送同意请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/approve", response_model=Response, description='同意一个pull request')
async def approve_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.approve_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} approve failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送同意请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/merge", response_model=Response, description='合并一个pull request')
async def merge_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.merge_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} merge failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送合并请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/close", response_model=Response, description='关闭一个pull request')
async def close_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.close_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} close failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送关闭请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/press", response_model=Response, description='催促一个pull request')
async def press_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
# await self._check_project(name)
# service = PullRequestService()
# resp = await service.press_pull_request()
# if not resp:
# code = Code.INVALID_PARAMS
# msg = "发送催促请求失败"
# else:
# code = Code.SUCCESS
# msg = "发送催促请求成功"
return Response(
code=Code.SUCCESS,
msg="第二期功能,敬请期待"
)
async def _check_project(self, name: str):
project_service = ProjectService()
resp = await project_service.search_project(name=name)
if len(resp) == 0:
logger.error(
f"The project {name} is not exist")
raise Errors.QUERY_FAILD
return resp
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from typing import Optional
from starlette.exceptions import HTTPException
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from src.base.code import Code
from src.base.error_code import ErrorTemplate, Errors
from src.router import PULL_REQUEST as pull_request
from src.api.Controller import APIController as Controller
from src.dto.pull_request import PullRequest as PullRequestData
from src.dto.sync import PRSyncRequest, PRSyncResult
from src.service.pull_request import PullRequestService
from src.service.pr_sync import PRSyncService
from src.service.sync import ProjectService
from src.utils import github
class PullRequest(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@pull_request.get("/projects/{name}/pullrequests", response_model=Response[DataList[PullRequestData]], description='列出pull request')
async def list_pull_request(
self,
name: str = Query(..., description='工程名字'),
search: Optional[str] = Query(None, description='搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项')
):
await self._check_project(name)
pull_request_service = PullRequestService()
count = await pull_request_service.count_pull_request(name)
answer = await pull_request_service.fetch_pull_request(name)
if not answer:
logger.info(f"The project {name} has no pull request")
answer = []
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer)
)
@pull_request.get("/projects/{name}/pullrequests/sync", response_model=Response, description='列出pull request')
async def sync_pull_request(
self,
name: str = Query(..., description='工程名字')
):
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
await pull_request_service.sync_pull_request(name, organization, repo)
else:
logger.error(f"The pull rquest of project {name} sync failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送同意请求成功"
)
@pull_request.post("/pullrequests/sync", response_model=Response[PRSyncResult], description='同步PR请求')
async def sync_pull_requests(
self,
request: PRSyncRequest = Body(..., description='PR同步请求')
):
"""同步PR请求 - 支持单向和双向同步"""
try:
pr_sync_service = PRSyncService()
if request.sync_direction == "one_way":
result = await pr_sync_service.sync_pull_requests_one_way(request)
elif request.sync_direction == "bidirectional":
result = await pr_sync_service.sync_pull_requests_bidirectional(request)
else:
raise ValueError(f"不支持的同步方向: {request.sync_direction}")
return Response(
code=Code.SUCCESS if result.success else Code.OPERATION_FAILED,
msg=result.message,
data=result
)
except Exception as e:
logger.error(f"PR同步失败: {str(e)}")
raise Errors.QUERY_FAILD
@pull_request.get("/projects/{name}/pullrequests/{id}/approve", response_model=Response, description='同意一个pull request')
async def approve_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.approve_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} approve failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送同意请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/merge", response_model=Response, description='合并一个pull request')
async def merge_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.merge_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} merge failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送合并请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/close", response_model=Response, description='关闭一个pull request')
async def close_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
if not name or not id:
raise ErrorTemplate.ARGUMENT_LACK()
resp = await self._check_project(name)
organization, repo = github.transfer_github_to_name(
resp[0].github_address)
if organization and repo:
pull_request_service = PullRequestService()
resp = await pull_request_service.close_pull_request(organization, repo, id)
if not resp:
logger.error(
f"The pull rquest #{id} of project {name} close failed")
raise Errors.QUERY_FAILD
else:
logger.error(
f"Get the project {name} organization and repo failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
msg="发送关闭请求成功"
)
@pull_request.get("/projects/{name}/pullrequests/{id}/press", response_model=Response, description='催促一个pull request')
async def press_pull_request(
self,
name: str = Query(..., description='同步工程名称'),
id: int = Query(..., description='pull request id')
):
# await self._check_project(name)
# service = PullRequestService()
# resp = await service.press_pull_request()
# if not resp:
# code = Code.INVALID_PARAMS
# msg = "发送催促请求失败"
# else:
# code = Code.SUCCESS
# msg = "发送催促请求成功"
return Response(
code=Code.SUCCESS,
msg="第二期功能,敬请期待"
)
async def _check_project(self, name: str):
project_service = ProjectService()
resp = await project_service.search_project(name=name)
if len(resp) == 0:
logger.error(
f"The project {name} is not exist")
raise Errors.QUERY_FAILD
return resp

View File

@ -1,328 +1,341 @@
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from typing import Optional
import asyncio
from sqlalchemy.sql.expression import false
from src.base.error_code import ErrorTemplate, Errors
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from extras.obfastapi.frame import OBHTTPException as HTTPException
from src.base.code import Code
from src.router import PROJECT as project
from src.router import JOB as job
from src.api.Controller import APIController as Controller
from src.dto.sync import Project as ProjectData
from src.dto.sync import Job as JobData
from src.dto.log import Log as LogData
from src.dto.sync import SyncType, CreateProjectItem, CreateJobItem
from src.service.sync import ProjectService, JobService
from src.service.pull_request import PullRequestService
from src.service.log import LogService
from src.utils import github, gitlab, gitee, gitcode, gitlink
class Project(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@project.get("", response_model=Response[DataList[ProjectData]], description='通过工程名获取一个同步工程')
async def get_project(
self,
search: Optional[str] = Query(None, description='同步工程搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(10, description="Page size")
):
# search
service = ProjectService()
if search is None:
count = await service.get_count()
answer = await service.list_projects(page=pageNum, size=pageSize)
else:
count = await service.get_count_by_search(search.replace(" ", ""))
answer = await service.search_project(name=search.replace(" ", ""))
if answer is None:
logger.error(f"The project list fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer)
)
@ project.post("", response_model=Response[ProjectData], description='创建一个同步工程')
async def create_project(
self,
item: CreateProjectItem = Body(..., description='同步工程属性')
):
# pre check
if not item:
raise ErrorTemplate.ARGUMENT_LACK("请求体")
if not item.name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if item.github_address:
if not github.check_github_address(item.github_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("GitHub仓库")
if item.gitlab_address:
if not gitlab.check_gitlab_address(item.gitlab_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitlab/Antcode仓库")
if item.gitee_address:
if not gitee.check_gitee_address(item.gitee_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitee仓库")
if item.code_china_address:
if not gitcode.check_gitcode_address(item.code_china_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("CodeChina仓库")
# if item.gitlink_address:
# if not gitlink.check_gitlink_address(item.gitlink_address):
# raise ErrorTemplate.ARGUMENT_ERROR("Gitlink仓库")
service = ProjectService()
resp = await service.insert_project(item)
if not resp:
logger.error(f"The project insert failed")
raise Errors.INSERT_FAILD
organization, repo = github.transfer_github_to_name(
item.github_address)
if organization and repo:
pull_request_service = PullRequestService()
task = asyncio.create_task(
pull_request_service.sync_pull_request(item.name, organization, repo))
return Response(
code=Code.SUCCESS,
data=resp,
msg="创建同步工程成功"
)
@ project.delete("", response_model=Response, description='通过id删除一个同步工程')
async def delete_project(
self,
id: int = Query(..., description='同步工程id')
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("id")
# if delete the project, the front page double check firstly
project_service = ProjectService()
project = await project_service.search_project(id=id)
name = project[0].name
# delete pull request
pull_request_service = PullRequestService()
resp = await pull_request_service.fetch_pull_request(name)
if resp:
if len(resp) > 0:
for pr in resp:
await pull_request_service.delete_pull_request(pr.id)
# delete sync job
job_service = JobService()
resp = await job_service.list_jobs(project=name)
if not resp:
pass
else:
for item in resp:
await job_service.delete_job(item.id)
# delete sync project
resp = await project_service.delete_project(id)
if not resp:
logger.error(f"The project #{id} delete failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg="删除同步工程成功"
)
class Job(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@ job.get("/projects/{name}/jobs", response_model=Response[DataList[JobData]], description='列出所有同步流')
async def list_jobs(
self,
name: str = Query(..., description='同步工程名'),
search: Optional[str] = Query(None, description='同步工程搜索内容'),
source: Optional[str] = Query(None, description='分支来源'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(10, description="Page size")
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
service = JobService()
if search is not None:
search = search.replace(" ", "")
answer = await service.list_jobs(project=name, search=search, source=source, page=pageNum, size=pageSize)
if not answer:
return Response(
code=Code.SUCCESS,
data=DataList(total=0, list=[]),
msg="没有同步流"
)
count = await service.count_job(project=name, search=search, source=source)
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer),
msg="查询同步流成功"
)
@ job.post("/projects/{name}/jobs", response_model=Response[JobData], description='创建一个同步流')
async def create_job(
self,
name: str = Query(..., description='同步工程名'),
item: CreateJobItem = Body(..., description='同步流属性')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not item:
raise ErrorTemplate.ARGUMENT_LACK("JSON")
if not item.type:
raise ErrorTemplate.ARGUMENT_LACK("分支同步类型")
service = JobService()
ans = await service.create_job(name, item)
if not ans:
logger.error(f"Create a job of project #{name} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
data=ans,
msg="创建同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/start", response_model=Response, description='开启一个同步流')
async def start_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.update_status(id, True)
if not ans:
logger.error(f"The job #{id} of project #{name} start failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="开启同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/stop", response_model=Response, description='停止一个同步流')
async def stop_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.update_status(id, False)
if not ans:
logger.error(f"The job #{id} of project #{name} stop failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="关闭同步流成功"
)
@ job.delete("/projects/{name}/jobs", response_model=Response, description='通过id删除一个同步流')
async def delete_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.delete_job(id)
if not ans:
logger.error(f"The job #{id} of project #{name} delete failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg="删除同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/set_commit", response_model=Response, description='通过id设置一个同步流的commit')
async def set_job_commit(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id'),
commit: str = Query(..., description='commit'),
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
job = await service.get_job(id)
if not job:
logger.error(f"The job #{id} of project #{name} is not exist")
raise Errors.UPDATE_FAILD
# only the sync type is oneway can use the commit
if job.type == SyncType.TwoWay:
logger.error(f"The job #{id} of project #{name} is two way sync")
raise HTTPException(Code.OPERATION_FAILED, 'Twoway同步方式无法修改commit值')
ans = await service.update_job_lateset_commit(id, commit)
if not ans:
logger.error(
f"The job #{id} of project #{name} update latest commit failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="设置同步流commit成功"
)
@ job.get("/projects/{name}/jobs/{id}/logs", response_model=Response[DataList[LogData]], description='列出所有同步流')
async def get_job_log(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(1000, description="Page size")
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
project_service = ProjectService()
projects = await project_service.search_project(name=name)
if len(projects) == 0:
raise ErrorTemplate.ARGUMENT_ERROR("工程名")
service = LogService()
log = await service.get_logs_by_job(id, pageNum, pageSize)
data = []
for rep_log in log:
log_str = rep_log.log
if projects[0].gitee_token:
log_str = log_str.replace(projects[0].gitee_token, "******")
if projects[0].github_token:
log_str = log_str.replace(projects[0].github_token, "******")
rep_log.log = log_str
data.append(rep_log)
if len(log) == 0:
logger.info(
f"The job #{id} of project #{name} has no logs")
count = await service.count_logs(id)
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=data)
)
import time
from fastapi import (
BackgroundTasks,
Query,
Depends,
Security,
Body
)
from pydantic.main import BaseModel
from typing import Optional
import asyncio
from sqlalchemy.sql.expression import false
from src.base.error_code import ErrorTemplate, Errors
from src.utils.logger import logger
from extras.obfastapi.frame import Trace, DataList
from extras.obfastapi.frame import OBResponse as Response
from extras.obfastapi.frame import OBHTTPException as HTTPException
from src.base.code import Code
from src.router import PROJECT as project
from src.router import JOB as job
from src.api.Controller import APIController as Controller
from src.dto.sync import Project as ProjectData
from src.dto.sync import Job as JobData
from src.dto.log import Log as LogData
from src.dto.sync import SyncType, CreateProjectItem, CreateJobItem
from src.service.sync import ProjectService, JobService
from src.service.pull_request import PullRequestService
from src.service.log import LogService
from src.utils import github, gitlab, gitee, gitcode, gitlink
class Project(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@project.get("", response_model=Response[DataList[ProjectData]], description='通过工程名获取一个同步工程')
async def get_project(
self,
search: Optional[str] = Query(None, description='同步工程搜索内容'),
orderby: Optional[str] = Query(None, description='排序选项'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(10, description="Page size")
):
# search
service = ProjectService()
if search is None:
count = await service.get_count()
answer = await service.list_projects(page=pageNum, size=pageSize)
else:
count = await service.get_count_by_search(search.replace(" ", ""))
answer = await service.search_project(name=search.replace(" ", ""))
if answer is None:
logger.error(f"The project list fetch failed")
raise Errors.QUERY_FAILD
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer)
)
@ project.post("", response_model=Response[ProjectData], description='创建一个同步工程')
async def create_project(
self,
item: CreateProjectItem = Body(..., description='同步工程属性')
):
# pre check
if not item:
raise ErrorTemplate.ARGUMENT_LACK("请求体")
if not item.name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if item.github_address:
if not github.check_github_address(item.github_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("GitHub仓库")
if item.gitlab_address:
if not gitlab.check_gitlab_address(item.gitlab_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitlab/Antcode仓库")
if item.gitee_address:
if not gitee.check_gitee_address(item.gitee_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("Gitee仓库")
if item.code_china_address:
if not gitcode.check_gitcode_address(item.code_china_address):
raise ErrorTemplate.TIP_ARGUMENT_ERROR("CodeChina仓库")
# if item.gitlink_address:
# if not gitlink.check_gitlink_address(item.gitlink_address):
# raise ErrorTemplate.ARGUMENT_ERROR("Gitlink仓库")
service = ProjectService()
resp = await service.insert_project(item)
if not resp:
logger.error(f"The project insert failed")
raise Errors.INSERT_FAILD
organization, repo = github.transfer_github_to_name(
item.github_address)
if organization and repo:
pull_request_service = PullRequestService()
task = asyncio.create_task(
pull_request_service.sync_pull_request(item.name, organization, repo))
return Response(
code=Code.SUCCESS,
data=resp,
msg="创建同步工程成功"
)
@ project.delete("", response_model=Response, description='通过id删除一个同步工程')
async def delete_project(
self,
id: int = Query(..., description='同步工程id')
):
if not id:
raise ErrorTemplate.ARGUMENT_LACK("id")
# if delete the project, the front page double check firstly
project_service = ProjectService()
project = await project_service.search_project(id=id)
name = project[0].name
# delete pull request
pull_request_service = PullRequestService()
resp = await pull_request_service.fetch_pull_request(name)
if resp:
if len(resp) > 0:
for pr in resp:
await pull_request_service.delete_pull_request(pr.id)
# delete sync job
job_service = JobService()
resp = await job_service.list_jobs(project=name)
if not resp:
pass
else:
for item in resp:
await job_service.delete_job(item.id)
# delete sync project
resp = await project_service.delete_project(id)
if not resp:
logger.error(f"The project #{id} delete failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg="删除同步工程成功"
)
@ project.delete("/clear", response_model=Response, description='清空所有项目并重置编号')
async def clear_all_projects(self):
"""清空所有项目并重置 AUTO_INCREMENT"""
project_service = ProjectService()
success = await project_service.clear_all_projects()
if not success:
logger.error("Failed to clear all projects")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg="清空所有项目成功下次插入将从编号1开始"
)
class Job(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token: str = None):
return super().get_user(cookie_key=cookie_key, token=token)
@ job.get("/projects/{name}/jobs", response_model=Response[DataList[JobData]], description='列出所有同步流')
async def list_jobs(
self,
name: str = Query(..., description='同步工程名'),
search: Optional[str] = Query(None, description='同步工程搜索内容'),
source: Optional[str] = Query(None, description='分支来源'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(10, description="Page size")
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
service = JobService()
if search is not None:
search = search.replace(" ", "")
answer = await service.list_jobs(project=name, search=search, source=source, page=pageNum, size=pageSize)
if not answer:
return Response(
code=Code.SUCCESS,
data=DataList(total=0, list=[]),
msg="没有同步流"
)
count = await service.count_job(project=name, search=search, source=source)
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=answer),
msg="查询同步流成功"
)
@ job.post("/projects/{name}/jobs", response_model=Response[JobData], description='创建一个同步流')
async def create_job(
self,
name: str = Query(..., description='同步工程名'),
item: CreateJobItem = Body(..., description='同步流属性')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not item:
raise ErrorTemplate.ARGUMENT_LACK("JSON")
if not item.type:
raise ErrorTemplate.ARGUMENT_LACK("分支同步类型")
service = JobService()
ans = await service.create_job(name, item)
if not ans:
logger.error(f"Create a job of project #{name} failed")
raise Errors.INSERT_FAILD
return Response(
code=Code.SUCCESS,
data=ans,
msg="创建同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/start", response_model=Response, description='开启一个同步流')
async def start_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.update_status(id, True)
if not ans:
logger.error(f"The job #{id} of project #{name} start failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="开启同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/stop", response_model=Response, description='停止一个同步流')
async def stop_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.update_status(id, False)
if not ans:
logger.error(f"The job #{id} of project #{name} stop failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="关闭同步流成功"
)
@ job.delete("/projects/{name}/jobs", response_model=Response, description='通过id删除一个同步流')
async def delete_job(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id')
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
ans = await service.delete_job(id)
if not ans:
logger.error(f"The job #{id} of project #{name} delete failed")
raise Errors.DELETE_FAILD
return Response(
code=Code.SUCCESS,
msg="删除同步流成功"
)
@ job.put("/projects/{name}/jobs/{id}/set_commit", response_model=Response, description='通过id设置一个同步流的commit')
async def set_job_commit(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id'),
commit: str = Query(..., description='commit'),
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
service = JobService()
job = await service.get_job(id)
if not job:
logger.error(f"The job #{id} of project #{name} is not exist")
raise Errors.UPDATE_FAILD
# only the sync type is oneway can use the commit
if job.type == SyncType.TwoWay:
logger.error(f"The job #{id} of project #{name} is two way sync")
raise HTTPException(Code.OPERATION_FAILED, 'Twoway同步方式无法修改commit值')
ans = await service.update_job_lateset_commit(id, commit)
if not ans:
logger.error(
f"The job #{id} of project #{name} update latest commit failed")
raise Errors.UPDATE_FAILD
return Response(
code=Code.SUCCESS,
msg="设置同步流commit成功"
)
@ job.get("/projects/{name}/jobs/{id}/logs", response_model=Response[DataList[LogData]], description='列出所有同步流')
async def get_job_log(
self,
name: str = Query(..., description='同步工程名'),
id: int = Query(..., description='同步流id'),
pageNum: Optional[int] = Query(1, description="Page number"),
pageSize: Optional[int] = Query(1000, description="Page size")
):
if not name:
raise ErrorTemplate.ARGUMENT_LACK("工程名")
if not id:
raise ErrorTemplate.ARGUMENT_LACK("同步流id")
project_service = ProjectService()
projects = await project_service.search_project(name=name)
if len(projects) == 0:
raise ErrorTemplate.ARGUMENT_ERROR("工程名")
service = LogService()
log = await service.get_logs_by_job(id, pageNum, pageSize)
data = []
for rep_log in log:
log_str = rep_log.log
if projects[0].gitee_token:
log_str = log_str.replace(projects[0].gitee_token, "******")
if projects[0].github_token:
log_str = log_str.replace(projects[0].github_token, "******")
rep_log.log = log_str
data.append(rep_log)
if len(log) == 0:
logger.info(
f"The job #{id} of project #{name} has no logs")
count = await service.count_logs(id)
return Response(
code=Code.SUCCESS,
data=DataList(total=count, list=data)
)

View File

@ -1,300 +1,300 @@
import time
from fastapi import (
Body,
Path,
Depends,
Query,
Security
)
from typing import Dict
from starlette.requests import Request
from src.utils import base
from src.utils.sync_log import sync_log, LogType, api_log
from src.api.Controller import APIController as Controller
from src.router import SYNC_CONFIG as router
from src.do.sync_config import SyncDirect
from src.dto.sync_config import SyncRepoDTO, SyncBranchDTO, LogDTO, ModifyRepoDTO
from src.service.sync_config import SyncService, LogService
from src.service.cronjob import sync_repo_task, sync_branch_task, modify_repos, delete_repo_dir
from src.base.status_code import Status, SYNCResponse, SYNCException
from src.service.cronjob import GITMSGException
class SyncDirection(Controller):
def __init__(self, *args, **kwargs):
self.service = SyncService()
self.log_service = LogService()
super().__init__(*args, **kwargs)
# 提供获取操作人员信息定义接口, 无任何实质性操作
def user(self):
return super().user()
@router.post("/repo", response_model=SYNCResponse, description='配置同步仓库')
async def create_sync_repo(
self, request: Request, user: str = Depends(user),
dto: SyncRepoDTO = Body(..., description="绑定同步仓库信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
# if not base.check_addr(dto.external_repo_address) or not base.check_addr(dto.internal_repo_address):
# return SYNCResponse(
# code_status=Status.REPO_ADDR_ILLEGAL.code,
# msg=Status.REPO_ADDR_ILLEGAL.msg
# )
if dto.sync_granularity not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_GRAN_ILLEGAL.code, msg=Status.SYNC_GRAN_ILLEGAL.msg)
if dto.sync_direction not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_DIRE_ILLEGAL.code, msg=Status.SYNC_DIRE_ILLEGAL.msg)
if await self.service.same_name_repo(repo_name=dto.repo_name):
return SYNCResponse(
code_status=Status.REPO_EXISTS.code,
msg=Status.REPO_EXISTS.msg
)
repo = await self.service.create_repo(dto)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=repo,
msg=Status.SUCCESS.msg
)
@router.post("/{repo_name}/branch", response_model=SYNCResponse, description='配置同步分支')
async def create_sync_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
dto: SyncBranchDTO = Body(..., description="绑定同步分支信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
try:
repo_id = await self.service.check_status(repo_name, dto)
except SYNCException as Error:
return SYNCResponse(
code_status=Error.code_status,
msg=Error.status_msg
)
branch = await self.service.create_branch(dto, repo_id=repo_id)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=branch,
msg=Status.SUCCESS.msg
)
@router.get("/repo", response_model=SYNCResponse, description='获取同步仓库信息')
async def get_sync_repos(
self, request: Request, user: str = Depends(user),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
repos = await self.service.get_sync_repo(page_num=page_num, page_size=page_size, create_sort=create_sort)
if repos is None:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=repos,
msg=Status.SUCCESS.msg
)
@router.get("/{repo_name}/branch", response_model=SYNCResponse, description='获取仓库对应的同步分支信息')
async def get_sync_branches(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="查询的仓库名称"),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
try:
repo_id = await self.service.get_repo_id(repo_name=repo_name)
except SYNCException as Error:
return SYNCResponse(
code_status=Error.code_status,
msg=Error.status_msg
)
branches = await self.service.get_sync_branches(repo_id=repo_id, page_num=page_num,
page_size=page_size, create_sort=create_sort)
if len(branches) < 1:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=branches,
msg=Status.SUCCESS.msg
)
@router.post("/repo/{repo_name}", response_model=SYNCResponse, description='执行仓库同步')
async def sync_repo(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
force_flag: bool = Query(False, description="是否强制同步")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
repo = await self.service.get_repo(repo_name=repo_name)
if repo is None:
return SYNCResponse(code_status=Status.REPO_NOTFOUND.code, msg=Status.REPO_NOTFOUND.msg)
if not repo.enable:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
try:
await sync_repo_task(repo, user, force_flag)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg
)
@router.post("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='执行分支同步')
async def sync_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称"),
sync_direct: int = Query(..., description="同步方向: 1 表示内部仓库同步到外部, 2 表示外部仓库同步到内部"),
force_flag: bool = Query(False, description="是否强制同步")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
repo = await self.service.get_repo(repo_name=repo_name)
if not repo.enable:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
if sync_direct not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_DIRE_ILLEGAL.code, msg=Status.SYNC_DIRE_ILLEGAL.msg)
direct = SyncDirect(sync_direct)
branches = await self.service.sync_branch(repo_id=repo.id, branch_name=branch_name, dire=direct)
if len(branches) < 1:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
try:
await sync_branch_task(repo, branches, direct, user, force_flag)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(code_status=Status.SUCCESS.code, msg=Status.SUCCESS.msg)
@router.delete("/repo/{repo_name}", response_model=SYNCResponse, description='仓库解绑')
async def delete_repo(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称")
):
api_log(LogType.INFO, f"用户 {user} 使用 DELETE 方法访问接口 {request.url.path} ", user)
data = await self.service.delete_repo(repo_name=repo_name)
try:
if data.code_status == 0:
delete_repo_dir(repo_name, user)
await self.log_service.delete_logs(repo_name=repo_name)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.delete("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='分支解绑')
async def delete_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称")
):
api_log(LogType.INFO, f"用户 {user} 使用 DELETE 方法访问接口 {request.url.path} ", user)
data = await self.service.delete_branch(repo_name=repo_name, branch_name=branch_name)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/repo/{repo_name}/repo_addr", response_model=SYNCResponse, description='更新仓库地址')
async def update_repo_addr(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
dto: ModifyRepoDTO = Body(..., description="更新仓库地址信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} 更新仓库信息", user)
data = await self.service.update_repo_addr(repo_name=repo_name, dto=dto)
try:
await modify_repos(repo_name, user)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/repo/{repo_name}", response_model=SYNCResponse, description='更新仓库同步状态')
async def update_repo_status(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
enable: bool = Query(..., description="同步启用状态")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} ", user)
data = await self.service.update_repo(repo_name=repo_name, enable=enable)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='更新分支同步状态')
async def update_branch_status(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称"),
enable: bool = Query(..., description="同步启用状态")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} ", user)
data = await self.service.update_branch(repo_name=repo_name, branch_name=branch_name, enable=enable)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.get("/repo/logs", response_model=SYNCResponse, description='获取仓库/分支日志')
async def get_logs(
self, request: Request, user: str = Depends(user),
repo_name: str = Query(None, description="仓库名称"),
branch_id: str = Query(None, description="分支id仓库粒度无需输入"),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
branch_id_list = branch_id.split(',') if branch_id is not None else []
repo_name_list = repo_name.split(',') if repo_name is not None else []
data = await self.log_service.get_logs(repo_name_list=repo_name_list, branch_id_list=branch_id_list,
page_num=page_num, page_size=page_size, create_sort=create_sort)
if not data:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
total=data[0],
data=data[1],
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
total=data[0],
data=data[1],
msg=Status.SUCCESS.msg
)
import time
from fastapi import (
Body,
Path,
Depends,
Query,
Security
)
from typing import Dict
from starlette.requests import Request
from src.utils import base
from src.utils.sync_log import sync_log, LogType, api_log
from src.api.Controller import APIController as Controller
from src.router import SYNC_CONFIG as router
from src.do.sync_config import SyncDirect
from src.dto.sync_config import SyncRepoDTO, SyncBranchDTO, LogDTO, ModifyRepoDTO
from src.service.sync_config import SyncService, LogService
from src.service.cronjob import sync_repo_task, sync_branch_task, modify_repos, delete_repo_dir
from src.base.status_code import Status, SYNCResponse, SYNCException
from src.service.cronjob import GITMSGException
class SyncDirection(Controller):
def __init__(self, *args, **kwargs):
self.service = SyncService()
self.log_service = LogService()
super().__init__(*args, **kwargs)
# 提供获取操作人员信息定义接口, 无任何实质性操作
def user(self):
return super().user()
@router.post("/repo", response_model=SYNCResponse, description='配置同步仓库')
async def create_sync_repo(
self, request: Request, user: str = Depends(user),
dto: SyncRepoDTO = Body(..., description="绑定同步仓库信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
# if not base.check_addr(dto.external_repo_address) or not base.check_addr(dto.internal_repo_address):
# return SYNCResponse(
# code_status=Status.REPO_ADDR_ILLEGAL.code,
# msg=Status.REPO_ADDR_ILLEGAL.msg
# )
if dto.sync_granularity not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_GRAN_ILLEGAL.code, msg=Status.SYNC_GRAN_ILLEGAL.msg)
if dto.sync_direction not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_DIRE_ILLEGAL.code, msg=Status.SYNC_DIRE_ILLEGAL.msg)
if await self.service.same_name_repo(repo_name=dto.repo_name):
return SYNCResponse(
code_status=Status.REPO_EXISTS.code,
msg=Status.REPO_EXISTS.msg
)
repo = await self.service.create_repo(dto)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=repo,
msg=Status.SUCCESS.msg
)
@router.post("/{repo_name}/branch", response_model=SYNCResponse, description='配置同步分支')
async def create_sync_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
dto: SyncBranchDTO = Body(..., description="绑定同步分支信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
try:
repo_id = await self.service.check_status(repo_name, dto)
except SYNCException as Error:
return SYNCResponse(
code_status=Error.code_status,
msg=Error.status_msg
)
branch = await self.service.create_branch(dto, repo_id=repo_id)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=branch,
msg=Status.SUCCESS.msg
)
@router.get("/repo", response_model=SYNCResponse, description='获取同步仓库信息')
async def get_sync_repos(
self, request: Request, user: str = Depends(user),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
repos = await self.service.get_sync_repo(page_num=page_num, page_size=page_size, create_sort=create_sort)
if repos is None:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=repos,
msg=Status.SUCCESS.msg
)
@router.get("/{repo_name}/branch", response_model=SYNCResponse, description='获取仓库对应的同步分支信息')
async def get_sync_branches(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="查询的仓库名称"),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
try:
repo_id = await self.service.get_repo_id(repo_name=repo_name)
except SYNCException as Error:
return SYNCResponse(
code_status=Error.code_status,
msg=Error.status_msg
)
branches = await self.service.get_sync_branches(repo_id=repo_id, page_num=page_num,
page_size=page_size, create_sort=create_sort)
if len(branches) < 1:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
data=branches,
msg=Status.SUCCESS.msg
)
@router.post("/repo/{repo_name}", response_model=SYNCResponse, description='执行仓库同步')
async def sync_repo(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
force_flag: bool = Query(False, description="是否强制同步")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
repo = await self.service.get_repo(repo_name=repo_name)
if repo is None:
return SYNCResponse(code_status=Status.REPO_NOTFOUND.code, msg=Status.REPO_NOTFOUND.msg)
if not repo.enable:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
try:
await sync_repo_task(repo, user, force_flag)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
msg=Status.SUCCESS.msg
)
@router.post("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='执行分支同步')
async def sync_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称"),
sync_direct: int = Query(..., description="同步方向: 1 表示内部仓库同步到外部, 2 表示外部仓库同步到内部"),
force_flag: bool = Query(False, description="是否强制同步")
):
api_log(LogType.INFO, f"用户 {user} 使用 POST 方法访问接口 {request.url.path} ", user)
repo = await self.service.get_repo(repo_name=repo_name)
if not repo.enable:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
if sync_direct not in [1, 2]:
return SYNCResponse(code_status=Status.SYNC_DIRE_ILLEGAL.code, msg=Status.SYNC_DIRE_ILLEGAL.msg)
direct = SyncDirect(sync_direct)
branches = await self.service.sync_branch(repo_id=repo.id, branch_name=branch_name, dire=direct)
if len(branches) < 1:
return SYNCResponse(code_status=Status.NOT_ENABLE.code, msg=Status.NOT_ENABLE.msg)
try:
await sync_branch_task(repo, branches, direct, user, force_flag)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(code_status=Status.SUCCESS.code, msg=Status.SUCCESS.msg)
@router.delete("/repo/{repo_name}", response_model=SYNCResponse, description='仓库解绑')
async def delete_repo(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称")
):
api_log(LogType.INFO, f"用户 {user} 使用 DELETE 方法访问接口 {request.url.path} ", user)
data = await self.service.delete_repo(repo_name=repo_name)
try:
if data.code_status == 0:
delete_repo_dir(repo_name, user)
await self.log_service.delete_logs(repo_name=repo_name)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.delete("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='分支解绑')
async def delete_branch(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称")
):
api_log(LogType.INFO, f"用户 {user} 使用 DELETE 方法访问接口 {request.url.path} ", user)
data = await self.service.delete_branch(repo_name=repo_name, branch_name=branch_name)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/repo/{repo_name}/repo_addr", response_model=SYNCResponse, description='更新仓库地址')
async def update_repo_addr(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
dto: ModifyRepoDTO = Body(..., description="更新仓库地址信息")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} 更新仓库信息", user)
data = await self.service.update_repo_addr(repo_name=repo_name, dto=dto)
try:
await modify_repos(repo_name, user)
except GITMSGException as GITError:
return SYNCResponse(
code_status=GITError.status,
msg=GITError.msg
)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/repo/{repo_name}", response_model=SYNCResponse, description='更新仓库同步状态')
async def update_repo_status(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
enable: bool = Query(..., description="同步启用状态")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} ", user)
data = await self.service.update_repo(repo_name=repo_name, enable=enable)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.put("/{repo_name}/branch/{branch_name}", response_model=SYNCResponse, description='更新分支同步状态')
async def update_branch_status(
self, request: Request, user: str = Depends(user),
repo_name: str = Path(..., description="仓库名称"),
branch_name: str = Path(..., description="分支名称"),
enable: bool = Query(..., description="同步启用状态")
):
api_log(LogType.INFO, f"用户 {user} 使用 PUT 方法访问接口 {request.url.path} ", user)
data = await self.service.update_branch(repo_name=repo_name, branch_name=branch_name, enable=enable)
return SYNCResponse(
code_status=data.code_status,
msg=data.status_msg
)
@router.get("/repo/logs", response_model=SYNCResponse, description='获取仓库/分支日志')
async def get_logs(
self, request: Request, user: str = Depends(user),
repo_name: str = Query(None, description="仓库名称"),
branch_id: str = Query(None, description="分支id仓库粒度无需输入"),
page_num: int = Query(1, description="页数"), page_size: int = Query(10, description="条数"),
create_sort: bool = Query(False, description="创建时间排序, 默认倒序")
):
api_log(LogType.INFO, f"用户 {user} 使用 GET 方法访问接口 {request.url.path} ", user)
branch_id_list = branch_id.split(',') if branch_id is not None else []
repo_name_list = repo_name.split(',') if repo_name is not None else []
data = await self.log_service.get_logs(repo_name_list=repo_name_list, branch_id_list=branch_id_list,
page_num=page_num, page_size=page_size, create_sort=create_sort)
if not data:
return SYNCResponse(
code_status=Status.NOT_DATA.code,
total=data[0],
data=data[1],
msg=Status.NOT_DATA.msg
)
return SYNCResponse(
code_status=Status.SUCCESS.code,
total=data[0],
data=data[1],
msg=Status.SUCCESS.msg
)

View File

@ -1,26 +1,26 @@
from fastapi import Security, Depends
from src.dto.user import UserInfoDto
from extras.obfastapi.frame import OBResponse as Response
from src.api.Controller import APIController as Controller
from src.router import USER as user
class User(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token=None):
return super().get_user(cookie_key=cookie_key, token=token)
@user.get("/info", response_model=Response[UserInfoDto], description="获得用户信息")
async def get_user_info(
self,
user: Security = Depends(get_user)
):
return Response(
data=UserInfoDto(
name=user.name,
nick=user.nick,
emp_id=user.emp_id,
email=user.email,
dept=user.dept
)
)
from fastapi import Security, Depends
from src.dto.user import UserInfoDto
from extras.obfastapi.frame import OBResponse as Response
from src.api.Controller import APIController as Controller
from src.router import USER as user
class User(Controller):
def get_user(self, cookie_key=Security(Controller.API_KEY_BUC_COOKIE), token=None):
return super().get_user(cookie_key=cookie_key, token=token)
@user.get("/info", response_model=Response[UserInfoDto], description="获得用户信息")
async def get_user_info(
self,
user: Security = Depends(get_user)
):
return Response(
data=UserInfoDto(
name=user.name,
nick=user.nick,
emp_id=user.emp_id,
email=user.email,
dept=user.dept
)
)

View File

@ -1,16 +1,16 @@
class Code:
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
FORBIDDEN = 403
NOT_FOUND = 404
OPERATION_FAILED = 406
class LogType:
INFO = 'info'
ERROR = 'ERROR'
WARNING = 'warning'
DEBUG = "debug"
class Code:
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
FORBIDDEN = 403
NOT_FOUND = 404
OPERATION_FAILED = 406
class LogType:
INFO = 'info'
ERROR = 'ERROR'
WARNING = 'warning'
DEBUG = "debug"

View File

@ -1,168 +1,242 @@
# coding: utf-8
import os
from extras.obfastapi.config import ConfigsUtil, MysqlConfig, RedisConfig
def getenv(key, default=None, _type=None):
value = os.getenv(key)
if value:
if _type == bool:
return value.lower() == 'true'
else:
return _type(value) if _type else value
else:
return default
LOCAL_ENV = 'LOCAL'
DEV_ENV = 'DEV'
PORD_ENV = 'PROD'
SYS_ENV = getenv('SYS_ENV', LOCAL_ENV)
LOG_PATH = getenv('LOG_PATH')
LOG_LEVEL = getenv('LOG_LV', 'DEBUG')
LOG_SAVE = getenv('LOG_SAVE', True)
DELETE_SYNC_DIR = getenv('DELETE_SYNC_DIR', False)
LOG_DETAIL = getenv('LOG_DETAIL', True)
SYNC_DIR = os.getenv("SYNC_DIR", "/tmp/sync_dir/")
buc_key = getenv('BUC_KEY', "OBRDE_DEV_USER_SIGN")
buc_key and ConfigsUtil.set_obfastapi_config('buc_key', buc_key)
DB_ENV = getenv('DB_ENV', 'test_env')
DB = {
'test_env': {
'host': getenv('CEROBOT_MYSQL_HOST', 'localhost'),
'port': getenv('CEROBOT_MYSQL_PORT', 3306, int),
'user': getenv('CEROBOT_MYSQL_USER', 'root'),
'passwd': getenv('CEROBOT_MYSQL_PWD', '123456789LY@'),
'dbname': getenv('CEROBOT_MYSQL_DB', 'reposync')
},
'local': {
'host': getenv('CEROBOT_MYSQL_HOST', 'localhost'),
'port': getenv('CEROBOT_MYSQL_PORT', 3306, int),
'user': getenv('CEROBOT_MYSQL_USER', 'root'),
'passwd': getenv('CEROBOT_MYSQL_PWD', '123456789LY@'),
'dbname': getenv('CEROBOT_MYSQL_DB', 'reposync')
}
}
for key in DB:
conf = MysqlConfig(**DB[key])
ConfigsUtil.set_mysql_config(key, conf)
SERVER_HOST = getenv('SERVER_HOST', '')
TOKEN_KEY = int(getenv("TOKEN_KEY", 1))
ACCOUNT = {
'username': getenv('OB_ROBOT_USERNAME', ''),
'email': getenv('OB_ROBOT_USERNAME', ''),
'github_token': getenv('GITHUB_TOKEN', ''),
'gitee_token': getenv('GITEE_TOKEN', ''),
'gitlab_token': getenv('GITLAB_TOKEN', ''), # 暂时还是我的token待替代为一个内部账号
'antcode_token': getenv('ANTCODE_TOKEN', ''), # 暂时还是我的token待替代为一个内部账号
'gitcode_token': getenv('GITCODE_TOKEN', ''), # 暂时还是我的token待替代为ob-robot账号
'robot_code_token': getenv('ROBOT_CODE_TOKEN', ''),
'robot_antcode_token': getenv('ROBOT_ANTCODE_TOKEN', '')
}
GITLAB_ENV = {
'gitlab_api_address': getenv('GITLAB_API_HOST', ''),
'gitlab_api_pullrequest_address': getenv('GITLAB_API_PULLREQUEST_HOST', '')
}
GITHUB_ENV = {
'github_api_address': getenv('GITHUB_API_HOST', ''),
'github_api_diff_address': getenv('GITHUB_API_DIFF_HOST', ''),
}
GITEE_ENV = {
'gitee_api_address': getenv('GITEE_API_HOST', 'https://api.gitee.com/repos'),
'gitee_api_diff_address': getenv('GITEE_API_DIFF_HOST', 'https://gitee.com'),
}
GITLINK_ENV = {
'gitlink_api_address': getenv('GITLINK_API_HOST', ''),
'gitlink_api_diff_address': getenv('GITLINK_API_DIFF_HOST', ''),
}
GITCODE_ENV = {
'gitcode_api_address': getenv('GITCODE_API_HOST', ''),
'gitcode_api_diff_address': getenv('GITCODE_API_DIFF_HOST', ''),
}
DOCKER_ENV = {
'el7': getenv('EL7_DOCKER_IMAGE'),
'el8': getenv('EL8_DOCKER_IMAGE')
}
ERROR_REPORT_EMAIL = getenv('EORROR_TO', '')
DEFAULT_DK_RECEIVERS = getenv('DF_DK_TO', '')
DEFAULT_EMAIL_RECEIVERS = getenv('DF_EMAIL_TO', '')
NOTIFY = {
# todo yaml当中配置新的生产环境host
'host': getenv('NOTIFY_HOST', ''),
'user': getenv('NOTIFY_USER', ''), # 一般当前项目的项目名
'report_sender': getenv('NOTIFY_REPORT_SENDER', ''),
'email_sender': getenv('EMAIL_NOTIFY_SENDER'),
'dk_sender': getenv('DK_NOTIFY_SENDER', ''),
'dd_sender': getenv('DD_NOTIFY_SENDER', ''),
'dd_sender_issue': getenv('DD_NOTIFY_SENDER_ISSUE', ''),
'dd_sender_log': getenv('DD_NOTIFY_SENDER_LOG', ''),
'dd_sender_ghpr': getenv('DD_NOTIFY_SENDER_PR', ''),
}
# log level
ConfigsUtil.set_obfastapi_config('log_level', 'WARN')
# ConfigsUtil.set_obfastapi_config('log_name', 'mysql_test')
# ConfigsUtil.set_obfastapi_config('log_path', 'test.log')
SECRET_SCAN = getenv('SECRET_SCAN', True)
# Symmetric encryption key
DATA_ENCRYPT_KEY = getenv('DATA_ENCRYPT_KEY', '')
# web base_url
base_url = getenv('OB_ROBOT_BASE_URL', '')
CACHE_DB = {
"cerobot": {
"host": getenv("CEROBOT_REDIS_HOST", ""),
"port": getenv("CEROBOT_REDIS_PORT", 6379, int),
"password": getenv("CEROBOT_REDIS_PWD", "")
}
}
for key in CACHE_DB:
conf = RedisConfig(**CACHE_DB[key])
ConfigsUtil.set_redis_config(key, conf)
GIT_DEPTH = getenv('GIT_DEPTH', 100, int)
SECRET_SCAN_THREAD = getenv('SECRET_SCAN_THREAD', 4, int)
OCEANBASE = getenv('OCEANBASE', 'ob-mirror')
OBProjectIdInAone = getenv('OB_PROJECT_ID_AONE', 2015510, int)
strip_name = getenv("STRIP_NAME", "/.ce")
# observer repo cache
OCEANBASE_REPO_BASE_DIR = getenv('OCEANBASE_REPO_BASE_DIR', '')
# oceanbase internal repo, create github feature branch
OCEANBASE_REPO = getenv('OCEANBASE_REPO', '')
# oceanbase ce publish repo, the ob backup repo
# it need origin(oceanbase-ce-publish oceanbase) and github(github oceanbase) git configration
OCEANBASE_BACKUP_REPO = getenv('OCEANBASE_BACKUP_REPO', '')
# github repo
OCEANBASE_GITHUB_REPO = getenv('OCEANBASE_GITHUB_REPO', '')
ROLE_CHECK = getenv('ROLE_CHECK', True, bool)
# oss config
OSS_CONFIG = {
"id": getenv("OSSIV", ""),
"secret": getenv("OSSKEY", ""),
"bucket": getenv("OSSBUCKET", ""),
"endpoint": getenv("OSSENDPOINT", ""),
"download_base": getenv("DOWNLOAD_BASE", "")
}
# coding: utf-8
import os
from extras.obfastapi.config import ConfigsUtil, MysqlConfig, RedisConfig
def getenv(key, default=None, _type=None):
value = os.getenv(key)
if value:
if _type == bool:
return value.lower() == 'true'
else:
return _type(value) if _type else value
else:
return default
LOCAL_ENV = 'LOCAL'
DEV_ENV = 'DEV'
PORD_ENV = 'PROD'
SYS_ENV = getenv('SYS_ENV', LOCAL_ENV)
LOG_PATH = getenv('LOG_PATH')
LOG_LEVEL = getenv('LOG_LV', 'DEBUG')
LOG_SAVE = getenv('LOG_SAVE', True)
DELETE_SYNC_DIR = getenv('DELETE_SYNC_DIR', False)
LOG_DETAIL = getenv('LOG_DETAIL', True)
SYNC_DIR = os.getenv("SYNC_DIR", "/tmp/sync_dir/")
buc_key = getenv('BUC_KEY', "OBRDE_DEV_USER_SIGN")
buc_key and ConfigsUtil.set_obfastapi_config('buc_key', buc_key)
DB_ENV = getenv('DB_ENV', 'test_env')
DB = {
'test_env': {
'host': 'localhost',
'port': 3306,
'user': 'root',
'passwd': 'c261730l',
'dbname': 'reposync7'
},
'local': {
'host': 'localhost',
'port': 3306,
'user': 'root',
'passwd': 'c261730l',
'dbname': 'reposync7'
}
}
for key in DB:
conf = MysqlConfig(**DB[key])
ConfigsUtil.set_mysql_config(key, conf)
SERVER_HOST = getenv('SERVER_HOST', '')
TOKEN_KEY = int(getenv("TOKEN_KEY", 1))
ACCOUNT = {
'username': getenv('OB_ROBOT_USERNAME', ''),
'email': getenv('OB_ROBOT_USERNAME', ''),
'github_token': getenv('GITHUB_TOKEN', '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'),
'gitee_token': getenv('GITEE_TOKEN', '53217cc695cda3be6275a7213df9aaab'),
'gitlab_token': getenv('GITLAB_TOKEN', ''), # 暂时还是我的token待替代为一个内部账号
'antcode_token': getenv('ANTCODE_TOKEN', ''), # 暂时还是我的token待替代为一个内部账号
'gitcode_token': getenv('GITCODE_TOKEN', ''), # 暂时还是我的token待替代为ob-robot账号
'gitlink_token': getenv('GITLINK_TOKEN', ''), # Gitlink token
'gitlink_cookie': 'autologin_trustie=4e25b144d7a870842f3063bbdb6f54fd3cc9b914',
'robot_code_token': getenv('ROBOT_CODE_TOKEN', ''),
'robot_antcode_token': getenv('ROBOT_ANTCODE_TOKEN', '')
}
GITLAB_ENV = {
'gitlab_api_address': getenv('GITLAB_API_HOST', ''),
'gitlab_api_pullrequest_address': getenv('GITLAB_API_PULLREQUEST_HOST', '')
}
GITHUB_ENV = {
'github_api_address': getenv('GITHUB_API_HOST', ''),
'github_api_diff_address': getenv('GITHUB_API_DIFF_HOST', ''),
}
GITEE_ENV = {
'gitee_api_address': getenv('GITEE_API_HOST', 'https://api.gitee.com/repos'),
'gitee_api_diff_address': getenv('GITEE_API_DIFF_HOST', 'https://gitee.com'),
}
GITLINK_ENV = {
'gitlink_api_address': getenv('GITLINK_API_HOST', ''),
'gitlink_api_diff_address': getenv('GITLINK_API_DIFF_HOST', ''),
}
GITCODE_ENV = {
'gitcode_api_address': getenv('GITCODE_API_HOST', ''),
'gitcode_api_diff_address': getenv('GITCODE_API_DIFF_HOST', ''),
}
DOCKER_ENV = {
'el7': getenv('EL7_DOCKER_IMAGE'),
'el8': getenv('EL8_DOCKER_IMAGE')
}
ERROR_REPORT_EMAIL = getenv('EORROR_TO', '')
DEFAULT_DK_RECEIVERS = getenv('DF_DK_TO', '')
DEFAULT_EMAIL_RECEIVERS = getenv('DF_EMAIL_TO', '')
NOTIFY = {
# todo yaml当中配置新的生产环境host
'host': getenv('NOTIFY_HOST', ''),
'user': getenv('NOTIFY_USER', ''), # 一般当前项目的项目名
'report_sender': getenv('NOTIFY_REPORT_SENDER', ''),
'email_sender': getenv('EMAIL_NOTIFY_SENDER'),
'dk_sender': getenv('DK_NOTIFY_SENDER', ''),
'dd_sender': getenv('DD_NOTIFY_SENDER', ''),
'dd_sender_issue': getenv('DD_NOTIFY_SENDER_ISSUE', ''),
'dd_sender_log': getenv('DD_NOTIFY_SENDER_LOG', ''),
'dd_sender_ghpr': getenv('DD_NOTIFY_SENDER_PR', ''),
}
# log level
ConfigsUtil.set_obfastapi_config('log_level', 'WARN')
# ConfigsUtil.set_obfastapi_config('log_name', 'mysql_test')
# ConfigsUtil.set_obfastapi_config('log_path', 'test.log')
SECRET_SCAN = getenv('SECRET_SCAN', True)
# Symmetric encryption key
DATA_ENCRYPT_KEY = getenv('DATA_ENCRYPT_KEY', '')
# web base_url
base_url = getenv('OB_ROBOT_BASE_URL', '')
CACHE_DB = {
"cerobot": {
"host": getenv("CEROBOT_REDIS_HOST", ""),
"port": getenv("CEROBOT_REDIS_PORT", 6379, int),
"password": getenv("CEROBOT_REDIS_PWD", "")
}
}
for key in CACHE_DB:
conf = RedisConfig(**CACHE_DB[key])
ConfigsUtil.set_redis_config(key, conf)
GIT_DEPTH = getenv('GIT_DEPTH', 100, int)
SECRET_SCAN_THREAD = getenv('SECRET_SCAN_THREAD', 4, int)
OCEANBASE = getenv('OCEANBASE', 'ob-mirror')
OBProjectIdInAone = getenv('OB_PROJECT_ID_AONE', 2015510, int)
strip_name = getenv("STRIP_NAME", "/.ce")
# observer repo cache
OCEANBASE_REPO_BASE_DIR = getenv('OCEANBASE_REPO_BASE_DIR', '')
# oceanbase internal repo, create github feature branch
OCEANBASE_REPO = getenv('OCEANBASE_REPO', '')
# oceanbase ce publish repo, the ob backup repo
# it need origin(oceanbase-ce-publish oceanbase) and github(github oceanbase) git configration
OCEANBASE_BACKUP_REPO = getenv('OCEANBASE_BACKUP_REPO', '')
# github repo
OCEANBASE_GITHUB_REPO = getenv('OCEANBASE_GITHUB_REPO', '')
ROLE_CHECK = getenv('ROLE_CHECK', True, bool)
# oss config
OSS_CONFIG = {
"id": getenv("OSSIV", ""),
"secret": getenv("OSSKEY", ""),
"bucket": getenv("OSSBUCKET", ""),
"endpoint": getenv("OSSENDPOINT", ""),
"download_base": getenv("DOWNLOAD_BASE", "")
}
class IssueConfig:
# Gitee issue删除替代策略配置
GITEE_DELETE_STRATEGY = "delete" # 可选值: delete, mark_deprecated, close_only, skip
# 重新尝试使用真正的DELETE API
# 废弃标记配置
DEPRECATED_PREFIX = "[已废弃]"
REPLACED_PREFIX = "[已替换]"
# 是否启用替换策略当无法更新时创建新issue
ENABLE_REPLACEMENT_STRATEGY = True
# 同步策略配置
SYNC_STRATEGIES = {
'GitHub': {
'delete_method': 'close', # close, deleteGitHub只支持close
'update_method': 'direct', # direct, replace
'supports_delete': False
},
'Gitee': {
'delete_method': 'delete', # 重新尝试使用DELETE API
'update_method': 'direct', # 重新尝试直接更新
'supports_delete': True, # 重新测试DELETE功能
'supports_update': True # 重新测试UPDATE功能
},
'Gitlink': {
'delete_method': 'close', # close, deleteGitlink只支持close
'update_method': 'direct', # direct, replace
'supports_delete': False
}
}
# 废弃issue模板
DEPRECATED_ISSUE_TEMPLATE = """# 🗑️ 此Issue已废弃
**废弃原因**: 此issue在源仓库中不存在已被自动标记为废弃状态
**废弃时间**: {timestamp}
**原始内容**:
{original_body}
---
*此标记由同步系统自动添加*
"""
# 替换issue模板
REPLACED_ISSUE_TEMPLATE = """# 🔄 此Issue已被替换
**替换原因**: 此issue的内容已过时已创建新issue替代
**替换时间**: {timestamp}
**原始内容**:
{original_body}
---
*此标记由同步系统自动添加*
"""
# 新创建issue的引用模板
REPLACEMENT_REFERENCE_TEMPLATE = "*此issue替换了 #{original_number}*\n\n{content}"
# Gitee特殊说明
GITEE_LIMITATION_NOTE = """
注意Gitee平台限制
- 由于Gitee API的更新/删除功能不可用系统采用以下策略
1. 对于多余的issue跳过删除保持现状
2. 对于需要更新的issue创建新issue旧issue保持不变
3. 建议定期手动清理Gitee仓库中的过期issue
"""

View File

@ -1,26 +1,26 @@
from .code import Code
from extras.obfastapi.frame import OBHTTPException as HTTPException
class Errors:
FORBIDDEN = HTTPException(Code.FORBIDDEN, '权限不足')
QUERY_FAILD = HTTPException(Code.OPERATION_FAILED, '记录查询失败')
INSERT_FAILD = HTTPException(Code.OPERATION_FAILED, '记录插入失败')
DELETE_FAILD = HTTPException(Code.OPERATION_FAILED, '记录删除失败')
UPDATE_FAILD = HTTPException(Code.OPERATION_FAILED, '记录更新失败')
METHOD_EORROR = HTTPException(Code.INVALID_PARAMS, '错误的请求方式')
NOT_INIT = HTTPException(555, '服务器缺少配置, 未能完成初始化')
class ErrorTemplate:
def ARGUMENT_LACK(did): return HTTPException(
Code.NOT_FOUND, '参数[%s]不能为空' % did)
def ARGUMENT_ERROR(did): return HTTPException(
Code.NOT_FOUND, '参数[%s]有错' % did)
def TIP_ARGUMENT_ERROR(did): return HTTPException(
Code.NOT_FOUND, '请输入正确的%s地址' % did)
from .code import Code
from extras.obfastapi.frame import OBHTTPException as HTTPException
class Errors:
FORBIDDEN = HTTPException(Code.FORBIDDEN, '权限不足')
QUERY_FAILD = HTTPException(Code.OPERATION_FAILED, '记录查询失败')
INSERT_FAILD = HTTPException(Code.OPERATION_FAILED, '记录插入失败')
DELETE_FAILD = HTTPException(Code.OPERATION_FAILED, '记录删除失败')
UPDATE_FAILD = HTTPException(Code.OPERATION_FAILED, '记录更新失败')
METHOD_EORROR = HTTPException(Code.INVALID_PARAMS, '错误的请求方式')
NOT_INIT = HTTPException(555, '服务器缺少配置, 未能完成初始化')
class ErrorTemplate:
def ARGUMENT_LACK(did): return HTTPException(
Code.NOT_FOUND, '参数[%s]不能为空' % did)
def ARGUMENT_ERROR(did): return HTTPException(
Code.NOT_FOUND, '参数[%s]有错' % did)
def TIP_ARGUMENT_ERROR(did): return HTTPException(
Code.NOT_FOUND, '请输入正确的%s地址' % did)

View File

@ -1,95 +1,95 @@
from enum import Enum, unique
from pydantic import BaseModel
from typing import Optional, Generic, TypeVar, Dict, Any
Data = TypeVar('Data')
@unique
class Status(Enum):
# 成功返回
SUCCESS = (0, "操作成功")
# 请求异常
REPO_ADDR_ILLEGAL = (10001, "仓库地址格式有误,请检查")
REPO_EXISTS = (10002, "仓库已存在,请勿重复创建。如果同步方向不同,请更换易识别名称再次创建")
BRANCH_EXISTS = (10003, "分支已存在,请勿重复绑定")
GRANULARITY_ERROR = (10004, "仓库粒度同步,无需添加分支信息")
NOT_FOUND = (10005, "分支信息获取为空")
NOT_ENABLE = (10006, "仓库/分支未启用同步,请检查更新同步启用状态")
SYNC_GRAN_ILLEGAL = (10007, "sync_granularity: 1 表示仓库粒度的同步, 2 表示分支粒度的同步")
SYNC_DIRE_ILLEGAL = (10008, "sync_direction: 1 表示内部仓库同步到外部, 2 表示外部仓库同步到内部")
REPO_NULL = (10009, "仓库未绑定,请先绑定仓库,再绑定分支")
REPO_NOTFOUND = (10010, "未查找到仓库")
GRANULARITY_DELETE = (10011, "仓库粒度同步,没有分支可解绑")
BRANCH_DELETE = (10012, "仓库中不存在此分支")
NOT_DATA = (10013, "没有数据")
GRANULARITY = (10014, "仓库粒度同步,没有分支信息")
CHECK_IN = (10015, "请检查输入的仓库和分支ID信息是否对应")
# git执行异常
PERMISSION_DENIED = (20001, "SSH 密钥未授权或未添加")
REPO_NOT_FOUND = (20002, "仓库不存在或私有仓库访问权限不足")
RESOLVE_HOST_FAIL = (20003, "无法解析主机")
CONNECT_TIME_OUT = (20004, "连接超时")
AUTH_FAIL = (20005, "认证失败 (用户名和密码、个人访问令牌、SSH 密钥等)")
CREATE_WORK_TREE_FAIL = (20006, "没有权限在指定的本地目录创建文件或目录")
DIRECTORY_EXIST = (20007, "本地目录冲突 (本地已存在同名目录,无法创建新的工作树目录)")
NOT_REPO = (20008, "当前的工作目录不是一个git仓库")
NOT_BRANCH = (20009, "分支不存在")
PUST_REJECT = (20010, "推送冲突")
REFUSE_PUST = (20011, "推送到受保护的分支被拒绝")
UNKNOWN_ERROR = (20012, "Unknown git error.")
@property
def code(self) -> int:
# 返回状态码信息
return self.value[0]
@property
def msg(self) -> str:
# 返回状态码说明信息
return self.value[1]
git_error_mapping = {
"Permission denied": Status.PERMISSION_DENIED,
"Repository not found": Status.REPO_NOT_FOUND,
"not a git repository": Status.REPO_NOT_FOUND,
"Could not resolve host": Status.RESOLVE_HOST_FAIL,
"Connection timed out": Status.CONNECT_TIME_OUT,
"Could not read from remote repository.": Status.REPO_NOT_FOUND,
"Authentication failed": Status.AUTH_FAIL,
"could not create work tree": Status.CREATE_WORK_TREE_FAIL,
"already exists and is not an empty directory": Status.DIRECTORY_EXIST,
"The current directory is not a git repository": Status.NOT_REPO,
"couldn't find remote ref": Status.NOT_BRANCH,
"is not a commit and a branch": Status.NOT_BRANCH,
"[rejected]": Status.PUST_REJECT,
"refusing to update": Status.REFUSE_PUST
}
class SYNCException(Exception):
def __init__(self, status: Status):
self.code_status = status.code
self.status_msg = status.msg
class SYNCResponse(BaseModel):
code_status: Optional[int] = 0
data: Optional[Data] = None
total: Optional[int] = None
msg: Optional[str] = ''
class GITMSGException(Exception):
def __init__(self, status: Status, repo='', branch=''):
self.status = status.code
self.msg = status.msg
# class SYNCResponse(GenericModel, Generic[Data]):
# code_status: int = 200
# data: Optional[Data] = None
# msg: str = ''
# success: bool = True
# finished: bool = True
from enum import Enum, unique
from pydantic import BaseModel
from typing import Optional, Generic, TypeVar, Dict, Any
Data = TypeVar('Data')
@unique
class Status(Enum):
# 成功返回
SUCCESS = (0, "操作成功")
# 请求异常
REPO_ADDR_ILLEGAL = (10001, "仓库地址格式有误,请检查")
REPO_EXISTS = (10002, "仓库已存在,请勿重复创建。如果同步方向不同,请更换易识别名称再次创建")
BRANCH_EXISTS = (10003, "分支已存在,请勿重复绑定")
GRANULARITY_ERROR = (10004, "仓库粒度同步,无需添加分支信息")
NOT_FOUND = (10005, "分支信息获取为空")
NOT_ENABLE = (10006, "仓库/分支未启用同步,请检查更新同步启用状态")
SYNC_GRAN_ILLEGAL = (10007, "sync_granularity: 1 表示仓库粒度的同步, 2 表示分支粒度的同步")
SYNC_DIRE_ILLEGAL = (10008, "sync_direction: 1 表示内部仓库同步到外部, 2 表示外部仓库同步到内部")
REPO_NULL = (10009, "仓库未绑定,请先绑定仓库,再绑定分支")
REPO_NOTFOUND = (10010, "未查找到仓库")
GRANULARITY_DELETE = (10011, "仓库粒度同步,没有分支可解绑")
BRANCH_DELETE = (10012, "仓库中不存在此分支")
NOT_DATA = (10013, "没有数据")
GRANULARITY = (10014, "仓库粒度同步,没有分支信息")
CHECK_IN = (10015, "请检查输入的仓库和分支ID信息是否对应")
# git执行异常
PERMISSION_DENIED = (20001, "SSH 密钥未授权或未添加")
REPO_NOT_FOUND = (20002, "仓库不存在或私有仓库访问权限不足")
RESOLVE_HOST_FAIL = (20003, "无法解析主机")
CONNECT_TIME_OUT = (20004, "连接超时")
AUTH_FAIL = (20005, "认证失败 (用户名和密码、个人访问令牌、SSH 密钥等)")
CREATE_WORK_TREE_FAIL = (20006, "没有权限在指定的本地目录创建文件或目录")
DIRECTORY_EXIST = (20007, "本地目录冲突 (本地已存在同名目录,无法创建新的工作树目录)")
NOT_REPO = (20008, "当前的工作目录不是一个git仓库")
NOT_BRANCH = (20009, "分支不存在")
PUST_REJECT = (20010, "推送冲突")
REFUSE_PUST = (20011, "推送到受保护的分支被拒绝")
UNKNOWN_ERROR = (20012, "Unknown git error.")
@property
def code(self) -> int:
# 返回状态码信息
return self.value[0]
@property
def msg(self) -> str:
# 返回状态码说明信息
return self.value[1]
git_error_mapping = {
"Permission denied": Status.PERMISSION_DENIED,
"Repository not found": Status.REPO_NOT_FOUND,
"not a git repository": Status.REPO_NOT_FOUND,
"Could not resolve host": Status.RESOLVE_HOST_FAIL,
"Connection timed out": Status.CONNECT_TIME_OUT,
"Could not read from remote repository.": Status.REPO_NOT_FOUND,
"Authentication failed": Status.AUTH_FAIL,
"could not create work tree": Status.CREATE_WORK_TREE_FAIL,
"already exists and is not an empty directory": Status.DIRECTORY_EXIST,
"The current directory is not a git repository": Status.NOT_REPO,
"couldn't find remote ref": Status.NOT_BRANCH,
"is not a commit and a branch": Status.NOT_BRANCH,
"[rejected]": Status.PUST_REJECT,
"refusing to update": Status.REFUSE_PUST
}
class SYNCException(Exception):
def __init__(self, status: Status):
self.code_status = status.code
self.status_msg = status.msg
class SYNCResponse(BaseModel):
code_status: Optional[int] = 0
data: Optional[Data] = None
total: Optional[int] = None
msg: Optional[str] = ''
class GITMSGException(Exception):
def __init__(self, status: Status, repo='', branch=''):
self.status = status.code
self.msg = status.msg
# class SYNCResponse(GenericModel, Generic[Data]):
# code_status: int = 200
# data: Optional[Data] = None
# msg: str = ''
# success: bool = True
# finished: bool = True

View File

@ -1,24 +1,24 @@
import json
import requests
def Fetch(url: str, way: str, query=None, header=None, data=None):
if url == None:
return None
if way == 'Get':
response = requests.get(url=url, params=query, headers=header)
return response.json()
elif way == 'Post':
response = requests.post(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
elif way == 'Patch':
response = requests.patch(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
elif way == 'Put':
response = requests.put(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
else:
return None
import json
import requests
def Fetch(url: str, way: str, query=None, header=None, data=None):
if url == None:
return None
if way == 'Get':
response = requests.get(url=url, params=query, headers=header)
return response.json()
elif way == 'Post':
response = requests.post(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
elif way == 'Patch':
response = requests.patch(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
elif way == 'Put':
response = requests.put(
url=url, params=query, headers=header, data=json.dumps(data))
return response.json()
else:
return None

Some files were not shown because too many files have changed in this diff Show More