├── .github ├── ISSUE_TEMPLATE │ └── question.md ├── pull-request-template.md ├── release-drafter.yml └── workflows │ ├── TOC.yml │ ├── docker-image.yml │ ├── go-binary-release.yml │ ├── lint.yaml │ ├── reademe-contributors.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config.example.yml ├── config ├── config.go └── prompt.go ├── docker-compose.yml ├── docs └── userGuide.md ├── go.mod ├── go.sum ├── main.go ├── pkg ├── cache │ ├── user_base.go │ ├── user_chatid.go │ ├── user_context.go │ ├── user_mode.go │ └── user_requese.go ├── chatgpt │ ├── LICENSE │ ├── README.md │ ├── chatgpt.go │ ├── chatgpt_test.go │ ├── context.go │ ├── context_test.go │ ├── errors.go │ ├── export.go │ ├── format.go │ ├── go.mod │ ├── go.sum │ └── models.go ├── db │ ├── chat.go │ └── sqlite.go ├── dingbot │ ├── client.go │ ├── client_test.go │ └── dingbot.go ├── logger │ └── logger.go ├── ops │ └── opstools.go └── process │ ├── db.go │ ├── image.go │ ├── opstools.go │ ├── process_request.go │ └── prompt.go ├── prompt.yml ├── public ├── balance.go ├── chat.go ├── public.go ├── tools.go └── tools_test.go └── scripts └── goimports-reviser.sh /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🙋 问题交流 3 | about: 有任何问题,都可以在这里交流。 4 | title: "🙋 简明扼要起个标题" 5 | labels: ["question"] 6 | --- 7 | 8 | ## 问题反馈 9 | 10 | - 搜索打开和关闭的 [GitHub 问题](https://github.com/eryajf/chatgpt-dingtalk/issues),请勿重复提交issue。 11 | 12 | **重要:提交问题时,请务必带上输出日志,以及个人排查的成果。** -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | 2 | **在提出此拉取请求时,我确认了以下几点(请复选框):** 3 | 4 | - [ ] 我已阅读并理解[贡献者指南](https://github.com/eryajf/chatgpt-dingtalk/blob/main/CONTRIBUTING.md)。 5 | - [ ] 我已检查没有与此请求重复的拉取请求。 6 | - [ ] 我已经考虑过,并确认这份呈件对其他人很有价值。 7 | - [ ] 我接受此提交可能不会被使用,并根据维护人员的意愿关闭拉取请求。 -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 3 | tag-template: 'v$NEXT_PATCH_VERSION' 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - 'kind/feature' 12 | - title: '🐛 Bug Fixes' 13 | labels: 14 | - 'fix' 15 | - 'bugfix' 16 | - 'bug' 17 | - 'regression' 18 | - 'kind/bug' 19 | - title: 📝 Documentation updates 20 | labels: 21 | - 'doc' 22 | - 'documentation' 23 | - 'kind/doc' 24 | - title: 👻 Maintenance 25 | labels: 26 | - chore 27 | - dependencies 28 | - 'kind/chore' 29 | - 'kind/dep' 30 | - title: 🚦 Tests 31 | labels: 32 | - test 33 | - tests 34 | exclude-labels: 35 | - reverted 36 | - no-changelog 37 | - skip-changelog 38 | - invalid 39 | change-template: '* $TITLE (#$NUMBER) @$AUTHOR' 40 | template: | 41 | ## What’s Changed 42 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/TOC.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Automatic Generation TOC 3 | jobs: 4 | generateTOC: 5 | name: TOC Generator 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: technote-space/toc-generator@v4 9 | with: 10 | TOC_TITLE: "**目录**" 11 | MAX_HEADER_LEVEL: 4 -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build docker image 4 | 5 | # Controls when the action will run. 6 | on: 7 | push: 8 | branches: 9 | - main 10 | release: 11 | types: [created,published] # 表示在创建新的 Release 时触发 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | # 可以手动触发 15 | workflow_dispatch: 16 | inputs: 17 | logLevel: 18 | description: 'Log level' 19 | required: true 20 | default: 'warning' 21 | tags: 22 | description: 'Test scenario tags' 23 | 24 | jobs: 25 | buildx: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | 31 | - name: Inject slug/short variables 32 | uses: rlespinasse/github-slug-action@v4 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v1 36 | 37 | - name: Set up Docker Buildx 38 | id: buildx 39 | uses: docker/setup-buildx-action@v1 40 | 41 | - name: Available platforms 42 | run: echo ${{ steps.buildx.outputs.platforms }} 43 | 44 | - name: Login to DockerHub 45 | uses: docker/login-action@v1 46 | with: 47 | username: ${{ secrets.DOCKERHUB_USERNAME }} 48 | password: ${{ secrets.DOCKERHUB_TOKEN }} 49 | 50 | - name: Build and push to DockerHub 51 | uses: docker/build-push-action@v2 52 | with: 53 | context: . 54 | file: ./Dockerfile 55 | # 所需要的体系结构,可以在 Available platforms 步骤中获取所有的可用架构 56 | platforms: linux/amd64,linux/arm64/v8 57 | # 镜像推送时间 58 | push: ${{ github.event_name != 'pull_request' }} 59 | # 给清单打上多个标签 60 | tags: | 61 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-dingtalk:${{ env.GITHUB_REF_NAME }} 62 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-dingtalk:latest 63 | 64 | # 镜像推送到 阿里云仓库 65 | - name: Login to the Ali Registry 66 | uses: docker/login-action@v2 67 | with: 68 | registry: registry.cn-hangzhou.aliyuncs.com 69 | username: ${{ secrets.ALIHUB_USERNAME }} 70 | password: ${{ secrets.ALIHUB_TOKEN }} 71 | 72 | - name: Build and push to Ali 73 | uses: docker/build-push-action@v3 74 | with: 75 | context: . 76 | push: true 77 | platforms: linux/amd64,linux/arm64 78 | tags: | 79 | registry.cn-hangzhou.aliyuncs.com/${{ secrets.ALIHUB_USERNAME }}/chatgpt-dingtalk:${{ env.GITHUB_REF_NAME }} 80 | registry.cn-hangzhou.aliyuncs.com/${{ secrets.ALIHUB_USERNAME }}/chatgpt-dingtalk:latest -------------------------------------------------------------------------------- /.github/workflows/go-binary-release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [created,published] # 表示在创建新的 Release 时触发 6 | 7 | jobs: 8 | build-go-binary: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | goos: [linux, windows, darwin] # 需要打包的系统 13 | goarch: [amd64, arm64] # 需要打包的架构 14 | exclude: # 排除某些平台和架构 15 | - goarch: arm64 16 | goos: windows 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: wangyoucao577/go-release-action@v1.30 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} # 一个默认的变量,用来实现往 Release 中添加文件 22 | goos: ${{ matrix.goos }} 23 | goarch: ${{ matrix.goarch }} 24 | goversion: 1.18 # 可以指定编译使用的 Golang 版本 25 | binary_name: "chatgpt-dingtalk" # 可以指定二进制文件的名称 26 | extra_files: LICENSE config.example.yml prompt.yml README.md # 需要包含的额外文件 -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/** 8 | pull_request: 9 | branches: 10 | - main 11 | - release/** 12 | permissions: read-all 13 | jobs: 14 | gofmt: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup Go Environment 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: '1.20.3' 23 | - name: Run gofmt Check 24 | working-directory: ./ 25 | run: | 26 | diffs=`gofmt -l .` 27 | if [[ -n $diffs ]]; then 28 | echo "Files are not formatted by gofmt:" 29 | echo $diffs 30 | exit 1 31 | fi 32 | golint: 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 10 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: Setup Go Environment 38 | uses: actions/setup-go@v3 39 | with: 40 | go-version: '1.20.3' 41 | - name: Download golangci-lint 42 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 43 | - name: Run Golang Linters 44 | working-directory: ./ 45 | run: | 46 | PATH=${PATH}:$(go env GOPATH)/bin make lint 47 | -------------------------------------------------------------------------------- /.github/workflows/reademe-contributors.yml: -------------------------------------------------------------------------------- 1 | name: Generate a list of contributors 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | contrib-readme-en-job: 10 | runs-on: ubuntu-latest 11 | name: A job to automate contrib in readme 12 | steps: 13 | - name: Contribute List 14 | uses: akhilmhdh/contributors-readme-action@v2.3.6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | with: 18 | image_size: 75 19 | columns_per_row: 8 20 | commit_message: '🫶 更新贡献者列表' -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | # pull_request event is required only for autolabeler 9 | pull_request: 10 | # Only following types are handled by the action, but one can default to all as well 11 | types: [opened, reopened, synchronize] 12 | # pull_request_target event is required for autolabeler to support PRs from forks 13 | # pull_request_target: 14 | # types: [opened, reopened, synchronize] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | update_release_draft: 21 | permissions: 22 | contents: write # for release-drafter/release-drafter to create a github release 23 | pull-requests: write # for release-drafter/release-drafter to add label to PR 24 | runs-on: ubuntu-latest 25 | steps: 26 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 27 | #- name: Set GHE_HOST 28 | # run: | 29 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 30 | 31 | # Drafts your next Release notes as Pull Requests are merged into "master" 32 | - uses: release-drafter/release-drafter@v5 33 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 34 | # with: 35 | # config-name: my-config.yml 36 | # disable-autolabeler: true 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .idea/ 8 | .vscode/ 9 | 10 | chatgpt-dingtalk 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | config.yml 21 | dingtalkbot.sqlite 22 | tmp 23 | test/ 24 | images/ 25 | data/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-added-large-files -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献者指南 2 | 3 | 欢迎反馈、bug报告和拉取请求,可点击[issue](https://github.com/eryajf/chatgpt-dingtalk/issues) 提交. 4 | 5 | 如果你是第一次进行GitHub的协作,可参阅: [协同开发流程](https://eryajf.github.io/HowToStartOpenSource/views/01-basic-content/03-collaborative-development-process.html) 6 | 7 | ## 注意事项 8 | 9 | - 如果你的变更中新增或者减少了配置,那么需要注意有这么几个地方需要同步调整: 10 | 11 | - [config.go](https://github.com/eryajf/chatgpt-dingtalk/blob/main/config/config.go) 12 | - [config.example.yml](https://github.com/eryajf/chatgpt-dingtalk/blob/main/config.example.yml) 13 | - [docker-compose.yml](https://github.com/eryajf/chatgpt-dingtalk/blob/main/docker-compose.yml) 14 | - [README.md](https://github.com/eryajf/chatgpt-dingtalk/blob/main/README.md) 15 | - docker [启动命令](https://github.com/eryajf/chatgpt-dingtalk/blob/main/README.md#docker%E9%83%A8%E7%BD%B2)中要添加。 16 | - [配置文件说明](https://github.com/eryajf/chatgpt-dingtalk/blob/main/README.md#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E)要添加。 17 | 18 | 一定要检查这几个地方,而且注意务必加好注释,否则将会影响用户升级体验新功能。 19 | 20 | - 关于配置管理还有一个很重要的点在于,`config.example.yml`中的配置务必配置为最大权限,以免出现用户首次部署就无法走到正常逻辑的情况。 21 | 22 | - 请务必检查你的提交,是否包含secret,api_key之类的信息,如果要贴示例,注意数据脱敏。 23 | 24 | - 如果新增了功能性的模板,则务必在[使用指南](./docs/userGuide.md)中添加对应说明文档。 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.5-alpine3.20 AS builder 2 | 3 | # ENV GOPROXY https://goproxy.io 4 | 5 | RUN mkdir /app 6 | ADD . /app/ 7 | WORKDIR /app 8 | RUN go build -o chatgpt-dingtalk . 9 | 10 | FROM alpine:3.20 11 | 12 | ARG TZ="Asia/Shanghai" 13 | 14 | ENV TZ ${TZ} 15 | 16 | RUN mkdir /app && apk upgrade \ 17 | && apk add bash tzdata \ 18 | && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ 19 | && echo ${TZ} > /etc/timezone 20 | 21 | WORKDIR /app 22 | COPY --from=builder /app/ . 23 | RUN chmod +x chatgpt-dingtalk && cp config.example.yml config.yml 24 | 25 | CMD ./chatgpt-dingtalk -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 二丫讲梵 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | run: 4 | GIN_MODE=release go run main.go 5 | 6 | build: 7 | go build -o chatgpt-dingtalk main.go 8 | 9 | build-linux: 10 | CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o chatgpt-dingtalk main.go 11 | 12 | build-linux-arm: 13 | CGO_ENABLED=0 GOARCH=arm64 GOOS=linux go build -o chatgpt-dingtalk main.go 14 | 15 | lint: 16 | env GOGC=25 golangci-lint run --fix -j 8 --timeout 10m -v ./... 17 | 18 | goimports: 19 | @bash ./scripts/goimports-reviser.sh 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 🚀 ChatGPT DingTalk 🚀 4 |

5 | 6 |

🌉 基于GO语言实现的钉钉集成ChatGPT机器人 🌉

