├── .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 | [](https://github.com/eryajf)
11 | [](https://github.com/eryajf/chatgpt-dingtalk)
12 | [](https://github.com/eryajf/chatgpt-dingtalk/pulls)
13 | [](https://github.com/eryajf/chatgpt-dingtalk/stargazers)
14 | [](https://github.com/eryajf/chatgpt-dingtalk)
15 | [](https://hub.docker.com/r/eryajf/chatgpt-dingtalk)
16 | [](https://hub.docker.com/r/eryajf/chatgpt-dingtalk)
17 | [](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://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 | 
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 | > 
446 |
447 | ## Star 历史
448 |
449 | [](https://star-history.com/#ConnectAI-E/Dingtalk-OpenAI&Date)
450 |
451 | ## 贡献者列表
452 |
453 |
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", 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图片:,因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为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 |
--------------------------------------------------------------------------------