7 | 8 |
9 | 10 | [![Auth](https://img.shields.io/badge/Auth-eryajf-ff69b4)](https://github.com/eryajf) 11 | [![Go Version](https://img.shields.io/github/go-mod/go-version/eryajf/chatgpt-dingtalk)](https://github.com/eryajf/chatgpt-dingtalk) 12 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/eryajf/chatgpt-dingtalk)](https://github.com/eryajf/chatgpt-dingtalk/pulls) 13 | [![GitHub Pull Requests](https://img.shields.io/github/stars/eryajf/chatgpt-dingtalk)](https://github.com/eryajf/chatgpt-dingtalk/stargazers) 14 | [![HitCount](https://views.whatilearened.today/views/github/eryajf/chatgpt-dingtalk.svg)](https://github.com/eryajf/chatgpt-dingtalk) 15 | [![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/eryajf/chatgpt-dingtalk)](https://hub.docker.com/r/eryajf/chatgpt-dingtalk) 16 | [![Docker Pulls](https://img.shields.io/docker/pulls/eryajf/chatgpt-dingtalk)](https://hub.docker.com/r/eryajf/chatgpt-dingtalk) 17 | [![GitHub license](https://img.shields.io/github/license/eryajf/chatgpt-dingtalk)](https://github.com/eryajf/chatgpt-dingtalk/blob/main/LICENSE) 18 | 19 |
20 | 21 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | --- 31 | 32 | 33 | 34 | **目录** 35 | 36 | - [前言](#%E5%89%8D%E8%A8%80) 37 | - [功能介绍](#%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D) 38 | - [使用前提](#%E4%BD%BF%E7%94%A8%E5%89%8D%E6%8F%90) 39 | - [使用教程](#%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B) 40 | - [第一步,部署应用](#%E7%AC%AC%E4%B8%80%E6%AD%A5%E9%83%A8%E7%BD%B2%E5%BA%94%E7%94%A8) 41 | - [docker 部署](#docker-%E9%83%A8%E7%BD%B2) 42 | - [二进制部署](#%E4%BA%8C%E8%BF%9B%E5%88%B6%E9%83%A8%E7%BD%B2) 43 | - [第二步,添加应用](#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E6%B7%BB%E5%8A%A0%E5%BA%94%E7%94%A8) 44 | - [亮点特色](#%E4%BA%AE%E7%82%B9%E7%89%B9%E8%89%B2) 45 | - [与机器人私聊](#%E4%B8%8E%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%A7%81%E8%81%8A) 46 | - [帮助列表](#%E5%B8%AE%E5%8A%A9%E5%88%97%E8%A1%A8) 47 | - [切换模式](#%E5%88%87%E6%8D%A2%E6%A8%A1%E5%BC%8F) 48 | - [查询余额](#%E6%9F%A5%E8%AF%A2%E4%BD%99%E9%A2%9D) 49 | - [日常问题](#%E6%97%A5%E5%B8%B8%E9%97%AE%E9%A2%98) 50 | - [通过内置 prompt 聊天](#%E9%80%9A%E8%BF%87%E5%86%85%E7%BD%AE-prompt-%E8%81%8A%E5%A4%A9) 51 | - [生成图片](#%E7%94%9F%E6%88%90%E5%9B%BE%E7%89%87) 52 | - [支持 gpt-4](#%E6%94%AF%E6%8C%81-gpt-4) 53 | - [本地开发](#%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91) 54 | - [配置文件说明](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E) 55 | - [常见问题](#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) 56 | - [进群交流](#%E8%BF%9B%E7%BE%A4%E4%BA%A4%E6%B5%81) 57 | - [感谢](#%E6%84%9F%E8%B0%A2) 58 | - [赞赏](#%E8%B5%9E%E8%B5%8F) 59 | - [高光时刻](#%E9%AB%98%E5%85%89%E6%97%B6%E5%88%BB) 60 | - [Star 历史](#star-%E5%8E%86%E5%8F%B2) 61 | - [贡献者列表](#%E8%B4%A1%E7%8C%AE%E8%80%85%E5%88%97%E8%A1%A8) 62 | 63 | 64 | 65 | ## 前言 66 | 67 | 本项目可以助你将 GPT 机器人集成到钉钉群聊当中。当前默认模型为`gpt-3.5`,支持`gpt-4`以及`gpt-4o-mini`。同时支持 Azure-OpenAI。 68 | 69 | > - `📢 注意`:当下部署以及配置流程都已非常成熟,文档和 issue 中基本都覆盖到了,因此不再回答任何项目安装部署与配置使用上的问题,如果完全不懂,可考虑通过 **[邮箱](mailto:eryajf@163.com)** 联系我进行付费的技术支持。 70 | > 71 | > - `📢 注意`:这个项目所有的功能,都汇聚在[使用指南](./docs/userGuide.md)中,请务必仔细阅读,以体验其完整精髓。 72 | 73 | 🥳 **欢迎关注我的其他开源项目:** 74 | 75 | > - [Go-Ldap-Admin](https://github.com/eryajf/go-ldap-admin):🌉 基于 Go+Vue 实现的 openLDAP 后台管理项目。 76 | > - [learning-weekly](https://github.com/eryajf/learning-weekly):📝 周刊内容以运维技术和 Go 语言周边为主,辅以 GitHub 上优秀项目或他人优秀经验。 77 | > - [HowToStartOpenSource](https://github.com/eryajf/HowToStartOpenSource):🌈 GitHub 开源项目维护协同指南。 78 | > - [read-list](https://github.com/eryajf/read-list):📖 优质内容订阅,阅读方为根本 79 | > - [awesome-github-profile-readme-chinese](https://github.com/eryajf/awesome-github-profile-readme-chinese):🦩 优秀的中文区个人主页搜集 80 | 81 | 🚜 我还创建了一个项目 **[awesome-chatgpt-answer](https://github.com/eryajf/awesome-chatgpt-answer)** :记录那些问得好,答得妙的时刻,欢迎提交你与 ChatGPT 交互过程中遇到的那些精妙对话。 82 | 83 | ⚗️ openai 官方提供了一个 **[状态页](https://status.openai.com/)** 来呈现当前 openAI 服务的状态,同时如果有问题发布公告也会在这个页面,如果你感觉它有问题了,可以在这个页面看看。 84 | 85 | **赞助商** 86 | 87 | [![](https://cdn.jsdelivr.net/gh/eryajf/tu@main/img/image_20241231_214509.webp)](https://gpt302.saaslink.net/fGvlvo/) 88 | 89 | > [302.AI](https://gpt302.saaslink.net/fGvlvo) 是一个按需付费的一站式企业级AI应用平台,开放平台,开源生态。 90 | > 91 | > - [点击注册](https://gpt302.saaslink.net/fGvlvo): 立即获得 1PTC(1PTC=1 美金,约为 7 人民币)代币。 92 | > - 集合了最新最全的AI模型和品牌,包括但不限于语言模型、图像模型、声音模型、视频模型。 93 | > - 在基础模型上进行深度应用开发,做到让小白用户都可以零门槛上手使用,无需学习成本。 94 | > - 零月费,所有功能按需付费,全面开放,做到真正的门槛低,上限高。 95 | > - 创新的使用模式,管理和使用分离,面向团队和中小企业,一人管理,多人使用。 96 | > - 所有AI能力均提供API接入,所有应用开源支持自行定制(进行中)。 97 | > - 强大的开发团队,每周推出2-3个新应用,平台功能每日更新。 98 | 99 | ## 功能介绍 100 | 101 | - 🚀 帮助菜单:通过发送 `帮助` 将看到帮助列表,[🖼 查看示例](#%E5%B8%AE%E5%8A%A9%E5%88%97%E8%A1%A8) 102 | - 🥷 私聊:支持与机器人单独私聊(无需艾特),[🖼 查看示例](#%E4%B8%8E%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%A7%81%E8%81%8A) 103 | - 💬 群聊:支持在群里艾特机器人进行对话 104 | - 🙋 单聊模式:每次对话都是一次新的对话,没有历史聊天上下文联系 105 | - 🗣 串聊模式:带上下文理解的对话模式 106 | - 🎨 图片生成:通过发送 `#图片`关键字开头的内容进行生成图片,[🖼 查看示例](#%E7%94%9F%E6%88%90%E5%9B%BE%E7%89%87) 107 | - 🎭 角色扮演:支持场景模式,通过 `#周报` 的方式触发内置 prompt 模板 [🖼 查看示例](#%E9%80%9A%E8%BF%87%E5%86%85%E7%BD%AEprompt%E8%81%8A%E5%A4%A9) 108 | - 🧑‍💻 频率限制:通过配置指定,自定义单个用户单日最大对话次数 109 | - 💵 余额查询:通过发送 `余额` 关键字查询当前 key 所剩额度,[🖼 查看示例](#%E6%9F%A5%E8%AF%A2%E4%BD%99%E9%A2%9D) 110 | - 🔗 自定义 api 域名:通过配置指定,解决国内服务器无法直接访问 openai 的问题 111 | - 🪜 添加代理:通过配置指定,通过给应用注入代理解决国内服务器无法访问的问题 112 | - 👐 默认模式:支持自定义默认的聊天模式,通过配置化指定 113 | - 📝 查询对话:通过发送`#查对话 username:xxx`查询 xxx 的对话历史,可在线预览,可下载到本地 114 | - 👹 白名单机制:通过配置指定,支持指定群组名称和用户名称作为白名单,从而实现可控范围与机器人对话 115 | - 💂‍♀️ 管理员机制:通过配置指定管理员,部分敏感操作,以及一些应用配置,管理员有权限进行操作 116 | - ㊙️ 敏感词过滤:通过配置指定敏感词,提问时触发,则不允许提问,回答的内容中触发,则以 🚫 代替 117 | - 🚇 stream 模式:指定钉钉的 stream 模式,目前钉钉已全量开放该功能,项目也默认以此模式启动 118 | 119 | ## 使用前提 120 | 121 | - 有 Openai 账号,并且创建好`api_key`,注册相关事项可以参考[此文章](https://juejin.cn/post/7173447848292253704) 。访问[这里](https://beta.openai.com/account/api-keys),申请个人秘钥。 122 | - 在钉钉开发者后台创建应用,在应用的消息推送功能块添加机器人,将消息接收模式指定为 stream 模式。 123 | 124 | ## 使用教程 125 | 126 | ### 第一步,部署应用 127 | 128 | #### docker 部署 129 | 130 | 推荐你使用 docker 快速运行本项目。 131 | 132 | ``` 133 | 第一种:基于环境变量运行 134 | # 运行项目 135 | $ docker run -itd --name chatgpt -p 8090:8090 \ 136 | -v ./data:/app/data --add-host="host.docker.internal:host-gateway" \ 137 | -e LOG_LEVEL="info" -e APIKEY=换成你的key -e BASE_URL="" \ 138 | -e MODEL="gpt-3.5-turbo" -e SESSION_TIMEOUT=600 \ 139 | -e MAX_QUESTION_LENL=2048 -e MAX_ANSWER_LEN=2048 -e MAX_TEXT=4096 \ 140 | -e HTTP_PROXY="http://host.docker.internal:15732" \ 141 | -e DEFAULT_MODE="单聊" -e MAX_REQUEST=0 -e PORT=8090 \ 142 | -e SERVICE_URL="你当前服务外网可访问的URL" -e CHAT_TYPE="0" \ 143 | -e ALLOW_GROUPS=a,b -e ALLOW_OUTGOING_GROUPS=a,b -e ALLOW_USERS=a,b -e DENY_USERS=a,b -e VIP_USERS=a,b -e ADMIN_USERS=a,b -e APP_SECRETS="xxx,yyy" \ 144 | -e SENSITIVE_WORDS="aa,bb" -e RUN_MODE="http" \ 145 | -e AZURE_ON="false" -e AZURE_API_VERSION="" -e AZURE_RESOURCE_NAME="" \ 146 | -e AZURE_DEPLOYMENT_NAME="" -e AZURE_OPENAI_TOKEN="" \ 147 | -e DINGTALK_CREDENTIALS="your_client_id1:secret1,your_client_id2:secret2" \ 148 | -e HELP="欢迎使用本工具\n\n你可以查看:[用户指南](https://github.com/eryajf/chatgpt-dingtalk/blob/main/docs/userGuide.md)\n\n这是一个[开源项目](https://github.com/eryajf/chatgpt-dingtalk/) 149 | ,觉得不错你可以来波素质三连." \ 150 | --restart=always registry.cn-hangzhou.aliyuncs.com/eryajf/chatgpt-dingtalk 151 | ``` 152 | 153 | > 运行命令中映射的配置文件参考下边的[配置文件说明](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E)。 154 | 155 | - `📢 注意:`如果使用 docker 部署,那么 PORT 参数不需要进行任何调整。 156 | - `📢 注意:`ALLOW_GROUPS,ALLOW_USERS,DENY_USERS,VIP_USERS,ADMIN_USERS 参数为数组,如果需要指定多个,可用英文逗号分割。outgoing 机器人模式下这些参数无效。 157 | - `📢 注意:`如果服务器节点本身就在国外或者自定义了`BASE_URL`,那么就把`HTTP_PROXY`参数留空即可。 158 | - `📢 注意:`如果使用 docker 部署,那么 proxy 地址可以直接使用如上方式部署,`host.docker.internal`会指向容器所在宿主机的 IP,只需要更改端口为你的代理端口即可。参见:[Docker 容器如何优雅地访问宿主机网络](https://wiki.eryajf.net/pages/674f53/) 159 | 160 | ``` 161 | 第二种:基于配置文件挂载运行 162 | # 复制配置文件,根据自己实际情况,调整配置里的内容 163 | $ cp config.example.yml config.yml # 其中 config.example.yml 从项目的根目录获取 164 | 165 | # 运行项目 166 | $ docker run -itd --name chatgpt -p 8090:8090 -v `pwd`/config.yml:/app/config.yml --restart=always registry.cn-hangzhou.aliyuncs.com/eryajf/chatgpt-dingtalk 167 | ``` 168 | 169 | 其中配置文件参考下边的配置文件说明。 170 | 171 | ``` 172 | 第三种:使用 docker compose 运行 173 | $ wget https://raw.githubusercontent.com/eryajf/chatgpt-dingtalk/main/docker-compose.yml 174 | 175 | $ vim docker-compose.yml # 编辑 APIKEY 等信息 176 | 177 | $ docker compose up -d 178 | ``` 179 | 180 | 之前部署完成之后还有一个配置 Nginx 的步骤,现在将模式默认指定为 stream 模式,因此不再需要配置 Nginx。 181 | 182 | #### 二进制部署 183 | 184 | 如果你想通过命令行直接部署,可以直接下载 release 中的[压缩包](https://github.com/eryajf/chatgpt-dingtalk/releases) ,请根据自己系统以及架构选择合适的压缩包,下载之后直接解压运行。 185 | 186 | 下载之后,在本地解压,即可看到可执行程序,与配置文件: 187 | 188 | ```sh 189 | $ tar xf chatgpt-dingtalk-v0.0.4-darwin-arm64.tar.gz 190 | $ cd chatgpt-dingtalk-v0.0.4-darwin-arm64 191 | $ cp config.example.yml config.yml 192 | $ ./chatgpt-dingtalk # 直接运行 193 | 194 | # 如果要守护在后台运行 195 | $ nohup ./chatgpt-dingtalk &> run.log & 196 | $ tail -f run.log 197 | ``` 198 | 199 | ### 第二步,添加应用 200 | 201 | 钉钉官方在 2023 年 5 月份全面推出了 stream 模式,因此这里也推荐大家直接使用这个模式,其他 HTTP 的仍旧支持,只不过不再深入研究,因此下边的文档也以 stream 模式的配置流程来介绍。 202 | 203 | 创建步骤参考文档:[企业内部应用](https://open.dingtalk.com/document/orgapp/create-orgapp),或者根据如下步骤进行配置。 204 | 205 | 1. 创建应用。 206 |
207 | 🖼 点我查看示例图 208 | 209 |
210 | 211 | > `📢 注意:`可能现在创建机器人的时候名字为`chatgpt`会被钉钉限制,请用其他名字命名。 212 | 213 | 在`基础信息` --> `应用信息`当中能够获取到机器人的`AppKey`和`AppSecret`。 214 | 215 | 2. 配置机器人。 216 |
217 | 🖼 点我查看示例图 218 | 219 |
220 | 221 | 3. 发布机器人。 222 |
223 | 🖼 点我查看示例图 224 | 225 |
226 | 227 | 点击`版本管理与发布`,然后点击`上线`,这个时候就能在钉钉的群里中添加这个机器人了。 228 | 229 | 4. 群聊添加机器人。 230 |
231 | 🖼 点我查看示例图 232 | 233 |
234 | 235 | ## 亮点特色 236 | 237 | ### 与机器人私聊 238 | 239 | `2023-03-08`补充,我发现也可以不在群里艾特机器人聊天,还可点击机器人,然后点击发消息,通过与机器人直接对话进行聊天: 240 | 241 | > 由 [@Raytow](https://github.com/Raytow) 同学发现,在机器人自动生成的测试群里无法直接私聊机器人,在其他群里单独添加这个机器人,然后再点击就可以跟它私聊了。 242 | 243 |
244 | 🖼 点我查看示例图 245 | 246 |
247 | 248 | ### 帮助列表 249 | 250 | > 艾特机器人发送空内容或者帮助,会返回帮助列表。 251 | 252 |
253 | 🖼 点我查看示例图 254 | 255 |
256 | 257 | ### 切换模式 258 | 259 | > 发送指定关键字,可以切换不同的模式。 260 | 261 |
262 | 🖼 点我查看示例图 263 | 264 |
265 | 266 | > 📢 注意:串聊模式下,群里每个人的聊天上下文是独立的。 267 | > 📢 注意:默认对话模式为单聊,因此不必发送单聊即可进入单聊模式,而要进入串聊,则需要发送串聊关键字进行切换,当串聊内容超过最大限制的时候,你可以发送重置,然后再次进入串聊模式。 268 | 269 | ### 查询余额 270 | 271 | > 艾特机器人发送 `余额` 二字,会返回当前 key 对应的账号的剩余额度以及可用日期。 272 | 273 |
274 | 🖼 点我查看示例图 275 | 276 |
277 | 278 | ### 日常问题 279 | 280 |
281 | 🖼 点我查看示例图 282 | 283 |
284 | 285 | ### 通过内置 prompt 聊天 286 | 287 | > 发送模板两个字,会返回当前内置支持的 prompt 列表。 288 | 289 |
290 | 🖼 点我查看示例图 291 | 292 |
293 | 294 | > 如果你发现有比较优秀的 prompt,欢迎 PR。注意:一些与钉钉使用场景不是很匹配的,就不要提交了。 295 | 296 | ### 生成图片 297 | 298 | > 发送以 `#图片`开头的内容,将会触发绘画能力,图片生成之后,将会保存在程序根目录下的`images目录`下。 299 | > 300 | > 如果你绘图没有思路,可以在[这里](https://www.clickprompt.org/zh-CN/)以及[这里](https://lexica.art/)找到一些不错的 prompt。 301 | 302 |
303 | 🖼 点我查看示例图 304 | 305 |
306 | 307 | ### 支持 gpt-4 308 | 309 | 如果你的账号通过了官方的白名单,那么可以将模型配置为:`gpt-4-0314`、`gpt-4`或`gpt-4o-mini`,目前 gpt-4 的余额查询以及图片生成功能暂不可用,可能是接口限制,也可能是其他原因,等我有条件的时候,会对这些功能进行测试验证。 310 | 311 | > 以下是 gpt-3.5 与 gpt-4 对数学计算方面的区别。 312 | 313 |
314 | 🖼 点我查看示例图 315 | 316 |
317 | 318 | 感谢[@PIRANHACHAN](https://github.com/PIRANHACHAN)同学提供的 gpt-4 的 key,使得项目在 gpt-4 的对接上能够进行验证测试,达到了可用状态。 319 | 320 | ## 本地开发 321 | 322 | ```sh 323 | # 获取项目 324 | $ git clone https://github.com/eryajf/chatgpt-dingtalk.git 325 | 326 | # 进入项目目录 327 | $ cd chatgpt-dingtalk 328 | 329 | # 复制配置文件,根据个人实际情况进行配置 330 | $ cp config.example.yml config.yml 331 | 332 | # 启动项目 333 | $ go run main.go 334 | ``` 335 | 336 | ## 配置文件说明 337 | 338 | ```yaml 339 | # 应用的日志级别,info or debug 340 | log_level: "info" 341 | # 运行模式,http 或者 stream ,强烈建议你使用stream模式,通过此链接了解:https://open.dingtalk.com/document/isvapp/stream 342 | run_mode: "stream" 343 | # openai api_key,如果你是用的是azure,则该配置项可以留空或者直接忽略 344 | api_key: "xxxxxxxxx" 345 | # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议,如果你是用的是azure,则该配置项可以留空或者直接忽略 346 | base_url: "" 347 | # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-32k-0613", "gpt-4-32k-0314", "gpt-4-32k", "gpt-4-0613", "gpt-4-0314", "gpt-4", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单,如果你是用的是azure,则该配置项可以留空或者直接忽略 348 | model: "gpt-3.5-turbo" 349 | # 指定绘画模型,默认为 dall-e-2 , 可选参数有:"dall-e-2", "dall-e-3" 350 | image_model: "dall-e-2" 351 | # 会话超时时间,默认600秒,在会话时间内所有发送给机器人的信息会作为上下文 352 | session_timeout: 600 353 | # 最大问题长度 354 | max_question_len: 2048 355 | # 最大回答长度 356 | max_answer_len: 2048 357 | # 最大上下文文本长度,通常该参数可设置为与模型Token限制相同 358 | max_text: 4096 359 | # 指定请求时使用的代理,如果为空,则不使用代理,注意需要带上 http 协议 或 socks5 协议,如果你是用的是azure,则该配置项可以留空或者直接忽略 360 | http_proxy: "" 361 | # 指定默认的对话模式,可根据实际需求进行自定义,如果不设置,默认为单聊,即无上下文关联的对话模式 362 | default_mode: "单聊" 363 | # 单人单日请求次数上限,默认为0,即不限制 364 | max_request: 0 365 | # 指定服务启动端口,默认为 8090,一般在二进制宿主机部署时,遇到端口冲突时使用,如果run_mode为stream模式,则可以忽略该配置项 366 | port: "8090" 367 | # 指定服务的地址,就是当前服务可供外网访问的地址(或者直接理解为你配置在钉钉回调那里的地址),用于生成图片时给钉钉做渲染,最新版本中将图片上传到了钉钉服务器,理论上你可以忽略该配置项,如果run_mode为stream模式,则可以忽略该配置项 368 | service_url: "http://xxxxxx" 369 | # 限定对话类型 0:不限 1:只能单聊 2:只能群聊 370 | chat_type: "0" 371 | # 哪些群组可以进行对话(仅在chat_type为0、2时有效),如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 372 | # 群ID,可在群组中 @机器人 群ID 来查看日志获取,例如日志会输出:[🙋 企业内部机器人 在『测试』群的ConversationID为: "cidrabcdefgh1234567890AAAAA"],获取后可填写该参数并重启程序 373 | allow_groups: [] 374 | # 哪些普通群(使用outgoing机器人)可以进行对话,如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 375 | # 群ID,可在群组中 @机器人 群ID 来查看日志获取,例如日志会输出:[🙋 outgoing机器人 在『测试』群的ConversationID为: "cidrabcdefgh1234567890AAAAA"],获取后可填写该参数并重启程序 376 | # 如果不想支持outgoing机器人功能,这里可以随意设置一个内部群组,例如:cidrabcdefgh1234567890AAAAA;或随意一个字符串,例如:disabled 377 | # 建议该功能默认关闭:除非你必须要用到outgoing机器人 378 | allow_outgoing_groups: [] 379 | # 以下 allow_users、deny_users、vip_users、admin_users 配置中填写的是用户的userid,outgoing机器人模式下不适用这些配置 380 | # 比如 ["1301691029702722","1301691029702733"],这个信息需要在钉钉管理后台的通讯录当中获取:https://oa.dingtalk.com/contacts.htm#/contacts 381 | # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则列表中写用户的userid 382 | allow_users: [] 383 | # 哪些用户不可以进行对话,如果留空,则表示允许所有用户(如allow_user有配置,需满足相应条件),如果要限制,则列表中写用户的userid,黑名单优先级高于白名单 384 | deny_users: [] 385 | # 哪些用户可以进行无限对话,如果留空,则表示只允许管理员(如max_request配置为0,则允许所有人) 386 | # 如果要针对指定VIP用户放开限制(如max_request配置不为0),则列表中写用户的userid 387 | vip_users: [] 388 | # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的userid 389 | # 注意:如果下边的app_secrets为空,以及使用outgoing的方式配置机器人,这两种情况下,都表示没有人是管理员 390 | admin_users: [] 391 | # 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret,如果你的服务对接给多个机器人,这里可以配置多个机器人的secret 392 | app_secrets: [] 393 | # 敏感词,提问时触发,则不允许提问,回答的内容中触发,则以 🚫 代替 394 | sensitive_words: [] 395 | # 帮助信息,放在配置文件,可供自定义 396 | help: "### 发送信息\n\n若您想给机器人发送信息,有如下两种方式:\n\n1. **群聊:** 在机器人所在群里 **@机器人** 后边跟着要提问的内容。\n\n2. **私聊:** 点击机器人的 **头像** 后,再点击 **发消息。** \n\n### 系统指令\n\n系统指令是一些特殊的词语,当您向机器人发送这些词语时,会触发对应的功能。\n\n**📢 注意:系统指令,即只发指令,没有特殊标识,也没有内容。**\n\n以下是系统指令详情:\n\n| 指令 | 描述 | 示例 |\n| :--------: | :------------------------------------------: | :----------------------------------------------------------: |\n| **单聊** | 每次对话都是一次新的对话,没有聊天上下文联系 |

预览

|\n| **串聊** | 带上下文联系的对话模式 |

预览

|\n| **重置** | 重置上下文模式,回归到默认模式 |

预览

|\n| **余额** | 查询机器人所用OpenAI账号的余额 |

预览

|\n| **模板** | 查看应用内置的prompt模板 |

预览

|\n| **图片** | 查看如何根据提示生成图片 |

预览

|\n| **查对话** | 获取指定人员的对话历史 |

预览

|\n| **帮助** | 获取帮助信息 |

预览

|\n\n\n### 功能指令\n\n除去系统指令,还有一些功能指令,功能指令是直接与应用交互,达到交互目的的一种指令。\n\n**📢 注意:功能指令,一律以 #+关键字 为开头,通常需要在关键字后边加个空格,然后再写描述或参数。**\n\n以下是功能指令详情\n\n| 指令 | 说明 | 示例 |\n| :--: | :--: | :--: |\n| **#图片** | 根据提示咒语生成对应图片 |

预览

|\n| **#域名** | 查询域名相关信息 |

预览

|\n| **#证书** | 查询域名证书相关信息 |

预览

|\n| **#Linux命令** | 根据自然语言描述生成对应命令 |

预览

|\n| **#解释代码** | 分析一段代码的功能或含义 |

预览

|\n| **#正则** | 根据自然语言描述生成正则 |

预览

|\n| **#周报** | 应用周报的prompt |

预览

|\n| **#生成sql** | 根据自然语言描述生成sql语句 |

预览

|\n\n如上大多数能力,都是依赖prompt模板实现,如果你有更好的prompt,欢迎提交PR。\n\n### 友情提示\n\n使用 **串聊模式** 会显著加快机器人所用账号的余额消耗速度,因此,若无保留上下文的需求,建议使用 **单聊模式。** \n\n即使有保留上下文的需求,也应适时使用 **重置** 指令来重置上下文。\n\n### 项目地址\n\n本项目已在GitHub开源,[查看源代码](https://github.com/eryajf/chatgpt-dingtalk)。" 397 | 398 | # Azure OpenAI 配置 399 | # 例如你的示例请求为: curl https://eryajf.openai.azure.com/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-03-15-preview 那么对应配置如下,如果配置完成之后还是无法正常使用,请新建应用,重新配置回调试试看 400 | azure_on: false # 如果是true,则会走azure的openai接口 401 | azure_resource_name: "eryajf" # 对应你的主个性域名 402 | azure_deployment_name: "gpt-35-turbo" # 对应的是 /deployments/ 后边跟着的这个值 403 | azure_api_version: "2023-03-15-preview" # 对应的是请求中的 api-version 后边的值 404 | azure_openai_token: "xxxxxxx" 405 | 406 | # 钉钉应用鉴权凭据信息,支持多个应用。通过请求时候鉴权来识别是来自哪个机器人应用的消息 407 | # 设置credentials 之后,即具备了访问钉钉平台绝大部分 OpenAPI 的能力;例如上传图片到钉钉平台,提升图片体验,结合 Stream 模式简化服务部署 408 | # client_id 对应钉钉平台 AppKey/SuiteKey;client_secret 对应 AppSecret/SuiteSecret 409 | credentials: 410 | - client_id: "put-your-client-id-here" 411 | client_secret: "put-your-client-secret-here" 412 | ``` 413 | 414 | ## 常见问题 415 | 416 | 如何更好地使用 ChatGPT:这里有[许多案例](https://github.com/f/awesome-chatgpt-prompts)可供参考。 417 | 418 | `🗣 重要重要` 一些常见的问题,我单独开 issue 放在这里:[👉 点我 👈](https://github.com/eryajf/chatgpt-dingtalk/issues/44),可以查看这里辅助你解决问题,如果里边没有,请对历史 issue 进行搜索(不要提交重复的 issue),也欢迎大家补充。 419 | 420 | ## 进群交流 421 | 422 | 我创建了一个钉钉的交流群,欢迎进群交流。 423 | 424 | ![](https://cdn.jsdelivr.net/gh/eryajf/tu/img/image_20230405_191425.jpg) 425 | 426 | ## 感谢 427 | 428 | 这个项目能够成立,离不开这些开源项目: 429 | 430 | - [go-resty/resty](https://github.com/go-resty/resty) 431 | - [patrickmn/go-cache](https://github.com/patrickmn/go-cache) 432 | - [solywsh/chatgpt](https://github.com/solywsh/chatgpt) 433 | - [xgfone/ship](https://github.com/xgfone/ship) 434 | - [avast/retry-go](https://github.com/avast/retry-go) 435 | - [sashabaranov/go-openapi](https://github.com/sashabaranov/go-openai) 436 | - [charmbracelet/log](https://github.com/charmbracelet/log) 437 | 438 | ## 赞赏 439 | 440 | 如果觉得这个项目对你有帮助,你可以请作者[喝杯咖啡 ☕️](https://wiki.eryajf.net/reward/) 441 | 442 | ## 高光时刻 443 | 444 | > 本项目曾在 | [2022-12-12](https://github.com/bonfy/github-trending/blob/master/2022/2022-12-12.md#go) | [2022-12-18](https://github.com/bonfy/github-trending/blob/master/2022/2022-12-18.md#go) | [2022-12-19](https://github.com/bonfy/github-trending/blob/master/2022/2022-12-19.md#go) | [2022-12-20](https://github.com/bonfy/github-trending/blob/master/2022/2022-12-20.md#go) | [2023-02-09](https://github.com/bonfy/github-trending/blob/master/2023-02-09.md#go) | [2023-02-10](https://github.com/bonfy/github-trending/blob/master/2023-02-10.md#go) | [2023-02-11](https://github.com/bonfy/github-trending/blob/master/2023-02-11.md#go) | [2023-02-12](https://github.com/bonfy/github-trending/blob/master/2023-02-12.md#go) | [2023-02-13](https://github.com/bonfy/github-trending/blob/master/2023-02-13.md#go) | [2023-02-14](https://github.com/bonfy/github-trending/blob/master/2023-02-14.md#go) | [2023-02-15](https://github.com/bonfy/github-trending/blob/master/2023-02-15.md#go) | [2023-03-04](https://github.com/bonfy/github-trending/blob/master/2023-03-04.md#go) | [2023-03-05](https://github.com/bonfy/github-trending/blob/master/2023-03-05.md#go) | [2023-03-19](https://github.com/bonfy/github-trending/blob/master/2023-03-19.md#go) | [2023-03-22](https://github.com/bonfy/github-trending/blob/master/2023-03-22.md#go) | [2023-03-25](https://github.com/bonfy/github-trending/blob/master/2023-03-25.md#go) | [2023-03-26](https://github.com/bonfy/github-trending/blob/master/2023-03-26.md#go) | [2023-03-27](https://github.com/bonfy/github-trending/blob/master/2023-03-27.md#go) | [2023-03-29](https://github.com/bonfy/github-trending/blob/master/2023-03-29.md#go), 这些天里,登上 GitHub Trending。而且还在持续登榜中,可见最近 openai 的热度。 445 | > ![image_20230316_114915](https://cdn.jsdelivr.net/gh/eryajf/tu/img/image_20230316_114915.jpg) 446 | 447 | ## Star 历史 448 | 449 | [![Star History Chart](https://api.star-history.com/svg?repos=ConnectAI-E/Dingtalk-OpenAI&type=Date)](https://star-history.com/#ConnectAI-E/Dingtalk-OpenAI&Date) 450 | 451 | ## 贡献者列表 452 | 453 |
454 | 455 | 456 | 457 | 464 | 471 | 478 | 485 | 492 | 499 | 506 | 513 | 514 | 521 | 528 | 535 | 542 | 549 | 556 | 563 | 570 | 571 | 578 | 585 | 592 |
458 | 459 | eryajf 460 |
461 | 二丫讲梵 462 |
463 |
465 | 466 | Leizhenpeng 467 |
468 | RiverRay 469 |
470 |
472 | 473 | DDMeaqua 474 |
475 | Null 476 |
477 |
479 | 480 | ffinly 481 |
482 | Finly 483 |
484 |
486 | 487 | FrankCheungDev 488 |
489 | Frank Cheung 490 |
491 |
493 | 494 | b3nguang 495 |
496 | 本光 497 |
498 |
500 | 501 | ronething 502 |
503 | Ashing Zheng 504 |
505 |
507 | 508 | laorange 509 |
510 | 辣橙 511 |
512 |
515 | 516 | chzealot 517 |
518 | 金喜@DingTalk 519 |
520 |
522 | 523 | WinMin 524 |
525 | Swing 526 |
527 |
529 | 530 | suyunkai 531 |
532 | Null 533 |
534 |
536 | 537 | stoneflying 538 |
539 | Stoneflying 540 |
541 |
543 | 544 | cnmill 545 |
546 | Mill Peng 547 |
548 |
550 | 551 | little-huang 552 |
553 | Little_huang 554 |
555 |
557 | 558 | iblogc 559 |
560 | Iblogc 561 |
562 |
564 | 565 | wangbooth 566 |
567 | WangBooth 568 |
569 |
572 | 573 | fantasticmao 574 |
575 | Mao Mao 576 |
577 |
579 | 580 | luoxufeiyan 581 |
582 | Hugh Gao 583 |
584 |
586 | 587 | AydenLii 588 |
589 | AydenLii 590 |
591 |
593 | 594 |
595 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | # 应用的日志级别,info or debug 2 | log_level: "info" 3 | # 运行模式,http 或者 stream ,强烈建议你使用stream模式,通过此链接了解:https://open.dingtalk.com/document/isvapp/stream 4 | run_mode: "stream" 5 | # openai api_key,如果你是用的是azure,则该配置项可以留空或者直接忽略 6 | api_key: "xxxxxxxxx" 7 | # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议,如果你是用的是azure,则该配置项可以留空或者直接忽略 8 | base_url: "" 9 | # 指定模型,默认为 gpt-3.5-turbo , 可选参数有:"gpt-4-32k-0613", "gpt-4-32k-0314", "gpt-4-32k", "gpt-4-0613", "gpt-4-0314", "gpt-4-turbo-preview", "gpt-4-vision-preview", "gpt-4", "gpt-4o-mini", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单,如果你是用的是azure,则该配置项可以留空或者直接忽略 10 | model: "gpt-3.5-turbo" 11 | # 指定绘画模型,默认为 dall-e-2 , 可选参数有:"dall-e-2", "dall-e-3" 12 | image_model: "dall-e-2" 13 | # 会话超时时间,默认600秒,在会话时间内所有发送给机器人的信息会作为上下文 14 | session_timeout: "600s" 15 | # 最大问题长度 16 | max_question_len: 2048 17 | # 最大回答长度 18 | max_answer_len: 2048 19 | # 最大上下文文本长度,通常该参数可设置为与模型Token限制相同 20 | max_text: 4096 21 | # 指定请求时使用的代理,如果为空,则不使用代理,注意需要带上 http 协议 或 socks5 协议,如果你是用的是azure,则该配置项可以留空或者直接忽略 22 | http_proxy: "" 23 | # 指定默认的对话模式,可根据实际需求进行自定义,如果不设置,默认为单聊,即无上下文关联的对话模式 24 | default_mode: "单聊" 25 | # 单人单日请求次数上限,默认为0,即不限制 26 | max_request: 0 27 | # 指定服务启动端口,默认为 8090,一般在二进制宿主机部署时,遇到端口冲突时使用,如果run_mode为stream模式,则可以忽略该配置项 28 | port: "8090" 29 | # 指定服务的地址,就是当前服务可供外网访问的地址(或者直接理解为你配置在钉钉回调那里的地址),用于生成图片时给钉钉做渲染,最新版本中将图片上传到了钉钉服务器,理论上你可以忽略该配置项,如果run_mode为stream模式,则可以忽略该配置项 30 | service_url: "http://xxxxxx" 31 | # 限定对话类型 0:不限 1:只能单聊 2:只能群聊 32 | chat_type: "0" 33 | # 哪些群组可以进行对话(仅在chat_type为0、2时有效),如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 34 | # 群ID,可在群组中 @机器人 群ID 来查看日志获取,例如日志会输出:[🙋 企业内部机器人 在『测试』群的ConversationID为: "cidrabcdefgh1234567890AAAAA"],获取后可填写该参数并重启程序 35 | allow_groups: [] 36 | # 哪些普通群(使用outgoing机器人)可以进行对话,如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 37 | # 群ID,可在群组中 @机器人 群ID 来查看日志获取,例如日志会输出:[🙋 outgoing机器人 在『测试』群的ConversationID为: "cidrabcdefgh1234567890AAAAA"],获取后可填写该参数并重启程序 38 | # 如果不想支持outgoing机器人功能,这里可以随意设置一个内部群组,例如:cidrabcdefgh1234567890AAAAA;或随意一个字符串,例如:disabled 39 | # 建议该功能默认关闭:除非你必须要用到outgoing机器人 40 | allow_outgoing_groups: [] 41 | # 以下 allow_users、deny_users、vip_users、admin_users 配置中填写的是用户的userid,outgoing机器人模式下不适用这些配置 42 | # 比如 ["1301691029702722","1301691029702733"],这个信息需要在钉钉管理后台的通讯录当中获取:https://oa.dingtalk.com/contacts.htm#/contacts 43 | # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则列表中写用户的userid 44 | allow_users: [] 45 | # 哪些用户不可以进行对话,如果留空,则表示允许所有用户(如allow_user有配置,需满足相应条件),如果要限制,则列表中写用户的userid,黑名单优先级高于白名单 46 | deny_users: [] 47 | # 哪些用户可以进行无限对话,如果留空,则表示只允许管理员(如max_request配置为0,则允许所有人) 48 | # 如果要针对指定VIP用户放开限制(如max_request配置不为0),则列表中写用户的userid 49 | vip_users: [] 50 | # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的userid 51 | # 注意:如果下边的app_secrets为空,以及使用outgoing的方式配置机器人,这两种情况下,都表示没有人是管理员 52 | admin_users: [] 53 | # 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret,如果你的服务对接给多个机器人,这里可以配置多个机器人的secret 54 | app_secrets: [] 55 | # 敏感词,提问时触发,则不允许提问,回答的内容中触发,则以 🚫 代替 56 | sensitive_words: [] 57 | # 帮助信息,放在配置文件,可供自定义 58 | help: "### 发送信息\n\n若您想给机器人发送信息,有如下两种方式:\n\n1. **群聊:** 在机器人所在群里 **@机器人** 后边跟着要提问的内容。\n\n2. **私聊:** 点击机器人的 **头像** 后,再点击 **发消息。** \n\n### 系统指令\n\n系统指令是一些特殊的词语,当您向机器人发送这些词语时,会触发对应的功能。\n\n**📢 注意:系统指令,即只发指令,没有特殊标识,也没有内容。**\n\n以下是系统指令详情:\n\n| 指令 | 描述 | 示例 |\n| :--------: | :------------------------------------------: | :----------------------------------------------------------: |\n| **单聊** | 每次对话都是一次新的对话,没有聊天上下文联系 |

预览

|\n| **串聊** | 带上下文联系的对话模式 |

预览

|\n| **重置** | 重置上下文模式,回归到默认模式 |

预览

|\n| **余额** | 查询机器人所用OpenAI账号的余额 |

预览

|\n| **模板** | 查看应用内置的prompt模板 |

预览

|\n| **图片** | 查看如何根据提示生成图片 |

预览

|\n| **查对话** | 获取指定人员的对话历史 |

预览

|\n| **帮助** | 获取帮助信息 |

预览

|\n\n\n### 功能指令\n\n除去系统指令,还有一些功能指令,功能指令是直接与应用交互,达到交互目的的一种指令。\n\n**📢 注意:功能指令,一律以 #+关键字 为开头,通常需要在关键字后边加个空格,然后再写描述或参数。**\n\n以下是功能指令详情\n\n| 指令 | 说明 | 示例 |\n| :--: | :--: | :--: |\n| **#图片** | 根据提示咒语生成对应图片 |

预览

|\n| **#域名** | 查询域名相关信息 |

预览

|\n| **#证书** | 查询域名证书相关信息 |

预览

|\n| **#Linux命令** | 根据自然语言描述生成对应命令 |

预览

|\n| **#解释代码** | 分析一段代码的功能或含义 |

预览

|\n| **#正则** | 根据自然语言描述生成正则 |

预览

|\n| **#周报** | 应用周报的prompt |

预览

|\n| **#生成sql** | 根据自然语言描述生成sql语句 |

预览

|\n\n如上大多数能力,都是依赖prompt模板实现,如果你有更好的prompt,欢迎提交PR。\n\n### 友情提示\n\n使用 **串聊模式** 会显著加快机器人所用账号的余额消耗速度,因此,若无保留上下文的需求,建议使用 **单聊模式。** \n\n即使有保留上下文的需求,也应适时使用 **重置** 指令来重置上下文。\n\n### 项目地址\n\n本项目已在GitHub开源,[查看源代码](https://github.com/eryajf/chatgpt-dingtalk)。" 59 | 60 | # Azure OpenAI 配置 61 | # 例如你的示例请求为: curl https://eryajf.openai.azure.com/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-03-15-preview 那么对应配置如下,如果配置完成之后还是无法正常使用,请新建应用,重新配置回调试试看 62 | azure_on: false # 如果是true,则会走azure的openai接口 63 | azure_resource_name: "eryajf" # 对应你的主个性域名 64 | azure_deployment_name: "gpt-35-turbo" # 对应的是 /deployments/ 后边跟着的这个值 65 | azure_api_version: "2023-03-15-preview" # 对应的是请求中的 api-version 后边的值 66 | azure_openai_token: "xxxxxxx" 67 | 68 | # 钉钉应用鉴权凭据信息,支持多个应用。通过请求时候鉴权来识别是来自哪个机器人应用的消息 69 | # 设置credentials 之后,即具备了访问钉钉平台绝大部分 OpenAPI 的能力;例如上传图片到钉钉平台,提升图片体验,结合 Stream 模式简化服务部署 70 | # client_id 对应钉钉平台 AppKey/SuiteKey;client_secret 对应 AppSecret/SuiteSecret 71 | credentials: 72 | - client_id: "put-your-client-id-here" 73 | client_secret: "put-your-client-secret-here" 74 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "gopkg.in/yaml.v3" 13 | 14 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 15 | ) 16 | 17 | type Credential struct { 18 | ClientID string `yaml:"client_id"` 19 | ClientSecret string `yaml:"client_secret"` 20 | } 21 | 22 | // Configuration 项目配置 23 | type Configuration struct { 24 | // 日志级别,info或者debug 25 | LogLevel string `yaml:"log_level"` 26 | // gpt apikey 27 | ApiKey string `yaml:"api_key"` 28 | // 运行模式 29 | RunMode string `yaml:"run_mode"` 30 | // 请求的 URL 地址 31 | BaseURL string `yaml:"base_url"` 32 | // 使用模型 33 | Model string `yaml:"model"` 34 | // 使用绘画模型 35 | ImageModel string `yaml:"image_model"` 36 | // 会话超时时间 37 | SessionTimeout time.Duration `yaml:"session_timeout"` 38 | // 最大问题长度 39 | MaxQuestionLen int `yaml:"max_question_len"` 40 | // 最大答案长度 41 | MaxAnswerLen int `yaml:"max_answer_len"` 42 | // 最大文本 = 问题 + 回答, 接口限制 43 | MaxText int `yaml:"max_text"` 44 | // 默认对话模式 45 | DefaultMode string `yaml:"default_mode"` 46 | // 代理地址 47 | HttpProxy string `yaml:"http_proxy"` 48 | // 用户单日最大请求次数 49 | MaxRequest int `yaml:"max_request"` 50 | // 指定服务启动端口,默认为 8090 51 | Port string `yaml:"port"` 52 | // 指定服务的地址,就是钉钉机器人配置的回调地址,比如: http://chat.eryajf.net 53 | ServiceURL string `yaml:"service_url"` 54 | // 限定对话类型 0:不限 1:单聊 2:群聊 55 | ChatType string `yaml:"chat_type"` 56 | // 哪些群组可以进行对话 57 | AllowGroups []string `yaml:"allow_groups"` 58 | // 哪些outgoing群组可以进行对话 59 | AllowOutgoingGroups []string `yaml:"allow_outgoing_groups"` 60 | // 哪些用户可以进行对话 61 | AllowUsers []string `yaml:"allow_users"` 62 | // 哪些用户不可以进行对话 63 | DenyUsers []string `yaml:"deny_users"` 64 | // 哪些Vip用户可以进行无限对话 65 | VipUsers []string `yaml:"vip_users"` 66 | // 指定哪些人为此系统的管理员,必须指定,否则所有人都是 67 | AdminUsers []string `yaml:"admin_users"` 68 | // 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果你的服务对接给多个机器人,这里可以配置多个机器人的secret 69 | AppSecrets []string `yaml:"app_secrets"` 70 | // 敏感词,提问时触发,则不允许提问,回答的内容中触发,则以 🚫 代替 71 | SensitiveWords []string `yaml:"sensitive_words"` 72 | // 自定义帮助信息 73 | Help string `yaml:"help"` 74 | // AzureOpenAI 配置 75 | AzureOn bool `yaml:"azure_on"` 76 | AzureApiVersion string `yaml:"azure_api_version"` 77 | AzureResourceName string `yaml:"azure_resource_name"` 78 | AzureDeploymentName string `yaml:"azure_deployment_name"` 79 | AzureOpenAIToken string `yaml:"azure_openai_token"` 80 | // 钉钉应用鉴权凭据 81 | Credentials []Credential `yaml:"credentials"` 82 | } 83 | 84 | var ( 85 | config *Configuration 86 | once sync.Once 87 | ) 88 | 89 | // LoadConfig 加载配置 90 | func LoadConfig() *Configuration { 91 | once.Do(func() { 92 | // 从文件中读取 93 | config = &Configuration{} 94 | data, err := os.ReadFile("config.yml") 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | err = yaml.Unmarshal(data, &config) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | 103 | // 如果环境变量有配置,读取环境变量 104 | logLevel := os.Getenv("LOG_LEVEL") 105 | if logLevel != "" { 106 | config.LogLevel = logLevel 107 | } 108 | apiKey := os.Getenv("APIKEY") 109 | if apiKey != "" { 110 | config.ApiKey = apiKey 111 | } 112 | runMode := os.Getenv("RUN_MODE") 113 | if runMode != "" { 114 | config.RunMode = runMode 115 | } 116 | baseURL := os.Getenv("BASE_URL") 117 | if baseURL != "" { 118 | config.BaseURL = baseURL 119 | } 120 | model := os.Getenv("MODEL") 121 | if model != "" { 122 | config.Model = model 123 | } 124 | sessionTimeout := os.Getenv("SESSION_TIMEOUT") 125 | if sessionTimeout != "" { 126 | duration, err := strconv.ParseInt(sessionTimeout, 10, 64) 127 | if err != nil { 128 | logger.Fatal(fmt.Sprintf("config session timeout err: %v ,get is %v", err, sessionTimeout)) 129 | return 130 | } 131 | config.SessionTimeout = time.Duration(duration) * time.Second 132 | } else { 133 | config.SessionTimeout = time.Duration(config.SessionTimeout) * time.Second 134 | } 135 | maxQuestionLen := os.Getenv("MAX_QUESTION_LEN") 136 | if maxQuestionLen != "" { 137 | newLen, _ := strconv.Atoi(maxQuestionLen) 138 | config.MaxQuestionLen = newLen 139 | } 140 | maxAnswerLen := os.Getenv("MAX_ANSWER_LEN") 141 | if maxAnswerLen != "" { 142 | newLen, _ := strconv.Atoi(maxAnswerLen) 143 | config.MaxAnswerLen = newLen 144 | } 145 | maxText := os.Getenv("MAX_TEXT") 146 | if maxText != "" { 147 | newLen, _ := strconv.Atoi(maxText) 148 | config.MaxText = newLen 149 | } 150 | defaultMode := os.Getenv("DEFAULT_MODE") 151 | if defaultMode != "" { 152 | config.DefaultMode = defaultMode 153 | } 154 | httpProxy := os.Getenv("HTTP_PROXY") 155 | if httpProxy != "" { 156 | config.HttpProxy = httpProxy 157 | } 158 | maxRequest := os.Getenv("MAX_REQUEST") 159 | if maxRequest != "" { 160 | newMR, _ := strconv.Atoi(maxRequest) 161 | config.MaxRequest = newMR 162 | } 163 | port := os.Getenv("PORT") 164 | if port != "" { 165 | config.Port = port 166 | } 167 | serviceURL := os.Getenv("SERVICE_URL") 168 | if serviceURL != "" { 169 | config.ServiceURL = serviceURL 170 | } 171 | chatType := os.Getenv("CHAT_TYPE") 172 | if chatType != "" { 173 | config.ChatType = chatType 174 | } 175 | allowGroups := os.Getenv("ALLOW_GROUPS") 176 | if allowGroups != "" { 177 | config.AllowGroups = strings.Split(allowGroups, ",") 178 | } 179 | allowOutgoingGroups := os.Getenv("ALLOW_OUTGOING_GROUPS") 180 | if allowOutgoingGroups != "" { 181 | config.AllowOutgoingGroups = strings.Split(allowOutgoingGroups, ",") 182 | } 183 | allowUsers := os.Getenv("ALLOW_USERS") 184 | if allowUsers != "" { 185 | config.AllowUsers = strings.Split(allowUsers, ",") 186 | } 187 | denyUsers := os.Getenv("DENY_USERS") 188 | if denyUsers != "" { 189 | config.DenyUsers = strings.Split(denyUsers, ",") 190 | } 191 | vipUsers := os.Getenv("VIP_USERS") 192 | if vipUsers != "" { 193 | config.VipUsers = strings.Split(vipUsers, ",") 194 | } 195 | adminUsers := os.Getenv("ADMIN_USERS") 196 | if adminUsers != "" { 197 | config.AdminUsers = strings.Split(adminUsers, ",") 198 | } 199 | appSecrets := os.Getenv("APP_SECRETS") 200 | if appSecrets != "" { 201 | config.AppSecrets = strings.Split(appSecrets, ",") 202 | } 203 | sensitiveWords := os.Getenv("SENSITIVE_WORDS") 204 | if sensitiveWords != "" { 205 | config.SensitiveWords = strings.Split(sensitiveWords, ",") 206 | } 207 | help := os.Getenv("HELP") 208 | if help != "" { 209 | config.Help = help 210 | } 211 | azureOn := os.Getenv("AZURE_ON") 212 | if azureOn != "" { 213 | config.AzureOn = azureOn == "true" 214 | } 215 | azureApiVersion := os.Getenv("AZURE_API_VERSION") 216 | if azureApiVersion != "" { 217 | config.AzureApiVersion = azureApiVersion 218 | } 219 | azureResourceName := os.Getenv("AZURE_RESOURCE_NAME") 220 | if azureResourceName != "" { 221 | config.AzureResourceName = azureResourceName 222 | } 223 | azureDeploymentName := os.Getenv("AZURE_DEPLOYMENT_NAME") 224 | if azureDeploymentName != "" { 225 | config.AzureDeploymentName = azureDeploymentName 226 | } 227 | azureOpenaiToken := os.Getenv("AZURE_OPENAI_TOKEN") 228 | if azureOpenaiToken != "" { 229 | config.AzureOpenAIToken = azureOpenaiToken 230 | } 231 | 232 | credentials := os.Getenv("DINGTALK_CREDENTIALS") 233 | if credentials != "" { 234 | config.Credentials = []Credential{} 235 | for _, idSecret := range strings.Split(credentials, ",") { 236 | items := strings.SplitN(idSecret, ":", 2) 237 | if len(items) == 2 { 238 | config.Credentials = append(config.Credentials, Credential{ClientID: items[0], ClientSecret: items[1]}) 239 | } 240 | } 241 | } 242 | }) 243 | 244 | // 一些默认值 245 | if config.LogLevel == "" { 246 | config.LogLevel = "info" 247 | } 248 | if config.RunMode == "" { 249 | config.RunMode = "http" 250 | } 251 | if config.Model == "" { 252 | config.Model = "gpt-3.5-turbo" 253 | } 254 | if config.DefaultMode == "" { 255 | config.DefaultMode = "单聊" 256 | } 257 | if config.Port == "" { 258 | config.Port = "8090" 259 | } 260 | if config.ChatType == "" { 261 | config.ChatType = "0" 262 | } 263 | if !config.AzureOn { 264 | if config.ApiKey == "" { 265 | panic("config err: api key required") 266 | } 267 | } 268 | if config.MaxQuestionLen == 0 { 269 | config.MaxQuestionLen = 4096 270 | } 271 | if config.MaxAnswerLen == 0 { 272 | config.MaxAnswerLen = 4096 273 | } 274 | if config.MaxText == 0 { 275 | config.MaxText = 4096 276 | } 277 | return config 278 | } 279 | -------------------------------------------------------------------------------- /config/prompt.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type Prompt struct { 11 | Title string `yaml:"title"` 12 | Prefix string `yaml:"prefix"` 13 | Suffix string `yaml:"suffix"` 14 | } 15 | 16 | var prompTmp *[]Prompt 17 | 18 | // LoadPrompt 加载Prompt 19 | func LoadPrompt() *[]Prompt { 20 | data, err := os.ReadFile("prompt.yml") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | err = yaml.Unmarshal(data, &prompTmp) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | return prompTmp 30 | } 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | chatgpt: 5 | container_name: chatgpt 6 | image: registry.cn-hangzhou.aliyuncs.com/eryajf/chatgpt-dingtalk 7 | restart: always 8 | environment: 9 | LOG_LEVEL: "info" # 应用的日志级别 info/debug 10 | APIKEY: xxxxxx # 你的 api_key 11 | RUN_MODE: "stream" # 运行模式,http 或者 stream ,强烈建议你使用stream模式,通过此链接了解:https://open.dingtalk.com/document/isvapp/stream 12 | BASE_URL: "" # 如果你使用官方的接口地址 https://api.openai.com,则留空即可,如果你想指定请求url的地址,可通过这个参数进行配置,注意需要带上 http 协议 13 | MODEL: "gpt-3.5-turbo" # 指定模型,默认为 gpt-3.5-turbo , 可选参数有: "gpt-4-32k-0613", "gpt-4-32k-0314", "gpt-4-32k", "gpt-4-0613", "gpt-4-0314", "gpt-4", "gpt-4o-mini", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-0301", "gpt-3.5-turbo",如果使用gpt-4,请确认自己是否有接口调用白名单,如果你是用的是azure,则该配置项可以留空或者直接忽略 14 | IMAGE_MODEL: "dall-e-2" # 指定绘画模型,默认为 dall-e-2 , 可选参数有:"dall-e-2", "dall-e-3" 15 | SESSION_TIMEOUT: 600 # 会话超时时间,默认600秒,在会话时间内所有发送给机器人的信息会作为上下文 16 | MAX_QUESTION_LEN: 2048 # 最大问题长度,默认4096 token,正常情况默认值即可,如果使用gpt4-8k或gpt4-32k,可根据模型token上限修改。 17 | MAX_ANSWER_LEN: 2048 # 最大回答长度,默认4096 token,正常情况默认值即可,如果使用gpt4-8k或gpt4-32k,可根据模型token上限修改。 18 | MAX_TEXT: 4096 # 最大文本 = 问题 + 回答, 接口限制,默认4096 token,正常情况默认值即可,如果使用gpt4-8k或gpt4-32k,可根据模型token上限修改。 19 | HTTP_PROXY: http://host.docker.internal:15777 # 指定请求时使用的代理,如果为空,则不使用代理,注意需要带上 http 协议 或 socks5 协议 20 | DEFAULT_MODE: "单聊" # 指定默认的对话模式,可根据实际需求进行自定义,如果不设置,默认为单聊,即无上下文关联的对话模式 21 | MAX_REQUEST: 0 # 单人单日请求次数上限,默认为0,即不限制 22 | PORT: 8090 # 指定服务启动端口,默认为 8090,容器化部署时,不需要调整,一般在二进制宿主机部署时,遇到端口冲突时使用,如果run_mode为stream模式,则可以忽略该配置项 23 | SERVICE_URL: "" # 指定服务的地址,就是当前服务可供外网访问的地址(或者直接理解为你配置在钉钉回调那里的地址),用于生成图片时给钉钉做渲染 24 | CHAT_TYPE: "0" # 限定对话类型 0:不限 1:只能单聊 2:只能群聊 25 | ALLOW_GROUPS: "" # 哪些群组可以进行对话(仅在CHAT_TYPE为0、2时有效),如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 26 | # 群ID,可在群组中 @机器人 群ID 来查看日志获取,例如日志会输出:[🙋 企业内部机器人 在『测试』群的ConversationID为: "cidrabcdefgh1234567890AAAAA"],获取后可填写该参数并重启程序 27 | # 如果不想支持outgoing机器人功能,这里可以随意设置一个内部群组,例如:cidrabcdefgh1234567890AAAAA;或随意一个字符串,例如:disabled 28 | # 建议该功能默认关闭:除非你必须要用到outgoing机器人 29 | ALLOW_OUTGOING_GROUPS: "" # 哪些普通群(使用outgoing机器人)可以进行对话,如果留空,则表示允许所有群组,如果要限制,则列表中写群ID(ConversationID) 30 | # 以下 ALLOW_USERS、DENY_USERS、VIP_USERS、ADMIN_USERS 配置中填写的是用户的userid 31 | # 比如 ["1301691029702722","1301691029702733"],这个信息需要在钉钉管理后台的通讯录当中获取:https://oa.dingtalk.com/contacts.htm#/contacts 32 | # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则列表中写用户的userid 33 | ALLOW_USERS: "" # 哪些用户可以进行对话,如果留空,则表示允许所有用户,如果要限制,则填写用户的userid 34 | DENY_USERS: "" # 哪些用户不可以进行对话,如果留空,则表示允许所有用户(如allow_user有配置,需满足相应条件),如果要限制,则列表中写用户的userid,黑名单优先级高于白名单 35 | VIP_USERS: "" # 哪些用户可以进行无限对话,如果留空,则表示只允许管理员(如max_request配置为0,则允许所有人),如果要针对指定VIP用户放开限制(如max_request配置不为0),则列表中写用户的userid 36 | ADMIN_USERS: "" # 指定哪些人为此系统的管理员,如果留空,则表示没有人是管理员,如果要限制,则列表中写用户的userid 37 | # 注意:如果下边的app_secrets为空,以及使用outgoing的方式配置机器人,这两种情况下,都表示没有人是管理员 38 | APP_SECRETS: "" # 钉钉机器人在应用信息中的AppSecret,为了校验回调的请求是否合法,如果留空,将会忽略校验,则该接口将会存在其他人也能随意调用的安全隐患,因此强烈建议配置正确的secret,如果你的服务对接给多个机器人,这里可以配置多个机器人的secret,比如 "xxxx,yyyy" 39 | SENSITIVE_WORDS: "" # 敏感词,提问时触发,则不允许提问,回答的内容中触发,则以 🚫 代替 40 | AZURE_ON: "false" # 是否走Azure OpenAi API, 默认false ,如果为true,则需要配置下边的四个参数 41 | AZURE_API_VERSION: "" # Azure OpenAi API 版本,比如 "2023-03-15-preview" 42 | AZURE_RESOURCE_NAME: "" # Azure OpenAi API 资源名称,比如 "openai" 43 | AZURE_DEPLOYMENT_NAME: "" # Azure OpenAi API 部署名称,比如 "openai" 44 | AZURE_OPENAI_TOKEN: "" # Azure token 45 | DINGTALK_CREDENTIALS: "" # 钉钉应用访问凭证,比如 "client_id1:secret1,client_id2:secret2" 46 | HELP: "### 发送信息\n\n若您想给机器人发送信息,有如下两种方式:\n\n1. **群聊:** 在机器人所在群里 **@机器人** 后边跟着要提问的内容。\n\n2. **私聊:** 点击机器人的 **头像** 后,再点击 **发消息。** \n\n### 系统指令\n\n系统指令是一些特殊的词语,当您向机器人发送这些词语时,会触发对应的功能。\n\n**📢 注意:系统指令,即只发指令,没有特殊标识,也没有内容。**\n\n以下是系统指令详情:\n\n| 指令 | 描述 | 示例 |\n| :--------: | :------------------------------------------: | :----------------------------------------------------------: |\n| **单聊** | 每次对话都是一次新的对话,没有聊天上下文联系 |

预览

|\n| **串聊** | 带上下文联系的对话模式 |

预览

|\n| **重置** | 重置上下文模式,回归到默认模式 |

预览

|\n| **余额** | 查询机器人所用OpenAI账号的余额 |

预览

|\n| **模板** | 查看应用内置的prompt模板 |

预览

|\n| **图片** | 查看如何根据提示生成图片 |

预览

|\n| **查对话** | 获取指定人员的对话历史 |

预览

|\n| **帮助** | 获取帮助信息 |

预览

|\n\n\n### 功能指令\n\n除去系统指令,还有一些功能指令,功能指令是直接与应用交互,达到交互目的的一种指令。\n\n**📢 注意:功能指令,一律以 #+关键字 为开头,通常需要在关键字后边加个空格,然后再写描述或参数。**\n\n以下是功能指令详情\n\n| 指令 | 说明 | 示例 |\n| :--: | :--: | :--: |\n| **#图片** | 根据提示咒语生成对应图片 |

预览

|\n| **#域名** | 查询域名相关信息 |

预览

|\n| **#证书** | 查询域名证书相关信息 |

预览

|\n| **#Linux命令** | 根据自然语言描述生成对应命令 |

预览

|\n| **#解释代码** | 分析一段代码的功能或含义 |

预览

|\n| **#正则** | 根据自然语言描述生成正则 |

预览

|\n| **#周报** | 应用周报的prompt |

预览

|\n| **#生成sql** | 根据自然语言描述生成sql语句 |

预览

|\n\n如上大多数能力,都是依赖prompt模板实现,如果你有更好的prompt,欢迎提交PR。\n\n### 友情提示\n\n使用 **串聊模式** 会显著加快机器人所用账号的余额消耗速度,因此,若无保留上下文的需求,建议使用 **单聊模式。** \n\n即使有保留上下文的需求,也应适时使用 **重置** 指令来重置上下文。\n\n### 项目地址\n\n本项目已在GitHub开源,[查看源代码](https://github.com/eryajf/chatgpt-dingtalk)。" # 帮助信息,放在配置文件,可供自定义 47 | volumes: 48 | - ./data:/app/data 49 | ports: 50 | - "8090:8090" 51 | extra_hosts: 52 | - host.docker.internal:host-gateway 53 | -------------------------------------------------------------------------------- /docs/userGuide.md: -------------------------------------------------------------------------------- 1 | 本文是 chatgpt-dingtalk 项目的使用指南,该项目涉及的指令,以及特性,都会在本文呈现。 2 | 3 | ## 发送信息 4 | 5 | 若您想给机器人发送信息,有如下两种方式: 6 | 7 | 1. **群聊:** 在机器人所在群里`@机器人` 后边跟着要提问的内容。 8 | 2. **私聊:** 点击机器人的`头像`后,再点击`发消息`。 9 | 10 | ## 系统指令 11 | 12 | 系统指令是一些特殊的词语,当您向机器人发送这些词语时,会触发对应的功能。 13 | 14 | **📢 注意:系统指令,即只发指令,没有特殊标识,也没有内容。** 15 | 16 | 以下是系统指令详情: 17 | 18 | | 指令 | 描述 | 示例 | 补充 | 19 | | :--------: | :------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------: | :--: | 20 | | **单聊** | 每次对话都是一次新的对话,没有聊天上下文联系 |

点击查看

| | 21 | | **串聊** | 带上下文联系的对话模式 |

点击查看

| | 22 | | **重置** | 重置上下文模式,回归到默认模式 |

点击查看

| | 23 | | **余额** | 查询机器人所用 OpenAI 账号的余额 |

点击查看

| | 24 | | **模板** | 查看应用内置的 prompt 模板 |

点击查看

| | 25 | | **图片** | 查看如何根据提示生成图片 |

点击查看

| | 26 | | **查对话** | 获取指定人员的对话历史 |

点击查看

| | 27 | | **帮助** | 获取帮助信息 |

点击查看

| | 28 | 29 | ## 功能指令 30 | 31 | 除去系统指令,还有一些功能指令,功能指令是直接与应用交互,达到交互目的的一种指令。 32 | 33 | **📢 注意:功能指令,一律以 #+关键字 为开头,通常需要在关键字后边加个空格,然后再写描述或参数。** 34 | 35 | 以下是功能指令详情 36 | 37 | | 指令 | 说明 | 示例 | 补充 | 38 | | :-------------: | :---------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------: | 39 | | **#图片** | 根据提示咒语生成对应图片 |

点击查看

| | 40 | | **#域名** | 查询域名相关信息 |

点击查看

| | 41 | | **#证书** | 查询域名证书相关信息 |

点击查看

| | 42 | | **#Linux 命令** | 根据自然语言描述生成对应命令 |

点击查看

| 此指令中的 Linux 开头字幕可以大写 | 43 | | **#解释代码** | 分析一段代码的功能或含义 |

点击查看

| | 44 | | **#正则** | 根据自然语言描述生成正则 |

点击查看

| | 45 | | **#周报** | 应用周报的 prompt |

点击查看

| | 46 | | **#生成 sql** | 根据自然语言描述生成 sql 语句 |

点击查看

| | 47 | 48 | 如上大多数能力,都是依赖 prompt 模板实现,如果你有更好的 prompt,欢迎提交 PR。 49 | 50 | ## 友情提示 51 | 52 | 使用`串聊模式`会显著加快机器人所用账号的余额消耗速度,因此,若无保留上下文的需求,建议使用`单聊模式`。 53 | 54 | 即使有保留上下文的需求,也应适时使用`重置`指令来重置上下文。 55 | 56 | ## 项目地址 57 | 58 | 本项目已在 GitHub 开源,[查看源代码](https://github.com/eryajf/chatgpt-dingtalk)。 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eryajf/chatgpt-dingtalk 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/charmbracelet/log v0.4.0 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/glebarez/sqlite v1.11.0 9 | github.com/go-resty/resty/v2 v2.13.1 10 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/sashabaranov/go-openai v1.27.1 13 | github.com/solywsh/chatgpt v0.0.14 14 | gopkg.in/yaml.v3 v3.0.1 15 | gorm.io/gorm v1.25.11 16 | ) 17 | 18 | require ( 19 | github.com/avast/retry-go v3.0.0+incompatible // indirect 20 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 21 | github.com/bytedance/sonic v1.12.0 // indirect 22 | github.com/bytedance/sonic/loader v0.2.0 // indirect 23 | github.com/charmbracelet/lipgloss v0.12.1 // indirect 24 | github.com/charmbracelet/x/ansi v0.1.4 // indirect 25 | github.com/cloudwego/base64x v0.1.4 // indirect 26 | github.com/cloudwego/iasm v0.2.0 // indirect 27 | github.com/dlclark/regexp2 v1.11.2 // indirect 28 | github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 // indirect 29 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc // indirect 30 | github.com/dustin/go-humanize v1.0.1 // indirect 31 | github.com/gabriel-vasile/mimetype v1.4.5 // indirect 32 | github.com/gin-contrib/sse v0.1.0 // indirect 33 | github.com/glebarez/go-sqlite v1.22.0 // indirect 34 | github.com/go-logfmt/logfmt v0.6.0 // indirect 35 | github.com/go-playground/locales v0.14.1 // indirect 36 | github.com/go-playground/universal-translator v0.18.1 // indirect 37 | github.com/go-playground/validator/v10 v10.22.0 // indirect 38 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 39 | github.com/goccy/go-json v0.10.3 // indirect 40 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect 41 | github.com/google/uuid v1.6.0 // indirect 42 | github.com/gorilla/websocket v1.5.0 // indirect 43 | github.com/jinzhu/inflection v1.0.0 // indirect 44 | github.com/jinzhu/now v1.1.5 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 47 | github.com/leodido/go-urn v1.4.0 // indirect 48 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/mattn/go-runewidth v0.0.16 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/muesli/termenv v0.15.2 // indirect 54 | github.com/ncruces/go-strftime v0.1.9 // indirect 55 | github.com/pandodao/tokenizer-go v0.2.0 // indirect 56 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 57 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 58 | github.com/rivo/uniseg v0.4.7 // indirect 59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 60 | github.com/ugorji/go/codec v1.2.12 // indirect 61 | golang.org/x/arch v0.8.0 // indirect 62 | golang.org/x/crypto v0.25.0 // indirect 63 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 64 | golang.org/x/image v0.18.0 // indirect 65 | golang.org/x/net v0.27.0 // indirect 66 | golang.org/x/sys v0.22.0 // indirect 67 | golang.org/x/text v0.16.0 // indirect 68 | google.golang.org/protobuf v1.34.2 // indirect 69 | modernc.org/libc v1.55.6 // indirect 70 | modernc.org/mathutil v1.6.0 // indirect 71 | modernc.org/memory v1.8.0 // indirect 72 | modernc.org/sqlite v1.31.1 // indirect 73 | ) 74 | 75 | replace github.com/solywsh/chatgpt => ./pkg/chatgpt 76 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 2 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= 6 | github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 7 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 8 | github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= 9 | github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 10 | github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= 11 | github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= 12 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 13 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 14 | github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= 15 | github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 16 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 17 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 18 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 19 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68= 24 | github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 25 | github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 h1:ZRqTaoW9WZ2DqeOQGhK9q73eCb47SEs30GV2IRHT9bo= 26 | github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= 27 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co= 28 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU= 29 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 30 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 31 | github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= 32 | github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= 33 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 34 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 35 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 36 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 37 | github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 38 | github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 39 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 40 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 41 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 42 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 43 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 44 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 45 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 46 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 47 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 48 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 49 | github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= 50 | github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 51 | github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= 52 | github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= 53 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 54 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 55 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 56 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 57 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 58 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= 61 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 62 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 63 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 65 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 66 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 67 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 68 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 69 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 70 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 71 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 72 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 73 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 74 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 75 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 76 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 77 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 78 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 79 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 80 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 81 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 82 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 83 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 84 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 86 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 87 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 88 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 89 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 90 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 91 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 92 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 93 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0 h1:DL64ORGMk6AUB8q5LbRp8KRFn4oHhdrSepBmbMrtmNo= 94 | github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= 95 | github.com/pandodao/tokenizer-go v0.2.0 h1:NhfI8fGvQkDld2cZCag6NEU3pJ/ugU9zoY1R/zi9YCs= 96 | github.com/pandodao/tokenizer-go v0.2.0/go.mod h1:t6qFbaleKxbv0KNio2XUN/mfGM5WKv4haPXDQWVDG00= 97 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 98 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 99 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 100 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 101 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 102 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 103 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 104 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 105 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 106 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 107 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 108 | github.com/sashabaranov/go-openai v1.27.1 h1:7Nx6db5NXbcoutNmAUQulEQZEpHG/SkzfexP2X5RWMk= 109 | github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 110 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 111 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 112 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 113 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 114 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 115 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 116 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 118 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 119 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 120 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 121 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 122 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 123 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 124 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 125 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 126 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 127 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 128 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 129 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 130 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 131 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 132 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 133 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 134 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 135 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 136 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 137 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 138 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 139 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 140 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 141 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 142 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 143 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 144 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 145 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 146 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 147 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 148 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 149 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 150 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 151 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 152 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 153 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 154 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 155 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 156 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 157 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 158 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 161 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 165 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 166 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 167 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 168 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 169 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 170 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 171 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 172 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 173 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 174 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 175 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 176 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 177 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 178 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 179 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 180 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 181 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 182 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 183 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 184 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 185 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 186 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 188 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 189 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 190 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 191 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 192 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 193 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 194 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 198 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 199 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 201 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= 203 | gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 204 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= 205 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 206 | modernc.org/ccgo/v4 v4.20.5 h1:s04akhT2dysD0DFOlv9fkQ6oUTLPYgMnnDk9oaqjszM= 207 | modernc.org/ccgo/v4 v4.20.5/go.mod h1:fYXClPUMWxWaz1Xj5sHbzW/ZENEFeuHLToqBxUk41nE= 208 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 209 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 210 | modernc.org/gc/v2 v2.4.3 h1:Ik4ZcMbC7aY4ZDPUhzXVXi7GMub9QcXLTfXn3mWpNw8= 211 | modernc.org/gc/v2 v2.4.3/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 212 | modernc.org/libc v1.55.6 h1:WfOvqnRFSjVT3WZIAVzGkKcYSk/toAXXnPAMWnTKBZc= 213 | modernc.org/libc v1.55.6/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA= 214 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 215 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 216 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 217 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 218 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 219 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 220 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 221 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 222 | modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs= 223 | modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= 224 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 225 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 226 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 227 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 228 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 229 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" 14 | "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" 15 | 16 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 17 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 18 | "github.com/eryajf/chatgpt-dingtalk/pkg/process" 19 | "github.com/eryajf/chatgpt-dingtalk/public" 20 | ) 21 | 22 | func init() { 23 | // 初始化加载配置,数据库,模板等 24 | public.InitSvc() 25 | // 指定日志等级 26 | logger.InitLogger(public.Config.LogLevel) 27 | } 28 | 29 | func main() { 30 | if public.Config.RunMode == "http" { 31 | StartHttp() 32 | } else { 33 | for _, credential := range public.Config.Credentials { 34 | StartStream(credential.ClientID, credential.ClientSecret) 35 | } 36 | logger.Info("✌️ 当前正在使用的模型是", public.Config.Model) 37 | logger.Info("🚀 The Server Is Running On Stream Mode") 38 | select {} 39 | } 40 | } 41 | 42 | type ChatReceiver struct { 43 | clientId string 44 | clientSecret string 45 | } 46 | 47 | func NewChatReceiver(clientId, clientSecret string) *ChatReceiver { 48 | return &ChatReceiver{ 49 | clientId: clientId, 50 | clientSecret: clientSecret, 51 | } 52 | } 53 | 54 | // 启动为 stream 模式 55 | func StartStream(clientId, clientSecret string) { 56 | receiver := NewChatReceiver(clientId, clientSecret) 57 | cli := client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret))) 58 | 59 | //注册callback类型的处理函数 60 | cli.RegisterChatBotCallbackRouter(receiver.OnChatBotMessageReceived) 61 | 62 | err := cli.Start(context.Background()) 63 | if err != nil { 64 | logger.Fatal("strar stream failed: %v\n", err) 65 | } 66 | defer cli.Close() 67 | } 68 | 69 | // OnChatBotMessageReceived 简单的应答机器人实现 70 | func (r *ChatReceiver) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) { 71 | msgObj := dingbot.ReceiveMsg{ 72 | ConversationID: data.ConversationId, 73 | AtUsers: []struct { 74 | DingtalkID string "json:\"dingtalkId\"" 75 | }{}, 76 | ChatbotUserID: data.ChatbotUserId, 77 | MsgID: data.MsgId, 78 | SenderNick: data.SenderNick, 79 | IsAdmin: data.IsAdmin, 80 | SenderStaffId: data.SenderStaffId, 81 | SessionWebhookExpiredTime: data.SessionWebhookExpiredTime, 82 | CreateAt: data.CreateAt, 83 | ConversationType: data.ConversationType, 84 | SenderID: data.SenderId, 85 | ConversationTitle: data.ConversationTitle, 86 | IsInAtList: data.IsInAtList, 87 | SessionWebhook: data.SessionWebhook, 88 | Text: dingbot.Text(data.Text), 89 | RobotCode: "", 90 | Msgtype: dingbot.MsgType(data.Msgtype), 91 | } 92 | clientId := r.clientId 93 | var c gin.Context 94 | c.Set(public.DingTalkClientIdKeyName, clientId) 95 | DoRequest(msgObj, &c) 96 | 97 | return []byte(""), nil 98 | } 99 | 100 | func StartHttp() { 101 | app := gin.Default() 102 | app.POST("/", func(c *gin.Context) { 103 | var msgObj dingbot.ReceiveMsg 104 | err := c.Bind(&msgObj) 105 | if err != nil { 106 | return 107 | } 108 | DoRequest(msgObj, c) 109 | }) 110 | // 解析生成后的图片 111 | app.GET("/images/:filename", func(c *gin.Context) { 112 | filename := c.Param("filename") 113 | c.File("./data/images/" + filename) 114 | }) 115 | // 解析生成后的历史聊天 116 | app.GET("/history/:filename", func(c *gin.Context) { 117 | filename := c.Param("filename") 118 | c.File("./data/chatHistory/" + filename) 119 | }) 120 | // 直接下载文件 121 | app.GET("/download/:filename", func(c *gin.Context) { 122 | filename := c.Param("filename") 123 | c.Header("Content-Disposition", "attachment; filename="+filename) 124 | c.Header("Content-Type", "application/octet-stream") 125 | c.File("./data/chatHistory/" + filename) 126 | }) 127 | // 服务器健康检测 128 | app.GET("/", func(c *gin.Context) { 129 | c.JSON(200, gin.H{ 130 | "status": "ok", 131 | "message": "🚀 欢迎使用钉钉机器人 🤖", 132 | }) 133 | }) 134 | port := ":" + public.Config.Port 135 | srv := &http.Server{ 136 | Addr: port, 137 | Handler: app, 138 | } 139 | 140 | // Initializing the server in a goroutine so that 141 | // it won't block the graceful shutdown handling below 142 | go func() { 143 | logger.Info("🚀 The HTTP Server is running on", port) 144 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 145 | logger.Fatal("listen: %s\n", err) 146 | } 147 | }() 148 | 149 | // Wait for interrupt signal to gracefully shutdown the server with 150 | // a timeout of 5 seconds. 151 | quit := make(chan os.Signal, 1) 152 | // kill (no param) default send syscall.SIGTERM 153 | // kill -2 is syscall.SIGINT 154 | // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it 155 | // signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 156 | signal.Notify(quit, os.Interrupt) 157 | <-quit 158 | logger.Info("Shutting down server...") 159 | 160 | // 5秒后强制退出 161 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 162 | defer cancel() 163 | if err := srv.Shutdown(ctx); err != nil { 164 | logger.Fatal("Server forced to shutdown:", err) 165 | } 166 | logger.Info("Server exiting!") 167 | } 168 | 169 | func DoRequest(msgObj dingbot.ReceiveMsg, c *gin.Context) { 170 | // 先校验回调是否合法 171 | if public.Config.RunMode == "http" { 172 | clientId, checkOk := public.CheckRequestWithCredentials(c.GetHeader("timestamp"), c.GetHeader("sign")) 173 | if !checkOk { 174 | logger.Warning("该请求不合法,可能是其他企业或者未经允许的应用调用所致,请知悉!") 175 | return 176 | } 177 | // 通过 context 传递 OAuth ClientID,用于后续流程中调用钉钉OpenAPI 178 | c.Set(public.DingTalkClientIdKeyName, clientId) 179 | } 180 | // 再校验回调参数是否有价值 181 | if msgObj.Text.Content == "" || msgObj.ChatbotUserID == "" { 182 | logger.Warning("从钉钉回调过来的内容为空,根据过往的经验,或许重新创建一下机器人,能解决这个问题") 183 | return 184 | } 185 | // 去除问题的前后空格 186 | msgObj.Text.Content = strings.TrimSpace(msgObj.Text.Content) 187 | if public.JudgeSensitiveWord(msgObj.Text.Content) { 188 | logger.Info(fmt.Sprintf("🙋 %s提问的问题中包含敏感词汇,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content)) 189 | _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您提问的问题中包含敏感词汇,请审核自己的对话内容之后再进行!**") 190 | if err != nil { 191 | logger.Warning(fmt.Errorf("send message error: %v", err)) 192 | return 193 | } 194 | return 195 | } 196 | // 打印钉钉回调过来的请求明细,调试时打开 197 | logger.Debug(fmt.Sprintf("dingtalk callback parameters: %#v", msgObj)) 198 | 199 | if public.Config.ChatType != "0" && msgObj.ConversationType != public.Config.ChatType { 200 | logger.Info(fmt.Sprintf("🙋 %s使用了禁用的聊天方式", msgObj.SenderNick)) 201 | _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,管理员禁用了这种聊天方式,请选择其他聊天方式与机器人对话!**") 202 | if err != nil { 203 | logger.Warning(fmt.Errorf("send message error: %v", err)) 204 | return 205 | } 206 | return 207 | } 208 | 209 | // 查询群ID,发送指令后,可通过查看日志来获取 210 | if msgObj.ConversationType == "2" && msgObj.Text.Content == "群ID" { 211 | if msgObj.RobotCode == "normal" { 212 | logger.Info(fmt.Sprintf("🙋 outgoing机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID)) 213 | } else { 214 | logger.Info(fmt.Sprintf("🙋 企业内部机器人 在『%s』群的ConversationID为: %#v", msgObj.ConversationTitle, msgObj.ConversationID)) 215 | } 216 | return 217 | } 218 | 219 | // 不在允许群组,不在允许用户(包括在黑名单),满足任一条件,拒绝会话;管理员不受限制 220 | if msgObj.ConversationType == "2" && !public.JudgeGroup(msgObj.ConversationID) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" { 221 | logger.Info(fmt.Sprintf("🙋『%s』群组未被验证通过,群ID: %#v,userid:%#v, 昵称: %#v,消息: %#v", msgObj.ConversationTitle, msgObj.ConversationID, msgObj.SenderStaffId, msgObj.SenderNick, msgObj.Text.Content)) 222 | _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,该群组未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。") 223 | if err != nil { 224 | logger.Warning(fmt.Errorf("send message error: %v", err)) 225 | return 226 | } 227 | return 228 | } else if !public.JudgeUsers(msgObj.SenderStaffId) && !public.JudgeAdminUsers(msgObj.SenderStaffId) && msgObj.SenderStaffId != "" { 229 | logger.Info(fmt.Sprintf("🙋 %s身份信息未被验证通过,userid:%#v,消息: %#v", msgObj.SenderNick, msgObj.SenderStaffId, msgObj.Text.Content)) 230 | _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您的身份信息未被认证通过,无法使用机器人对话功能。**\n>如需继续使用,请联系管理员申请访问权限。") 231 | if err != nil { 232 | logger.Warning(fmt.Errorf("send message error: %v", err)) 233 | return 234 | } 235 | return 236 | } 237 | if len(msgObj.Text.Content) == 0 || msgObj.Text.Content == "帮助" { 238 | // 欢迎信息 239 | _, err := msgObj.ReplyToDingtalk(string(dingbot.MARKDOWN), public.Config.Help) 240 | if err != nil { 241 | logger.Warning(fmt.Errorf("send message error: %v", err)) 242 | return 243 | } 244 | } else { 245 | logger.Info(fmt.Sprintf("🙋 %s发起的问题: %#v", msgObj.SenderNick, msgObj.Text.Content)) 246 | // 除去帮助之外的逻辑分流在这里处理 247 | switch { 248 | case strings.HasPrefix(msgObj.Text.Content, "#图片"): 249 | err := process.ImageGenerate(c, &msgObj) 250 | if err != nil { 251 | logger.Warning(fmt.Errorf("process request: %v", err)) 252 | return 253 | } 254 | return 255 | case strings.HasPrefix(msgObj.Text.Content, "#查对话"): 256 | err := process.SelectHistory(&msgObj) 257 | if err != nil { 258 | logger.Warning(fmt.Errorf("process request: %v", err)) 259 | return 260 | } 261 | return 262 | case strings.HasPrefix(msgObj.Text.Content, "#域名"): 263 | err := process.DomainMsg(&msgObj) 264 | if err != nil { 265 | logger.Warning(fmt.Errorf("process request: %v", err)) 266 | return 267 | } 268 | return 269 | case strings.HasPrefix(msgObj.Text.Content, "#证书"): 270 | err := process.DomainCertMsg(&msgObj) 271 | if err != nil { 272 | logger.Warning(fmt.Errorf("process request: %v", err)) 273 | return 274 | } 275 | return 276 | default: 277 | var err error 278 | msgObj.Text.Content, err = process.GeneratePrompt(msgObj.Text.Content) 279 | // err不为空:提示词之后没有文本 -> 直接返回提示词所代表的内容 280 | if err != nil { 281 | _, err = msgObj.ReplyToDingtalk(string(dingbot.TEXT), msgObj.Text.Content) 282 | if err != nil { 283 | logger.Warning(fmt.Errorf("send message error: %v", err)) 284 | return 285 | } 286 | return 287 | } 288 | err = process.ProcessRequest(&msgObj) 289 | if err != nil { 290 | logger.Warning(fmt.Errorf("process request: %v", err)) 291 | return 292 | } 293 | return 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /pkg/cache/user_base.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/patrickmn/go-cache" 7 | 8 | "github.com/eryajf/chatgpt-dingtalk/config" 9 | ) 10 | 11 | // UserServiceInterface 用户业务接口 12 | type UserServiceInterface interface { 13 | // 用户聊天模式 14 | GetUserMode(userId string) string 15 | SetUserMode(userId, mode string) 16 | ClearUserMode(userId string) 17 | // 用户聊天上下文 18 | GetUserSessionContext(userId string) string 19 | SetUserSessionContext(userId, content string) 20 | ClearUserSessionContext(userId string) 21 | // 用户请求次数 22 | SetUseRequestCount(userId string, current int) 23 | GetUseRequestCount(uerId string) int 24 | // 用户对话ID 25 | SetAnswerID(userId, chattype string, current uint) 26 | GetAnswerID(uerId, chattype string) uint 27 | ClearAnswerID(userId, chattitle string) 28 | } 29 | 30 | var _ UserServiceInterface = (*UserService)(nil) 31 | 32 | // UserService 用戶业务 33 | type UserService struct { 34 | // 缓存 35 | cache *cache.Cache 36 | } 37 | 38 | var Config *config.Configuration 39 | 40 | // NewUserService 创建新的业务层 41 | func NewUserService() UserServiceInterface { 42 | // 加载配置 43 | Config = config.LoadConfig() 44 | return &UserService{cache: cache.New(Config.SessionTimeout, time.Hour*1)} 45 | } 46 | -------------------------------------------------------------------------------- /pkg/cache/user_chatid.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "time" 4 | 5 | // SetAnswerID 设置用户获得答案的ID 6 | func (s *UserService) SetAnswerID(userId, chattitle string, current uint) { 7 | s.cache.Set(userId+"_"+chattitle, current, time.Hour*24) 8 | } 9 | 10 | // GetAnswerID 获取当前用户获得答案的ID 11 | func (s *UserService) GetAnswerID(userId, chattitle string) uint { 12 | sessionContext, ok := s.cache.Get(userId + "_" + chattitle) 13 | if !ok { 14 | return 0 15 | } 16 | return sessionContext.(uint) 17 | } 18 | 19 | // ClearUserSessionContext 清空GPT上下文,接收文本中包含 SessionClearToken 20 | func (s *UserService) ClearAnswerID(userId, chattitle string) { 21 | s.cache.Delete(userId + "_" + chattitle) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/cache/user_context.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "github.com/patrickmn/go-cache" 4 | 5 | // SetUserSessionContext 设置用户会话上下文文本,question用户提问内容,GPT回复内容 6 | func (s *UserService) SetUserSessionContext(userId string, content string) { 7 | s.cache.Set(userId+"_content", content, cache.DefaultExpiration) 8 | } 9 | 10 | // GetUserSessionContext 获取用户会话上下文文本 11 | func (s *UserService) GetUserSessionContext(userId string) string { 12 | sessionContext, ok := s.cache.Get(userId + "_content") 13 | if !ok { 14 | return "" 15 | } 16 | return sessionContext.(string) 17 | } 18 | 19 | // ClearUserSessionContext 清空GPT上下文,接收文本中包含 SessionClearToken 20 | func (s *UserService) ClearUserSessionContext(userId string) { 21 | s.cache.Delete(userId + "_content") 22 | } 23 | -------------------------------------------------------------------------------- /pkg/cache/user_mode.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "github.com/patrickmn/go-cache" 4 | 5 | // GetUserMode 获取当前对话模式 6 | func (s *UserService) GetUserMode(userId string) string { 7 | sessionContext, ok := s.cache.Get(userId + "_mode") 8 | if !ok { 9 | return "" 10 | } 11 | return sessionContext.(string) 12 | } 13 | 14 | // SetUserMode 设置用户对话模式 15 | func (s *UserService) SetUserMode(userId string, mode string) { 16 | s.cache.Set(userId+"_mode", mode, cache.DefaultExpiration) 17 | } 18 | 19 | // ClearUserMode 重置用户对话模式 20 | func (s *UserService) ClearUserMode(userId string) { 21 | s.cache.Delete(userId + "_mode") 22 | } 23 | -------------------------------------------------------------------------------- /pkg/cache/user_requese.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // SetUseRequestCount 设置用户请求次数 8 | func (s *UserService) SetUseRequestCount(userId string, current int) { 9 | expiration := time.Now().Add(time.Hour * 24).Truncate(time.Hour * 24) 10 | duration := time.Until(expiration) 11 | // 设置缓存失效时间为第二天零点 12 | s.cache.Set(userId+"_request", current, duration) 13 | } 14 | 15 | // GetUseRequestCount 获取当前用户已请求次数 16 | func (s *UserService) GetUseRequestCount(userId string) int { 17 | sessionContext, ok := s.cache.Get(userId + "_request") 18 | if !ok { 19 | return 0 20 | } 21 | return sessionContext.(int) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/chatgpt/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shihao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pkg/chatgpt/README.md: -------------------------------------------------------------------------------- 1 | > 因为三方包写死了很多参数,这里转到本地,便于二次改造。 感谢:https://github.com/solywsh/chatgpt 2 | -------------------------------------------------------------------------------- /pkg/chatgpt/chatgpt.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | 9 | openai "github.com/sashabaranov/go-openai" 10 | 11 | "github.com/eryajf/chatgpt-dingtalk/public" 12 | ) 13 | 14 | type ChatGPT struct { 15 | client *openai.Client 16 | ctx context.Context 17 | userId string 18 | maxQuestionLen int 19 | maxText int 20 | maxAnswerLen int 21 | timeOut time.Duration // 超时时间, 0表示不超时 22 | doneChan chan struct{} 23 | cancel func() 24 | 25 | ChatContext *ChatContext 26 | } 27 | 28 | func New(userId string) *ChatGPT { 29 | var ctx context.Context 30 | var cancel func() 31 | 32 | ctx, cancel = context.WithTimeout(context.Background(), 600*time.Second) 33 | timeOutChan := make(chan struct{}, 1) 34 | go func() { 35 | <-ctx.Done() 36 | timeOutChan <- struct{}{} // 发送超时信号,或是提示结束,用于聊天机器人场景,配合GetTimeOutChan() 使用 37 | }() 38 | 39 | config := openai.DefaultConfig(public.Config.ApiKey) 40 | if public.Config.AzureOn { 41 | config = openai.DefaultAzureConfig( 42 | public.Config.AzureOpenAIToken, 43 | "https://"+public.Config.AzureResourceName+".openai.azure.com", 44 | ) 45 | config.APIVersion = public.Config.AzureApiVersion 46 | config.AzureModelMapperFunc = func(model string) string { 47 | return public.Config.AzureDeploymentName 48 | } 49 | } else { 50 | if public.Config.HttpProxy != "" { 51 | config.HTTPClient.Transport = &http.Transport{ 52 | // 设置代理 53 | Proxy: func(req *http.Request) (*url.URL, error) { 54 | return url.Parse(public.Config.HttpProxy) 55 | }} 56 | } 57 | if public.Config.BaseURL != "" { 58 | config.BaseURL = public.Config.BaseURL + "/v1" 59 | } 60 | } 61 | 62 | return &ChatGPT{ 63 | client: openai.NewClientWithConfig(config), 64 | ctx: ctx, 65 | userId: userId, 66 | maxQuestionLen: public.Config.MaxQuestionLen, // 最大问题长度 67 | maxAnswerLen: public.Config.MaxAnswerLen, // 最大答案长度 68 | maxText: public.Config.MaxText, // 最大文本 = 问题 + 回答, 接口限制 69 | timeOut: public.Config.SessionTimeout, 70 | doneChan: timeOutChan, 71 | cancel: func() { 72 | cancel() 73 | }, 74 | ChatContext: NewContext(), 75 | } 76 | } 77 | func (c *ChatGPT) Close() { 78 | c.cancel() 79 | } 80 | 81 | func (c *ChatGPT) GetDoneChan() chan struct{} { 82 | return c.doneChan 83 | } 84 | 85 | func (c *ChatGPT) SetMaxQuestionLen(maxQuestionLen int) int { 86 | if maxQuestionLen > c.maxText-c.maxAnswerLen { 87 | maxQuestionLen = c.maxText - c.maxAnswerLen 88 | } 89 | c.maxQuestionLen = maxQuestionLen 90 | return c.maxQuestionLen 91 | } 92 | -------------------------------------------------------------------------------- /pkg/chatgpt/chatgpt_test.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestChatGPT_ChatWithContext(t *testing.T) { 9 | chat := New("") 10 | defer chat.Close() 11 | //go func() { 12 | // select { 13 | // case <-chat.GetDoneChan(): 14 | // fmt.Println("time out") 15 | // } 16 | //}() 17 | question := "现在你是一只猫,接下来你只能用\"喵喵喵\"回答." 18 | fmt.Printf("Q: %s\n", question) 19 | answer, err := chat.ChatWithContext(question) 20 | if err != nil { 21 | fmt.Println(err) 22 | } 23 | fmt.Printf("A: %s\n", answer) 24 | question = "你是一只猫吗?" 25 | fmt.Printf("Q: %s\n", question) 26 | answer, err = chat.ChatWithContext(question) 27 | if err != nil { 28 | fmt.Println(err) 29 | } 30 | fmt.Printf("A: %s\n", answer) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/chatgpt/context.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/gob" 8 | "errors" 9 | "fmt" 10 | 11 | "golang.org/x/image/webp" 12 | "image" 13 | _ "image/gif" 14 | _ "image/jpeg" 15 | "image/png" 16 | 17 | "os" 18 | "strings" 19 | "time" 20 | 21 | "github.com/pandodao/tokenizer-go" 22 | openai "github.com/sashabaranov/go-openai" 23 | 24 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 25 | "github.com/eryajf/chatgpt-dingtalk/public" 26 | ) 27 | 28 | var ( 29 | DefaultAiRole = "AI" 30 | DefaultHumanRole = "Human" 31 | 32 | DefaultCharacter = []string{"helpful", "creative", "clever", "friendly", "lovely", "talkative"} 33 | DefaultBackground = "The following is a conversation with AI assistant. The assistant is %s" 34 | DefaultPreset = "\n%s: 你好,让我们开始愉快的谈话!\n%s: 我是 AI assistant ,请问你有什么问题?" 35 | ) 36 | 37 | type ( 38 | ChatContext struct { 39 | background string // 对话背景 40 | preset string // 预设对话 41 | maxSeqTimes int // 最大对话次数 42 | aiRole *role // AI角色 43 | humanRole *role // 人类角色 44 | 45 | old []conversation // 旧对话 46 | restartSeq string // 重新开始对话的标识 47 | startSeq string // 开始对话的标识 48 | 49 | seqTimes int // 对话次数 50 | 51 | maintainSeqTimes bool // 是否维护对话次数 (自动移除旧对话) 52 | } 53 | 54 | ChatContextOption func(*ChatContext) 55 | 56 | conversation struct { 57 | Role *role 58 | Prompt string 59 | } 60 | 61 | role struct { 62 | Name string 63 | } 64 | ) 65 | 66 | func NewContext(options ...ChatContextOption) *ChatContext { 67 | ctx := &ChatContext{ 68 | aiRole: &role{Name: DefaultAiRole}, 69 | humanRole: &role{Name: DefaultHumanRole}, 70 | background: "", 71 | maxSeqTimes: 1000, 72 | preset: "", 73 | old: []conversation{}, 74 | seqTimes: 0, 75 | restartSeq: "\n" + DefaultHumanRole + ": ", 76 | startSeq: "\n" + DefaultAiRole + ": ", 77 | maintainSeqTimes: false, 78 | } 79 | 80 | for _, option := range options { 81 | option(ctx) 82 | } 83 | return ctx 84 | } 85 | 86 | // PollConversation 移除最旧的一则对话 87 | func (c *ChatContext) PollConversation() { 88 | c.old = c.old[1:] 89 | c.seqTimes-- 90 | } 91 | 92 | // ResetConversation 重置对话 93 | func (c *ChatContext) ResetConversation(userid string) { 94 | public.UserService.ClearUserSessionContext(userid) 95 | } 96 | 97 | // SaveConversation 保存对话 98 | func (c *ChatContext) SaveConversation(userid string) error { 99 | var buffer bytes.Buffer 100 | enc := gob.NewEncoder(&buffer) 101 | err := enc.Encode(c.old) 102 | if err != nil { 103 | return err 104 | } 105 | public.UserService.SetUserSessionContext(userid, buffer.String()) 106 | return nil 107 | } 108 | 109 | // LoadConversation 加载对话 110 | func (c *ChatContext) LoadConversation(userid string) error { 111 | dec := gob.NewDecoder(strings.NewReader(public.UserService.GetUserSessionContext(userid))) 112 | err := dec.Decode(&c.old) 113 | if err != nil { 114 | return err 115 | } 116 | c.seqTimes = len(c.old) 117 | return nil 118 | } 119 | 120 | func (c *ChatContext) SetHumanRole(role string) { 121 | c.humanRole.Name = role 122 | c.restartSeq = "\n" + c.humanRole.Name + ": " 123 | } 124 | 125 | func (c *ChatContext) SetAiRole(role string) { 126 | c.aiRole.Name = role 127 | c.startSeq = "\n" + c.aiRole.Name + ": " 128 | } 129 | 130 | func (c *ChatContext) SetMaxSeqTimes(times int) { 131 | c.maxSeqTimes = times 132 | } 133 | 134 | func (c *ChatContext) GetMaxSeqTimes() int { 135 | return c.maxSeqTimes 136 | } 137 | 138 | func (c *ChatContext) SetBackground(background string) { 139 | c.background = background 140 | } 141 | 142 | func (c *ChatContext) SetPreset(preset string) { 143 | c.preset = preset 144 | } 145 | 146 | // 通过 base64 编码字符串开头字符判断图像类型 147 | func getImageTypeFromBase64(base64Str string) string { 148 | switch { 149 | case strings.HasPrefix(base64Str, "/9j/"): 150 | return "JPEG" 151 | case strings.HasPrefix(base64Str, "iVBOR"): 152 | return "PNG" 153 | case strings.HasPrefix(base64Str, "R0lG"): 154 | return "GIF" 155 | case strings.HasPrefix(base64Str, "UklG"): 156 | return "WebP" 157 | default: 158 | return "Unknown" 159 | } 160 | } 161 | 162 | func (c *ChatGPT) ChatWithContext(question string) (answer string, err error) { 163 | question = question + "." 164 | if tokenizer.MustCalToken(question) > c.maxQuestionLen { 165 | return "", OverMaxQuestionLength 166 | } 167 | if c.ChatContext.seqTimes >= c.ChatContext.maxSeqTimes { 168 | if c.ChatContext.maintainSeqTimes { 169 | c.ChatContext.PollConversation() 170 | } else { 171 | return "", OverMaxSequenceTimes 172 | } 173 | } 174 | var promptTable []string 175 | promptTable = append(promptTable, c.ChatContext.background) 176 | promptTable = append(promptTable, c.ChatContext.preset) 177 | for _, v := range c.ChatContext.old { 178 | if v.Role == c.ChatContext.humanRole { 179 | promptTable = append(promptTable, "\n"+v.Role.Name+": "+v.Prompt) 180 | } else { 181 | promptTable = append(promptTable, v.Role.Name+": "+v.Prompt) 182 | } 183 | } 184 | promptTable = append(promptTable, "\n"+c.ChatContext.restartSeq+question) 185 | prompt := strings.Join(promptTable, "\n") 186 | prompt += c.ChatContext.startSeq 187 | // 删除对话,直到prompt的长度满足条件 188 | for tokenizer.MustCalToken(prompt) > c.maxText { 189 | if len(c.ChatContext.old) > 1 { // 至少保留一条记录 190 | c.ChatContext.PollConversation() // 删除最旧的一条对话 191 | // 重新构建 prompt,计算长度 192 | promptTable = promptTable[1:] // 删除promptTable中对应的对话 193 | prompt = strings.Join(promptTable, "\n") + c.ChatContext.startSeq 194 | } else { 195 | break // 如果已经只剩一条记录,那么跳出循环 196 | } 197 | } 198 | // if tokenizer.MustCalToken(prompt) > c.maxText-c.maxAnswerLen { 199 | // return "", OverMaxTextLength 200 | // } 201 | model := public.Config.Model 202 | userId := c.userId 203 | if public.Config.AzureOn { 204 | userId = "" 205 | } 206 | if isModelSupportedChatCompletions(model) { 207 | req := openai.ChatCompletionRequest{ 208 | Model: model, 209 | Messages: []openai.ChatCompletionMessage{ 210 | { 211 | Role: "user", 212 | Content: prompt, 213 | }, 214 | }, 215 | MaxTokens: c.maxAnswerLen, 216 | Temperature: 0.6, 217 | User: userId, 218 | } 219 | resp, err := c.client.CreateChatCompletion(c.ctx, req) 220 | if err != nil { 221 | return "", err 222 | } 223 | resp.Choices[0].Message.Content = formatAnswer(resp.Choices[0].Message.Content) 224 | c.ChatContext.old = append(c.ChatContext.old, conversation{ 225 | Role: c.ChatContext.humanRole, 226 | Prompt: question, 227 | }) 228 | c.ChatContext.old = append(c.ChatContext.old, conversation{ 229 | Role: c.ChatContext.aiRole, 230 | Prompt: resp.Choices[0].Message.Content, 231 | }) 232 | c.ChatContext.seqTimes++ 233 | return resp.Choices[0].Message.Content, nil 234 | } else { 235 | req := openai.CompletionRequest{ 236 | Model: model, 237 | MaxTokens: c.maxAnswerLen, 238 | Prompt: prompt, 239 | Temperature: 0.6, 240 | User: c.userId, 241 | Stop: []string{c.ChatContext.aiRole.Name + ":", c.ChatContext.humanRole.Name + ":"}, 242 | } 243 | resp, err := c.client.CreateCompletion(c.ctx, req) 244 | if err != nil { 245 | return "", err 246 | } 247 | resp.Choices[0].Text = formatAnswer(resp.Choices[0].Text) 248 | c.ChatContext.old = append(c.ChatContext.old, conversation{ 249 | Role: c.ChatContext.humanRole, 250 | Prompt: question, 251 | }) 252 | c.ChatContext.old = append(c.ChatContext.old, conversation{ 253 | Role: c.ChatContext.aiRole, 254 | Prompt: resp.Choices[0].Text, 255 | }) 256 | c.ChatContext.seqTimes++ 257 | return resp.Choices[0].Text, nil 258 | } 259 | } 260 | func (c *ChatGPT) GenerateImage(ctx context.Context, prompt string) (string, error) { 261 | model := public.Config.Model 262 | imageModel := public.Config.ImageModel 263 | if isModelSupportedChatCompletions(model) { 264 | req := openai.ImageRequest{ 265 | Prompt: prompt, 266 | Model: imageModel, 267 | Size: openai.CreateImageSize1024x1024, 268 | ResponseFormat: openai.CreateImageResponseFormatB64JSON, 269 | N: 1, 270 | User: c.userId, 271 | } 272 | respBase64, err := c.client.CreateImage(c.ctx, req) 273 | if err != nil { 274 | return "", err 275 | } 276 | imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON) 277 | if err != nil { 278 | return "", err 279 | } 280 | 281 | r := bytes.NewReader(imgBytes) 282 | 283 | // dall-e-3 返回的是 WebP 格式的图片,需要判断处理 284 | imgType := getImageTypeFromBase64(respBase64.Data[0].B64JSON) 285 | var imgData image.Image 286 | var imgErr error 287 | if imgType == "WebP" { 288 | imgData, imgErr = webp.Decode(r) 289 | } else { 290 | imgData, _, imgErr = image.Decode(r) 291 | } 292 | if imgErr != nil { 293 | return "", imgErr 294 | } 295 | 296 | imageName := time.Now().Format("20060102-150405") + ".png" 297 | clientId, _ := ctx.Value(public.DingTalkClientIdKeyName).(string) 298 | client := public.DingTalkClientManager.GetClientByOAuthClientID(clientId) 299 | mediaResult, uploadErr := &dingbot.MediaUploadResult{}, errors.New(fmt.Sprintf("unknown clientId: %s", clientId)) 300 | if client != nil { 301 | mediaResult, uploadErr = client.UploadMedia(imgBytes, imageName, dingbot.MediaTypeImage, dingbot.MimeTypeImagePng) 302 | } 303 | 304 | err = os.MkdirAll("data/images", 0755) 305 | if err != nil { 306 | return "", err 307 | } 308 | file, err := os.Create("data/images/" + imageName) 309 | if err != nil { 310 | return "", err 311 | } 312 | defer file.Close() 313 | 314 | if err := png.Encode(file, imgData); err != nil { 315 | return "", err 316 | } 317 | if uploadErr == nil { 318 | return mediaResult.MediaID, nil 319 | } else { 320 | return public.Config.ServiceURL + "/images/" + imageName, nil 321 | } 322 | } 323 | return "", nil 324 | } 325 | 326 | func WithMaxSeqTimes(times int) ChatContextOption { 327 | return func(c *ChatContext) { 328 | c.SetMaxSeqTimes(times) 329 | } 330 | } 331 | 332 | // WithOldConversation 从文件中加载对话 333 | func WithOldConversation(userid string) ChatContextOption { 334 | return func(c *ChatContext) { 335 | _ = c.LoadConversation(userid) 336 | } 337 | } 338 | 339 | func WithMaintainSeqTimes(maintain bool) ChatContextOption { 340 | return func(c *ChatContext) { 341 | c.maintainSeqTimes = maintain 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /pkg/chatgpt/context_test.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestOfflineContext(t *testing.T) { 9 | key := os.Getenv("CHATGPT_API_KEY") 10 | if key == "" { 11 | t.Skip("CHATGPT_API_KEY is not set") 12 | } 13 | cli := New("") 14 | reply, err := cli.ChatWithContext("我叫老三,你是?") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | t.Logf("我叫老三,你是? => %s", reply) 20 | 21 | err = cli.ChatContext.SaveConversation("test.conversation") 22 | if err != nil { 23 | t.Fatalf("储存对话记录失败: %v", err) 24 | } 25 | cli.ChatContext.ResetConversation("") 26 | 27 | reply, err = cli.ChatWithContext("你知道我是谁吗?") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | t.Logf("你知道我是谁吗? => %s", reply) 33 | // assert.NotContains(t, reply, "老三") 34 | 35 | err = cli.ChatContext.LoadConversation("test.conversation") 36 | if err != nil { 37 | t.Fatalf("读取对话记录失败: %v", err) 38 | } 39 | 40 | reply, err = cli.ChatWithContext("你知道我是谁吗?") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | t.Logf("你知道我是谁吗? => %s", reply) 46 | 47 | // AI 理应知道他叫老三 48 | // assert.Contains(t, reply, "老三") 49 | } 50 | 51 | func TestMaintainContext(t *testing.T) { 52 | key := os.Getenv("CHATGPT_API_KEY") 53 | if key == "" { 54 | t.Skip("CHATGPT_API_KEY is not set") 55 | } 56 | cli := New("") 57 | cli.ChatContext = NewContext( 58 | WithMaxSeqTimes(1), 59 | WithMaintainSeqTimes(true), 60 | ) 61 | 62 | reply, err := cli.ChatWithContext("我叫老三,你是?") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | t.Logf("我叫老三,你是? => %s", reply) 67 | 68 | reply, err = cli.ChatWithContext("你知道我是谁吗?") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | t.Logf("你知道我是谁吗? => %s", reply) 73 | 74 | // 对话次数已经超过 1 次,因此最先前的对话已被移除,AI 理应不知道他叫老三 75 | // assert.NotContains(t, reply, "老三") 76 | } 77 | 78 | func init() { 79 | // 本地加载适用于本地测试,如果要在github进行测试,可以透过传入 secrets 到环境参数 80 | // _ = godotenv.Load(".env.local") 81 | } 82 | -------------------------------------------------------------------------------- /pkg/chatgpt/errors.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import "errors" 4 | 5 | // OverMaxSequenceTimes 超过最大对话时间 6 | var OverMaxSequenceTimes = errors.New("maximum conversation times exceeded") 7 | 8 | // OverMaxTextLength 超过最大文本长度 9 | var OverMaxTextLength = errors.New("maximum text length exceeded") 10 | 11 | // OverMaxQuestionLength 超过最大问题长度 12 | var OverMaxQuestionLength = errors.New("maximum question length exceeded") 13 | -------------------------------------------------------------------------------- /pkg/chatgpt/export.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/avast/retry-go" 8 | 9 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 10 | "github.com/eryajf/chatgpt-dingtalk/public" 11 | ) 12 | 13 | // SingleQa 单聊 14 | func SingleQa(question, userId string) (answer string, err error) { 15 | chat := New(userId) 16 | defer chat.Close() 17 | // 定义一个重试策略 18 | retryStrategy := []retry.Option{ 19 | retry.Delay(100 * time.Millisecond), 20 | retry.Attempts(3), 21 | retry.LastErrorOnly(true), 22 | } 23 | // 使用重试策略进行重试 24 | err = retry.Do( 25 | func() error { 26 | answer, err = chat.ChatWithContext(question) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | }, 32 | retryStrategy...) 33 | return 34 | } 35 | 36 | // ContextQa 串聊 37 | func ContextQa(question, userId string) (chat *ChatGPT, answer string, err error) { 38 | chat = New(userId) 39 | if public.UserService.GetUserSessionContext(userId) != "" { 40 | err := chat.ChatContext.LoadConversation(userId) 41 | if err != nil { 42 | logger.Warning("load station failed: %v\n", err) 43 | } 44 | } 45 | retryStrategy := []retry.Option{ 46 | retry.Delay(100 * time.Millisecond), 47 | retry.Attempts(3), 48 | retry.LastErrorOnly(true)} 49 | // 使用重试策略进行重试 50 | err = retry.Do( 51 | func() error { 52 | answer, err = chat.ChatWithContext(question) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | }, 58 | retryStrategy...) 59 | return 60 | } 61 | 62 | // ImageQa 生成图片 63 | func ImageQa(ctx context.Context, question, userId string) (answer string, err error) { 64 | chat := New(userId) 65 | defer chat.Close() 66 | // 定义一个重试策略 67 | retryStrategy := []retry.Option{ 68 | retry.Delay(100 * time.Millisecond), 69 | retry.Attempts(3), 70 | retry.LastErrorOnly(true), 71 | } 72 | // 使用重试策略进行重试 73 | err = retry.Do( 74 | func() error { 75 | answer, err = chat.GenerateImage(ctx, question) 76 | if err != nil { 77 | return err 78 | } 79 | return nil 80 | }, 81 | retryStrategy...) 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /pkg/chatgpt/format.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // 适配 deepseek r1 9 | func formatAnswer(answer string) string { 10 | answer = strings.TrimSpace(answer) 11 | 12 | re := regexp.MustCompile(`(?s).*?`) 13 | answer = re.ReplaceAllString(answer, "") 14 | 15 | answer = strings.ReplaceAll(answer, "", "") 16 | answer = strings.ReplaceAll(answer, "", "") 17 | 18 | answer = strings.TrimSpace(answer) 19 | 20 | return answer 21 | } 22 | -------------------------------------------------------------------------------- /pkg/chatgpt/go.mod: -------------------------------------------------------------------------------- 1 | module chatgpt 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/avast/retry-go v3.0.0+incompatible 9 | github.com/chai2010/webp v1.1.1 10 | github.com/eryajf/chatgpt-dingtalk v1.0.11 11 | github.com/pandodao/tokenizer-go v0.2.0 12 | github.com/sashabaranov/go-openai v1.27.1 13 | ) 14 | 15 | replace github.com/eryajf/chatgpt-dingtalk v1.0.11 => ../.. 16 | 17 | require ( 18 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 19 | github.com/charmbracelet/lipgloss v0.12.1 // indirect 20 | github.com/charmbracelet/log v0.4.0 // indirect 21 | github.com/dlclark/regexp2 v1.11.2 // indirect 22 | github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 // indirect 23 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc // indirect 24 | github.com/dustin/go-humanize v1.0.1 // indirect 25 | github.com/glebarez/go-sqlite v1.22.0 // indirect 26 | github.com/glebarez/sqlite v1.11.0 // indirect 27 | github.com/go-logfmt/logfmt v0.6.0 // indirect 28 | github.com/go-resty/resty/v2 v2.13.1 // indirect 29 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 30 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/jinzhu/now v1.1.5 // indirect 34 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.16 // indirect 37 | github.com/muesli/reflow v0.3.0 // indirect 38 | github.com/muesli/termenv v0.15.2 // indirect 39 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 40 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 41 | github.com/rivo/uniseg v0.4.7 // indirect 42 | golang.org/x/net v0.27.0 // indirect 43 | golang.org/x/sys v0.22.0 // indirect 44 | golang.org/x/text v0.16.0 // indirect 45 | gopkg.in/yaml.v2 v2.4.0 // indirect 46 | gorm.io/gorm v1.25.11 // indirect 47 | modernc.org/libc v1.55.6 // indirect 48 | modernc.org/mathutil v1.6.0 // indirect 49 | modernc.org/memory v1.8.0 // indirect 50 | modernc.org/sqlite v1.31.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /pkg/chatgpt/go.sum: -------------------------------------------------------------------------------- 1 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 2 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= 6 | github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= 7 | github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= 8 | github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 9 | github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= 10 | github.com/charmbracelet/log v0.2.1 h1:1z7jpkk4yKyjwlmKmKMM5qnEDSpV32E7XtWhuv0mTZE= 11 | github.com/charmbracelet/log v0.2.1/go.mod h1:GwFfjewhcVDWLrpAbY5A0Hin9YOlEn40eWT4PNaxFT4= 12 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 13 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 14 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 15 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 19 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 20 | github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= 21 | github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 22 | github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 23 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= 24 | github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= 25 | github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 h1:xkbJGxVnk5sM8/LXeTKaBOfAZrI+iqvIPyH8oK1c6CQ= 26 | github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= 27 | github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= 28 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= 29 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 30 | github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124 h1:QDuDMgEkC/lnmvk0d/fZfcUUml18uUbS9TY5QtbdFhs= 31 | github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0= 32 | github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU= 33 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 34 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 35 | github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= 36 | github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= 37 | github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 38 | github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI= 39 | github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk= 40 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 41 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 42 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 43 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 44 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 45 | github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= 46 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 47 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 48 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 49 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 50 | github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ= 51 | github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= 52 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 53 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 54 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 55 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 56 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 58 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 59 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 60 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 61 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 62 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 63 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 64 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 65 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 69 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 70 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 71 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 72 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 73 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 74 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 75 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 76 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 77 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 78 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 79 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 80 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 81 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= 82 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 83 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 84 | github.com/pandodao/tokenizer-go v0.2.0 h1:NhfI8fGvQkDld2cZCag6NEU3pJ/ugU9zoY1R/zi9YCs= 85 | github.com/pandodao/tokenizer-go v0.2.0/go.mod h1:t6qFbaleKxbv0KNio2XUN/mfGM5WKv4haPXDQWVDG00= 86 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 87 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 90 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 91 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 92 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 93 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 94 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 95 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 96 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 97 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 98 | github.com/sashabaranov/go-openai v1.17.6 h1:hYXRPM1xO6QLOJhWEOMlSg/l3jERiKDKd1qIoK22lvs= 99 | github.com/sashabaranov/go-openai v1.17.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 100 | github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 101 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 102 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 103 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 104 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 105 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 106 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 107 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 108 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 109 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 112 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 113 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 114 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 115 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 116 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 117 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 118 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 119 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 120 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 121 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 122 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 134 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 137 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 138 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 139 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 140 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 141 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 142 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 143 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 144 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 145 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 146 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 147 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 148 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 149 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 150 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 151 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 152 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 153 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 154 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 155 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 156 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 157 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 158 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 159 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 161 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 162 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 163 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 167 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 168 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 169 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 170 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 171 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 172 | gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s= 173 | gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 174 | gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 175 | modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= 176 | modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= 177 | modernc.org/libc v1.55.6/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA= 178 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 179 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 180 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 181 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 182 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 183 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 184 | modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= 185 | modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= 186 | modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= 187 | -------------------------------------------------------------------------------- /pkg/chatgpt/models.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import openai "github.com/sashabaranov/go-openai" 4 | 5 | var ModelsSupportChatCompletions = []string{ 6 | openai.GPT432K0613, 7 | openai.GPT432K0314, 8 | openai.GPT432K, 9 | openai.GPT40613, 10 | openai.GPT40314, 11 | openai.GPT4TurboPreview, 12 | openai.GPT4VisionPreview, 13 | openai.GPT4, 14 | openai.GPT4oMini, 15 | openai.GPT3Dot5Turbo1106, 16 | openai.GPT3Dot5Turbo0613, 17 | openai.GPT3Dot5Turbo0301, 18 | openai.GPT3Dot5Turbo16K, 19 | openai.GPT3Dot5Turbo16K0613, 20 | openai.GPT3Dot5Turbo, 21 | } 22 | 23 | func isModelSupportedChatCompletions(model string) bool { 24 | for _, m := range ModelsSupportChatCompletions { 25 | if m == model { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /pkg/db/chat.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type ChatType uint 11 | 12 | const Q ChatType = 1 13 | const A ChatType = 2 14 | 15 | type Chat struct { 16 | gorm.Model 17 | Username string `gorm:"type:varchar(50);not null;comment:'用户名'" json:"username"` // 用户名 18 | Source string `gorm:"type:varchar(50);comment:'用户来源:群聊名字,私聊'" json:"source"` // 对话来源 19 | ChatType ChatType `gorm:"type:tinyint(1);default:1;comment:'类型:1问, 2答'" json:"chat_type"` // 状态 20 | ParentContent uint `gorm:"default:0;comment:'父消息编号(编号为0时表示为首条)'" json:"parent_content"` 21 | Content string `gorm:"type:varchar(128);comment:'内容'" json:"content"` // 问题或回答的内容 22 | } 23 | 24 | type ChatListReq struct { 25 | Username string `json:"username" form:"username"` 26 | Source string `json:"source" form:"source"` 27 | } 28 | 29 | // Add 添加资源 30 | func (c Chat) Add() (uint, error) { 31 | err := DB.Create(&c).Error 32 | return c.ID, err 33 | } 34 | 35 | // Find 获取单个资源 36 | func (c Chat) Find(filter map[string]interface{}, data *Chat) error { 37 | return DB.Where(filter).First(&data).Error 38 | } 39 | 40 | // List 获取数据列表 41 | func (c Chat) List(req ChatListReq) ([]*Chat, error) { 42 | var list []*Chat 43 | db := DB.Model(&Chat{}).Order("created_at ASC") 44 | 45 | userName := strings.TrimSpace(req.Username) 46 | if userName != "" { 47 | db = db.Where("username = ?", userName) 48 | } 49 | source := strings.TrimSpace(req.Source) 50 | if source != "" { 51 | db = db.Where("source = ?", source) 52 | } 53 | 54 | err := db.Find(&list).Error 55 | return list, err 56 | } 57 | 58 | // Exist 判断资源是否存在 59 | func (c Chat) Exist(filter map[string]interface{}) bool { 60 | var dataObj Chat 61 | err := DB.Where(filter).First(&dataObj).Error 62 | return !errors.Is(err, gorm.ErrRecordNotFound) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/db/sqlite.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/glebarez/sqlite" 7 | "gorm.io/gorm" 8 | 9 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 10 | ) 11 | 12 | // 全局数据库对象 13 | var DB *gorm.DB 14 | 15 | // 初始化数据库 16 | func InitDB() { 17 | DB = ConnSqlite() 18 | 19 | dbAutoMigrate() 20 | } 21 | 22 | // 自动迁移表结构 23 | func dbAutoMigrate() { 24 | _ = DB.AutoMigrate( 25 | Chat{}, 26 | ) 27 | } 28 | 29 | func ConnSqlite() *gorm.DB { 30 | err := os.MkdirAll("data", 0755) 31 | if err != nil { 32 | return nil 33 | } 34 | db, err := gorm.Open(sqlite.Open("data/dingtalkbot.sqlite"), &gorm.Config{ 35 | // 禁用外键(指定外键时不会在mysql创建真实的外键约束) 36 | DisableForeignKeyConstraintWhenMigrating: true, 37 | }) 38 | if err != nil { 39 | logger.Fatal("failed to connect sqlite3: %v", err) 40 | } 41 | dbObj, err := db.DB() 42 | if err != nil { 43 | logger.Fatal("failed to get sqlite3 obj: %v", err) 44 | } 45 | // 参见: https://github.com/glebarez/sqlite/issues/52 46 | dbObj.SetMaxOpenConns(1) 47 | return db 48 | } 49 | -------------------------------------------------------------------------------- /pkg/dingbot/client.go: -------------------------------------------------------------------------------- 1 | package dingbot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "mime/multipart" 10 | "net/http" 11 | url2 "net/url" 12 | "sync" 13 | "time" 14 | 15 | "github.com/eryajf/chatgpt-dingtalk/config" 16 | ) 17 | 18 | // OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files 19 | const ( 20 | MediaTypeImage string = "image" 21 | MediaTypeVoice string = "voice" 22 | MediaTypeVideo string = "video" 23 | MediaTypeFile string = "file" 24 | ) 25 | const ( 26 | MimeTypeImagePng string = "image/png" 27 | ) 28 | 29 | type MediaUploadResult struct { 30 | ErrorCode int64 `json:"errcode"` 31 | ErrorMessage string `json:"errmsg"` 32 | MediaID string `json:"media_id"` 33 | CreatedAt int64 `json:"created_at"` 34 | Type string `json:"type"` 35 | } 36 | 37 | type OAuthTokenResult struct { 38 | ErrorCode int `json:"errcode"` 39 | ErrorMessage string `json:"errmsg"` 40 | AccessToken string `json:"access_token"` 41 | ExpiresIn int `json:"expires_in"` 42 | } 43 | 44 | type DingTalkClientInterface interface { 45 | GetAccessToken() (string, error) 46 | UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error) 47 | } 48 | 49 | type DingTalkClientManagerInterface interface { 50 | GetClientByOAuthClientID(clientId string) DingTalkClientInterface 51 | } 52 | 53 | type DingTalkClient struct { 54 | Credential config.Credential 55 | AccessToken string 56 | expireAt int64 57 | mutex sync.Mutex 58 | } 59 | 60 | type DingTalkClientManager struct { 61 | Credentials []config.Credential 62 | Clients map[string]*DingTalkClient 63 | mutex sync.Mutex 64 | } 65 | 66 | func NewDingTalkClient(credential config.Credential) *DingTalkClient { 67 | return &DingTalkClient{ 68 | Credential: credential, 69 | } 70 | } 71 | 72 | func NewDingTalkClientManager(conf *config.Configuration) *DingTalkClientManager { 73 | clients := make(map[string]*DingTalkClient) 74 | 75 | if conf != nil && conf.Credentials != nil { 76 | for _, credential := range conf.Credentials { 77 | clients[credential.ClientID] = NewDingTalkClient(credential) 78 | } 79 | } 80 | return &DingTalkClientManager{ 81 | Credentials: conf.Credentials, 82 | Clients: clients, 83 | } 84 | } 85 | 86 | func (m *DingTalkClientManager) GetClientByOAuthClientID(clientId string) DingTalkClientInterface { 87 | m.mutex.Lock() 88 | defer m.mutex.Unlock() 89 | if client, ok := m.Clients[clientId]; ok { 90 | return client 91 | } 92 | return nil 93 | } 94 | 95 | func (c *DingTalkClient) GetAccessToken() (string, error) { 96 | accessToken := "" 97 | { 98 | // 先查询缓存 99 | c.mutex.Lock() 100 | now := time.Now().Unix() 101 | if c.expireAt > 0 && c.AccessToken != "" && (now+60) < c.expireAt { 102 | // 预留一分钟有效期避免在Token过期的临界点调用接口出现401错误 103 | accessToken = c.AccessToken 104 | } 105 | c.mutex.Unlock() 106 | } 107 | if accessToken != "" { 108 | return accessToken, nil 109 | } 110 | 111 | tokenResult, err := c.getAccessTokenFromDingTalk() 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | { 117 | // 更新缓存 118 | c.mutex.Lock() 119 | c.AccessToken = tokenResult.AccessToken 120 | c.expireAt = time.Now().Unix() + int64(tokenResult.ExpiresIn) 121 | c.mutex.Unlock() 122 | } 123 | return tokenResult.AccessToken, nil 124 | } 125 | 126 | func (c *DingTalkClient) UploadMedia(content []byte, filename, mediaType, mimeType string) (*MediaUploadResult, error) { 127 | // OpenAPI doc: https://open.dingtalk.com/document/isvapp/upload-media-files 128 | accessToken, err := c.GetAccessToken() 129 | if err != nil { 130 | return nil, err 131 | } 132 | if len(accessToken) == 0 { 133 | return nil, errors.New("empty access token") 134 | } 135 | body := &bytes.Buffer{} 136 | writer := multipart.NewWriter(body) 137 | part, err := writer.CreateFormFile("media", filename) 138 | if err != nil { 139 | return nil, err 140 | } 141 | _, err = part.Write(content) 142 | if err != nil { 143 | return nil, err 144 | } 145 | if err = writer.WriteField("type", mediaType); err != nil { 146 | return nil, err 147 | } 148 | err = writer.Close() 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | // Create a new HTTP request to upload the media file 154 | url := fmt.Sprintf("https://oapi.dingtalk.com/media/upload?access_token=%s", url2.QueryEscape(accessToken)) 155 | req, err := http.NewRequest("POST", url, body) 156 | if err != nil { 157 | return nil, err 158 | } 159 | req.Header.Set("Content-Type", writer.FormDataContentType()) 160 | 161 | // Send the HTTP request and parse the response 162 | client := &http.Client{ 163 | Timeout: time.Second * 60, 164 | } 165 | res, err := client.Do(req) 166 | if err != nil { 167 | return nil, err 168 | } 169 | defer res.Body.Close() 170 | 171 | // Parse the response body as JSON and extract the media ID 172 | media := &MediaUploadResult{} 173 | bodyBytes, err := io.ReadAll(res.Body) 174 | if err != nil { 175 | return nil, err 176 | } 177 | if err = json.Unmarshal(bodyBytes, media); err != nil { 178 | return nil, err 179 | } 180 | if media.ErrorCode != 0 { 181 | return nil, errors.New(media.ErrorMessage) 182 | } 183 | return media, nil 184 | } 185 | 186 | func (c *DingTalkClient) getAccessTokenFromDingTalk() (*OAuthTokenResult, error) { 187 | // OpenAPI doc: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token 188 | apiUrl := "https://oapi.dingtalk.com/gettoken" 189 | queryParams := url2.Values{} 190 | queryParams.Add("appkey", c.Credential.ClientID) 191 | queryParams.Add("appsecret", c.Credential.ClientSecret) 192 | 193 | // Create a new HTTP request to get the AccessToken 194 | req, err := http.NewRequest("GET", apiUrl+"?"+queryParams.Encode(), nil) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | // Send the HTTP request and parse the response body as JSON 200 | client := http.Client{ 201 | Timeout: time.Second * 60, 202 | } 203 | res, err := client.Do(req) 204 | if err != nil { 205 | return nil, err 206 | } 207 | defer res.Body.Close() 208 | body, err := io.ReadAll(res.Body) 209 | if err != nil { 210 | return nil, err 211 | } 212 | tokenResult := &OAuthTokenResult{} 213 | err = json.Unmarshal(body, tokenResult) 214 | if err != nil { 215 | return nil, err 216 | } 217 | if tokenResult.ErrorCode != 0 { 218 | return nil, errors.New(tokenResult.ErrorMessage) 219 | } 220 | return tokenResult, nil 221 | } 222 | -------------------------------------------------------------------------------- /pkg/dingbot/client_test.go: -------------------------------------------------------------------------------- 1 | package dingbot 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "os" 9 | "testing" 10 | 11 | "github.com/eryajf/chatgpt-dingtalk/config" 12 | ) 13 | 14 | func TestUploadMedia_Pass_WithValidConfig(t *testing.T) { 15 | // 设置了钉钉 ClientID 和 ClientSecret 的环境变量才执行以下测试,用于快速验证钉钉图片上传能力 16 | clientId, clientSecret := os.Getenv("DINGTALK_CLIENT_ID_FOR_TEST"), os.Getenv("DINGTALK_CLIENT_SECRET_FOR_TEST") 17 | if len(clientId) <= 0 || len(clientSecret) <= 0 { 18 | return 19 | } 20 | credentials := []config.Credential{ 21 | config.Credential{ 22 | ClientID: clientId, 23 | ClientSecret: clientSecret, 24 | }, 25 | } 26 | client := NewDingTalkClientManager(&config.Configuration{Credentials: credentials}).GetClientByOAuthClientID(clientId) 27 | var imageContent []byte 28 | { 29 | // 生成一张用于测试的图片 30 | img := image.NewRGBA(image.Rect(0, 0, 200, 100)) 31 | blue := color.RGBA{0, 0, 255, 255} 32 | for x := 0; x < img.Bounds().Dx(); x++ { 33 | for y := 0; y < img.Bounds().Dy(); y++ { 34 | img.Set(x, y, blue) 35 | } 36 | } 37 | buf := new(bytes.Buffer) 38 | err := png.Encode(buf, img) 39 | if err != nil { 40 | return 41 | } 42 | // get the byte array from the buffer 43 | imageContent = buf.Bytes() 44 | } 45 | result, err := client.UploadMedia(imageContent, "filename.png", "image", "image/png") 46 | if err != nil { 47 | t.Errorf("upload media failed, err=%s", err.Error()) 48 | return 49 | } 50 | if result.MediaID == "" { 51 | t.Errorf("upload media failed, empty media id") 52 | return 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/dingbot/dingbot.go: -------------------------------------------------------------------------------- 1 | package dingbot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | // 接收的消息体 11 | type ReceiveMsg struct { 12 | ConversationID string `json:"conversationId"` 13 | AtUsers []struct { 14 | DingtalkID string `json:"dingtalkId"` 15 | } `json:"atUsers"` 16 | ChatbotUserID string `json:"chatbotUserId"` 17 | MsgID string `json:"msgId"` 18 | SenderNick string `json:"senderNick"` 19 | IsAdmin bool `json:"isAdmin"` 20 | SenderStaffId string `json:"senderStaffId"` 21 | SessionWebhookExpiredTime int64 `json:"sessionWebhookExpiredTime"` 22 | CreateAt int64 `json:"createAt"` 23 | ConversationType string `json:"conversationType"` 24 | SenderID string `json:"senderId"` 25 | ConversationTitle string `json:"conversationTitle"` 26 | IsInAtList bool `json:"isInAtList"` 27 | SessionWebhook string `json:"sessionWebhook"` 28 | Text Text `json:"text"` 29 | RobotCode string `json:"robotCode"` 30 | Msgtype MsgType `json:"msgtype"` 31 | } 32 | 33 | // 消息类型 34 | type MsgType string 35 | 36 | const TEXT MsgType = "text" 37 | const MARKDOWN MsgType = "markdown" 38 | 39 | // Text 消息 40 | type TextMessage struct { 41 | MsgType MsgType `json:"msgtype"` 42 | At *At `json:"at"` 43 | Text *Text `json:"text"` 44 | } 45 | 46 | // Text 消息内容 47 | type Text struct { 48 | Content string `json:"content"` 49 | } 50 | 51 | // MarkDown 消息 52 | type MarkDownMessage struct { 53 | MsgType MsgType `json:"msgtype"` 54 | At *At `json:"at"` 55 | MarkDown *MarkDown `json:"markdown"` 56 | } 57 | 58 | // MarkDown 消息内容 59 | type MarkDown struct { 60 | Title string `json:"title"` 61 | Text string `json:"text"` 62 | } 63 | 64 | // at 内容 65 | type At struct { 66 | AtUserIds []string `json:"atUserIds"` 67 | AtMobiles []string `json:"atMobiles"` 68 | IsAtAll bool `json:"isAtAll"` 69 | } 70 | 71 | // 获取用户标识,兼容当 SenderStaffId 字段为空的场景,此处提供给发送消息是艾特使用 72 | func (r ReceiveMsg) GetSenderIdentifier() (uid string) { 73 | uid = r.SenderStaffId 74 | if uid == "" { 75 | uid = r.SenderNick 76 | } 77 | return 78 | } 79 | 80 | // GetChatTitle 获取聊天的群名字,如果是私聊,则命名为 昵称_私聊 81 | func (r ReceiveMsg) GetChatTitle() (chatType string) { 82 | chatType = r.ConversationTitle 83 | if chatType == "" { 84 | chatType = r.SenderNick + "_私聊" 85 | } 86 | return 87 | } 88 | 89 | // 发消息给钉钉 90 | func (r ReceiveMsg) ReplyToDingtalk(msgType, msg string) (statuscode int, err error) { 91 | atUser := r.SenderStaffId 92 | if atUser == "" { 93 | msg = fmt.Sprintf("%s\n\n@%s", msg, r.SenderNick) 94 | } 95 | var msgtmp interface{} 96 | switch msgType { 97 | case string(TEXT): 98 | msgtmp = &TextMessage{Text: &Text{Content: msg}, MsgType: TEXT, At: &At{AtUserIds: []string{atUser}}} 99 | case string(MARKDOWN): 100 | if atUser != "" && r.ConversationType != "1" { 101 | msg = fmt.Sprintf("%s\n\n@%s", msg, atUser) 102 | } 103 | msgtmp = &MarkDownMessage{MsgType: MARKDOWN, At: &At{AtUserIds: []string{atUser}}, MarkDown: &MarkDown{Title: "Markdown Msg", Text: msg}} 104 | default: 105 | msgtmp = &TextMessage{Text: &Text{Content: msg}, MsgType: TEXT, At: &At{AtUserIds: []string{atUser}}} 106 | } 107 | 108 | data, err := json.Marshal(msgtmp) 109 | if err != nil { 110 | return 0, err 111 | } 112 | 113 | req, err := http.NewRequest("POST", r.SessionWebhook, bytes.NewBuffer(data)) 114 | if err != nil { 115 | return 0, err 116 | } 117 | req.Header.Add("Accept", "*/*") 118 | req.Header.Add("Content-Type", "application/json") 119 | client := &http.Client{} 120 | resp, err := client.Do(req) 121 | if err != nil { 122 | return 0, err 123 | } 124 | defer resp.Body.Close() 125 | return resp.StatusCode, nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/charmbracelet/log" 8 | ) 9 | 10 | var Logger *log.Logger 11 | var once sync.Once 12 | 13 | func InitLogger(level string) { 14 | once.Do(func() { 15 | Logger = log.NewWithOptions(os.Stderr, log.Options{ReportTimestamp: true}) 16 | }) 17 | if level == "debug" { 18 | Logger.SetLevel(log.DebugLevel) 19 | } else { 20 | Logger.SetLevel(log.InfoLevel) 21 | } 22 | } 23 | 24 | func Info(args ...interface{}) { 25 | Logger.Info(args) 26 | } 27 | 28 | func Warning(args ...interface{}) { 29 | Logger.Warn(args) 30 | } 31 | 32 | func Debug(args ...interface{}) { 33 | Logger.Debug(args) 34 | } 35 | 36 | func Error(args ...interface{}) { 37 | Logger.Error(args) 38 | } 39 | 40 | func Fatal(args ...interface{}) { 41 | Logger.Fatal(args) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/ops/opstools.go: -------------------------------------------------------------------------------- 1 | package ops 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "regexp" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // 域名信息 12 | type DomainMsg struct { 13 | CreateDate string `json:"create_date"` 14 | ExpiryDate string `json:"expiry_date"` 15 | Registrar string `json:"registrar"` 16 | } 17 | 18 | // GetDomainMsg 获取域名信息 19 | func GetDomainMsg(domain string) (dm DomainMsg, err error) { 20 | var conn net.Conn 21 | conn, err = net.Dial("tcp", "whois.verisign-grs.com:43") 22 | if err != nil { 23 | return 24 | } 25 | defer conn.Close() 26 | 27 | _, err = conn.Write([]byte(domain + "\r\n")) 28 | if err != nil { 29 | return 30 | } 31 | buf := make([]byte, 1024) 32 | var num int 33 | num, err = conn.Read(buf) 34 | if err != nil { 35 | return 36 | } 37 | response := string(buf[:num]) 38 | re := regexp.MustCompile(`Creation Date: (.*)\n.*Expiry Date: (.*)\n.*Registrar: (.*)`) 39 | match := re.FindStringSubmatch(response) 40 | if len(match) > 3 { 41 | dm.CreateDate = strings.TrimSpace(strings.Split(match[1], "Creation Date:")[0]) 42 | dm.ExpiryDate = strings.TrimSpace(strings.Split(match[2], "Expiry Date:")[0]) 43 | dm.Registrar = strings.TrimSpace(strings.Split(match[3], "Registrar:")[0]) 44 | } 45 | return 46 | } 47 | 48 | // GetDomainCertMsg 获取域名证书信息 49 | func GetDomainCertMsg(domain string) (cm tls.ConnectionState, err error) { 50 | var conn net.Conn 51 | conn, err = net.DialTimeout("tcp", domain+":443", time.Second*10) 52 | if err != nil { 53 | return 54 | } 55 | defer conn.Close() 56 | tlsConn := tls.Client(conn, &tls.Config{ 57 | ServerName: domain, 58 | }) 59 | defer tlsConn.Close() 60 | err = tlsConn.Handshake() 61 | if err != nil { 62 | return 63 | } 64 | cm = tlsConn.ConnectionState() 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /pkg/process/db.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/eryajf/chatgpt-dingtalk/pkg/db" 9 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 10 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 11 | "github.com/eryajf/chatgpt-dingtalk/public" 12 | ) 13 | 14 | // 与数据库交互的请求处理在此 15 | 16 | // SelectHistory 查询会话历史 17 | func SelectHistory(rmsg *dingbot.ReceiveMsg) error { 18 | name := strings.TrimSpace(strings.Split(rmsg.Text.Content, ":")[1]) 19 | if !public.JudgeAdminUsers(rmsg.SenderStaffId) { 20 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), "**🤷 抱歉,您没有查询对话记录的权限,只有程序管理员可以查询!**") 21 | if err != nil { 22 | logger.Error(fmt.Errorf("send message error: %v", err)) 23 | return err 24 | } 25 | return nil 26 | } 27 | // 获取数据列表 28 | var chat db.Chat 29 | if !chat.Exist(map[string]interface{}{"username": name}) { 30 | _, err := rmsg.ReplyToDingtalk(string(dingbot.TEXT), "用户名错误,这个用户不存在,请核实之后再进行查询") 31 | if err != nil { 32 | logger.Error(fmt.Errorf("send message error: %v", err)) 33 | return err 34 | } 35 | return fmt.Errorf("用户名错误,这个用户不存在,请核实之后重新查询") 36 | } 37 | chats, err := chat.List(db.ChatListReq{ 38 | Username: name, 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | var rst string 44 | for _, chatTmp := range chats { 45 | ctime := chatTmp.CreatedAt.Format("2006-01-02 15:04:05") 46 | if chatTmp.ChatType == 1 { 47 | rst += fmt.Sprintf("## 🙋 %s 问\n\n**时间:** %v\n\n**问题为:** %s\n\n", chatTmp.Username, ctime, chatTmp.Content) 48 | } else { 49 | rst += fmt.Sprintf("## 🤖 机器人 答\n\n**时间:** %v\n\n**回答如下:** \n\n%s\n\n", ctime, chatTmp.Content) 50 | } 51 | // TODO: 答案应该严格放在问题之后,目前只根据ID排序进行的陈列,当一个用户同时提出多个问题时,最终展示的可能会有点问题 52 | } 53 | fileName := time.Now().Format("20060102-150405") + ".md" 54 | // 写入文件 55 | if err = public.WriteToFile("./data/chatHistory/"+fileName, []byte(rst)); err != nil { 56 | return err 57 | } 58 | // 回复@我的用户 59 | reply := fmt.Sprintf("- 在线查看: [点我](%s)\n- 下载文件: [点我](%s)\n- 在线预览请安装插件:[Markdown Preview Plus](https://chrome.google.com/webstore/detail/markdown-preview-plus/febilkbfcbhebfnokafefeacimjdckgl)", public.Config.ServiceURL+"/history/"+fileName, public.Config.ServiceURL+"/download/"+fileName) 60 | logger.Info(fmt.Sprintf("🤖 %s 得到的答案: %#v", rmsg.SenderNick, reply)) 61 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), reply) 62 | if err != nil { 63 | logger.Error(fmt.Errorf("send message error: %v", err)) 64 | return err 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/process/image.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/solywsh/chatgpt" 9 | 10 | "github.com/eryajf/chatgpt-dingtalk/pkg/db" 11 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 12 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 13 | "github.com/eryajf/chatgpt-dingtalk/public" 14 | ) 15 | 16 | // ImageGenerate openai生成图片 17 | func ImageGenerate(ctx context.Context, rmsg *dingbot.ReceiveMsg) error { 18 | if public.Config.AzureOn { 19 | _, err := rmsg.ReplyToDingtalk(string(dingbot. 20 | MARKDOWN), "azure 模式下暂不支持图片创作功能") 21 | if err != nil { 22 | logger.Warning(fmt.Errorf("send message error: %v", err)) 23 | } 24 | return err 25 | } 26 | qObj := db.Chat{ 27 | Username: rmsg.SenderNick, 28 | Source: rmsg.GetChatTitle(), 29 | ChatType: db.Q, 30 | ParentContent: 0, 31 | Content: rmsg.Text.Content, 32 | } 33 | qid, err := qObj.Add() 34 | if err != nil { 35 | logger.Error("往MySQL新增数据失败,错误信息:", err) 36 | } 37 | reply, err := chatgpt.ImageQa(ctx, rmsg.Text.Content, rmsg.GetSenderIdentifier()) 38 | if err != nil { 39 | logger.Info(fmt.Errorf("gpt request error: %v", err)) 40 | _, err = rmsg.ReplyToDingtalk(string(dingbot.TEXT), fmt.Sprintf("请求openai失败了,错误信息:%v", err)) 41 | if err != nil { 42 | logger.Error(fmt.Errorf("send message error: %v", err)) 43 | return err 44 | } 45 | } 46 | if reply == "" { 47 | logger.Warning(fmt.Errorf("get gpt result falied: %v", err)) 48 | return nil 49 | } else { 50 | reply = strings.TrimSpace(reply) 51 | reply = strings.Trim(reply, "\n") 52 | reply = fmt.Sprintf(">点击图片可旋转或放大。\n![](%s)", reply) 53 | aObj := db.Chat{ 54 | Username: rmsg.SenderNick, 55 | Source: rmsg.GetChatTitle(), 56 | ChatType: db.A, 57 | ParentContent: qid, 58 | Content: reply, 59 | } 60 | _, err := aObj.Add() 61 | if err != nil { 62 | logger.Error("往MySQL新增数据失败,错误信息:", err) 63 | } 64 | logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply)) 65 | // 回复@我的用户 66 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), reply) 67 | if err != nil { 68 | logger.Error(fmt.Errorf("send message error: %v", err)) 69 | return err 70 | } 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/process/opstools.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/eryajf/chatgpt-dingtalk/pkg/db" 8 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 9 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 10 | "github.com/eryajf/chatgpt-dingtalk/pkg/ops" 11 | "github.com/eryajf/chatgpt-dingtalk/public" 12 | ) 13 | 14 | // 一些运维方面的工具在此 15 | 16 | // 域名信息 17 | func DomainMsg(rmsg *dingbot.ReceiveMsg) error { 18 | qObj := db.Chat{ 19 | Username: rmsg.SenderNick, 20 | Source: rmsg.GetChatTitle(), 21 | ChatType: db.Q, 22 | ParentContent: 0, 23 | Content: rmsg.Text.Content, 24 | } 25 | qid, err := qObj.Add() 26 | if err != nil { 27 | logger.Error("往MySQL新增数据失败,错误信息:", err) 28 | } 29 | domain := strings.TrimSpace(strings.Split(rmsg.Text.Content, " ")[1]) 30 | dm, err := ops.GetDomainMsg(domain) 31 | if err != nil { 32 | return err 33 | } 34 | // 回复@我的用户 35 | reply := fmt.Sprintf("**创建时间:** %v\n\n**到期时间:** %v\n\n**服务商:** %v", dm.CreateDate, dm.ExpiryDate, dm.Registrar) 36 | aObj := db.Chat{ 37 | Username: rmsg.SenderNick, 38 | Source: rmsg.GetChatTitle(), 39 | ChatType: db.A, 40 | ParentContent: qid, 41 | Content: reply, 42 | } 43 | _, err = aObj.Add() 44 | if err != nil { 45 | logger.Error("往MySQL新增数据失败,错误信息:", err) 46 | } 47 | logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply)) 48 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), reply) 49 | if err != nil { 50 | logger.Error(fmt.Errorf("send message error: %v", err)) 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | // 证书信息 57 | func DomainCertMsg(rmsg *dingbot.ReceiveMsg) error { 58 | qObj := db.Chat{ 59 | Username: rmsg.SenderNick, 60 | Source: rmsg.GetChatTitle(), 61 | ChatType: db.Q, 62 | ParentContent: 0, 63 | Content: rmsg.Text.Content, 64 | } 65 | qid, err := qObj.Add() 66 | if err != nil { 67 | logger.Error("往MySQL新增数据失败,错误信息:", err) 68 | } 69 | domain := strings.TrimSpace(strings.Split(rmsg.Text.Content, " ")[1]) 70 | dm, err := ops.GetDomainCertMsg(domain) 71 | if err != nil { 72 | return err 73 | } 74 | cert := dm.PeerCertificates[0] 75 | // 回复@我的用户 76 | reply := fmt.Sprintf("**证书创建时间:** %v\n\n**证书到期时间:** %v\n\n**证书颁发机构:** %v\n\n", public.GetReadTime(cert.NotBefore), public.GetReadTime(cert.NotAfter), cert.Issuer.Organization) 77 | aObj := db.Chat{ 78 | Username: rmsg.SenderNick, 79 | Source: rmsg.GetChatTitle(), 80 | ChatType: db.A, 81 | ParentContent: qid, 82 | Content: reply, 83 | } 84 | _, err = aObj.Add() 85 | if err != nil { 86 | logger.Error("往MySQL新增数据失败,错误信息:", err) 87 | } 88 | logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply)) 89 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), reply) 90 | if err != nil { 91 | logger.Error(fmt.Errorf("send message error: %v", err)) 92 | return err 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/process/process_request.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "strings" 7 | "time" 8 | 9 | "github.com/solywsh/chatgpt" 10 | 11 | "github.com/eryajf/chatgpt-dingtalk/pkg/db" 12 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 13 | "github.com/eryajf/chatgpt-dingtalk/pkg/logger" 14 | "github.com/eryajf/chatgpt-dingtalk/public" 15 | ) 16 | 17 | // ProcessRequest 分析处理请求逻辑 18 | func ProcessRequest(rmsg *dingbot.ReceiveMsg) error { 19 | if CheckRequestTimes(rmsg) { 20 | content := strings.TrimSpace(rmsg.Text.Content) 21 | timeoutStr := "" 22 | if content != public.Config.DefaultMode { 23 | timeoutStr = fmt.Sprintf("\n\n>%s 后将恢复默认聊天模式:%s", FormatTimeDuation(public.Config.SessionTimeout), public.Config.DefaultMode) 24 | } 25 | switch content { 26 | case "单聊": 27 | public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), content) 28 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("**[Concentrate] 现在进入与 %s 的单聊模式**%s", rmsg.SenderNick, timeoutStr)) 29 | if err != nil { 30 | logger.Warning(fmt.Errorf("send message error: %v", err)) 31 | } 32 | case "串聊": 33 | public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), content) 34 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("**[Concentrate] 现在进入与 %s 的串聊模式**%s", rmsg.SenderNick, timeoutStr)) 35 | if err != nil { 36 | logger.Warning(fmt.Errorf("send message error: %v", err)) 37 | } 38 | case "重置", "退出", "结束": 39 | // 重置用户对话模式 40 | public.UserService.ClearUserMode(rmsg.GetSenderIdentifier()) 41 | // 清空用户对话上下文 42 | public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier()) 43 | // 清空用户对话的答案ID 44 | public.UserService.ClearAnswerID(rmsg.SenderNick, rmsg.GetChatTitle()) 45 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[RecyclingSymbol]已重置与**%s** 的对话模式\n\n> 可以开始新的对话 [Bubble]", rmsg.SenderNick)) 46 | if err != nil { 47 | logger.Warning(fmt.Errorf("send message error: %v", err)) 48 | } 49 | case "模板": 50 | var title string 51 | for _, v := range *public.Prompt { 52 | title = title + v.Title + " | " 53 | } 54 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("%s 您好,当前程序内置集成了这些提示词:\n\n-----\n\n| %s \n\n-----\n\n您可以选择某个提示词作为对话内容的开头。\n\n以周报为例,可发送\"#周报 我本周用Go写了一个钉钉集成ChatGPT的聊天应用\",可将工作内容填充为一篇完整的周报。\n\n-----\n\n若您不清楚某个提示词的所代表的含义,您可以直接发送提示词,例如直接发送\"#周报\"", rmsg.SenderNick, title)) 55 | if err != nil { 56 | logger.Warning(fmt.Errorf("send message error: %v", err)) 57 | } 58 | case "图片": 59 | if public.Config.AzureOn { 60 | _, err := rmsg.ReplyToDingtalk(string(dingbot. 61 | MARKDOWN), "azure 模式下暂不支持图片创作功能") 62 | if err != nil { 63 | logger.Warning(fmt.Errorf("send message error: %v", err)) 64 | } 65 | return err 66 | } 67 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), "发送以 **#图片** 开头的内容,将会触发绘画能力,图片生成之后,将会通过消息回复给您。建议尽可能描述需要生成的图片内容及相关细节。\n 如果你绘图没有思路,可以在这两个网站寻找灵感。\n - [https://lexica.art/](https://lexica.art/)\n- [https://www.clickprompt.org/zh-CN/](https://www.clickprompt.org/zh-CN/)") 68 | if err != nil { 69 | logger.Warning(fmt.Errorf("send message error: %v", err)) 70 | } 71 | case "余额": 72 | if public.JudgeAdminUsers(rmsg.SenderStaffId) { 73 | cacheMsg := public.UserService.GetUserMode("system_balance") 74 | if cacheMsg == "" { 75 | rst, err := public.GetBalance() 76 | if err != nil { 77 | logger.Warning(fmt.Errorf("get balance error: %v", err)) 78 | return err 79 | } 80 | cacheMsg = rst 81 | } 82 | _, err := rmsg.ReplyToDingtalk(string(dingbot.TEXT), cacheMsg) 83 | if err != nil { 84 | logger.Warning(fmt.Errorf("send message error: %v", err)) 85 | } 86 | } 87 | case "查对话": 88 | if public.JudgeAdminUsers(rmsg.SenderStaffId) { 89 | msg := "使用如下指令进行查询:\n\n---\n\n**#查对话 username:张三**\n\n---\n\n需要注意格式必须严格与上边一致,否则将会查询失败\n\n只有程序系统管理员有权限查询,即config.yml中的admin_users指定的人员。" 90 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), msg) 91 | if err != nil { 92 | logger.Warning(fmt.Errorf("send message error: %v", err)) 93 | } 94 | } 95 | default: 96 | if public.FirstCheck(rmsg) { 97 | return Do("串聊", rmsg) 98 | } else { 99 | return Do("单聊", rmsg) 100 | } 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | // 执行处理请求 107 | func Do(mode string, rmsg *dingbot.ReceiveMsg) error { 108 | // 先把模式注入 109 | public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), mode) 110 | switch mode { 111 | case "单聊": 112 | qObj := db.Chat{ 113 | Username: rmsg.SenderNick, 114 | Source: rmsg.GetChatTitle(), 115 | ChatType: db.Q, 116 | ParentContent: 0, 117 | Content: rmsg.Text.Content, 118 | } 119 | qid, err := qObj.Add() 120 | if err != nil { 121 | logger.Error("往MySQL新增数据失败,错误信息:", err) 122 | } 123 | reply, err := chatgpt.SingleQa(rmsg.Text.Content, rmsg.GetSenderIdentifier()) 124 | if err != nil { 125 | logger.Info(fmt.Errorf("gpt request error: %v", err)) 126 | if strings.Contains(fmt.Sprintf("%v", err), "maximum question length exceeded") { 127 | public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier()) 128 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 已超过最大文本限制,请缩短提问文字的字数。", err)) 129 | if err != nil { 130 | logger.Warning(fmt.Errorf("send message error: %v", err)) 131 | return err 132 | } 133 | } else { 134 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err)) 135 | if err != nil { 136 | logger.Warning(fmt.Errorf("send message error: %v", err)) 137 | return err 138 | } 139 | } 140 | } 141 | if reply == "" { 142 | logger.Warning(fmt.Errorf("get gpt result falied: %v", err)) 143 | return nil 144 | } else { 145 | reply = strings.TrimSpace(reply) 146 | reply = strings.Trim(reply, "\n") 147 | aObj := db.Chat{ 148 | Username: rmsg.SenderNick, 149 | Source: rmsg.GetChatTitle(), 150 | ChatType: db.A, 151 | ParentContent: qid, 152 | Content: reply, 153 | } 154 | _, err := aObj.Add() 155 | if err != nil { 156 | logger.Error("往MySQL新增数据失败,错误信息:", err) 157 | } 158 | logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply)) 159 | if public.JudgeSensitiveWord(reply) { 160 | reply = public.SolveSensitiveWord(reply) 161 | } 162 | // 回复@我的用户 163 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(reply)) 164 | if err != nil { 165 | logger.Warning(fmt.Errorf("send message error: %v", err)) 166 | return err 167 | } 168 | } 169 | case "串聊": 170 | lastAid := public.UserService.GetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle()) 171 | qObj := db.Chat{ 172 | Username: rmsg.SenderNick, 173 | Source: rmsg.GetChatTitle(), 174 | ChatType: db.Q, 175 | ParentContent: lastAid, 176 | Content: rmsg.Text.Content, 177 | } 178 | qid, err := qObj.Add() 179 | if err != nil { 180 | logger.Error("往MySQL新增数据失败,错误信息:", err) 181 | } 182 | cli, reply, err := chatgpt.ContextQa(rmsg.Text.Content, rmsg.GetSenderIdentifier()) 183 | if err != nil { 184 | logger.Info(fmt.Sprintf("gpt request error: %v", err)) 185 | if strings.Contains(fmt.Sprintf("%v", err), "maximum text length exceeded") { 186 | public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier()) 187 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 串聊已超过最大文本限制,对话已重置,请重新发起。", err)) 188 | if err != nil { 189 | logger.Warning(fmt.Errorf("send message error: %v", err)) 190 | return err 191 | } 192 | } else { 193 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err)) 194 | if err != nil { 195 | logger.Warning(fmt.Errorf("send message error: %v", err)) 196 | return err 197 | } 198 | } 199 | } 200 | if reply == "" { 201 | logger.Warning(fmt.Errorf("get gpt result falied: %v", err)) 202 | return nil 203 | } else { 204 | reply = strings.TrimSpace(reply) 205 | reply = strings.Trim(reply, "\n") 206 | aObj := db.Chat{ 207 | Username: rmsg.SenderNick, 208 | Source: rmsg.GetChatTitle(), 209 | ChatType: db.A, 210 | ParentContent: qid, 211 | Content: reply, 212 | } 213 | aid, err := aObj.Add() 214 | if err != nil { 215 | logger.Error("往MySQL新增数据失败,错误信息:", err) 216 | } 217 | // 将当前回答的ID放入缓存 218 | public.UserService.SetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle(), aid) 219 | logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply)) 220 | if public.JudgeSensitiveWord(reply) { 221 | reply = public.SolveSensitiveWord(reply) 222 | } 223 | // 回复@我的用户 224 | _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(reply)) 225 | if err != nil { 226 | logger.Warning(fmt.Errorf("send message error: %v", err)) 227 | return err 228 | } 229 | _ = cli.ChatContext.SaveConversation(rmsg.GetSenderIdentifier()) 230 | } 231 | default: 232 | 233 | } 234 | return nil 235 | } 236 | 237 | // FormatTimeDuation 格式化时间 238 | // 主要提示单聊/群聊切换时多久后恢复默认聊天模式 239 | func FormatTimeDuation(duration time.Duration) string { 240 | minutes := int64(duration.Minutes()) 241 | seconds := int64(duration.Seconds()) - minutes*60 242 | timeoutStr := "" 243 | if seconds == 0 { 244 | timeoutStr = fmt.Sprintf("%d分钟", minutes) 245 | } else { 246 | timeoutStr = fmt.Sprintf("%d分%d秒", minutes, seconds) 247 | } 248 | return timeoutStr 249 | } 250 | 251 | // FormatMarkdown 格式化Markdown 252 | // 主要修复ChatGPT返回多行代码块,钉钉会将代码块中的#当作Markdown语法里的标题来处理,进行转义;如果Markdown格式内存在html,将Markdown中的html标签转义 253 | // 代码块缩进问题暂无法解决,因不管是四个空格,还是Tab,在钉钉上均会顶格显示,建议复制代码后用IDE进行代码格式化,针对缩进严格的语言,例如Python,不确定的建议手机端查看下代码块的缩进 254 | func FormatMarkdown(md string) string { 255 | lines := strings.Split(md, "\n") 256 | codeblock := false 257 | existHtml := strings.Contains(md, "<") 258 | 259 | for i, line := range lines { 260 | if strings.HasPrefix(line, "```") { 261 | codeblock = !codeblock 262 | } 263 | if codeblock { 264 | lines[i] = strings.ReplaceAll(line, "#", "\\#") 265 | } else if existHtml { 266 | lines[i] = html.EscapeString(line) 267 | } 268 | } 269 | 270 | return strings.Join(lines, "\n") 271 | } 272 | 273 | // CheckRequestTimes 分析处理请求逻辑 274 | // 主要提供单日请求限额的功能 275 | func CheckRequestTimes(rmsg *dingbot.ReceiveMsg) bool { 276 | if public.Config.MaxRequest == 0 { 277 | return true 278 | } 279 | count := public.UserService.GetUseRequestCount(rmsg.GetSenderIdentifier()) 280 | // 用户是管理员或VIP用户,不判断访问次数是否超过限制 281 | if public.JudgeAdminUsers(rmsg.SenderStaffId) || public.JudgeVipUsers(rmsg.SenderStaffId) { 282 | return true 283 | } else { 284 | // 用户不是管理员和VIP用户,判断访问次数是否超过限制 285 | if count >= public.Config.MaxRequest { 286 | logger.Info(fmt.Sprintf("亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick)) 287 | _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Staple] **一个好的问题,胜过十个好的答案!** \n\n亲爱的%s:\n\n您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!\n\n如有需要,可联系管理员升级为VIP用户。", rmsg.SenderNick)) 288 | if err != nil { 289 | logger.Warning(fmt.Errorf("send message error: %v", err)) 290 | } 291 | return false 292 | } 293 | } 294 | // 访问次数未超过限制,将计数加1 295 | public.UserService.SetUseRequestCount(rmsg.GetSenderIdentifier(), count+1) 296 | return true 297 | } 298 | -------------------------------------------------------------------------------- /pkg/process/prompt.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/eryajf/chatgpt-dingtalk/public" 9 | ) 10 | 11 | // GeneratePrompt 生成当次请求的 Prompt 12 | func GeneratePrompt(msg string) (rst string, err error) { 13 | for _, prompt := range *public.Prompt { 14 | if strings.HasPrefix(msg, prompt.Title) { 15 | if strings.TrimSpace(msg) == prompt.Title { 16 | rst = fmt.Sprintf("%s:\n%s___输入内容___%s", prompt.Title, prompt.Prefix, prompt.Suffix) 17 | err = errors.New("消息内容为空") // 当提示词之后没有文本,抛出异常,以便直接返回Prompt所代表的内容 18 | } else { 19 | rst = prompt.Prefix + strings.TrimSpace(strings.Replace(msg, prompt.Title, "", -1)) + prompt.Suffix 20 | } 21 | return 22 | } else { 23 | rst = msg 24 | } 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /prompt.yml: -------------------------------------------------------------------------------- 1 | # 可在此处提交你认为不错的 prompt, 注意保持格式一致,prefix为内容前缀,suffix是内容后缀,如果文本中间有双引号,那么就去掉最外层的双引号即可 2 | - title: "#周报" 3 | prefix: "请帮我把以下的工作内容填充为一篇完整的周报,用 markdown 格式以分点叙述的形式输出:" 4 | suffix: "" 5 | - title: "#前端" 6 | prefix: "我想让你充当前端开发专家。我将提供一些关于 Js、Node 等前端代码问题的具体信息,而你的工作就是想出为我解决问题的策略。这可能包括建议代码、代码逻辑思路策略。我的第一个请求是:" 7 | suffix: "" 8 | - title: "#架构师" 9 | prefix: "我希望你担任 IT 架构师。我将提供有关应用程序或其他数字产品功能的一些详细信息,而您的工作是想出将其集成到 IT 环境中的方法。这可能涉及分析业务需求、执行差距分析以及将新系统的功能映射到现有 IT 环境。接下来的步骤是创建解决方案设计、物理网络蓝图、系统集成接口定义和部署环境蓝图。我的第一个请求是:" 10 | suffix: "" 11 | - title: "#产品经理" 12 | prefix: "请确认我的以下请求。请您作为产品经理回复我。我将会提供一个主题,您将帮助我编写一份包括以下章节标题的 PRD 文档:主题、简介、问题陈述、目标与目的、用户故事、技术要求、收益、KPI 指标、开发风险以及结论。在我要求具体主题、功能或开发的 PRD 之前,请不要先写任何一份 PRD 文档。" 13 | suffix: "" 14 | - title: "#网络安全" 15 | prefix: "我想让你充当网络安全专家。我将提供一些关于如何存储和共享数据的具体信息,而你的工作就是想出保护这些数据免受恶意行为者攻击的策略。这可能包括建议加密方法、创建防火墙或实施将某些活动标记为可疑的策略。我的第一个请求是:" 16 | suffix: "" 17 | - title: "#正则" 18 | prefix: "我希望你充当正则表达式生成器。您的角色是生成匹配文本中特定模式的正则表达式。您应该以一种可以轻松复制并粘贴到支持正则表达式的文本编辑器或编程语言中的格式提供正则表达式。不要写正则表达式如何工作的解释或例子;只需提供正则表达式本身。我的第一个提示是:" 19 | suffix: "" 20 | - title: "#招聘" 21 | prefix: "我想让你担任招聘人员。我将提供一些关于职位空缺的信息,而你的工作是制定寻找合格申请人的策略。这可能包括通过社交媒体、社交活动甚至参加招聘会接触潜在候选人,以便为每个职位找到最合适的人选。我的第一个请求是:" 22 | suffix: "" 23 | - title: "#知乎" 24 | prefix: 知乎的风格是:用"谢邀"开头,用很多学术语言,引用很多名言,做大道理的论述,提到自己很厉害的教育背景并且经验丰富,最后还要引用一些论文。请用知乎风格: 25 | suffix: "" 26 | - title: "#翻译" 27 | prefix: "下面我让你来充当翻译家,你的目标是把任何语言翻译成中文,请翻译时不要带翻译腔,而是要翻译得自然、流畅和地道,最重要的是要简明扼要。请翻译下面这句话:" 28 | suffix: "" 29 | - title: "#小红书" 30 | prefix: "小红书的风格是:很吸引眼球的标题,每个段落都加 emoji, 最后加一些 tag。请用小红书风格:" 31 | suffix: "" 32 | - title: "#解梦" 33 | prefix: "我要你充当解梦师。我会给你描述我的梦,你会根据梦中出现的符号和主题提供解释。不要提供关于梦者的个人意见或假设。仅根据所提供的信息提供事实解释。我的梦是:" 34 | suffix: "" 35 | - title: "#linux命令" 36 | prefix: "我希望你只用 linux 命令回复。不要写解释。我想:" 37 | suffix: "" 38 | - title: "#英语学术润色" 39 | prefix: "Below is a paragraph from an academic paper. Polish the writing to meet the academic style, improve the spelling, grammar, clarity, concision and overall readability. When neccessary, rewrite the whole sentence. Furthermore, list all modification and explain the reasons to do so in markdown table.\n" 40 | suffix: "" 41 | - title: "#中文学术润色" 42 | prefix: "作为一名中文学术论文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。请编辑以下文本:" 43 | suffix: "" 44 | - title: "#查找语法错误" 45 | prefix: "Below is a paragraph from an academic paper. Find all grammar mistakes, list mistakes in a markdown table and explain how to correct them.\n" 46 | suffix: "" 47 | - title: "#中译英" 48 | prefix: "Please translate following sentence to English: \n" 49 | suffix: "" 50 | - title: "#学术中译英" 51 | prefix: "Please translate following sentence to English with academic writing, and provide some related authoritative examples: \n" 52 | suffix: "" 53 | - title: "#英译中" 54 | prefix: "下面我让你来充当翻译家,你的目标是把任何语言翻译成中文,翻译时请不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译:" 55 | suffix: "" 56 | - title: "#解释代码" 57 | prefix: "请解释以下代码:\n```\n" 58 | suffix: "\n```" 59 | - title: "#生成域名" 60 | prefix: "I want you to act as a smart domain name generator. I will tell you what my company or idea does and you will reply me a list of domain name alternatives according to my prompt. You will only reply the domain list, and nothing else. Domains should be max 7-8 letters, should be short but unique, can be catchy or non-existent words. Do not write explanations. Reply 'OK' to confirm." 61 | suffix: "" 62 | - title: "#生成sql" 63 | prefix: "我希望你能成为数据库方面的专家。当我问你与SQL相关的问题时,我需要你把它们翻译成标准的SQL语句。如果我的描述不够准确,请提供适当的反馈." 64 | suffix: "" 65 | - title: "#编程问题" 66 | prefix: "我想让你充当 stackoverflow 的帖子。我会问编程相关的问题,你会回答应该是什么答案。我希望你只回答给定的答案,并在不够详细的时候写解释。不要写解释。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个问题是:" 67 | suffix: "" 68 | - title: "#画图" 69 | prefix: "system: 助手善于判断用户意图,当确定需要提供图片时,助手会变得沉默寡言,只使用以下格式输出markdown图片:![描述](https://image.pollinations.ai/prompt/描述),因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示,以大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记,因为那样只会渲染出代码块或原始块而不是图片。\nuser: Can you draw some pictures for me?\nassistant: Sure, what do you want me to draw?\nuser: " 70 | suffix: "" -------------------------------------------------------------------------------- /public/balance.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-resty/resty/v2" 9 | ) 10 | 11 | func InitAiCli() *resty.Client { 12 | if Config.HttpProxy != "" { 13 | return resty.New().SetTimeout(10*time.Second).SetHeader("Authorization", fmt.Sprintf("Bearer %s", Config.ApiKey)).SetProxy(Config.HttpProxy).SetRetryCount(3).SetRetryWaitTime(2 * time.Second) 14 | } 15 | return resty.New().SetTimeout(10*time.Second).SetHeader("Authorization", fmt.Sprintf("Bearer %s", Config.ApiKey)).SetRetryCount(3).SetRetryWaitTime(2 * time.Second) 16 | } 17 | 18 | type Bill struct { 19 | Object string `json:"object"` 20 | DailyCosts []DailyCost `json:"daily_costs"` 21 | TotalUsage float64 `json:"total_usage"` 22 | } 23 | 24 | type DailyCost struct { 25 | Timestamp float64 `json:"timestamp"` 26 | LineItems []LineItem `json:"line_items"` 27 | } 28 | 29 | type LineItem struct { 30 | Name string `json:"name"` 31 | Cost float64 `json:"cost"` 32 | } 33 | 34 | // GetBalance 获取账号余额 35 | func GetBalance() (string, error) { 36 | var data Bill 37 | path := "/v1/dashboard/billing/usage" 38 | var url string = "https://api.openai.com" + path 39 | if Config.BaseURL != "" { 40 | url = Config.BaseURL + path 41 | } 42 | d, _ := time.ParseDuration("-24h") 43 | resp, err := InitAiCli().R().SetQueryParams(map[string]string{ 44 | "start_date": time.Now().Add(d * 90).Format("2006-01-02"), 45 | "end_date": time.Now().Format("2006-01-02"), 46 | }).Get(url) 47 | if err != nil { 48 | return "", err 49 | } 50 | err = json.Unmarshal(resp.Body(), &data) 51 | if err != nil { 52 | return "", err 53 | } 54 | sub, err := GetSub() 55 | if err != nil { 56 | return "", err 57 | } 58 | expireDate := time.Unix(sub.AccessUntil, 0).Format("2006-01-02 15:04:05") 59 | used := data.TotalUsage / 100 60 | totalAvailable := sub.HardLimitUsd - used 61 | msg := fmt.Sprintf("💵 已用: 💲%v\n💵 剩余: 💲%v\n🕰 到期时间: %v", fmt.Sprintf("%.2f", used), fmt.Sprintf("%.2f", totalAvailable), expireDate) 62 | // 放入缓存 63 | UserService.SetUserMode("system_balance", msg) 64 | return msg, nil 65 | } 66 | 67 | type Subscription struct { 68 | Object string `json:"object"` 69 | HasPaymentMethod bool `json:"has_payment_method"` 70 | Canceled bool `json:"canceled"` 71 | CanceledAt interface{} `json:"canceled_at"` 72 | Delinquent interface{} `json:"delinquent"` 73 | AccessUntil int64 `json:"access_until"` 74 | SoftLimit int64 `json:"soft_limit"` 75 | HardLimit int64 `json:"hard_limit"` 76 | SystemHardLimit int64 `json:"system_hard_limit"` 77 | SoftLimitUsd float64 `json:"soft_limit_usd"` 78 | HardLimitUsd float64 `json:"hard_limit_usd"` 79 | SystemHardLimitUsd float64 `json:"system_hard_limit_usd"` 80 | Plan Plan `json:"plan"` 81 | AccountName string `json:"account_name"` 82 | PoNumber interface{} `json:"po_number"` 83 | BillingEmail interface{} `json:"billing_email"` 84 | TaxIDS interface{} `json:"tax_ids"` 85 | BillingAddress interface{} `json:"billing_address"` 86 | BusinessAddress interface{} `json:"business_address"` 87 | } 88 | 89 | type Plan struct { 90 | Title string `json:"title"` 91 | ID string `json:"id"` 92 | } 93 | 94 | func GetSub() (Subscription, error) { 95 | var data Subscription 96 | path := "/v1/dashboard/billing/subscription" 97 | var url string = "https://api.openai.com" + path 98 | if Config.BaseURL != "" { 99 | url = Config.BaseURL + path 100 | } 101 | resp, err := InitAiCli().R().Get(url) 102 | if err != nil { 103 | return data, err 104 | } 105 | err = json.Unmarshal(resp.Body(), &data) 106 | if err != nil { 107 | return data, err 108 | } 109 | return data, nil 110 | } 111 | -------------------------------------------------------------------------------- /public/chat.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 7 | ) 8 | 9 | func FirstCheck(rmsg *dingbot.ReceiveMsg) bool { 10 | lc := UserService.GetUserMode(rmsg.GetSenderIdentifier()) 11 | if lc == "" { 12 | if Config.DefaultMode == "串聊" { 13 | return true 14 | } else { 15 | return false 16 | } 17 | } 18 | if lc != "" && strings.Contains(lc, "串聊") { 19 | return true 20 | } 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /public/public.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "github.com/sashabaranov/go-openai" 5 | 6 | "github.com/eryajf/chatgpt-dingtalk/config" 7 | "github.com/eryajf/chatgpt-dingtalk/pkg/cache" 8 | "github.com/eryajf/chatgpt-dingtalk/pkg/db" 9 | "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot" 10 | ) 11 | 12 | var UserService cache.UserServiceInterface 13 | var Config *config.Configuration 14 | var Prompt *[]config.Prompt 15 | var DingTalkClientManager dingbot.DingTalkClientManagerInterface 16 | 17 | const DingTalkClientIdKeyName = "DingTalkClientId" 18 | 19 | func InitSvc() { 20 | // 加载配置 21 | Config = config.LoadConfig() 22 | // 加载prompt 23 | Prompt = config.LoadPrompt() 24 | // 初始化缓存 25 | UserService = cache.NewUserService() 26 | // 初始化钉钉开放平台的客户端,用于访问上传图片等能力 27 | DingTalkClientManager = dingbot.NewDingTalkClientManager(Config) 28 | // 初始化数据库 29 | db.InitDB() 30 | // 暂时不在初始化时获取余额 31 | if Config.Model == openai.GPT3Dot5Turbo0613 || Config.Model == openai.GPT3Dot5Turbo0301 || Config.Model == openai.GPT3Dot5Turbo { 32 | _, _ = GetBalance() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/tools.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | "unicode/utf8" 12 | ) 13 | 14 | // 将内容写入到文件,如果文件名带路径,则会判断路径是否存在,不存在则创建 15 | func WriteToFile(path string, data []byte) error { 16 | tmp := strings.Split(path, "/") 17 | if len(tmp) > 0 { 18 | tmp = tmp[:len(tmp)-1] 19 | } 20 | 21 | err := os.MkdirAll(strings.Join(tmp, "/"), os.ModePerm) 22 | if err != nil { 23 | return err 24 | } 25 | err = os.WriteFile(path, data, 0755) 26 | if err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // JudgeGroup 判断群ID是否在白名单 33 | func JudgeGroup(s string) bool { 34 | if len(Config.AllowGroups) == 0 { 35 | return true 36 | } 37 | for _, v := range Config.AllowGroups { 38 | if v == s { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | 45 | // JudgeOutgoingGroup 判断群ID是否在为outgoing白名单 46 | func JudgeOutgoingGroup(s string) bool { 47 | if len(Config.AllowOutgoingGroups) == 0 { 48 | return true 49 | } 50 | for _, v := range Config.AllowOutgoingGroups { 51 | if v == s { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // JudgeUsers 判断用户是否在白名单 59 | func JudgeUsers(s string) bool { 60 | // 优先判断黑名单,黑名单用户返回:不在白名单 61 | if len(Config.DenyUsers) != 0 { 62 | for _, v := range Config.DenyUsers { 63 | if v == s { 64 | return false 65 | } 66 | } 67 | } 68 | // 白名单配置逻辑处理 69 | if len(Config.AllowUsers) == 0 { 70 | return true 71 | } 72 | for _, v := range Config.AllowUsers { 73 | if v == s { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | // JudgeAdminUsers 判断用户是否为系统管理员 81 | func JudgeAdminUsers(s string) bool { 82 | // 如果secret或者用户的userid都为空的话,那么默认没有管理员 83 | if len(Config.AppSecrets) == 0 || s == "" { 84 | return false 85 | } 86 | // 如果没有指定,则没有人是管理员 87 | if len(Config.AdminUsers) == 0 { 88 | return false 89 | } 90 | for _, v := range Config.AdminUsers { 91 | if v == s { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | 98 | // JudgeVipUsers 判断用户是否为VIP用户 99 | func JudgeVipUsers(s string) bool { 100 | // 如果secret或者用户的userid都为空的话,那么默认不是VIP用户 101 | if len(Config.AppSecrets) == 0 || s == "" { 102 | return false 103 | } 104 | // 管理员默认是VIP用户 105 | for _, v := range Config.AdminUsers { 106 | if v == s { 107 | return true 108 | } 109 | } 110 | // 如果没有指定,则没有人是VIP用户 111 | if len(Config.VipUsers) == 0 { 112 | return false 113 | } 114 | for _, v := range Config.VipUsers { 115 | if v == s { 116 | return true 117 | } 118 | } 119 | return false 120 | } 121 | 122 | func GetReadTime(t time.Time) string { 123 | return t.Format("2006-01-02 15:04:05") 124 | } 125 | 126 | func CheckRequestWithCredentials(ts, sg string) (clientId string, pass bool) { 127 | clientId, pass = "", false 128 | credentials := Config.Credentials 129 | if len(credentials) == 0 || len(Config.AllowOutgoingGroups) == 0 { 130 | return "", true 131 | } 132 | for _, credential := range Config.Credentials { 133 | stringToSign := fmt.Sprintf("%s\n%s", ts, credential.ClientSecret) 134 | mac := hmac.New(sha256.New, []byte(credential.ClientSecret)) 135 | _, _ = mac.Write([]byte(stringToSign)) 136 | if base64.StdEncoding.EncodeToString(mac.Sum(nil)) == sg { 137 | return credential.ClientID, true 138 | } 139 | } 140 | return 141 | } 142 | 143 | func CheckRequest(ts, sg string) bool { 144 | appSecrets := Config.AppSecrets 145 | // 如果没有指定或者outgoing类型机器人下使用,则默认不做校验 146 | if len(appSecrets) == 0 || sg == "" { 147 | return true 148 | } 149 | // 校验appSecret 150 | for _, secret := range appSecrets { 151 | stringToSign := fmt.Sprintf("%s\n%s", ts, secret) 152 | mac := hmac.New(sha256.New, []byte(secret)) 153 | _, _ = mac.Write([]byte(stringToSign)) 154 | if base64.StdEncoding.EncodeToString(mac.Sum(nil)) == sg { 155 | return true 156 | } 157 | } 158 | return false 159 | } 160 | 161 | // JudgeSensitiveWord 判断内容是否包含敏感词 162 | func JudgeSensitiveWord(s string) bool { 163 | if len(Config.SensitiveWords) == 0 { 164 | return false 165 | } 166 | for _, v := range Config.SensitiveWords { 167 | if strings.Contains(s, v) { 168 | return true 169 | } 170 | } 171 | return false 172 | } 173 | 174 | // SolveSensitiveWord 将敏感词用 🚫 占位 175 | func SolveSensitiveWord(s string) string { 176 | for _, v := range Config.SensitiveWords { 177 | if strings.Contains(s, v) { 178 | return strings.Replace(s, v, printStars(utf8.RuneCountInString(v)), -1) 179 | } 180 | } 181 | return s 182 | } 183 | 184 | // 将对应敏感词替换为 🚫 185 | func printStars(num int) string { 186 | s := "" 187 | for i := 0; i < num; i++ { 188 | s += "🚫" 189 | } 190 | return s 191 | } 192 | -------------------------------------------------------------------------------- /public/tools_test.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eryajf/chatgpt-dingtalk/config" 7 | ) 8 | 9 | func TestCheckRequestWithCredentials_Pass_WithNilConfig(t *testing.T) { 10 | Config = &config.Configuration{ 11 | Credentials: nil, 12 | } 13 | clientId, pass := CheckRequestWithCredentials("ts", "sg") 14 | if !pass { 15 | t.Errorf("pass should be true, but false") 16 | return 17 | } 18 | if len(clientId) > 0 { 19 | t.Errorf("client id should be empty") 20 | return 21 | } 22 | } 23 | 24 | func TestCheckRequestWithCredentials_Pass_WithEmptyConfig(t *testing.T) { 25 | Config = &config.Configuration{ 26 | Credentials: []config.Credential{}, 27 | } 28 | clientId, pass := CheckRequestWithCredentials("ts", "sg") 29 | if !pass { 30 | t.Errorf("pass should be true, but false") 31 | return 32 | } 33 | if len(clientId) > 0 { 34 | t.Errorf("client id should be empty") 35 | return 36 | } 37 | } 38 | 39 | func TestCheckRequestWithCredentials_Pass_WithValidConfig(t *testing.T) { 40 | Config = &config.Configuration{ 41 | Credentials: []config.Credential{ 42 | config.Credential{ 43 | ClientID: "client-id-for-test", 44 | ClientSecret: "client-secret-for-test", 45 | }, 46 | }, 47 | } 48 | clientId, pass := CheckRequestWithCredentials("1684493546276", "nwBJQmaBLv9+5/sSS/66jcFc1/kGY5wo38L88LOGfRU=") 49 | if !pass { 50 | t.Errorf("pass should be true, but false") 51 | return 52 | } 53 | if clientId != "client-id-for-test" { 54 | t.Errorf("client id should be \"%s\", but \"%s\"", "client-id-for-test", clientId) 55 | return 56 | } 57 | } 58 | 59 | func TestCheckRequestWithCredentials_Failed_WithInvalidConfig(t *testing.T) { 60 | Config = &config.Configuration{ 61 | Credentials: []config.Credential{ 62 | config.Credential{ 63 | ClientID: "client-id-for-test", 64 | ClientSecret: "invalid-client-secret-for-test", 65 | }, 66 | }, 67 | } 68 | clientId, pass := CheckRequestWithCredentials("1684493546276", "nwBJQmaBLv9+5/sSS/66jcFc1/kGY5wo38L88LOGfRU=") 69 | if pass { 70 | t.Errorf("pass should be false, but true") 71 | return 72 | } 73 | if clientId != "" { 74 | t.Errorf("client id should be empty") 75 | return 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/goimports-reviser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | go install github.com/incu6us/goimports-reviser/v2@latest 6 | 7 | PROJECT_NAME=github.com/eryajf/chatgpt-dingtalk 8 | 9 | find . -name '*.go' -print0 | while IFS= read -r -d '' file; do 10 | goimports-reviser -file-path "$file" -project-name "$PROJECT_NAME" 11 | done 12 | --------------------------------------------------------------------------------