Full Code of Mrs4s/go-cqhttp for AI

master a5923f179b36 cached
118 files
520.7 KB
176.5k tokens
722 symbols
1 requests
Download .txt
Showing preview only (552K chars total). Download the full file or copy to clipboard to get everything.
Repository: Mrs4s/go-cqhttp
Branch: master
Commit: a5923f179b36
Files: 118
Total size: 520.7 KB

Directory structure:
gitextract_q284w6qz/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yaml
│   │   └── feat--.md
│   └── workflows/
│       ├── build_docker_image.yml
│       ├── ci.yml
│       ├── close_pr.yml
│       ├── golint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd/
│   ├── api-generator/
│   │   ├── main.go
│   │   └── supported.go
│   └── gocq/
│       ├── login.go
│       ├── main.go
│       └── qsign.go
├── coolq/
│   ├── api.go
│   ├── api_v12.go
│   ├── bot.go
│   ├── converter.go
│   ├── cqcode.go
│   ├── doc.go
│   ├── event.go
│   └── feed.go
├── db/
│   ├── database.go
│   ├── leveldb/
│   │   ├── const.go
│   │   ├── leveldb.go
│   │   ├── reader.go
│   │   ├── structs.go
│   │   └── writer.go
│   ├── mongodb/
│   │   └── mongodb.go
│   ├── multidb.go
│   └── sqlite3/
│       ├── model.go
│       └── sqlite3.go
├── docker-entrypoint.sh
├── docs/
│   ├── EventFilter.md
│   ├── QA.md
│   ├── README.md
│   ├── adminApi.md
│   ├── config.md
│   ├── cqhttp.md
│   ├── file.md
│   ├── guild.md
│   ├── quick_start.md
│   └── slider.md
├── global/
│   ├── all_test.go
│   ├── buffer.go
│   ├── codec.go
│   ├── doc.go
│   ├── fs.go
│   ├── log_hook.go
│   ├── net.go
│   ├── param.go
│   ├── signal.go
│   ├── signal_unix.go
│   ├── signal_windows.go
│   └── terminal/
│       ├── doc.go
│       ├── double_click.go
│       ├── double_click_windows.go
│       ├── quick_edit.go
│       ├── quick_edit_windows.go
│       ├── title.go
│       ├── title_windows.go
│       ├── vt100.go
│       └── vt100_windows.go
├── go.mod
├── go.sum
├── internal/
│   ├── base/
│   │   ├── feature.go
│   │   ├── flag.go
│   │   └── version.go
│   ├── cache/
│   │   └── cache.go
│   ├── download/
│   │   └── download.go
│   ├── mime/
│   │   └── mime.go
│   ├── msg/
│   │   ├── element.go
│   │   ├── element_test.go
│   │   ├── local.go
│   │   ├── parse.go
│   │   └── parse_test.go
│   ├── param/
│   │   └── param.go
│   ├── selfdiagnosis/
│   │   └── diagnoses.go
│   └── selfupdate/
│       ├── update.go
│       ├── update_others.go
│       └── update_windows.go
├── main.go
├── modules/
│   ├── api/
│   │   ├── api.go
│   │   └── caller.go
│   ├── config/
│   │   ├── config.go
│   │   ├── config_test.go
│   │   └── default_config.yml
│   ├── filter/
│   │   ├── filter.go
│   │   └── middlewares.go
│   ├── pprof/
│   │   └── pprof.go
│   ├── servers/
│   │   └── servers.go
│   └── silk/
│       ├── codec.go
│       ├── codec_unsupported.go
│       └── stubs.go
├── pkg/
│   └── onebot/
│       ├── attr.go
│       ├── kind_string.go
│       ├── onebot.go
│       ├── spec.go
│       ├── supported.go
│       └── value.go
├── scripts/
│   ├── bootstrap
│   └── upload_dist.sh
├── server/
│   ├── daemon.go
│   ├── doc.go
│   ├── http.go
│   ├── http_test.go
│   ├── middlewares.go
│   ├── scf.go
│   └── websocket.go
└── winres/
    ├── .gitignore
    ├── gen/
    │   └── json.go
    └── init.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.gitlab-ci.yml
.dockerignore
Dockerfile
README.md
LICENSE


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yaml
================================================
name: 回报错误
description: 在使用 go-cqhttp 的过程中遇到了错误
title: '[Bug]: '
labels: [ "bug?" ]

body:
  # User's README and agreement
  - type: markdown
    attributes:
      value: |
        ## 感谢您愿意填写错误回报!
        ## 以下是一些注意事项,请务必阅读让我们能够更容易处理

        ### ❗ | 确定没有相同问题的ISSUE已被提出. (教程: https://forums.go-cqhttp.org/t/topic/141)
        ### 🌎| 请准确填写环境信息
        ### ❔ | 打开DEBUG模式复现,并提供出现问题前后至少 10 秒的完整日志内容。请自行删除日志内存在的个人信息及敏感内容。
        ### ⚠ | 如果涉及内存泄漏/CPU占用异常请打开DEBUG模式并下载pprof性能分析.

        ## 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》
        链接: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
        ---
  - type: checkboxes
    id: terms
    attributes:
      label: 请确保您已阅读以上注意事项,并勾选下方的确认框。
      options:
        - label: "我已经仔细阅读上述教程和 [\"提问前需知\"](https://forums.go-cqhttp.org/t/topic/141)"
          required: true
        - label: "我已经使用 [dev分支版本](https://github.com/Mrs4s/go-cqhttp/actions/workflows/ci.yml) 测试过,问题依旧存在。"
          required: true
        - label: "我已经在 [Issue Tracker](https://github.com/Mrs4s/go-cqhttp/issues) 中找过我要提出的问题,没有找到相同问题的ISSUE。"
          required: true
        - label: 我已知晓并同意,此处仅用于汇报程序中存在的问题。若这个 Issue 是关于其他非程序本身问题,则我的 Issue 可能会被无条件自动关闭或/并锁定。(这些问题应当在 Discussion 板块提出。)
          required: true

  # User's data
  - type: markdown
    attributes:
      value: |
        ## 环境信息
        请根据实际使用环境修改以下信息。

  # Env | go-cqhttp Version
  - type: input
    id: env-gocq-ver
    attributes:
      label: go-cqhttp 版本
    validations:
      required: true

  # Env | VM Version
  - type: dropdown
    id: env-vm-ver
    attributes:
      label: 运行环境
      description: 选择运行 go-cqhttp 的系统版本
      options:
        - Windows (64)
        - Windows (32/x84)
        - MacOS
        - Linux
        - Ubuntu
        - CentOS
        - ArchLinux
        - UNIX (Android)
        - 其它(请在下方说明)
    validations:
      required: true

  # Env | VM Arch
  - type: dropdown
    id: env-vm-arch
    attributes:
      label: 运行架构
      description: (可选) 选择运行 go-cqhttp 的系统架构
      options:
        - AMD64
        - x86
        - ARM [32] (别名:AArch32 / ARMv7)
        - ARM [64] (别名:AArch64 / ARMv8)
        - 其它

  # Env | Connection type
  - type: dropdown
    id: env-conn-type
    attributes:
      label: 连接方式
      description: 选择对接机器人的连接方式
      options:
        - HTTP
        - WebSocket (正向)
        - WebSocket (反向)
        - LambdaServer
    validations:
      required: true

  # Env | Protocol
  - type: dropdown
    id: env-protocol
    attributes:
      label: 使用协议
      description: 选择使用的协议
      options:
        - 0 | Default
        - 1 | Android Phone
        - 2 | Android Watch
        - 3 | MacOS
        - 4 | 企点
        - 5 | iPad
        - 6 | aPad
    validations:
      required: true

  # Input | Reproduce
  - type: textarea
    id: reproduce-steps
    attributes:
      label: 重现步骤
      description: |
        我们需要执行哪些操作才能让 bug 出现?
        简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
    validations:
      required: true

  # Input | Expected result
  - type: textarea
    id: expected
    attributes:
      label: 期望的结果是什么?
    validations:
      required: true

  # Input | Actual result
  - type: textarea
    id: actual
    attributes:
      label: 实际的结果是什么?
    validations:
      required: true

  # Optional | Reproduce code
  - type: textarea
    id: reproduce-code
    attributes:
      label: 简单的复现代码/链接(可选)
      render: golang

  # Optional | Logging
  - type: textarea
    id: logging
    attributes:
      label: 日志记录(可选)
      render: golang

  # Optional | Extra description
  - type: textarea
    id: extra-desc
    attributes:
      label: 补充说明(可选)


================================================
FILE: .github/ISSUE_TEMPLATE/feat--.md
================================================
---
name: 新功能提议
about: 提出新功能
title: ''
labels: feature request
assignees: ''

---

**环境信息**
<!-- 请尽量填写 -->
go-cqhttp版本: 

**需要添加的功能内容**
<!-- 请在这里详细描述新功能的实现方法 -->


================================================
FILE: .github/workflows/build_docker_image.yml
================================================
name: Build And Push Docker Image

on:
  push:
    branches:
      - 'master'
      - 'dev'
    # Sequence of patterns matched against refs/tags
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

  workflow_dispatch:

jobs:
  build:

    runs-on: ubuntu-latest

    permissions:
      packages: write
      contents: read

    steps:
      - uses: actions/checkout@v3

      - name: Set time zone
        uses: szenius/set-timezone@v1.1
        with:
          timezoneLinux: "Asia/Shanghai"
          timezoneMacos: "Asia/Shanghai"
          timezoneWindows: "China Standard Time"

      # # 如果有 dockerhub 账户,可以在github的secrets中配置下面两个,然后取消下面注释的这几行,并在meta步骤的images增加一行 ${{ github.repository }}
      # - name: Login to DockerHub
      #   uses: docker/login-action@v1
      #   with:
      #     username: ${{ secrets.DOCKERHUB_USERNAME }}
      #     password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GHCR
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: |
            ghcr.io/${{ github.repository }}
          # generate Docker tags based on the following events/attributes
          #   nightly, master, pr-2, 1.2.3, 1.2, 1
          tags: |
            type=schedule,pattern=nightly
            type=edge
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v4
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request,workflow_dispatch]

env:
  BINARY_PREFIX: "go-cqhttp_"
  BINARY_SUFFIX: ""
  COMMIT_ID: "${{ github.sha }}"
  PR_PROMPT: "::warning:: Build artifact will not be uploaded due to the workflow is trigged by pull request."

jobs:
  build:
    name: Build binary CI
    runs-on: ubuntu-latest
    strategy:
      matrix:
        # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64, darwin/arm64
        goos: [linux, windows, darwin]
        goarch: ["386", amd64, arm, arm64]
        exclude:
          - goos: darwin
            goarch: arm
          - goos: darwin
            goarch: "386"
      fail-fast: true
    steps:
      - uses: actions/checkout@v3
      - name: Setup Go environment
        uses: actions/setup-go@v3
        with:
          cache: true
          go-version: '1.20'
      - name: Build binary file
        env:
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
          IS_PR: ${{ !!github.head_ref }}
        run: |
          if [ $GOOS = "windows" ]; then export BINARY_SUFFIX="$BINARY_SUFFIX.exe"; fi
          if $IS_PR ; then echo $PR_PROMPT; fi
          export BINARY_NAME="$BINARY_PREFIX"$GOOS"_$GOARCH$BINARY_SUFFIX"
          export CGO_ENABLED=0
          export LD_FLAGS="-w -s -X github.com/Mrs4s/go-cqhttp/internal/base.Version=${COMMIT_ID::7}"
          go build -o "output/$BINARY_NAME" -trimpath -ldflags "$LD_FLAGS" .
      - name: Upload artifact
        uses: actions/upload-artifact@v3
        if: ${{ !github.head_ref }}
        with:
          name: ${{ matrix.goos }}_${{ matrix.goarch }}
          path: output/


================================================
FILE: .github/workflows/close_pr.yml
================================================
name: Check and Close Invalid PR

on:
  pull_request_target:
    types: [opened, reopened]

jobs:
  # This workflow closes invalid PR
  close_pr:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    permissions: write-all

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      - name: Close PR if it is not pointed to dev branch
        if: github.event.pull_request.base.ref != 'dev'
        uses: superbrothers/close-pull-request@v3
        with:
          # Optional. Post a issue comment just before closing a pull request.
          comment: "Invalid PR to `non-dev` branch `${{ github.event.pull_request.base.ref }}`."


================================================
FILE: .github/workflows/golint.yml
================================================
name: Lint

on: [push,pull_request,workflow_dispatch]

jobs:
  golangci:
    name: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Go environment
        uses: actions/setup-go@v3
        with:
          go-version: '1.20'

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest

      - name: Tests
        run: |
          go test $(go list ./...)

      - name: Commit back
        if: ${{ github.repository_owner == 'Mrs4s' && !github.event.pull_request }}
        continue-on-error: true
        run: |
          git config --local user.name 'github-actions[bot]'
          git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add --all
          git commit -m "ci(chore): Fix stylings"
          git push

      - name: Suggester
        if: ${{ github.event.pull_request }}
        uses: reviewdog/action-suggester@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          tool_name: golangci-lint


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        run: |
          git version
          git clone "${{ github.event.repository.html_url }}" /home/runner/work/go-cqhttp/go-cqhttp
          git checkout "${{ github.ref }}"

      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: '1.20'

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v4
        with:
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      #- name: Checkout Dist
      #  uses: actions/checkout@v2
      #  with:
      #    repository: 'gocq/dist'
      #    ref: master
      #    ssh-key: ${{ secrets.SSH_KEY }}
      #    path: upstream/dist

      #- name: Update Dist
      #  run: |
      #    chmod +x scripts/upload_dist.sh
      #    ./scripts/upload_dist.sh


================================================
FILE: .gitignore
================================================
vendor/
.idea
.vscode
config.hjson
config.yml
session.token
device.json
data/
logs/
internal/btree/*.lock
internal/btree/*.db

# binary builds
go-cqhttp
*.exe

# macos
.DS_Store

# windwos rc
*.syso


================================================
FILE: .golangci.yml
================================================
linters-settings:
  errcheck:
    ignore: fmt:.*,io/ioutil:^Read.*
    ignoretests: true

  goimports:
    local-prefixes: github.com/Mrs4s/go-cqhttp

  gocritic:
    disabled-checks:
      - exitAfterDefer

  forbidigo:
    # Forbid the following identifiers
    forbid:
      - ^fmt\.Errorf$ # consider errors.Errorf in github.com/pkg/errors

linters:
  # please, do not use `enable-all`: it's deprecated and will be removed soon.
  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
  disable-all: true
  fast: false
  enable:
    - bodyclose
    - durationcheck
    - gofmt
    - goimports
    - errcheck
    - exportloopref
    - exhaustive
    - bidichk
    - gocritic
    - gosimple
    - govet
    - ineffassign
    #- nolintlint
    - staticcheck
    - stylecheck
    - unconvert
    - usestdlibvars
    - unparam
    - unused
    - whitespace
    - prealloc
    - predeclared
    - asciicheck
    - revive
    - forbidigo
    - makezero

run:
  # default concurrency is a available CPU number.
  # concurrency: 4 # explicitly omit this value to fully utilize available resources.
  deadline: 5m
  issues-exit-code: 1
  skip-dirs:
    - db
    - cmd/api-generator
    - internal/encryption
  tests: true

# output configuration options
output:
  format: "colored-line-number"
  print-issued-lines: true
  print-linter-name: true
  uniq-by-line: true

issues:
  # Fix found issues (if it's supported by the linter)
  fix: true
  exclude-use-default: false
  exclude:
    - "Error return value of .((os.)?std(out|err)..*|.*Close|.*Seek|.*Flush|os.Remove(All)?|.*print(f|ln)?|os.(Un)?Setenv). is not check"


================================================
FILE: .goreleaser.yml
================================================
env:
  - GO111MODULE=on
before:
  hooks:
    - go mod tidy
    - go install github.com/tc-hib/go-winres@latest
    - go generate winres/init.go
    - go-winres make
release:
  draft: true
  discussion_category_name: General
builds:
  - id: nowin
    env:
      - CGO_ENABLED=0
      - GO111MODULE=on
    goos:
      - linux
      - darwin
    goarch:
      - '386'
      - amd64
      - arm
      - arm64
    goarm:
      - '7'
    ignore:
      - goos: darwin
        goarch: arm
      - goos: darwin
        goarch: '386'
    mod_timestamp: "{{ .CommitTimestamp }}"
    flags:
      - -trimpath
    ldflags:
      - -s -w -X github.com/Mrs4s/go-cqhttp/internal/base.Version=v{{.Version}}
  - id: win
    env:
      - CGO_ENABLED=0
      - GO111MODULE=on
    goos:
      - windows
    goarch:
      - '386'
      - amd64
      - arm
      - arm64
    goarm:
      - '7'
    mod_timestamp: "{{ .CommitTimestamp }}"
    flags:
      - -trimpath
    ldflags:
      - -s -w -X github.com/Mrs4s/go-cqhttp/internal/base.Version=v{{.Version}}

checksum:
  name_template: "{{ .ProjectName }}_checksums.txt"
changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"
      - fix typo
      - Merge pull request
      - Merge branch
      - Merge remote-tracking
      - go mod tidy

archives:
  - id: binary
    builds:
      - win
    name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
    format_overrides:
      - goos: windows
        format: binary
  - id: nowin
    builds:
      - nowin
      - win
    name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
    format_overrides:
      - goos: windows
        format: zip

nfpms:
  - license: AGPL 3.0
    homepage: https://go-cqhttp.org
    file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    formats:
      - deb
      - rpm
    maintainer: Mrs4s


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to go-cqhttp

想要成为 go-cqhttp 的 Contributor? Awesome!

这个页面提供了一些 Tips ,可能对您的开发提供一些帮助.

## 开发环境准备

go-cqhttp 使用了 `golangci-lint` 检查可能的问题,规范代码风格,为了减少不必要的麻烦,
我们推荐在开发环境中安装 `golangci-lint` 工具.

```shell
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
```

在提交代码前运行 `golangci-lint` 检查你的代码:

```shell
golangci-lint run
```

**注意**: `golangci-lint` 需要 `diff` 工具,在 windows 环境中,你可能需要使用 `Git Bash` 运行。

## Pull requests

首先,为了方便项目管理,请将您的 PR 推送至**dev**分支。

### 检查 issue 列表

不管你是已经明确了要提交什么代码,还是正在寻找一个想法,你都应该先到 issue 列表看一下。
如果在 issue 中找到了感兴趣的,请在 issue 表明正在对这个 issue 进行开发。

### 项目结构

下面是 go-cqhttp 项目结构的简单介绍.

<table class="tg">
  <tr>
    <td>coolq</td>
    <td>
      包含与 MiraiGo 交互部分, CQ码解析等部分
    </td>
  </tr>
  <tr>
    <td>server</td>
    <td>
      包含 http,ws 通信的实现部分
    </td>
  </tr>
  <tr>
    <td>global</td>
    <td>
      一个<del>实用的</del>工具包
    </td>
  </tr>
  <tr>
    <td>docs</td>
    <td>
      使用教程与文档
    </td>
  </tr>
</table>

## 社区准则
为了让社区保持强大,不断发展,我们向整个社区提出了一些通用准则:

**友善**:对社区成员要礼貌,尊重和礼貌。 请不要在社区中发布任何有关种族歧视、性别歧视、
地域歧视、人格侮辱等言论。

**鼓励参与**:在社区中讲礼貌的每个人都受到欢迎,无论他们的贡献程度如何,
我们鼓励一切人参与(不一定需要提交代码) `go-cqhttp` 的开发。

**紧贴主题**:请避免主题外的讨论。当您更新或回复时, 可能会给大量人员发送邮件,
请牢记,没有人喜欢垃圾邮件。


================================================
FILE: Dockerfile
================================================
FROM golang:1.20-alpine AS builder

RUN go env -w GO111MODULE=auto \
  && go env -w CGO_ENABLED=0 \
  && go env -w GOPROXY=https://goproxy.cn,direct

WORKDIR /build

COPY ./ .

RUN set -ex \
    && cd /build \
    && go build -ldflags "-s -w -extldflags '-static'" -o cqhttp

FROM alpine:latest

COPY docker-entrypoint.sh /docker-entrypoint.sh

RUN chmod +x /docker-entrypoint.sh && \
    apk add --no-cache --update \
      ffmpeg \
      coreutils \
      shadow \
      su-exec \
      tzdata && \
    rm -rf /var/cache/apk/* && \
    mkdir -p /app && \
    mkdir -p /data && \
    mkdir -p /config && \
    useradd -d /config -s /bin/sh abc && \
    chown -R abc /config && \
    chown -R abc /data

ENV TZ="Asia/Shanghai"
ENV UID=99
ENV GID=100
ENV UMASK=002

COPY --from=builder /build/cqhttp /app/

WORKDIR /data

VOLUME [ "/data" ]

ENTRYPOINT [ "/docker-entrypoint.sh" ]
CMD [ "/app/cqhttp" ]


================================================
FILE: LICENSE
================================================
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.


================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://ishkong.github.io/go-cqhttp-docs/">
    <img src="winres/icon.png" width="200" height="200" alt="go-cqhttp">
  </a>
</p>

<div align="center">

# go-cqhttp

_✨ 基于 [Mirai](https://github.com/mamoe/mirai) 以及 [MiraiGo](https://github.com/Mrs4s/MiraiGo) 的 [OneBot](https://github.com/howmanybots/onebot/blob/master/README.md) Golang 原生实现 ✨_  


</div>

<p align="center">
  <a href="https://raw.githubusercontent.com/Mrs4s/go-cqhttp/master/LICENSE">
    <img src="https://img.shields.io/github/license/Mrs4s/go-cqhttp" alt="license">
  </a>
  <a href="https://github.com/Mrs4s/go-cqhttp/releases">
    <img src="https://img.shields.io/github/v/release/Mrs4s/go-cqhttp?color=blueviolet&include_prereleases" alt="release">
  </a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FMrs4s%2Fgo-cqhttp?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FMrs4s%2Fgo-cqhttp.svg?type=shield"/></a>
  <a href="https://github.com/howmanybots/onebot/blob/master/README.md">
    <img src="https://img.shields.io/badge/OneBot-v11-blue?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
  </a>
  <a href="https://github.com/Mrs4s/go-cqhttp/actions">
    <img src="https://github.com/Mrs4s/go-cqhttp/workflows/CI/badge.svg" alt="action">
  </a>
  <a href="https://goreportcard.com/report/github.com/Mrs4s/go-cqhttp">
  <img src="https://goreportcard.com/badge/github.com/Mrs4s/go-cqhttp" alt="GoReportCard">
  </a>
</p>

<p align="center">
  <a href="https://docs.go-cqhttp.org/">文档</a>
  ·
  <a href="https://github.com/Mrs4s/go-cqhttp/releases">下载</a>
  ·
  <a href="https://docs.go-cqhttp.org/guide/quick_start.html">开始使用</a>
  ·
  <a href="https://github.com/Mrs4s/go-cqhttp/blob/master/CONTRIBUTING.md">参与贡献</a>
</p>

## 重要信息
由于QQ官方针对协议库的围追堵截, 不断更新加密方案, 我们已无力继续维护此项目.
建议Bot开发者尽快迁移至无头NTQQ项目 -> https://github.com/Mrs4s/go-cqhttp/issues/2471

## 兼容性
go-cqhttp 兼容 [OneBot-v11](https://github.com/botuniverse/onebot-11) 绝大多数内容,并在其基础上做了一些扩展,详情请看 go-cqhttp 的文档。

### 接口

- [x] HTTP API
- [x] 反向 HTTP POST
- [x] 正向 WebSocket
- [x] 反向 WebSocket

### 拓展支持

> 拓展 API 可前往 [文档](docs/cqhttp.md) 查看

- [x] HTTP POST 多点上报
- [x] 反向 WS 多点连接
- [x] 修改群名
- [x] 消息撤回事件
- [x] 解析/发送 回复消息
- [x] 解析/发送 合并转发
- [x] 使用代理请求网络图片

### 实现

<details>
<summary>已实现 CQ 码</summary>

#### 符合 OneBot 标准的 CQ 码

| CQ 码        | 功能                        |
| ------------ | --------------------------- |
| [CQ:face]    | [QQ 表情]                   |
| [CQ:record]  | [语音]                      |
| [CQ:video]   | [短视频]                    |
| [CQ:at]      | [@某人]                     |
| [CQ:share]   | [链接分享]                  |
| [CQ:music]   | [音乐分享] [音乐自定义分享] |
| [CQ:reply]   | [回复]                      |
| [CQ:forward] | [合并转发]                  |
| [CQ:node]    | [合并转发节点]              |
| [CQ:xml]     | [XML 消息]                  |
| [CQ:json]    | [JSON 消息]                 |

[qq 表情]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#qq-%E8%A1%A8%E6%83%85
[语音]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E8%AF%AD%E9%9F%B3
[短视频]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E7%9F%AD%E8%A7%86%E9%A2%91
[@某人]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E6%9F%90%E4%BA%BA
[链接分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%93%BE%E6%8E%A5%E5%88%86%E4%BA%AB
[音乐分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%9F%B3%E4%B9%90%E5%88%86%E4%BA%AB-
[音乐自定义分享]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E9%9F%B3%E4%B9%90%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%86%E4%BA%AB-
[回复]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%9B%9E%E5%A4%8D
[合并转发]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-
[合并转发节点]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E8%8A%82%E7%82%B9-
[xml 消息]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#xml-%E6%B6%88%E6%81%AF
[json 消息]: https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#json-%E6%B6%88%E6%81%AF

#### 拓展 CQ 码及与 OneBot 标准有略微差异的 CQ 码

| 拓展 CQ 码     | 功能                              |
| -------------- | --------------------------------- |
| [CQ:image]     | [图片]                            |
| [CQ:redbag]    | [红包]                            |
| [CQ:poke]      | [戳一戳]                          |
| [CQ:node]      | [合并转发消息节点]                |
| [CQ:cardimage] | [一种 xml 的图片消息(装逼大图)] |
| [CQ:tts]       | [文本转语音]                      |

[图片]: https://docs.go-cqhttp.org/cqcode/#%E5%9B%BE%E7%89%87
[红包]: https://docs.go-cqhttp.org/cqcode/#%E7%BA%A2%E5%8C%85
[戳一戳]: https://docs.go-cqhttp.org/cqcode/#%E6%88%B3%E4%B8%80%E6%88%B3
[合并转发消息节点]: https://docs.go-cqhttp.org/cqcode/#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9
[一种 xml 的图片消息(装逼大图)]: https://docs.go-cqhttp.org/cqcode/#cardimage
[文本转语音]: https://docs.go-cqhttp.org/cqcode/#%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3

</details>

<details>
<summary>已实现 API</summary>

#### 符合 OneBot 标准的 API

| API                      | 功能                   |
| ------------------------ | ---------------------- |
| /send_private_msg        | [发送私聊消息]         |
| /send_group_msg          | [发送群消息]           |
| /send_msg                | [发送消息]             |
| /delete_msg              | [撤回信息]             |
| /set_group_kick          | [群组踢人]             |
| /set_group_ban           | [群组单人禁言]         |
| /set_group_whole_ban     | [群组全员禁言]         |
| /set_group_admin         | [群组设置管理员]       |
| /set_group_card          | [设置群名片(群备注)] |
| /set_group_name          | [设置群名]             |
| /set_group_leave         | [退出群组]             |
| /set_group_special_title | [设置群组专属头衔]     |
| /set_friend_add_request  | [处理加好友请求]       |
| /set_group_add_request   | [处理加群请求/邀请]    |
| /get_login_info          | [获取登录号信息]       |
| /get_stranger_info       | [获取陌生人信息]       |
| /get_friend_list         | [获取好友列表]         |
| /get_group_info          | [获取群信息]           |
| /get_group_list          | [获取群列表]           |
| /get_group_member_info   | [获取群成员信息]       |
| /get_group_member_list   | [获取群成员列表]       |
| /get_group_honor_info    | [获取群荣誉信息]       |
| /can_send_image          | [检查是否可以发送图片] |
| /can_send_record         | [检查是否可以发送语音] |
| /get_version_info        | [获取版本信息]         |
| /set_restart             | [重启 go-cqhttp]       |
| /.handle_quick_operation | [对事件执行快速操作]   |

[发送私聊消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[发送群消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
[发送消息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
[撤回信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
[群组踢人]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
[群组单人禁言]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
[群组全员禁言]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
[群组设置管理员]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
[设置群名片(群备注)]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
[设置群名]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
[退出群组]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
[设置群组专属头衔]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
[处理加好友请求]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[处理加群请求/邀请]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
[获取登录号信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
[获取陌生人信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
[获取好友列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
[获取群信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
[获取群列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
[获取群成员信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
[获取群成员列表]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
[获取群荣誉信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
[检查是否可以发送图片]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
[检查是否可以发送语音]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
[获取版本信息]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
[重启 go-cqhttp]: https://github.com/botuniverse/onebot-11/blob/master/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
[对事件执行快速操作]: https://github.com/botuniverse/onebot-11/blob/master/api/hidden.md#handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C

#### 拓展 API 及与 OneBot 标准有略微差异的 API

| 拓展 API                    | 功能                   |
| --------------------------- | ---------------------- |
| /set_group_portrait         | [设置群头像]           |
| /get_image                  | [获取图片信息]         |
| /get_msg                    | [获取消息]             |
| /get_forward_msg            | [获取合并转发内容]     |
| /send_group_forward_msg     | [发送合并转发(群)]     |
| /.get_word_slices           | [获取中文分词]         |
| /.ocr_image                 | [图片 OCR]             |
| /get_group_system_msg       | [获取群系统消息]       |
| /get_group_file_system_info | [获取群文件系统信息]   |
| /get_group_root_files       | [获取群根目录文件列表] |
| /get_group_files_by_folder  | [获取群子目录文件列表] |
| /get_group_file_url         | [获取群文件资源链接]   |
| /get_status                 | [获取状态]             |

[设置群头像]: https://docs.go-cqhttp.org/api/#%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%A4%B4%E5%83%8F
[获取图片信息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87%E4%BF%A1%E6%81%AF
[获取消息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
[获取合并转发内容]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E5%86%85%E5%AE%B9
[发送合并转发(群)]: https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4
[获取中文分词]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D-%E9%9A%90%E8%97%8F-api
[图片 ocr]: https://docs.go-cqhttp.org/api/#%E5%9B%BE%E7%89%87-ocr
[获取群系统消息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF
[获取群文件系统信息]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF
[获取群根目录文件列表]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%A0%B9%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群子目录文件列表]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%AD%90%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
[获取群文件资源链接]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E8%B5%84%E6%BA%90%E9%93%BE%E6%8E%A5
[获取状态]: https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%8A%B6%E6%80%81

</details>

<details>
<summary>已实现 Event</summary>

#### 符合 OneBot 标准的 Event(部分 Event 比 OneBot 标准多上报几个字段,不影响使用)

| 事件类型 | Event            |
| -------- | ---------------- |
| 消息事件 | [私聊信息]       |
| 消息事件 | [群消息]         |
| 通知事件 | [群文件上传]     |
| 通知事件 | [群管理员变动]   |
| 通知事件 | [群成员减少]     |
| 通知事件 | [群成员增加]     |
| 通知事件 | [群禁言]         |
| 通知事件 | [好友添加]       |
| 通知事件 | [群消息撤回]     |
| 通知事件 | [好友消息撤回]   |
| 通知事件 | [群内戳一戳]     |
| 通知事件 | [群红包运气王]   |
| 通知事件 | [群成员荣誉变更] |
| 请求事件 | [加好友请求]     |
| 请求事件 | [加群请求/邀请]  |

[私聊信息]: https://github.com/botuniverse/onebot-11/blob/master/event/message.md#%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
[群消息]: https://github.com/botuniverse/onebot-11/blob/master/event/message.md#%E7%BE%A4%E6%B6%88%E6%81%AF
[群文件上传]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0
[群管理员变动]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%AE%A1%E7%90%86%E5%91%98%E5%8F%98%E5%8A%A8
[群成员减少]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%87%8F%E5%B0%91
[群成员增加]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%A2%9E%E5%8A%A0
[群禁言]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%A6%81%E8%A8%80
[好友添加]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B7%BB%E5%8A%A0
[群消息撤回]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[好友消息撤回]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E5%A5%BD%E5%8F%8B%E6%B6%88%E6%81%AF%E6%92%A4%E5%9B%9E
[群内戳一戳]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
[群红包运气王]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E7%BA%A2%E5%8C%85%E8%BF%90%E6%B0%94%E7%8E%8B
[群成员荣誉变更]: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E8%8D%A3%E8%AA%89%E5%8F%98%E6%9B%B4
[加好友请求]: https://github.com/botuniverse/onebot-11/blob/master/event/request.md#%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
[加群请求/邀请]: https://github.com/botuniverse/onebot-11/blob/master/event/request.md#%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7

#### 拓展 Event

| 事件类型 | 拓展 Event       |
| -------- | ---------------- |
| 通知事件 | [好友戳一戳]     |
| 通知事件 | [群内戳一戳]     |
| 通知事件 | [群成员名片更新] |
| 通知事件 | [接收到离线文件] |

[好友戳一戳]: https://docs.go-cqhttp.org/event/#%E5%A5%BD%E5%8F%8B%E6%88%B3%E4%B8%80%E6%88%B3
[群内戳一戳]: https://docs.go-cqhttp.org/event/#%E7%BE%A4%E5%86%85%E6%88%B3%E4%B8%80%E6%88%B3
[群成员名片更新]: https://docs.go-cqhttp.org/event/#%E7%BE%A4%E6%88%90%E5%91%98%E5%90%8D%E7%89%87%E6%9B%B4%E6%96%B0
[接收到离线文件]: https://docs.go-cqhttp.org/event/#%E6%8E%A5%E6%94%B6%E5%88%B0%E7%A6%BB%E7%BA%BF%E6%96%87%E4%BB%B6

</details>

## 关于 ISSUE

以下 ISSUE 会被直接关闭

- 提交 BUG 不使用 Template
- 询问已知问题
- 提问找不到重点
- 重复提问

> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。  
> 有关如何提问,请阅读[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)

## 性能

在关闭数据库的情况下, 加载 25 个好友 128 个群运行 24 小时后内存使用为 15MB 左右. 开启数据库后内存使用将根据消息量增加 10-20MB, 如果系统内存小于 128M 建议关闭数据库使用.


================================================
FILE: cmd/api-generator/main.go
================================================
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"io"
	"os"
	"reflect"
	"sort"
	"strconv"
	"strings"
)

var supported = flag.Bool("supported", false, "genRouter supported.go")
var output = flag.String("o", "", "output file")
var pkg = flag.String("pkg", "", "package name")
var src = flag.String("path", "", "source file")

type Param struct {
	Name    string
	Type    string
	Default string
}

type Router struct {
	Func    string
	Path    []string
	PathV11 []string // v11 only
	PathV12 []string // v12 only
	Params  []Param
}

type generator struct {
	out io.Writer
}

const (
	PathAll = 0
	PathV11 = 11
	PathV12 = 12
)

func (g *generator) WriteString(s string) {
	io.WriteString(g.out, s)
}

func (g *generator) writef(format string, a ...any) {
	fmt.Fprintf(g.out, format, a...)
}

func (g *generator) header() {
	g.WriteString("// Code generated by cmd/api-generator. DO NOT EDIT.\n\n")
	g.writef("package %s\n\n", *pkg)
}

func (g *generator) genRouter(routers []Router) {
	g.WriteString("import (\n\n")
	g.WriteString("\"github.com/Mrs4s/go-cqhttp/coolq\"\n")
	g.WriteString("\"github.com/Mrs4s/go-cqhttp/global\"\n")
	g.WriteString("\"github.com/Mrs4s/go-cqhttp/pkg/onebot\"\n")
	g.WriteString(")\n\n")
	g.WriteString(`func (c *Caller) call(action string, spec *onebot.Spec, p Getter) global.MSG {`)
	genVer := func(path int) {
		g.writef(`if spec.Version == %d {
		switch action {
	`, path)
		for _, router := range routers {
			g.router(router, path)
		}
		g.WriteString("}}\n")
	}
	genVer(PathV11)
	genVer(PathV12)
	// generic path
	g.WriteString("switch action {\n")
	for _, router := range routers {
		g.router(router, PathAll)
	}
	g.WriteString("}\n")
	g.WriteString("return coolq.Failed(404, \"API_NOT_FOUND\", \"API不存在\")}")
}

func (g *generator) router(router Router, pathVersion int) {
	path := router.Path
	if pathVersion == PathV11 {
		path = router.PathV11
	}
	if pathVersion == PathV12 {
		path = router.PathV12
	}
	if len(path) == 0 {
		return
	}

	g.WriteString(`case `)
	for i, p := range path {
		if i != 0 {
			g.WriteString(`, `)
		}
		g.WriteString(strconv.Quote(p))
	}
	g.WriteString(":\n")

	for i, p := range router.Params {
		if p.Type == "*onebot.Spec" {
			continue
		}
		if p.Default == "" {
			v := "p.Get(" + strconv.Quote(p.Name) + ")"
			g.writef("p%d := %s\n", i, conv(v, p.Type))
		} else {
			g.writef("p%d := %s\n", i, p.Default)
			g.writef("if pt := p.Get(%s); pt.Exists() {\n", strconv.Quote(p.Name))
			g.writef("p%d = %s\n}\n", i, conv("pt", p.Type))
		}
	}

	g.WriteString("\t\treturn c.bot." + router.Func + "(")
	for i, p := range router.Params {
		if i != 0 {
			g.WriteString(", ")
		}
		if p.Type == "*onebot.Spec" {
			g.WriteString("spec")
			continue
		}
		g.writef("p%d", i)
	}
	g.WriteString(")\n")
}

func conv(v, t string) string {
	switch t {
	default:
		panic("unsupported type: " + t)
	case "gjson.Result", "*onebot.Spec":
		return v
	case "int64":
		return v + ".Int()"
	case "bool":
		return v + ".Bool()"
	case "string":
		return v + ".String()"
	case "int32", "int":
		return t + "(" + v + ".Int())"
	case "uint64":
		return v + ".Uint()"
	case "uint32":
		return "uint32(" + v + ".Uint())"
	case "uint16":
		return "uint16(" + v + ".Uint())"
	}
}

func main() {
	var routers []Router
	flag.Parse()
	fset := token.NewFileSet()
	for _, s := range strings.Split(*src, ",") {
		file, err := parser.ParseFile(fset, s, nil, parser.ParseComments)
		if err != nil {
			panic(err)
		}

		for _, decl := range file.Decls {
			switch decl := decl.(type) {
			case *ast.FuncDecl:
				if !decl.Name.IsExported() || decl.Recv == nil ||
					typeName(decl.Recv.List[0].Type) != "*CQBot" {
					continue
				}
				router := Router{Func: decl.Name.Name}

				// compute params
				for _, p := range decl.Type.Params.List {
					typ := typeName(p.Type)
					for _, name := range p.Names {
						router.Params = append(router.Params, Param{Name: snakecase(name.Name), Type: typ})
					}
				}

				for _, comment := range decl.Doc.List {
					annotation, args := match(comment.Text)
					switch annotation {
					case "route":
						for _, route := range strings.Split(args, ",") {
							router.Path = append(router.Path, unquote(route))
						}
					case "route11":
						for _, route := range strings.Split(args, ",") {
							router.PathV11 = append(router.PathV11, unquote(route))
						}
					case "route12":
						for _, route := range strings.Split(args, ",") {
							router.PathV12 = append(router.PathV12, unquote(route))
						}
					case "default":
						for name, value := range parseMap(args, "=") {
							for i, p := range router.Params {
								if p.Name == name {
									router.Params[i].Default = convDefault(value, p.Type)
								}
							}
						}
					case "rename":
						for name, value := range parseMap(args, "->") {
							for i, p := range router.Params {
								if p.Name == name {
									router.Params[i].Name = value
								}
							}
						}
					}
					sort.Slice(router.Path, func(i, j int) bool {
						return router.Path[i] < router.Path[j]
					})
					sort.Slice(router.PathV11, func(i, j int) bool {
						return router.PathV11[i] < router.PathV11[j]
					})
					sort.Slice(router.PathV12, func(i, j int) bool {
						return router.PathV12[i] < router.PathV12[j]
					})
				}
				if router.Path != nil || router.PathV11 != nil || router.PathV12 != nil {
					routers = append(routers, router)
				} else {
					println(decl.Name.Name)
				}
			}
		}
	}

	sort.Slice(routers, func(i, j int) bool {
		path := func(r Router) string {
			if r.Path != nil {
				return r.Path[0]
			}
			if r.PathV11 != nil {
				return r.PathV11[0]
			}
			if r.PathV12 != nil {
				return r.PathV12[0]
			}
			return ""
		}
		return path(routers[i]) < path(routers[j])
	})

	out := new(bytes.Buffer)
	g := &generator{out: out}
	g.header()
	if *supported {
		g.genSupported(routers)
	} else {
		g.genRouter(routers)
	}
	source, err := format.Source(out.Bytes())
	if err != nil {
		panic(err)
	}
	err = os.WriteFile(*output, source, 0o644)
	if err != nil {
		panic(err)
	}
}

func unquote(s string) string {
	switch s[0] {
	case '"':
		s, _ = strconv.Unquote(s)
	case '`':
		s = strings.Trim(s, "`")
	}
	return s
}

func parseMap(input string, sep string) map[string]string {
	out := make(map[string]string)
	for _, arg := range strings.Split(input, ",") {
		k, v, ok := strings.Cut(arg, sep)
		if !ok {
			out[k] = "true"
		}
		k = strings.TrimSpace(k)
		v = unquote(strings.TrimSpace(v))
		out[k] = v
	}
	return out
}

func match(text string) (string, string) {
	text = strings.TrimPrefix(text, "//")
	text = strings.TrimSpace(text)
	if !strings.HasPrefix(text, "@") || !strings.HasSuffix(text, ")") {
		return "", ""
	}
	text = strings.Trim(text, "@)")
	cmd, args, ok := strings.Cut(text, "(")
	if !ok {
		return "", ""
	}
	return cmd, unquote(args)
}

// some abbreviations need translation before transforming ro snake case
var replacer = strings.NewReplacer("ID", "Id")

func snakecase(s string) string {
	s = replacer.Replace(s)
	t := make([]byte, 0, 32)
	for i := 0; i < len(s); i++ {
		c := s[i]
		if ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') {
			t = append(t, c)
		} else {
			t = append(t, '_')
			t = append(t, c^0x20)
		}
	}
	return string(t)
}

func convDefault(s string, t string) string {
	switch t {
	case "bool":
		if s == "true" {
			return s
		}
	case "uint32":
		if s != "0" {
			return t + "(" + s + ")"
		}
	default:
		panic("unhandled default value type:" + t)
	}
	return ""
}

func typeName(x ast.Node) string {
	switch x := x.(type) {
	case *ast.Ident:
		return x.Name
	case *ast.SelectorExpr:
		return typeName(x.X) + "." + x.Sel.Name
	case *ast.StarExpr:
		return "*" + typeName(x.X)
	default:
		panic("unhandled type: " + reflect.TypeOf(x).String())
	}
}


================================================
FILE: cmd/api-generator/supported.go
================================================
package main

import "html/template"

func (g *generator) genSupported(routers []Router) {
	var v11, v12 []string // for onebot v12 get_supported_actions
	for _, router := range routers {
		if len(router.PathV11) > 0 {
			v11 = append(v11, router.PathV11...)
		}
		if len(router.PathV11) > 0 {
			v12 = append(v12, router.PathV12...)
		}
		if len(router.Path) > 0 {
			v11 = append(v11, router.Path...)
			v12 = append(v12, router.Path...)
		}
	}

	type S struct {
		V11 []string
		V12 []string
	}

	tmpl, err := template.New("").Parse(supportedTemplete)
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(g.out, &S{V11: v11, V12: v12})
	if err != nil {
		panic(err)
	}
}

const supportedTemplete = `
var supportedV11 = []string{
	{{range .V11}}	"{{.}}",
{{end}}
}

var supportedV12 = []string{
	{{range .V12}}	"{{.}}",
{{end}}
}`


================================================
FILE: cmd/gocq/login.go
================================================
package gocq

import (
	"bufio"
	"bytes"
	"fmt"
	"image"
	"image/png"
	"os"
	"strings"
	"time"

	"github.com/Mrs4s/MiraiGo/client"
	"github.com/Mrs4s/MiraiGo/utils"
	"github.com/mattn/go-colorable"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"gopkg.ilharper.com/x/isatty"

	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/internal/download"
)

var console = bufio.NewReader(os.Stdin)

func readLine() (str string) {
	str, _ = console.ReadString('\n')
	str = strings.TrimSpace(str)
	return
}

func readLineTimeout(t time.Duration) {
	r := make(chan string)
	go func() {
		select {
		case r <- readLine():
		case <-time.After(t):
		}
	}()
	select {
	case <-r:
	case <-time.After(t):
	}
}

func readIfTTY(de string) (str string) {
	if isatty.Isatty(os.Stdin.Fd()) {
		return readLine()
	}
	log.Warnf("未检测到输入终端,自动选择%s.", de)
	return de
}

var cli *client.QQClient
var device *client.DeviceInfo

// ErrSMSRequestError SMS请求出错
var ErrSMSRequestError = errors.New("sms request error")

func commonLogin() error {
	res, err := cli.Login()
	if err != nil {
		return err
	}
	return loginResponseProcessor(res)
}

func printQRCode(imgData []byte) {
	const (
		black = "\033[48;5;0m  \033[0m"
		white = "\033[48;5;7m  \033[0m"
	)
	img, err := png.Decode(bytes.NewReader(imgData))
	if err != nil {
		log.Panic(err)
	}
	data := img.(*image.Gray).Pix
	bound := img.Bounds().Max.X
	buf := make([]byte, 0, (bound*4+1)*(bound))
	i := 0
	for y := 0; y < bound; y++ {
		i = y * bound
		for x := 0; x < bound; x++ {
			if data[i] != 255 {
				buf = append(buf, white...)
			} else {
				buf = append(buf, black...)
			}
			i++
		}
		buf = append(buf, '\n')
	}
	_, _ = colorable.NewColorableStdout().Write(buf)
}

func qrcodeLogin() error {
	rsp, err := cli.FetchQRCodeCustomSize(1, 2, 1)
	if err != nil {
		return err
	}
	_ = os.WriteFile("qrcode.png", rsp.ImageData, 0o644)
	defer func() { _ = os.Remove("qrcode.png") }()
	if cli.Uin != 0 {
		log.Infof("请使用账号 %v 登录手机QQ扫描二维码 (qrcode.png) : ", cli.Uin)
	} else {
		log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
	}
	time.Sleep(time.Second)
	printQRCode(rsp.ImageData)
	s, err := cli.QueryQRCodeStatus(rsp.Sig)
	if err != nil {
		return err
	}
	prevState := s.State
	for {
		time.Sleep(time.Second)
		s, _ = cli.QueryQRCodeStatus(rsp.Sig)
		if s == nil {
			continue
		}
		if prevState == s.State {
			continue
		}
		prevState = s.State
		switch s.State {
		case client.QRCodeCanceled:
			log.Fatalf("扫码被用户取消.")
		case client.QRCodeTimeout:
			log.Fatalf("二维码过期")
		case client.QRCodeWaitingForConfirm:
			log.Infof("扫码成功, 请在手机端确认登录.")
		case client.QRCodeConfirmed:
			res, err := cli.QRCodeLogin(s.LoginInfo)
			if err != nil {
				return err
			}
			return loginResponseProcessor(res)
		case client.QRCodeImageFetch, client.QRCodeWaitingForScan:
			// ignore
		}
	}
}

func loginResponseProcessor(res *client.LoginResponse) error {
	var err error
	for {
		if err != nil {
			return err
		}
		if res.Success {
			return nil
		}
		var text string
		switch res.Error {
		case client.SliderNeededError:
			log.Warnf("登录需要滑条验证码, 请验证后重试.")
			ticket := getTicket(res.VerifyUrl)
			if ticket == "" {
				log.Infof("按 Enter 继续....")
				readLine()
				os.Exit(0)
			}
			res, err = cli.SubmitTicket(ticket)
			continue
		case client.NeedCaptcha:
			log.Warnf("登录需要验证码.")
			_ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644)
			log.Warnf("请输入验证码 (captcha.jpg): (Enter 提交)")
			text = readLine()
			global.DelFile("captcha.jpg")
			res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
			continue
		case client.SMSNeededError:
			log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
			readLine()
			if !cli.RequestSMS() {
				log.Warnf("发送验证码失败,可能是请求过于频繁.")
				return errors.WithStack(ErrSMSRequestError)
			}
			log.Warn("请输入短信验证码: (Enter 提交)")
			text = readLine()
			res, err = cli.SubmitSMS(text)
			continue
		case client.SMSOrVerifyNeededError:
			log.Warnf("账号已开启设备锁,请选择验证方式:")
			log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
			log.Warnf("2. 使用手机QQ扫码验证.")
			log.Warn("请输入(1 - 2):")
			text = readIfTTY("2")
			if strings.Contains(text, "1") {
				if !cli.RequestSMS() {
					log.Warnf("发送验证码失败,可能是请求过于频繁.")
					return errors.WithStack(ErrSMSRequestError)
				}
				log.Warn("请输入短信验证码: (Enter 提交)")
				text = readLine()
				res, err = cli.SubmitSMS(text)
				continue
			}
			fallthrough
		case client.UnsafeDeviceError:
			log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyUrl)
			log.Infof("按 Enter 或等待 5s 后继续....")
			readLineTimeout(time.Second * 5)
			os.Exit(0)
		case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
			msg := res.ErrorMessage
			log.Warnf("登录失败: %v Code: %v", msg, res.Code)
			switch res.Code {
			case 235:
				log.Warnf("设备信息被封禁, 请删除 device.json 后重试.")
			case 237:
				log.Warnf("登录过于频繁, 请在手机QQ登录并根据提示完成认证后等一段时间重试")
			case 45:
				log.Warnf("你的账号被限制登录, 请配置 SignServer 后重试")
			}
			log.Infof("按 Enter 继续....")
			readLine()
			os.Exit(0)
		}
	}
}

func getTicket(u string) string {
	log.Warnf("请选择提交滑块ticket方式:")
	log.Warnf("1. 自动提交")
	log.Warnf("2. 手动抓取提交")
	log.Warn("请输入(1 - 2):")
	text := readLine()
	id := utils.RandomString(8)
	auto := !strings.Contains(text, "2")
	if auto {
		u = strings.ReplaceAll(u, "https://ssl.captcha.qq.com/template/wireless_mqq_captcha.html?", fmt.Sprintf("https://captcha.go-cqhttp.org/captcha?id=%v&", id))
	}
	log.Warnf("请前往该地址验证 -> %v ", u)
	if !auto {
		log.Warn("请输入ticket: (Enter 提交)")
		return readLine()
	}

	for count := 120; count > 0; count-- {
		str := fetchCaptcha(id)
		if str != "" {
			return str
		}
		time.Sleep(time.Second)
	}
	log.Warnf("验证超时")
	return ""
}

func fetchCaptcha(id string) string {
	g, err := download.Request{URL: "https://captcha.go-cqhttp.org/captcha/ticket?id=" + id}.JSON()
	if err != nil {
		log.Debugf("获取 Ticket 时出现错误: %v", err)
		return ""
	}
	if g.Get("ticket").Exists() {
		return g.Get("ticket").String()
	}
	return ""
}


================================================
FILE: cmd/gocq/main.go
================================================
// Package gocq 程序的主体部分
package gocq

import (
	"crypto/aes"
	"crypto/md5"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"os"
	"path"
	"sync"
	"time"

	"github.com/Mrs4s/MiraiGo/binary"
	"github.com/Mrs4s/MiraiGo/client"
	"github.com/Mrs4s/MiraiGo/wrapper"
	para "github.com/fumiama/go-hide-param"
	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"
	"golang.org/x/crypto/pbkdf2"
	"golang.org/x/term"

	"github.com/Mrs4s/go-cqhttp/coolq"
	"github.com/Mrs4s/go-cqhttp/db"
	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/global/terminal"
	"github.com/Mrs4s/go-cqhttp/internal/base"
	"github.com/Mrs4s/go-cqhttp/internal/cache"
	"github.com/Mrs4s/go-cqhttp/internal/download"
	"github.com/Mrs4s/go-cqhttp/internal/selfdiagnosis"
	"github.com/Mrs4s/go-cqhttp/internal/selfupdate"
	"github.com/Mrs4s/go-cqhttp/modules/servers"
	"github.com/Mrs4s/go-cqhttp/server"
)

// 允许通过配置文件设置的状态列表
var allowStatus = [...]client.UserOnlineStatus{
	client.StatusOnline, client.StatusAway, client.StatusInvisible, client.StatusBusy,
	client.StatusListening, client.StatusConstellation, client.StatusWeather, client.StatusMeetSpring,
	client.StatusTimi, client.StatusEatChicken, client.StatusLoving, client.StatusWangWang, client.StatusCookedRice,
	client.StatusStudy, client.StatusStayUp, client.StatusPlayBall, client.StatusSignal, client.StatusStudyOnline,
	client.StatusGaming, client.StatusVacationing, client.StatusWatchingTV, client.StatusFitness,
}

// InitBase 解析参数并检测
//
//	如果在 windows 下双击打开了程序,程序将在此函数释出脚本后终止;
//	如果传入 -h 参数,程序将打印帮助后终止;
//	如果传入 -d 参数,程序将在启动 daemon 后终止。
func InitBase() {
	base.Parse()
	if !base.FastStart && terminal.RunningByDoubleClick() {
		err := terminal.NoMoreDoubleClick()
		if err != nil {
			log.Errorf("遇到错误: %v", err)
			time.Sleep(time.Second * 5)
		}
		os.Exit(0)
	}
	switch {
	case base.LittleH:
		base.Help()
	case base.LittleD:
		server.Daemon()
	}
	if base.LittleWD != "" {
		err := os.Chdir(base.LittleWD)
		if err != nil {
			log.Fatalf("重置工作目录时出现错误: %v", err)
		}
	}
	base.Init()
}

// PrepareData 准备 log, 缓存, 数据库, 必须在 InitBase 之后执行
func PrepareData() {
	rotateOptions := []rotatelogs.Option{
		rotatelogs.WithRotationTime(time.Hour * 24),
	}
	rotateOptions = append(rotateOptions, rotatelogs.WithMaxAge(base.LogAging))
	if base.LogForceNew {
		rotateOptions = append(rotateOptions, rotatelogs.ForceNewFile())
	}
	w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotateOptions...)
	if err != nil {
		log.Errorf("rotatelogs init err: %v", err)
		panic(err)
	}

	consoleFormatter := global.LogFormat{EnableColor: base.LogColorful}
	fileFormatter := global.LogFormat{EnableColor: false}
	log.AddHook(global.NewLocalHook(w, consoleFormatter, fileFormatter, global.GetLogLevel(base.LogLevel)...))

	mkCacheDir := func(path string, _type string) {
		if !global.PathExists(path) {
			if err := os.MkdirAll(path, 0o755); err != nil {
				log.Fatalf("创建%s缓存文件夹失败: %v", _type, err)
			}
		}
	}
	mkCacheDir(global.ImagePath, "图片")
	mkCacheDir(global.VoicePath, "语音")
	mkCacheDir(global.VideoPath, "视频")
	mkCacheDir(global.CachePath, "发送图片")
	mkCacheDir(path.Join(global.ImagePath, "guild-images"), "频道图片缓存")
	mkCacheDir(global.VersionsPath, "版本缓存")
	cache.Init()

	db.Init()
	if err := db.Open(); err != nil {
		log.Fatalf("打开数据库失败: %v", err)
	}
}

// LoginInteract 登录交互, 可能需要键盘输入, 必须在 InitBase, PrepareData 之后执行
func LoginInteract() {
	var byteKey []byte
	arg := os.Args
	if len(arg) > 1 {
		for i := range arg {
			switch arg[i] {
			case "update":
				if len(arg) > i+1 {
					selfupdate.SelfUpdate(arg[i+1])
				} else {
					selfupdate.SelfUpdate("")
				}
			case "key":
				p := i + 1
				if len(arg) > p {
					byteKey = []byte(arg[p])
					para.Hide(p)
				}
			}
		}
	}

	if (base.Account.Uin == 0 || (base.Account.Password == "" && !base.Account.Encrypt)) && !global.PathExists("session.token") {
		log.Warn("账号密码未配置, 将使用二维码登录.")
		if !base.FastStart {
			log.Warn("将在 5秒 后继续.")
			time.Sleep(time.Second * 5)
		}
	}

	log.Info("当前版本:", base.Version)
	if base.Debug {
		log.SetLevel(log.DebugLevel)
		log.Warnf("已开启Debug模式.")
	}
	if !global.PathExists("device.json") {
		log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
		device = client.GenRandomDevice()
		_ = os.WriteFile("device.json", device.ToJson(), 0o644)
		log.Info("已生成设备信息并保存到 device.json 文件.")
	} else {
		log.Info("将使用 device.json 内的设备信息运行Bot.")
		device = new(client.DeviceInfo)
		if err := device.ReadJson([]byte(global.ReadAllText("device.json"))); err != nil {
			log.Fatalf("加载设备信息失败: %v", err)
		}
	}
	signServer, err := getAvaliableSignServer() // 获取可用签名服务器
	if err != nil {
		log.Warn(err)
	}
	if signServer != nil && len(signServer.URL) > 1 {
		log.Infof("使用签名服务器:%v", signServer.URL)
		go signStartRefreshToken(base.Account.RefreshInterval) // 定时刷新 token
		wrapper.DandelionEnergy = energy
		wrapper.FekitGetSign = sign
		if !base.IsBelow110 {
			if !base.Account.AutoRegister {
				log.Warn("自动注册实例已关闭,请配置 sign-server 端自动注册实例以保持正常签名")
			}
			if !base.Account.AutoRefreshToken {
				log.Info("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token")
			}
		} else {
			log.Warn("签名服务器版本 <= 1.1.0 ,无法使用刷新 token 等操作,建议使用 1.1.6 版本及以上签名服务器")
		}
	} else {
		log.Warnf("警告: 未配置签名服务器或签名服务器不可用, 这可能会导致登录 45 错误码或发送消息被风控")
	}

	if base.Account.Encrypt {
		if !global.PathExists("password.encrypt") {
			if base.Account.Password == "" {
				log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.")
			} else {
				log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
				byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
				base.PasswordHash = md5.Sum([]byte(base.Account.Password))
				_ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(base.PasswordHash[:], byteKey)), 0o644)
				log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
			}
			readLine()
			os.Exit(0)
		}
		if base.Account.Password != "" {
			log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
			readLine()
			os.Exit(0)
		}
		if len(byteKey) == 0 {
			log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
			cancel := make(chan struct{}, 1)
			state, _ := term.GetState(int(os.Stdin.Fd()))
			go func() {
				select {
				case <-cancel:
					return
				case <-time.After(time.Second * 45):
					log.Infof("解密key输入超时")
					time.Sleep(3 * time.Second)
					_ = term.Restore(int(os.Stdin.Fd()), state)
					os.Exit(0)
				}
			}()
			byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
			cancel <- struct{}{}
		} else {
			log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
		}

		encrypt, _ := os.ReadFile("password.encrypt")
		ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
		if err != nil {
			log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
		}
		copy(base.PasswordHash[:], ph)
	} else if len(base.Account.Password) > 0 {
		base.PasswordHash = md5.Sum([]byte(base.Account.Password))
	}
	if !base.FastStart {
		log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
		time.Sleep(time.Second * 5)
	}
	log.Info("开始尝试登录并同步消息...")
	log.Infof("使用协议: %s", device.Protocol.Version())
	cli = newClient()
	cli.UseDevice(device)
	isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt
	isTokenLogin := false

	if isQRCodeLogin && cli.Device().Protocol != 2 {
		log.Warn("当前协议不支持二维码登录, 请配置账号密码登录.")
		os.Exit(0)
	}

	// 加载本地版本信息, 一般是在上次登录时保存的
	versionFile := path.Join(global.VersionsPath, fmt.Sprint(int(cli.Device().Protocol))+".json")
	if global.PathExists(versionFile) {
		b, err := os.ReadFile(versionFile)
		if err != nil {
			log.Warnf("从文件 %s 读取本地版本信息文件出错.", versionFile)
			os.Exit(0)
		}
		err = cli.Device().Protocol.Version().UpdateFromJson(b)
		if err != nil {
			log.Warnf("从文件 %s 解析本地版本信息出错: %v", versionFile, err)
			os.Exit(0)
		}
		log.Infof("从文件 %s 读取协议版本 %v.", versionFile, cli.Device().Protocol.Version())
	}

	saveToken := func() {
		base.AccountToken = cli.GenToken()
		_ = os.WriteFile("session.token", base.AccountToken, 0o644)
	}
	if global.PathExists("session.token") {
		token, err := os.ReadFile("session.token")
		if err == nil {
			if base.Account.Uin != 0 {
				r := binary.NewReader(token)
				cu := r.ReadInt64()
				if cu != base.Account.Uin {
					log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", base.Account.Uin, cu)
					log.Warnf("1. 使用会话缓存继续.")
					log.Warnf("2. 删除会话缓存并重启.")
					log.Warnf("请选择:")
					text := readIfTTY("1")
					if text == "2" {
						_ = os.Remove("session.token")
						log.Infof("缓存已删除.")
						os.Exit(0)
					}
				}
			}
			if err = cli.TokenLogin(token); err != nil {
				_ = os.Remove("session.token")
				log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
				time.Sleep(time.Second)
				cli.Disconnect()
				cli.Release()
				cli = newClient()
				cli.UseDevice(device)
			} else {
				isTokenLogin = true
			}
		}
	}
	if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} {
		cli.Uin = base.Account.Uin
		cli.PasswordMd5 = base.PasswordHash
	}
	download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second)
	if !base.FastStart {
		log.Infof("正在检查协议更新...")
		currentVersionName := device.Protocol.Version().SortVersionName
		remoteVersion, err := getRemoteLatestProtocolVersion(int(device.Protocol.Version().Protocol))
		if err == nil {
			remoteVersionName := gjson.GetBytes(remoteVersion, "sort_version_name").String()
			if remoteVersionName != currentVersionName {
				switch {
				case !base.UpdateProtocol:
					log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
					log.Infof("如果登录时出现版本过低错误, 可尝试使用 -update-protocol 参数启动")
				case !isTokenLogin:
					_ = device.Protocol.Version().UpdateFromJson(remoteVersion)
					err := os.WriteFile(versionFile, remoteVersion, 0644)
					log.Infof("协议版本已更新: %s -> %s", currentVersionName, remoteVersionName)
					if err != nil {
						log.Warnln("更新协议版本缓存文件", versionFile, "失败:", err)
					}
				default:
					log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
					log.Infof("由于使用了会话缓存, 无法自动更新协议, 请删除缓存后重试")
				}
			}
		} else if err.Error() != "remote version unavailable" {
			log.Warnf("检查协议更新失败: %v", err)
		}
	}
	if !isTokenLogin {
		if !isQRCodeLogin {
			if err := commonLogin(); err != nil {
				log.Fatalf("登录时发生致命错误: %v", err)
			}
		} else {
			if err := qrcodeLogin(); err != nil {
				log.Fatalf("登录时发生致命错误: %v", err)
			}
		}
	}
	var times uint = 1 // 重试次数
	var reLoginLock sync.Mutex
	cli.DisconnectedEvent.Subscribe(func(_ *client.QQClient, e *client.ClientDisconnectedEvent) {
		reLoginLock.Lock()
		defer reLoginLock.Unlock()
		times = 1
		if cli.Online.Load() {
			return
		}
		log.Warnf("Bot已离线: %v", e.Message)
		time.Sleep(time.Second * time.Duration(base.Reconnect.Delay))
		for {
			if base.Reconnect.Disabled {
				log.Warnf("未启用自动重连, 将退出.")
				os.Exit(1)
			}
			if times > base.Reconnect.MaxTimes && base.Reconnect.MaxTimes != 0 {
				log.Fatalf("Bot重连次数超过限制, 停止")
			}
			times++
			if base.Reconnect.Interval > 0 {
				log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v/%v", base.Reconnect.Interval, times, base.Reconnect.MaxTimes)
				time.Sleep(time.Second * time.Duration(base.Reconnect.Interval))
			} else {
				time.Sleep(time.Second)
			}
			if cli.Online.Load() {
				log.Infof("登录已完成")
				break
			}
			log.Warnf("尝试重连...")
			err := cli.TokenLogin(base.AccountToken)
			if err == nil {
				saveToken()
				return
			}
			log.Warnf("快速重连失败: %v", err)
			if isQRCodeLogin {
				log.Fatalf("快速重连失败, 扫码登录无法恢复会话.")
			}
			log.Warnf("快速重连失败, 尝试普通登录. 这可能是因为其他端强行T下线导致的.")
			time.Sleep(time.Second)
			if err := commonLogin(); err != nil {
				log.Errorf("登录时发生致命错误: %v", err)
			} else {
				saveToken()
				break
			}
		}
	})
	saveToken()
	cli.AllowSlider = true
	log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
	log.Info("开始加载好友列表...")
	global.Check(cli.ReloadFriendList(), true)
	log.Infof("共加载 %v 个好友.", len(cli.FriendList))
	log.Infof("开始加载群列表...")
	global.Check(cli.ReloadGroupList(), true)
	log.Infof("共加载 %v 个群.", len(cli.GroupList))
	if uint(base.Account.Status) >= uint(len(allowStatus)) {
		base.Account.Status = 0
	}
	cli.SetOnlineStatus(allowStatus[base.Account.Status])
	servers.Run(coolq.NewQQBot(cli))
	log.Info("资源初始化完成, 开始处理信息.")
	log.Info("アトリは、高性能ですから!")
}

// WaitSignal 在新线程检查更新和网络并等待信号, 必须在 InitBase, PrepareData, LoginInteract 之后执行
//
//   - 直接返回: os.Interrupt, syscall.SIGTERM
//   - dump stack: syscall.SIGQUIT, syscall.SIGUSR1
func WaitSignal() {
	go func() {
		selfupdate.CheckUpdate()
		selfdiagnosis.NetworkDiagnosis(cli)
	}()

	<-global.SetupMainSignalHandler()
}

// PasswordHashEncrypt 使用key加密给定passwordHash
func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
	if len(passwordHash) != 16 {
		panic("密码加密参数错误")
	}

	key = pbkdf2.Key(key, key, 114514, 32, sha1.New)

	cipher, _ := aes.NewCipher(key)
	result := make([]byte, 16)
	cipher.Encrypt(result, passwordHash)

	return hex.EncodeToString(result)
}

// PasswordHashDecrypt 使用key解密给定passwordHash
func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, error) {
	ciphertext, err := hex.DecodeString(encryptedPasswordHash)
	if err != nil {
		return nil, err
	}

	key = pbkdf2.Key(key, key, 114514, 32, sha1.New)

	cipher, _ := aes.NewCipher(key)
	result := make([]byte, 16)
	cipher.Decrypt(result, ciphertext)

	return result, nil
}

func newClient() *client.QQClient {
	c := client.NewClientEmpty()
	c.UseFragmentMessage = base.ForceFragmented
	c.OnServerUpdated(func(_ *client.QQClient, _ *client.ServerUpdatedEvent) bool {
		if !base.UseSSOAddress {
			log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
			return false
		}
		log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
		return true
	})
	if global.PathExists("address.txt") {
		log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
		addr := global.ReadAddrFile("address.txt")
		if len(addr) > 0 {
			c.SetCustomServer(addr)
		}
		log.Infof("读取到 %v 个自定义地址.", len(addr))
	}
	c.SetLogger(protocolLogger{})
	return c
}

var remoteVersions = map[int]string{
	1: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone.json",
	6: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad.json",
}

func getRemoteLatestProtocolVersion(protocolType int) ([]byte, error) {
	url, ok := remoteVersions[protocolType]
	if !ok {
		return nil, errors.New("remote version unavailable")
	}
	response, err := download.Request{URL: url}.Bytes()
	if err != nil {
		return download.Request{URL: "https://mirror.ghproxy.com/" + url}.Bytes()
	}
	return response, nil
}

type protocolLogger struct{}

const fromProtocol = "Protocol -> "

func (p protocolLogger) Info(format string, arg ...any) {
	log.Infof(fromProtocol+format, arg...)
}

func (p protocolLogger) Warning(format string, arg ...any) {
	log.Warnf(fromProtocol+format, arg...)
}

func (p protocolLogger) Debug(format string, arg ...any) {
	log.Debugf(fromProtocol+format, arg...)
}

func (p protocolLogger) Error(format string, arg ...any) {
	log.Errorf(fromProtocol+format, arg...)
}

func (p protocolLogger) Dump(data []byte, format string, arg ...any) {
	if !global.PathExists(global.DumpsPath) {
		_ = os.MkdirAll(global.DumpsPath, 0o755)
	}
	dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix()))
	message := fmt.Sprintf(format, arg...)
	log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile)
	_ = os.WriteFile(dumpFile, data, 0o644)
}


================================================
FILE: cmd/gocq/qsign.go
================================================
package gocq

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"

	"github.com/Mrs4s/MiraiGo/utils"

	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/internal/base"
	"github.com/Mrs4s/go-cqhttp/internal/download"
	"github.com/Mrs4s/go-cqhttp/modules/config"
)

type currentSignServer atomic.Pointer[config.SignServer]

func (c *currentSignServer) get() *config.SignServer {
	if len(base.SignServers) == 1 {
		// 只配置了一个签名服务时不检查以及切换, 在get阶段返回,防止返回nil导致其他bug(可能)
		return &base.SignServers[0]
	}
	return (*atomic.Pointer[config.SignServer])(c).Load()
}

func (c *currentSignServer) set(server *config.SignServer) {
	(*atomic.Pointer[config.SignServer])(c).Store(server)
}

// 当前签名服务器
var ss currentSignServer

// 失败计数
type errconut atomic.Uintptr

func (ec *errconut) hasOver(count uintptr) bool {
	return (*atomic.Uintptr)(ec).Load() > count
}

func (ec *errconut) inc() {
	(*atomic.Uintptr)(ec).Add(1)
}

var errn errconut

// getAvaliableSignServer 获取可用的签名服务器,没有则返回空和相应错误
func getAvaliableSignServer() (*config.SignServer, error) {
	cs := ss.get()
	if cs != nil {
		return cs, nil
	}
	if len(base.SignServers) == 0 {
		return nil, errors.New("no sign server configured")
	}
	maxCount := base.Account.MaxCheckCount
	if maxCount == 0 {
		if errn.hasOver(3) {
			log.Warn("已连续 3 次获取不到可用签名服务器,将固定使用主签名服务器")
			ss.set(&base.SignServers[0])
			return ss.get(), nil
		}
	} else if errn.hasOver(uintptr(maxCount)) {
		log.Fatalf("获取可用签名服务器失败次数超过 %v 次, 正在离线", maxCount)
	}
	if cs != nil && len(cs.URL) > 0 {
		log.Warnf("当前签名服务器 %v 不可用,正在查找可用服务器", cs.URL)
	}
	cs = asyncCheckServer(base.SignServers)
	if cs == nil {
		return nil, errors.New("no usable sign server")
	}
	return cs, nil
}

func isServerAvaliable(signServer string) bool {
	resp, err := download.Request{
		Method: http.MethodGet,
		URL:    signServer,
	}.WithTimeout(3 * time.Second).Bytes()
	if err == nil && gjson.GetBytes(resp, "code").Int() == 0 {
		return true
	}
	log.Warnf("签名服务器 %v 可能不可用,请求出现错误:%v", signServer, err)
	return false
}

// asyncCheckServer 按同步顺序检查所有签名服务器直到找到可用的
func asyncCheckServer(servers []config.SignServer) *config.SignServer {
	doRegister := sync.Once{}
	wg := sync.WaitGroup{}
	wg.Add(len(servers))
	for i, s := range servers {
		go func(i int, server config.SignServer) {
			defer wg.Done()
			log.Infof("检查签名服务器:%v  (%v/%v)", server.URL, i+1, len(servers))
			if len(server.URL) < 4 {
				return
			}
			if isServerAvaliable(server.URL) {
				doRegister.Do(func() {
					ss.set(&server)
					log.Infof("使用签名服务器 url=%v, key=%v, auth=%v", server.URL, server.Key, server.Authorization)
					if base.Account.AutoRegister {
						// 若配置了自动注册实例则在切换后注册实例,否则不需要注册,签名时由qsign自动注册
						signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, server.Key)
					}
				})
			}
		}(i, s)
	}
	wg.Wait()
	return ss.get()
}

/*
请求签名服务器

	url: api + params 组合的字符串,无须包含签名服务器地址
	return: signServer, response, error
*/
func requestSignServer(method string, url string, headers map[string]string, body io.Reader) (string, []byte, error) {
	signServer, e := getAvaliableSignServer()
	if e != nil && len(signServer.URL) == 0 { // 没有可用的
		log.Warnf("获取可用签名服务器出错:%v, 将使用主签名服务器进行签名", e)
		errn.inc()
		signServer = &base.SignServers[0] // 没有获取到时使用第一个
	}
	if !strings.HasPrefix(url, signServer.URL) {
		url = strings.TrimSuffix(signServer.URL, "/") + "/" + strings.TrimPrefix(url, "/")
	}
	if headers == nil {
		headers = map[string]string{}
	}
	auth := signServer.Authorization
	if auth != "-" && auth != "" {
		headers["Authorization"] = auth
	}
	req := download.Request{
		Method: method,
		Header: headers,
		URL:    url,
		Body:   body,
	}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second)
	resp, err := req.Bytes()
	if err != nil {
		ss.set(nil) // 标记为不可用
	}
	return signServer.URL, resp, err
}

func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) {
	url := "custom_energy" + fmt.Sprintf("?data=%v&salt=%v&uin=%v&android_id=%v&guid=%v",
		id, hex.EncodeToString(salt), uin, utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid))
	if base.IsBelow110 {
		url = "custom_energy" + fmt.Sprintf("?data=%v&salt=%v", id, hex.EncodeToString(salt))
	}
	signServer, response, err := requestSignServer(http.MethodGet, url, nil, nil)
	if err != nil {
		log.Warnf("获取T544 sign时出现错误: %v. server: %v", err, signServer)
		return nil, err
	}
	data, err := hex.DecodeString(gjson.GetBytes(response, "data").String())
	if err != nil {
		log.Warnf("获取T544 sign时出现错误: %v (data: %v)", err, gjson.GetBytes(response, "data").String())
		return nil, err
	}
	if len(data) == 0 {
		log.Warnf("获取T544 sign时出现错误: %v.", "data is empty")
		return nil, errors.New("data is empty")
	}
	return data, nil
}

// signSubmit
// 提交回调 buffer
func signSubmit(uin string, cmd string, callbackID int64, buffer []byte, t string) {
	buffStr := hex.EncodeToString(buffer)
	if base.Debug {
		tail := 64
		endl := "..."
		if len(buffStr) < tail {
			tail = len(buffStr)
			endl = "."
		}
		log.Debugf("submit (%v): uin=%v, cmd=%v, callbackID=%v, buffer=%v%s", t, uin, cmd, callbackID, buffStr[:tail], endl)
	}

	signServer, _, err := requestSignServer(
		http.MethodGet,
		"submit"+fmt.Sprintf("?uin=%v&cmd=%v&callback_id=%v&buffer=%v",
			uin, cmd, callbackID, buffStr),
		nil, nil,
	)
	if err != nil {
		log.Warnf("提交 callback 时出现错误: %v. server: %v", err, signServer)
	}
}

// signCallback
// 刷新 token 和签名的回调
func signCallback(uin string, results []gjson.Result, t string) {
	for { // 等待至在线
		if cli.Online.Load() {
			break
		}
		time.Sleep(1 * time.Second)
	}
	for _, result := range results {
		cmd := result.Get("cmd").String()
		callbackID := result.Get("callbackId").Int()
		body, _ := hex.DecodeString(result.Get("body").String())
		ret, err := cli.SendSsoPacket(cmd, body)
		if err != nil || len(ret) == 0 {
			log.Warnf("Callback error: %v, or response data is empty", err)
			continue // 发送 SsoPacket 出错或返回数据为空时跳过
		}
		signSubmit(uin, cmd, callbackID, ret, t)
	}
}

func signRequset(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) {
	headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
	_, response, err := requestSignServer(
		http.MethodPost,
		"sign",
		headers,
		bytes.NewReader([]byte(fmt.Sprintf("uin=%v&qua=%s&cmd=%s&seq=%v&buffer=%v&android_id=%v&guid=%v",
			uin, qua, cmd, seq, hex.EncodeToString(buff), utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)))),
	)
	if err != nil {
		return nil, nil, nil, err
	}
	sign, _ = hex.DecodeString(gjson.GetBytes(response, "data.sign").String())
	extra, _ = hex.DecodeString(gjson.GetBytes(response, "data.extra").String())
	token, _ = hex.DecodeString(gjson.GetBytes(response, "data.token").String())
	if !base.IsBelow110 {
		go signCallback(uin, gjson.GetBytes(response, "data.requestCallback").Array(), "sign")
	}
	return sign, extra, token, nil
}

var registerLock sync.Mutex

func signRegister(uin int64, androidID, guid []byte, qimei36, key string) {
	if base.IsBelow110 {
		log.Warn("签名服务器版本低于1.1.0, 跳过实例注册")
		return
	}
	signServer, resp, err := requestSignServer(
		http.MethodGet,
		"register"+fmt.Sprintf("?uin=%v&android_id=%v&guid=%v&qimei36=%v&key=%s",
			uin, utils.B2S(androidID), hex.EncodeToString(guid), qimei36, key),
		nil, nil,
	)
	if err != nil {
		log.Warnf("注册QQ实例时出现错误: %v. server: %v", err, signServer)
		return
	}
	msg := gjson.GetBytes(resp, "msg")
	if gjson.GetBytes(resp, "code").Int() != 0 {
		log.Warnf("注册QQ实例时出现错误: %v. server: %v", msg, signServer)
		return
	}
	log.Infof("注册QQ实例 %v 成功: %v", uin, msg)
}

func signRefreshToken(uin string) error {
	log.Info("正在刷新 token")
	_, resp, err := requestSignServer(
		http.MethodGet,
		"request_token?uin="+uin,
		nil, nil,
	)
	if err != nil {
		return err
	}
	msg := gjson.GetBytes(resp, "msg")
	code := gjson.GetBytes(resp, "code")
	if code.Int() != 0 {
		return errors.New("code=" + code.String() + ", msg: " + msg.String())
	}
	go signCallback(uin, gjson.GetBytes(resp, "data").Array(), "request token")
	return nil
}

var missTokenCount = uint64(0)
var lastToken = ""

func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) {
	i := 0
	for {
		sign, extra, token, err = signRequset(seq, uin, cmd, qua, buff)
		cs := ss.get()
		if cs == nil {
			// 最好在请求后判断,否则若被设置为nil后不会再请求签名,
			// 导致在下一次有请求签名服务操作之前,ss无法更新
			err = errors.New("nil signserver")
			log.Warn("nil sign-server") // 返回的err并不会log出来,加条日志
			return
		}
		if err != nil {
			log.Warnf("获取sso sign时出现错误: %v. server: %v", err, cs.URL)
		}
		if i > 0 {
			break
		}
		i++
		if (!base.IsBelow110) && base.Account.AutoRegister && err == nil && len(sign) == 0 {
			if registerLock.TryLock() { // 避免并发时多处同时销毁并重新注册
				log.Debugf("请求签名:cmd=%v, qua=%v, buff=%v", seq, cmd, hex.EncodeToString(buff))
				log.Debugf("返回结果:sign=%v, extra=%v, token=%v",
					hex.EncodeToString(sign), hex.EncodeToString(extra), hex.EncodeToString(token))
				log.Warn("获取签名为空,实例可能丢失,正在尝试重新注册")
				defer registerLock.Unlock()
				err := signServerDestroy(uin)
				if err != nil {
					log.Warnln(err) // 实例真的丢失时则必出错,或许应该不 return , 以重新获取本次签名
					// return nil, nil, nil, err
				}
				signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, cs.Key)
			}
			continue
		}
		if (!base.IsBelow110) && base.Account.AutoRefreshToken && len(token) == 0 {
			log.Warnf("token 已过期, 总丢失 token 次数为 %v", atomic.AddUint64(&missTokenCount, 1))
			if registerLock.TryLock() {
				defer registerLock.Unlock()
				if err := signRefreshToken(uin); err != nil {
					log.Warnf("刷新 token 出现错误: %v. server: %v", err, cs.URL)
				} else {
					log.Info("刷新 token 成功")
				}
			}
			continue
		}
		break
	}
	if tokenString := hex.EncodeToString(token); lastToken != tokenString {
		log.Infof("token 已更新:%v -> %v", lastToken, tokenString)
		lastToken = tokenString
	}
	rule := base.Account.RuleChangeSignServer
	if (len(sign) == 0 && rule >= 1) || (len(token) == 0 && rule >= 2) {
		ss.set(nil)
	}
	return sign, extra, token, err
}

func signServerDestroy(uin string) error {
	signServer, signVersion, err := signVersion()
	if err != nil {
		return errors.Wrapf(err, "获取签名服务版本出现错误, server: %v", signServer)
	}
	if global.VersionNameCompare("v"+signVersion, "v1.1.6") {
		return errors.Errorf("当前签名服务器版本 %v 低于 1.1.6,无法使用 destroy 接口", signVersion)
	}
	cs := ss.get()
	if cs == nil {
		return errors.New("nil signserver")
	}
	signServer, resp, err := requestSignServer(
		http.MethodGet,
		"destroy"+fmt.Sprintf("?uin=%v&key=%v", uin, cs.Key),
		nil, nil,
	)
	if err != nil || gjson.GetBytes(resp, "code").Int() != 0 {
		return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer)
	}
	return nil
}

func signVersion() (signServer string, version string, err error) {
	signServer, resp, err := requestSignServer(http.MethodGet, "", nil, nil)
	if err != nil {
		return signServer, "", err
	}
	if gjson.GetBytes(resp, "code").Int() == 0 {
		return signServer, gjson.GetBytes(resp, "data.version").String(), nil
	}
	return signServer, "", errors.New("empty version")
}

// 定时刷新 token, interval 为间隔时间(分钟)
func signStartRefreshToken(interval int64) {
	if interval <= 0 {
		log.Warn("定时刷新 token 已关闭")
		return
	}
	log.Infof("每 %v 分钟将刷新一次签名 token", interval)
	if interval < 10 {
		log.Warnf("间隔时间 %v 分钟较短,推荐 30~40 分钟", interval)
	}
	if interval > 60 {
		log.Warn("间隔时间不能超过 60 分钟,已自动设置为 60 分钟")
		interval = 60
	}
	t := time.NewTicker(time.Duration(interval) * time.Minute)
	qqstr := strconv.FormatInt(base.Account.Uin, 10)
	defer t.Stop()
	for range t.C {
		cs, master := ss.get(), &base.SignServers[0]
		if (cs == nil || cs.URL != master.URL) && isServerAvaliable(master.URL) {
			ss.set(master)
			log.Infof("主签名服务器可用,已切换至主签名服务器 %v", master.URL)
		}
		cs = ss.get()
		if cs == nil {
			log.Warn("无法获得可用签名服务器,停止 token 定时刷新")
			return
		}
		err := signRefreshToken(qqstr)
		if err != nil {
			log.Warnf("刷新 token 出现错误: %v. server: %v", err, cs.URL)
		}
	}
}


================================================
FILE: coolq/api.go
================================================
package coolq

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/Mrs4s/MiraiGo/binary"
	"github.com/Mrs4s/MiraiGo/client"
	"github.com/Mrs4s/MiraiGo/message"
	"github.com/Mrs4s/MiraiGo/utils"
	"github.com/segmentio/asm/base64"
	log "github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"

	"github.com/Mrs4s/go-cqhttp/db"
	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/internal/base"
	"github.com/Mrs4s/go-cqhttp/internal/cache"
	"github.com/Mrs4s/go-cqhttp/internal/download"
	"github.com/Mrs4s/go-cqhttp/internal/msg"
	"github.com/Mrs4s/go-cqhttp/internal/param"
	"github.com/Mrs4s/go-cqhttp/modules/filter"
	"github.com/Mrs4s/go-cqhttp/pkg/onebot"
)

type guildMemberPageToken struct {
	guildID        uint64
	nextIndex      uint32
	nextRoleID     uint64
	nextQueryParam string
}

var defaultPageToken = guildMemberPageToken{
	guildID:    0,
	nextIndex:  0,
	nextRoleID: 2,
}

// CQGetLoginInfo 获取登录号信息
//
// https://git.io/Jtz1I
// @route11(get_login_info)
// @route12(get_self_info)
func (bot *CQBot) CQGetLoginInfo() global.MSG {
	return OK(global.MSG{"user_id": bot.Client.Uin, "nickname": bot.Client.Nickname})
}

// CQGetQiDianAccountInfo 获取企点账号信息
// @route(qidian_get_account_info)
func (bot *CQBot) CQGetQiDianAccountInfo() global.MSG {
	if bot.Client.QiDian == nil {
		return Failed(100, "QIDIAN_PROTOCOL_REQUEST", "请使用企点协议")
	}
	return OK(global.MSG{
		"master_id":   bot.Client.QiDian.MasterUin,
		"ext_name":    bot.Client.QiDian.ExtName,
		"create_time": bot.Client.QiDian.CreateTime,
	})
}

// CQGetGuildServiceProfile 获取频道系统个人资料
// @route(get_guild_service_profile)
func (bot *CQBot) CQGetGuildServiceProfile() global.MSG {
	return OK(global.MSG{
		"nickname":   bot.Client.GuildService.Nickname,
		"tiny_id":    fU64(bot.Client.GuildService.TinyId),
		"avatar_url": bot.Client.GuildService.AvatarUrl,
	})
}

// CQGetGuildList 获取已加入的频道列表
// @route(get_guild_list)
func (bot *CQBot) CQGetGuildList() global.MSG {
	fs := make([]global.MSG, 0, len(bot.Client.GuildService.Guilds))
	for _, info := range bot.Client.GuildService.Guilds {
		/* 做成单独的 api 可能会好些?
		channels := make([]global.MSG, 0, len(info.Channels))
		for _, channel := range info.Channels {
			channels = append(channels, global.MSG{
				"channel_id":   channel.ChannelId,
				"channel_name": channel.ChannelName,
				"channel_type": channel.ChannelType,
			})
		}
		*/
		fs = append(fs, global.MSG{
			"guild_id":         fU64(info.GuildId),
			"guild_name":       info.GuildName,
			"guild_display_id": fU64(info.GuildCode),
			// "channels":         channels,
		})
	}
	return OK(fs)
}

// CQGetGuildMetaByGuest 通过访客权限获取频道元数据
// @route(get_guild_meta_by_guest)
func (bot *CQBot) CQGetGuildMetaByGuest(guildID uint64) global.MSG {
	meta, err := bot.Client.GuildService.FetchGuestGuild(guildID)
	if err != nil {
		log.Errorf("获取频道元数据时出现错误: %v", err)
		return Failed(100, "API_ERROR", err.Error())
	}
	return OK(global.MSG{
		"guild_id":         fU64(meta.GuildId),
		"guild_name":       meta.GuildName,
		"guild_profile":    meta.GuildProfile,
		"create_time":      meta.CreateTime,
		"max_member_count": meta.MaxMemberCount,
		"max_robot_count":  meta.MaxRobotCount,
		"max_admin_count":  meta.MaxAdminCount,
		"member_count":     meta.MemberCount,
		"owner_id":         fU64(meta.OwnerId),
	})
}

// CQGetGuildChannelList 获取频道列表
// @route(get_guild_channel_list)
func (bot *CQBot) CQGetGuildChannelList(guildID uint64, noCache bool) global.MSG {
	guild := bot.Client.GuildService.FindGuild(guildID)
	if guild == nil {
		return Failed(100, "GUILD_NOT_FOUND")
	}
	if noCache {
		channels, err := bot.Client.GuildService.FetchChannelList(guildID)
		if err != nil {
			log.Warnf("获取频道 %v 子频道列表时出现错误: %v", guildID, err)
			return Failed(100, "API_ERROR", err.Error())
		}
		guild.Channels = channels
	}
	channels := make([]global.MSG, 0, len(guild.Channels))
	for _, c := range guild.Channels {
		channels = append(channels, convertChannelInfo(c))
	}
	return OK(channels)
}

// CQGetGuildMembers 获取频道成员列表
// @route(get_guild_member_list)
func (bot *CQBot) CQGetGuildMembers(guildID uint64, nextToken string) global.MSG {
	guild := bot.Client.GuildService.FindGuild(guildID)
	if guild == nil {
		return Failed(100, "GUILD_NOT_FOUND")
	}
	token := &defaultPageToken
	if nextToken != "" {
		i, exists := bot.nextTokenCache.Get(nextToken)
		if !exists {
			return Failed(100, "NEXT_TOKEN_NOT_EXISTS")
		}
		token = i
		if token.guildID != guildID {
			return Failed(100, "GUILD_NOT_MATCH")
		}
	}
	ret, err := bot.Client.GuildService.FetchGuildMemberListWithRole(guildID, 0, token.nextIndex, token.nextRoleID, token.nextQueryParam)
	if err != nil {
		return Failed(100, "API_ERROR", err.Error())
	}
	res := global.MSG{
		"members":    convertGuildMemberInfo(ret.Members),
		"finished":   ret.Finished,
		"next_token": nil,
	}
	if !ret.Finished {
		next := &guildMemberPageToken{
			guildID:        guildID,
			nextIndex:      ret.NextIndex,
			nextRoleID:     ret.NextRoleId,
			nextQueryParam: ret.NextQueryParam,
		}
		id := base64.StdEncoding.EncodeToString(binary.NewWriterF(func(w *binary.Writer) {
			w.WriteUInt64(uint64(time.Now().UnixNano()))
			w.WriteString(utils.RandomString(5))
		}))
		bot.nextTokenCache.Add(id, next, time.Minute*10)
		res["next_token"] = id
	}
	return OK(res)
}

// CQGetGuildMemberProfile 获取频道成员资料
// @route(get_guild_member_profile)
func (bot *CQBot) CQGetGuildMemberProfile(guildID, userID uint64) global.MSG {
	if bot.Client.GuildService.FindGuild(guildID) == nil {
		return Failed(100, "GUILD_NOT_FOUND")
	}
	profile, err := bot.Client.GuildService.FetchGuildMemberProfileInfo(guildID, userID)
	if err != nil {
		log.Warnf("获取频道 %v 成员 %v 资料时出现错误: %v", guildID, userID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	roles := make([]global.MSG, 0, len(profile.Roles))
	for _, role := range profile.Roles {
		roles = append(roles, global.MSG{
			"role_id":   fU64(role.RoleId),
			"role_name": role.RoleName,
		})
	}
	return OK(global.MSG{
		"tiny_id":    fU64(profile.TinyId),
		"nickname":   profile.Nickname,
		"avatar_url": profile.AvatarUrl,
		"join_time":  profile.JoinTime,
		"roles":      roles,
	})
}

// CQGetGuildRoles 获取频道角色列表
// @route(get_guild_roles)
func (bot *CQBot) CQGetGuildRoles(guildID uint64) global.MSG {
	r, err := bot.Client.GuildService.GetGuildRoles(guildID)
	if err != nil {
		log.Warnf("获取频道 %v 角色列表时出现错误: %v", guildID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	roles := make([]global.MSG, len(r))
	for i, role := range r {
		roles[i] = global.MSG{
			"role_id":      fU64(role.RoleId),
			"role_name":    role.RoleName,
			"argb_color":   role.ArgbColor,
			"independent":  role.Independent,
			"member_count": role.Num,
			"max_count":    role.MaxNum,
			"owned":        role.Owned,
			"disabled":     role.Disabled,
		}
	}
	return OK(roles)
}

// CQCreateGuildRole 创建频道角色
// @route(create_guild_role)
func (bot *CQBot) CQCreateGuildRole(guildID uint64, name string, color uint32, independent bool, initialUsers gjson.Result) global.MSG {
	userSlice := []uint64{}
	if initialUsers.IsArray() {
		for _, user := range initialUsers.Array() {
			userSlice = append(userSlice, user.Uint())
		}
	}
	role, err := bot.Client.GuildService.CreateGuildRole(guildID, name, color, independent, userSlice)
	if err != nil {
		log.Warnf("创建频道 %v 角色时出现错误: %v", guildID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	return OK(global.MSG{
		"role_id": fU64(role),
	})
}

// CQDeleteGuildRole 删除频道角色
// @route(delete_guild_role)
func (bot *CQBot) CQDeleteGuildRole(guildID uint64, roleID uint64) global.MSG {
	err := bot.Client.GuildService.DeleteGuildRole(guildID, roleID)
	if err != nil {
		log.Warnf("删除频道 %v 角色时出现错误: %v", guildID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQSetGuildMemberRole 设置用户在频道中的角色
// @route(set_guild_member_role)
func (bot *CQBot) CQSetGuildMemberRole(guildID uint64, set bool, roleID uint64, users gjson.Result) global.MSG {
	userSlice := []uint64{}
	if users.IsArray() {
		for _, user := range users.Array() {
			userSlice = append(userSlice, user.Uint())
		}
	}
	err := bot.Client.GuildService.SetUserRoleInGuild(guildID, set, roleID, userSlice)
	if err != nil {
		log.Warnf("设置用户在频道 %v 中的角色时出现错误: %v", guildID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQModifyRoleInGuild 修改频道角色
// @route(update_guild_role)
func (bot *CQBot) CQModifyRoleInGuild(guildID uint64, roleID uint64, name string, color uint32, indepedent bool) global.MSG {
	err := bot.Client.GuildService.ModifyRoleInGuild(guildID, roleID, name, color, indepedent)
	if err != nil {
		log.Warnf("修改频道 %v 角色时出现错误: %v", guildID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGetTopicChannelFeeds 获取话题频道帖子列表
// @route(get_topic_channel_feeds)
func (bot *CQBot) CQGetTopicChannelFeeds(guildID, channelID uint64) global.MSG {
	guild := bot.Client.GuildService.FindGuild(guildID)
	if guild == nil {
		return Failed(100, "GUILD_NOT_FOUND")
	}
	channel := guild.FindChannel(channelID)
	if channel == nil {
		return Failed(100, "CHANNEL_NOT_FOUND")
	}
	if channel.ChannelType != client.ChannelTypeTopic {
		return Failed(100, "CHANNEL_TYPE_ERROR")
	}
	feeds, err := bot.Client.GuildService.GetTopicChannelFeeds(guildID, channelID)
	if err != nil {
		log.Warnf("获取频道 %v 帖子时出现错误: %v", channelID, err)
		return Failed(100, "API_ERROR", err.Error())
	}
	c := make([]global.MSG, 0, len(feeds))
	for _, feed := range feeds {
		c = append(c, convertChannelFeedInfo(feed))
	}
	return OK(c)
}

// CQGetFriendList 获取好友列表
//
// https://git.io/Jtz1L
// @route(get_friend_list)
func (bot *CQBot) CQGetFriendList(spec *onebot.Spec) global.MSG {
	fs := make([]global.MSG, 0, len(bot.Client.FriendList))
	for _, f := range bot.Client.FriendList {
		fs = append(fs, global.MSG{
			"nickname": f.Nickname,
			"remark":   f.Remark,
			"user_id":  spec.ConvertID(f.Uin),
		})
	}
	return OK(fs)
}

// CQGetUnidirectionalFriendList 获取单向好友列表
//
// @route(get_unidirectional_friend_list)
func (bot *CQBot) CQGetUnidirectionalFriendList() global.MSG {
	list, err := bot.Client.GetUnidirectionalFriendList()
	if err != nil {
		log.Warnf("获取单向好友列表时出现错误: %v", err)
		return Failed(100, "API_ERROR", err.Error())
	}
	fs := make([]global.MSG, 0, len(list))
	for _, f := range list {
		fs = append(fs, global.MSG{
			"nickname": f.Nickname,
			"user_id":  f.Uin,
			"source":   f.Source,
		})
	}
	return OK(fs)
}

// CQDeleteUnidirectionalFriend 删除单向好友
//
// @route(delete_unidirectional_friend)
// @rename(uin->user_id)
func (bot *CQBot) CQDeleteUnidirectionalFriend(uin int64) global.MSG {
	list, err := bot.Client.GetUnidirectionalFriendList()
	if err != nil {
		log.Warnf("获取单向好友列表时出现错误: %v", err)
		return Failed(100, "API_ERROR", err.Error())
	}
	for _, f := range list {
		if f.Uin == uin {
			if err = bot.Client.DeleteUnidirectionalFriend(uin); err != nil {
				log.Warnf("删除单向好友时出现错误: %v", err)
				return Failed(100, "API_ERROR", err.Error())
			}
			return OK(nil)
		}
	}
	return Failed(100, "FRIEND_NOT_FOUND", "好友不存在")
}

// CQDeleteFriend 删除好友
// @route(delete_friend)
// @rename(uin->"[user_id\x2Cid].0")
func (bot *CQBot) CQDeleteFriend(uin int64) global.MSG {
	if bot.Client.FindFriend(uin) == nil {
		return Failed(100, "FRIEND_NOT_FOUND", "好友不存在")
	}
	if err := bot.Client.DeleteFriend(uin); err != nil {
		log.Warnf("删除好友时出现错误: %v", err)
		return Failed(100, "DELETE_API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGetGroupList 获取群列表
//
// https://git.io/Jtz1t
// @route(get_group_list)
func (bot *CQBot) CQGetGroupList(noCache bool, spec *onebot.Spec) global.MSG {
	gs := make([]global.MSG, 0, len(bot.Client.GroupList))
	if noCache {
		_ = bot.Client.ReloadGroupList()
	}
	for _, g := range bot.Client.GroupList {
		gs = append(gs, global.MSG{
			"group_id":          spec.ConvertID(g.Code),
			"group_name":        g.Name,
			"group_create_time": g.GroupCreateTime,
			"group_level":       g.GroupLevel,
			"max_member_count":  g.MaxMemberCount,
			"member_count":      g.MemberCount,
		})
	}
	return OK(gs)
}

// CQGetGroupInfo 获取群信息
//
// https://git.io/Jtz1O
// @route(get_group_info)
func (bot *CQBot) CQGetGroupInfo(groupID int64, noCache bool, spec *onebot.Spec) global.MSG {
	group := bot.Client.FindGroup(groupID)
	if group == nil || noCache {
		group, _ = bot.Client.GetGroupInfo(groupID)
	}
	if group == nil {
		gid := strconv.FormatInt(groupID, 10)
		info, err := bot.Client.SearchGroupByKeyword(gid)
		if err != nil {
			return Failed(100, "GROUP_SEARCH_ERROR", "群聊搜索失败")
		}
		for _, g := range info {
			if g.Code == groupID {
				return OK(global.MSG{
					"group_id":          spec.ConvertID(g.Code),
					"group_name":        g.Name,
					"group_memo":        g.Memo,
					"group_create_time": 0,
					"group_level":       0,
					"max_member_count":  0,
					"member_count":      0,
				})
			}
		}
	} else {
		return OK(global.MSG{
			"group_id":          spec.ConvertID(group.Code),
			"group_name":        group.Name,
			"group_create_time": group.GroupCreateTime,
			"group_level":       group.GroupLevel,
			"max_member_count":  group.MaxMemberCount,
			"member_count":      group.MemberCount,
		})
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQGetGroupMemberList 获取群成员列表
//
// https://git.io/Jtz13
// @route(get_group_member_list)
func (bot *CQBot) CQGetGroupMemberList(groupID int64, noCache bool) global.MSG {
	group := bot.Client.FindGroup(groupID)
	if group == nil {
		return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
	}
	if noCache {
		t, err := bot.Client.GetGroupMembers(group)
		if err != nil {
			log.Warnf("刷新群 %v 成员列表失败: %v", groupID, err)
			return Failed(100, "GET_MEMBERS_API_ERROR", err.Error())
		}
		group.Members = t
	}
	members := make([]global.MSG, 0, len(group.Members))
	for _, m := range group.Members {
		members = append(members, convertGroupMemberInfo(groupID, m))
	}
	return OK(members)
}

// CQGetGroupMemberInfo 获取群成员信息
//
// https://git.io/Jtz1s
// @route(get_group_member_info)
func (bot *CQBot) CQGetGroupMemberInfo(groupID, userID int64, noCache bool) global.MSG {
	group := bot.Client.FindGroup(groupID)
	if group == nil {
		return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
	}
	var member *client.GroupMemberInfo
	if noCache {
		var err error
		member, err = bot.Client.GetMemberInfo(groupID, userID)
		if err != nil {
			log.Warnf("刷新群 %v 中成员 %v 失败: %v", groupID, userID, err)
			return Failed(100, "GET_MEMBER_INFO_API_ERROR", err.Error())
		}
	} else {
		member = group.FindMember(userID)
	}
	if member == nil {
		return Failed(100, "MEMBER_NOT_FOUND", "群员不存在")
	}
	return OK(convertGroupMemberInfo(groupID, member))
}

// CQGetGroupFileSystemInfo 扩展API-获取群文件系统信息
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF
// @route(get_group_file_system_info)
func (bot *CQBot) CQGetGroupFileSystemInfo(groupID int64) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	return OK(fs)
}

// CQGetGroupRootFiles 扩展API-获取群根目录文件列表
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%A0%B9%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
// @route(get_group_root_files)
func (bot *CQBot) CQGetGroupRootFiles(groupID int64) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	files, folders, err := fs.Root()
	if err != nil {
		log.Warnf("获取群 %v 根目录文件失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	return OK(global.MSG{
		"files":   files,
		"folders": folders,
	})
}

// CQGetGroupFilesByFolderID 扩展API-获取群子目录文件列表
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%AD%90%E7%9B%AE%E5%BD%95%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8
// @route(get_group_files_by_folder)
func (bot *CQBot) CQGetGroupFilesByFolderID(groupID int64, folderID string) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	files, folders, err := fs.GetFilesByFolder(folderID)
	if err != nil {
		log.Warnf("获取群 %v 根目录 %v 子文件失败: %v", groupID, folderID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	return OK(global.MSG{
		"files":   files,
		"folders": folders,
	})
}

// CQGetGroupFileURL 扩展API-获取群文件资源链接
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%96%87%E4%BB%B6%E8%B5%84%E6%BA%90%E9%93%BE%E6%8E%A5
// @route(get_group_file_url)
// @rename(bus_id->"[busid\x2Cbus_id].0")
func (bot *CQBot) CQGetGroupFileURL(groupID int64, fileID string, busID int32) global.MSG {
	url := bot.Client.GetGroupFileUrl(groupID, fileID, busID)
	if url == "" {
		return Failed(100, "FILE_SYSTEM_API_ERROR")
	}
	return OK(global.MSG{
		"url": url,
	})
}

// CQUploadGroupFile 扩展API-上传群文件
//
// https://docs.go-cqhttp.org/api/#%E4%B8%8A%E4%BC%A0%E7%BE%A4%E6%96%87%E4%BB%B6
// @route(upload_group_file)
func (bot *CQBot) CQUploadGroupFile(groupID int64, file, name, folder string) global.MSG {
	if !global.PathExists(file) {
		log.Warnf("上传群文件 %v 失败: 文件不存在", file)
		return Failed(100, "FILE_NOT_FOUND", "文件不存在")
	}
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	if folder == "" {
		folder = "/"
	}
	if err = fs.UploadFile(file, name, folder); err != nil {
		log.Warnf("上传群 %v 文件 %v 失败: %v", groupID, file, err)
		return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQUploadPrivateFile 扩展API-上传私聊文件
//
// @route(upload_private_file)
func (bot *CQBot) CQUploadPrivateFile(userID int64, file, name string) global.MSG {
	target := message.Source{
		SourceType: message.SourcePrivate,
		PrimaryID:  userID,
	}
	fileBody, err := os.Open(file)
	if err != nil {
		log.Warnf("上传私聊文件 %v 失败: %+v", file, err)
		return Failed(100, "OPEN_FILE_ERROR", "打开文件失败")
	}
	defer func() { _ = fileBody.Close() }()
	localFile := &client.LocalFile{
		FileName: name,
		Body:     fileBody,
	}
	if err := bot.Client.UploadFile(target, localFile); err != nil {
		log.Warnf("上传私聊 %v 文件 %v 失败: %+v", userID, file, err)
		return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGroupFileCreateFolder 拓展API-创建群文件文件夹
//
// @route(create_group_file_folder)
func (bot *CQBot) CQGroupFileCreateFolder(groupID int64, parentID, name string) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	if err = fs.CreateFolder(parentID, name); err != nil {
		log.Warnf("创建群 %v 文件夹失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGroupFileDeleteFolder 拓展API-删除群文件文件夹
//
// @route(delete_group_folder)
// @rename(id->folder_id)
func (bot *CQBot) CQGroupFileDeleteFolder(groupID int64, id string) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	if err = fs.DeleteFolder(id); err != nil {
		log.Warnf("删除群 %v 文件夹 %v 时出现文件: %v", groupID, id, err)
		return Failed(200, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGroupFileDeleteFile 拓展API-删除群文件
//
// @route(delete_group_file)
// @rename(id->file_id, bus_id->"[busid\x2Cbus_id].0")
func (bot *CQBot) CQGroupFileDeleteFile(groupID int64, id string, busID int32) global.MSG {
	fs, err := bot.Client.GetGroupFileSystem(groupID)
	if err != nil {
		log.Warnf("获取群 %v 文件系统信息失败: %v", groupID, err)
		return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
	}
	if res := fs.DeleteFile("", id, busID); res != "" {
		log.Warnf("删除群 %v 文件 %v 时出现文件: %v", groupID, id, res)
		return Failed(200, "FILE_SYSTEM_API_ERROR", res)
	}
	return OK(nil)
}

// CQGetWordSlices 隐藏API-获取中文分词
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D-%E9%9A%90%E8%97%8F-api
// @route(.get_word_slices)
func (bot *CQBot) CQGetWordSlices(content string) global.MSG {
	slices, err := bot.Client.GetWordSegmentation(content)
	if err != nil {
		return Failed(100, "WORD_SEGMENTATION_API_ERROR", err.Error())
	}
	for i := 0; i < len(slices); i++ {
		slices[i] = strings.ReplaceAll(slices[i], "\u0000", "")
	}
	return OK(global.MSG{"slices": slices})
}

// CQSendMessage 发送消息
//
// @route11(send_msg)
// @rename(m->message)
func (bot *CQBot) CQSendMessage(groupID, userID int64, m gjson.Result, messageType string, autoEscape bool) global.MSG {
	switch {
	case messageType == "group":
		return bot.CQSendGroupMessage(groupID, m, autoEscape)
	case messageType == "private":
		fallthrough
	case userID != 0:
		return bot.CQSendPrivateMessage(userID, groupID, m, autoEscape)
	case groupID != 0:
		return bot.CQSendGroupMessage(groupID, m, autoEscape)
	}
	return global.MSG{}
}

// CQSendForwardMessage 发送合并转发消息
//
// @route11(send_forward_msg)
// @rename(m->messages)
func (bot *CQBot) CQSendForwardMessage(groupID, userID int64, m gjson.Result, messageType string) global.MSG {
	switch {
	case messageType == "group":
		return bot.CQSendGroupForwardMessage(groupID, m)
	case messageType == "private":
		fallthrough
	case userID != 0:
		return bot.CQSendPrivateForwardMessage(userID, m)
	case groupID != 0:
		return bot.CQSendGroupForwardMessage(groupID, m)
	}
	return global.MSG{}
}

// CQSendGroupMessage 发送群消息
//
// https://git.io/Jtz1c
// @route11(send_group_msg)
// @rename(m->message)
func (bot *CQBot) CQSendGroupMessage(groupID int64, m gjson.Result, autoEscape bool) global.MSG {
	group := bot.Client.FindGroup(groupID)
	if group == nil {
		return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
	}
	fixAt := func(elem []message.IMessageElement) {
		for _, e := range elem {
			if at, ok := e.(*message.AtElement); ok && at.Target != 0 && at.Display == "" {
				mem := group.FindMember(at.Target)
				if mem != nil {
					at.Display = "@" + mem.DisplayName()
				} else {
					at.Display = "@" + strconv.FormatInt(at.Target, 10)
				}
			}
		}
	}

	var elem []message.IMessageElement
	if m.Type == gjson.JSON {
		elem = bot.ConvertObjectMessage(onebot.V11, m, message.SourceGroup)
	} else {
		str := m.String()
		if str == "" {
			log.Warnf("群 %v 消息发送失败: 信息为空.", groupID)
			return Failed(100, "EMPTY_MSG_ERROR", "消息为空")
		}
		if autoEscape {
			elem = []message.IMessageElement{message.NewText(str)}
		} else {
			elem = bot.ConvertStringMessage(onebot.V11, str, message.SourceGroup)
		}
	}
	fixAt(elem)
	mid, err := bot.SendGroupMessage(groupID, &message.SendingMessage{Elements: elem})
	if err != nil {
		return Failed(100, "SEND_MSG_API_ERROR", err.Error())
	}
	log.Infof("发送群 %v(%v) 的消息: %v (%v)", group.Name, groupID, limitedString(m.String()), mid)
	return OK(global.MSG{"message_id": mid})
}

// CQSendGuildChannelMessage 发送频道消息
//
// @route(send_guild_channel_msg)
// @rename(m->message)
func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.Result, autoEscape bool) global.MSG {
	guild := bot.Client.GuildService.FindGuild(guildID)
	if guild == nil {
		return Failed(100, "GUILD_NOT_FOUND", "频道不存在")
	}
	channel := guild.FindChannel(channelID)
	if channel == nil {
		return Failed(100, "CHANNEL_NOT_FOUND", "子频道不存在")
	}
	if channel.ChannelType != client.ChannelTypeText {
		log.Warnf("无法发送频道信息: 频道类型错误, 不接受文本信息")
		return Failed(100, "CHANNEL_NOT_SUPPORTED_TEXT_MSG", "子频道类型错误, 无法发送文本信息")
	}
	fixAt := func(elem []message.IMessageElement) {
		for _, e := range elem {
			if at, ok := e.(*message.AtElement); ok && at.Target != 0 && at.Display == "" {
				mem, _ := bot.Client.GuildService.FetchGuildMemberProfileInfo(guildID, uint64(at.Target))
				if mem != nil {
					at.Display = "@" + mem.Nickname
				} else {
					at.Display = "@" + strconv.FormatInt(at.Target, 10)
				}
			}
		}
	}

	var elem []message.IMessageElement
	if m.Type == gjson.JSON {
		elem = bot.ConvertObjectMessage(onebot.V11, m, message.SourceGuildChannel)
	} else {
		str := m.String()
		if str == "" {
			log.Warn("频道发送失败: 信息为空.")
			return Failed(100, "EMPTY_MSG_ERROR", "消息为空")
		}
		if autoEscape {
			elem = []message.IMessageElement{message.NewText(str)}
		} else {
			elem = bot.ConvertStringMessage(onebot.V11, str, message.SourceGuildChannel)
		}
	}
	fixAt(elem)
	mid := bot.SendGuildChannelMessage(guildID, channelID, &message.SendingMessage{Elements: elem})
	if mid == "" {
		return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出")
	}
	log.Infof("发送频道 %v(%v) 子频道 %v(%v) 的消息: %v (%v)", guild.GuildName, guild.GuildId, channel.ChannelName, channel.ChannelId, limitedString(m.String()), mid)
	return OK(global.MSG{"message_id": mid})
}

func (bot *CQBot) uploadForwardElement(m gjson.Result, target int64, sourceType message.SourceType) *message.ForwardElement {
	ts := time.Now().Add(-time.Minute * 5)
	groupID := target
	source := message.Source{SourceType: sourceType, PrimaryID: target}
	if sourceType == message.SourcePrivate {
		// ios 设备的合并转发来源群号不能为 0
		if len(bot.Client.GroupList) == 0 {
			groupID = 1
		} else {
			groupID = bot.Client.GroupList[0].Uin
		}
	}
	builder := bot.Client.NewForwardMessageBuilder(groupID)

	var convertMessage func(m gjson.Result) *message.ForwardMessage
	convertMessage = func(m gjson.Result) *message.ForwardMessage {
		fm := message.NewForwardMessage()
		var w worker
		resolveElement := func(elems []message.IMessageElement) []message.IMessageElement {
			for i, elem := range elems {
				p := &elems[i]
				switch o := elem.(type) {
				case *msg.LocalVideo:
					w.do(func() {
						gm, err := bot.uploadLocalVideo(source, o)
						if err != nil {
							log.Warnf(uploadFailedTemplate, "合并转发", target, "视频", err)
						} else {
							*p = gm
						}
					})
				case *msg.LocalImage:
					w.do(func() {
						gm, err := bot.uploadLocalImage(source, o)
						if err != nil {
							log.Warnf(uploadFailedTemplate, "合并转发", target, "图片", err)
						} else {
							*p = gm
						}
					})
				}
			}
			return elems
		}

		convert := func(e gjson.Result) *message.ForwardNode {
			if e.Get("type").Str != "node" {
				return nil
			}
			if e.Get("data.id").Exists() {
				i := e.Get("data.id").Int()
				m, _ := db.GetMessageByGlobalID(int32(i))
				if m != nil {
					mSource := message.SourcePrivate
					if m.GetType() == "group" {
						mSource = message.SourceGroup
					}
					msgTime := m.GetAttribute().Timestamp
					if msgTime == 0 {
						msgTime = ts.Unix()
					}
					return &message.ForwardNode{
						SenderId:   m.GetAttribute().SenderUin,
						SenderName: m.GetAttribute().SenderName,
						Time:       int32(msgTime),
						Message:    resolveElement(bot.ConvertContentMessage(m.GetContent(), mSource, false)),
					}
				}
				log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str)
				return nil
			}
			uin := e.Get("data.[user_id,uin].0").Int()
			msgTime := e.Get("data.time").Int()
			if msgTime == 0 {
				msgTime = ts.Unix()
			}
			name := e.Get("data.[name,nickname].0").Str
			c := e.Get("data.content")
			if c.IsArray() {
				nested := false
				c.ForEach(func(_, value gjson.Result) bool {
					if value.Get("type").Str == "node" {
						nested = true
						return false
					}
					return true
				})
				if nested { // 处理嵌套
					nestedNode := builder.NestedNode()
					builder.Link(nestedNode, convertMessage(c))
					return &message.ForwardNode{
						SenderId:   uin,
						SenderName: name,
						Time:       int32(msgTime),
						Message:    []message.IMessageElement{nestedNode},
					}
				}
			}
			content := bot.ConvertObjectMessage(onebot.V11, c, sourceType)
			if uin != 0 && name != "" && len(content) > 0 {
				return &message.ForwardNode{
					SenderId:   uin,
					SenderName: name,
					Time:       int32(msgTime),
					Message:    resolveElement(content),
				}
			}
			log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content))
			return nil
		}

		if m.IsArray() {
			for _, item := range m.Array() {
				node := convert(item)
				if node != nil {
					fm.AddNode(node)
				}
			}
		} else {
			node := convert(m)
			if node != nil {
				fm.AddNode(node)
			}
		}

		w.wait()
		return fm
	}
	return builder.Main(convertMessage(m))
}

// CQSendGroupForwardMessage 扩展API-发送合并转发(群)
//
// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4
// @route11(send_group_forward_msg)
// @rename(m->messages)
func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) global.MSG {
	if m.Type != gjson.JSON {
		return Failed(100)
	}
	source := message.Source{
		SourceType: message.SourcePrivate,
		PrimaryID:  0,
	}
	fe := bot.uploadForwardElement(m, groupID, message.SourceGroup)
	if fe == nil {
		return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息")
	}
	ret := bot.Client.SendGroupForwardMessage(groupID, fe)
	if ret == nil || ret.Id == -1 {
		log.Warnf("合并转发(群 %v)消息发送失败: 账号可能被风控.", groupID)
		return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出")
	}
	mid := bot.InsertGroupMessage(ret, source)
	log.Infof("发送群 %v(%v)  的合并转发消息: %v (%v)", groupID, groupID, limitedString(m.String()), mid)
	return OK(global.MSG{
		"message_id": mid,
		"forward_id": fe.ResId,
	})
}

// CQSendPrivateForwardMessage 扩展API-发送合并转发(好友)
//
// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4
// @route11(send_private_forward_msg)
// @rename(m->messages)
func (bot *CQBot) CQSendPrivateForwardMessage(userID int64, m gjson.Result) global.MSG {
	if m.Type != gjson.JSON {
		return Failed(100)
	}
	fe := bot.uploadForwardElement(m, userID, message.SourcePrivate)
	if fe == nil {
		return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息")
	}
	mid := bot.SendPrivateMessage(userID, 0, &message.SendingMessage{Elements: []message.IMessageElement{fe}})
	if mid == -1 {
		log.Warnf("合并转发(好友 %v)消息发送失败: 账号可能被风控.", userID)
		return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出")
	}
	log.Infof("发送好友 %v(%v)  的合并转发消息: %v (%v)", userID, userID, limitedString(m.String()), mid)
	return OK(global.MSG{
		"message_id": mid,
		"forward_id": fe.ResId,
	})
}

// CQSendPrivateMessage 发送私聊消息
//
// https://git.io/Jtz1l
// @route11(send_private_msg)
// @rename(m->message)
func (bot *CQBot) CQSendPrivateMessage(userID int64, groupID int64, m gjson.Result, autoEscape bool) global.MSG {
	var elem []message.IMessageElement
	if m.Type == gjson.JSON {
		elem = bot.ConvertObjectMessage(onebot.V11, m, message.SourcePrivate)
	} else {
		str := m.String()
		if str == "" {
			return Failed(100, "EMPTY_MSG_ERROR", "消息为空")
		}
		if autoEscape {
			elem = []message.IMessageElement{message.NewText(str)}
		} else {
			elem = bot.ConvertStringMessage(onebot.V11, str, message.SourcePrivate)
		}
	}
	mid := bot.SendPrivateMessage(userID, groupID, &message.SendingMessage{Elements: elem})
	if mid == -1 {
		return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出")
	}
	log.Infof("发送好友 %v(%v)  的消息: %v (%v)", userID, userID, limitedString(m.String()), mid)
	return OK(global.MSG{"message_id": mid})
}

// CQSetGroupCard 设置群名片(群备注)
//
// https://git.io/Jtz1B
// @route(set_group_card)
func (bot *CQBot) CQSetGroupCard(groupID, userID int64, card string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		if m := g.FindMember(userID); m != nil {
			m.EditCard(card)
			return OK(nil)
		}
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupSpecialTitle 设置群组专属头衔
//
// https://git.io/Jtz10
// @route(set_group_special_title)
// @rename(title->special_title)
func (bot *CQBot) CQSetGroupSpecialTitle(groupID, userID int64, title string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		if m := g.FindMember(userID); m != nil {
			m.EditSpecialTitle(title)
			return OK(nil)
		}
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupName 设置群名
//
// https://git.io/Jtz12
// @route(set_group_name)
// @rename(name->group_name)
func (bot *CQBot) CQSetGroupName(groupID int64, name string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		g.UpdateName(name)
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQGetGroupMemo 扩展API-获取群公告
// @route(_get_group_notice)
func (bot *CQBot) CQGetGroupMemo(groupID int64) global.MSG {
	r, err := bot.Client.GetGroupNotice(groupID)
	if err != nil {
		return Failed(100, "获取群公告失败", err.Error())
	}

	return OK(r)
}

// CQSetGroupMemo 扩展API-发送群公告
//
// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E7%BE%A4%E5%85%AC%E5%91%8A
// @route(_send_group_notice)
// @rename(msg->content, img->image)
func (bot *CQBot) CQSetGroupMemo(groupID int64, msg, img string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		if g.SelfPermission() == client.Member {
			return Failed(100, "PERMISSION_DENIED", "权限不足")
		}
		if img != "" {
			data, err := global.FindFile(img, "", global.ImagePath)
			if err != nil {
				return Failed(100, "IMAGE_NOT_FOUND", "图片未找到")
			}
			noticeID, err := bot.Client.AddGroupNoticeWithPic(groupID, msg, data)
			if err != nil {
				return Failed(100, "SEND_NOTICE_ERROR", err.Error())
			}
			return OK(global.MSG{"notice_id": noticeID})
		}
		noticeID, err := bot.Client.AddGroupNoticeSimple(groupID, msg)
		if err != nil {
			return Failed(100, "SEND_NOTICE_ERROR", err.Error())
		}
		return OK(global.MSG{"notice_id": noticeID})
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQDelGroupMemo 扩展API-删除群公告
// @route(_del_group_notice)
// @rename(fid->notice_id)
func (bot *CQBot) CQDelGroupMemo(groupID int64, fid string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		if g.SelfPermission() == client.Member {
			return Failed(100, "PERMISSION_DENIED", "权限不足")
		}
		err := bot.Client.DelGroupNotice(groupID, fid)
		if err != nil {
			return Failed(100, "DELETE_NOTICE_ERROR", err.Error())
		}
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupKick 群组踢人
//
// https://git.io/Jtz1V
// @route(set_group_kick)
// @rename(msg->message, block->reject_add_request)
func (bot *CQBot) CQSetGroupKick(groupID int64, userID int64, msg string, block bool) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		m := g.FindMember(userID)
		if m == nil {
			return Failed(100, "MEMBER_NOT_FOUND", "人员不存在")
		}
		err := m.Kick(msg, block)
		if err != nil {
			return Failed(100, "NOT_MANAGEABLE", "机器人权限不足")
		}
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupBan 群组单人禁言
//
// https://git.io/Jtz1w
// @route(set_group_ban)
// @default(duration=1800)
func (bot *CQBot) CQSetGroupBan(groupID, userID int64, duration uint32) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		if m := g.FindMember(userID); m != nil {
			err := m.Mute(duration)
			if err != nil {
				if duration >= 2592000 {
					return Failed(100, "DURATION_IS_NOT_IN_RANGE", "非法的禁言时长")
				}
				return Failed(100, "NOT_MANAGEABLE", "机器人权限不足")
			}
			return OK(nil)
		}
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupWholeBan 群组全员禁言
//
// https://git.io/Jtz1o
// @route(set_group_whole_ban)
// @default(enable=true)
func (bot *CQBot) CQSetGroupWholeBan(groupID int64, enable bool) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		g.MuteAll(enable)
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupLeave 退出群组
//
// https://git.io/Jtz1K
// @route(set_group_leave)
func (bot *CQBot) CQSetGroupLeave(groupID int64) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		g.Quit()
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQGetAtAllRemain 扩展API-获取群 @全体成员 剩余次数
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4-%E5%85%A8%E4%BD%93%E6%88%90%E5%91%98-%E5%89%A9%E4%BD%99%E6%AC%A1%E6%95%B0
// @route(get_group_at_all_remain)
func (bot *CQBot) CQGetAtAllRemain(groupID int64) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		i, err := bot.Client.GetAtAllRemain(groupID)
		if err != nil {
			return Failed(100, "GROUP_REMAIN_API_ERROR", err.Error())
		}
		return OK(i)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQProcessFriendRequest 处理加好友请求
//
// https://git.io/Jtz11
// @route(set_friend_add_request)
// @default(approve=true)
func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) global.MSG {
	req, ok := bot.friendReqCache.Load(flag)
	if !ok {
		return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在")
	}
	if approve {
		req.Accept()
	} else {
		req.Reject()
	}
	return OK(nil)
}

// CQProcessGroupRequest 处理加群请求/邀请
//
// https://git.io/Jtz1D
// @route(set_group_add_request)
// @rename(sub_type->"[sub_type\x2Ctype].0")
// @default(approve=true)
func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bool) global.MSG {
	msgs, err := bot.Client.GetGroupSystemMessages()
	if err != nil {
		log.Warnf("获取群系统消息失败: %v", err)
		return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error())
	}
	if subType == "add" {
		for _, req := range msgs.JoinRequests {
			if strconv.FormatInt(req.RequestId, 10) == flag {
				if req.Checked {
					log.Warnf("处理群系统消息失败: 无法操作已处理的消息.")
					return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
				}
				if approve {
					req.Accept()
				} else {
					req.Reject(false, reason)
				}
				return OK(nil)
			}
		}
	} else {
		for _, req := range msgs.InvitedRequests {
			if strconv.FormatInt(req.RequestId, 10) == flag {
				if req.Checked {
					log.Warnf("处理群系统消息失败: 无法操作已处理的消息.")
					return Failed(100, "FLAG_HAS_BEEN_CHECKED", "消息已被处理")
				}
				if approve {
					req.Accept()
				} else {
					req.Reject(false, reason)
				}
				return OK(nil)
			}
		}
	}
	log.Warnf("处理群系统消息失败: 消息 %v 不存在.", flag)
	return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在")
}

// CQDeleteMessage 撤回消息
//
// https:// git.io/Jtz1y
// @route(delete_msg)
func (bot *CQBot) CQDeleteMessage(messageID int32) global.MSG {
	msg, err := db.GetMessageByGlobalID(messageID)
	if err != nil {
		log.Warnf("撤回消息时出现错误: %v", err)
		return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
	}
	switch o := msg.(type) {
	case *db.StoredGroupMessage:
		if err = bot.Client.RecallGroupMessage(o.GroupCode, o.Attribute.MessageSeq, o.Attribute.InternalID); err != nil {
			log.Warnf("撤回 %v 失败: %v", messageID, err)
			return Failed(100, "RECALL_API_ERROR", err.Error())
		}
	case *db.StoredPrivateMessage:
		if o.Attribute.SenderUin != bot.Client.Uin {
			log.Warnf("撤回 %v 失败: 好友会话无法撤回对方消息.", messageID)
			return Failed(100, "CANNOT_RECALL_FRIEND_MSG", "无法撤回对方消息")
		}
		if err = bot.Client.RecallPrivateMessage(o.TargetUin, o.Attribute.Timestamp, o.Attribute.MessageSeq, o.Attribute.InternalID); err != nil {
			log.Warnf("撤回 %v 失败: %v", messageID, err)
			return Failed(100, "RECALL_API_ERROR", err.Error())
		}
	default:
		return Failed(100, "UNKNOWN_ERROR")
	}
	return OK(nil)
}

// CQSetGroupAdmin 群组设置管理员
//
// https://git.io/Jtz1S
// @route(set_group_admin)
// @default(enable=true)
func (bot *CQBot) CQSetGroupAdmin(groupID, userID int64, enable bool) global.MSG {
	group := bot.Client.FindGroup(groupID)
	if group == nil || group.OwnerUin != bot.Client.Uin {
		return Failed(100, "PERMISSION_DENIED", "群不存在或权限不足")
	}
	mem := group.FindMember(userID)
	if mem == nil {
		return Failed(100, "GROUP_MEMBER_NOT_FOUND", "群成员不存在")
	}
	mem.SetAdmin(enable)
	t, err := bot.Client.GetGroupMembers(group)
	if err != nil {
		log.Warnf("刷新群 %v 成员列表失败: %v", groupID, err)
		return Failed(100, "GET_MEMBERS_API_ERROR", err.Error())
	}
	group.Members = t
	return OK(nil)
}

// CQSetGroupAnonymous 群组匿名
//
// https://beautyyu.one
// @route(set_group_anonymous)
// @default(enable=true)
func (bot *CQBot) CQSetGroupAnonymous(groupID int64, enable bool) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		g.SetAnonymous(enable)
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQGetGroupHonorInfo 获取群荣誉信息
//
// https://git.io/Jtz1H
// @route(get_group_honor_info)
// @rename(t->type)
func (bot *CQBot) CQGetGroupHonorInfo(groupID int64, t string) global.MSG {
	msg := global.MSG{"group_id": groupID}
	convertMem := func(memList []client.HonorMemberInfo) (ret []global.MSG) {
		for _, mem := range memList {
			ret = append(ret, global.MSG{
				"user_id":     mem.Uin,
				"nickname":    mem.Name,
				"avatar":      mem.Avatar,
				"description": mem.Desc,
			})
		}
		return
	}
	if t == "talkative" || t == "all" {
		if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Talkative); err == nil {
			if honor.CurrentTalkative.Uin != 0 {
				msg["current_talkative"] = global.MSG{
					"user_id":   honor.CurrentTalkative.Uin,
					"nickname":  honor.CurrentTalkative.Name,
					"avatar":    honor.CurrentTalkative.Avatar,
					"day_count": honor.CurrentTalkative.DayCount,
				}
			}
			msg["talkative_list"] = convertMem(honor.TalkativeList)
		} else {
			log.Infof("获取群龙王出错:%v", err)
		}
	}

	if t == "performer" || t == "all" {
		if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Performer); err == nil {
			msg["performer_list"] = convertMem(honor.ActorList)
		} else {
			log.Infof("获取群聊之火出错:%v", err)
		}
	}

	if t == "legend" || t == "all" {
		if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Legend); err == nil {
			msg["legend_list"] = convertMem(honor.LegendList)
		} else {
			log.Infof("获取群聊炽焰出错:%v", err)
		}
	}

	if t == "strong_newbie" || t == "all" {
		if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.StrongNewbie); err == nil {
			msg["strong_newbie_list"] = convertMem(honor.StrongNewbieList)
		} else {
			log.Infof("获取冒尖小春笋出错:%v", err)
		}
	}

	if t == "emotion" || t == "all" {
		if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Emotion); err == nil {
			msg["emotion_list"] = convertMem(honor.EmotionList)
		} else {
			log.Infof("获取快乐之源出错:%v", err)
		}
	}
	return OK(msg)
}

// CQGetStrangerInfo 获取陌生人信息
//
// https://git.io/Jtz17
// @route11(get_stranger_info)
// @route12(get_user_info)
func (bot *CQBot) CQGetStrangerInfo(userID int64) global.MSG {
	info, err := bot.Client.GetSummaryInfo(userID)
	if err != nil {
		return Failed(100, "SUMMARY_API_ERROR", err.Error())
	}
	return OK(global.MSG{
		"user_id":  info.Uin,
		"nickname": info.Nickname,
		"qid":      info.Qid,
		"sex": func() string {
			if info.Sex == 1 {
				return "female"
			} else if info.Sex == 0 {
				return "male"
			}
			// unknown = 0x2
			return "unknown"
		}(),
		"sign":       info.Sign,
		"age":        info.Age,
		"level":      info.Level,
		"login_days": info.LoginDays,
		"vip_level":  info.VipLevel,
	})
}

// CQHandleQuickOperation 隐藏API-对事件执行快速操作
//
// https://git.io/Jtz15
// @route11(".handle_quick_operation")
func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) global.MSG {
	postType := context.Get("post_type").Str

	switch postType {
	case "message":
		anonymous := context.Get("anonymous")
		isAnonymous := anonymous.Type != gjson.Null
		msgType := context.Get("message_type").Str
		reply := operation.Get("reply")

		if reply.Exists() {
			autoEscape := param.EnsureBool(operation.Get("auto_escape"), false)
			at := !isAnonymous && operation.Get("at_sender").Bool() && msgType == "group"
			if at && reply.IsArray() {
				// 在 reply 数组头部插入CQ码
				replySegments := make([]global.MSG, 0)
				segments := make([]global.MSG, 0)
				segments = append(segments, global.MSG{
					"type": "at",
					"data": global.MSG{
						"qq": context.Get("sender.user_id").Int(),
					},
				})

				err := json.Unmarshal(utils.S2B(reply.Raw), &replySegments)
				if err != nil {
					log.WithError(err).Warnf("处理 at_sender 过程中发生错误")
					return Failed(-1, "处理 at_sender 过程中发生错误", err.Error())
				}

				segments = append(segments, replySegments...)

				modified, err := json.Marshal(segments)
				if err != nil {
					log.WithError(err).Warnf("处理 at_sender 过程中发生错误")
					return Failed(-1, "处理 at_sender 过程中发生错误", err.Error())
				}

				reply = gjson.Parse(utils.B2S(modified))
			} else if at && reply.Type == gjson.String {
				reply = gjson.Parse(fmt.Sprintf(
					"\"[CQ:at,qq=%d]%s\"",
					context.Get("sender.user_id").Int(),
					reply.String(),
				))
			}

			if msgType == "group" {
				bot.CQSendGroupMessage(context.Get("group_id").Int(), reply, autoEscape)
			}
			if msgType == "private" {
				bot.CQSendPrivateMessage(context.Get("user_id").Int(), context.Get("group_id").Int(), reply, autoEscape)
			}
		}
		if msgType == "group" {
			if operation.Get("delete").Bool() {
				bot.CQDeleteMessage(int32(context.Get("message_id").Int()))
			}
			if !isAnonymous && operation.Get("kick").Bool() {
				bot.CQSetGroupKick(context.Get("group_id").Int(), context.Get("user_id").Int(), "", operation.Get("reject_add_request").Bool())
			}
			if operation.Get("ban").Bool() {
				var duration uint32 = 30 * 60
				if operation.Get("ban_duration").Exists() {
					duration = uint32(operation.Get("ban_duration").Uint())
				}
				// unsupported anonymous ban yet
				if !isAnonymous {
					bot.CQSetGroupBan(context.Get("group_id").Int(), context.Get("user_id").Int(), duration)
				}
			}
		}
	case "request":
		reqType := context.Get("request_type").Str
		if operation.Get("approve").Exists() {
			if reqType == "friend" {
				bot.CQProcessFriendRequest(context.Get("flag").String(), operation.Get("approve").Bool())
			}
			if reqType == "group" {
				bot.CQProcessGroupRequest(context.Get("flag").String(), context.Get("sub_type").Str, operation.Get("reason").Str, operation.Get("approve").Bool())
			}
		}
	}
	return OK(nil)
}

// CQGetImage 获取图片(修改自OneBot)
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87%E4%BF%A1%E6%81%AF
// @route(get_image)
func (bot *CQBot) CQGetImage(file string) global.MSG {
	var b []byte
	var err error
	if strings.HasSuffix(file, ".image") {
		var f []byte
		f, err = hex.DecodeString(strings.TrimSuffix(file, ".image"))
		b = cache.Image.Get(f)
	}

	if b == nil {
		if !global.PathExists(path.Join(global.ImagePath, file)) {
			return Failed(100)
		}
		b, err = os.ReadFile(path.Join(global.ImagePath, file))
	}

	if err == nil {
		r := binary.NewReader(b)
		r.ReadBytes(16)
		msg := global.MSG{
			"size":     r.ReadInt32(),
			"filename": r.ReadString(),
			"url":      r.ReadString(),
		}
		local := path.Join(global.CachePath, file+path.Ext(msg["filename"].(string)))
		if !global.PathExists(local) {
			r := download.Request{URL: msg["url"].(string)}
			if err := r.WriteToFile(local); err != nil {
				log.Warnf("下载图片 %v 时出现错误: %v", msg["url"], err)
				return Failed(100, "DOWNLOAD_IMAGE_ERROR", err.Error())
			}
		}
		msg["file"] = local
		return OK(msg)
	}
	return Failed(100, "LOAD_FILE_ERROR", err.Error())
}

// CQDownloadFile 扩展API-下载文件到缓存目录
//
// https://docs.go-cqhttp.org/api/#%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6%E5%88%B0%E7%BC%93%E5%AD%98%E7%9B%AE%E5%BD%95
// @route(download_file)
func (bot *CQBot) CQDownloadFile(url string, headers gjson.Result, threadCount int) global.MSG {
	h := map[string]string{}
	if headers.IsArray() {
		for _, sub := range headers.Array() {
			first, second, ok := strings.Cut(sub.String(), "=")
			if ok {
				h[first] = second
			}
		}
	}
	if headers.Type == gjson.String {
		lines := strings.Split(headers.String(), "\r\n")
		for _, sub := range lines {
			first, second, ok := strings.Cut(sub, "=")
			if ok {
				h[first] = second
			}
		}
	}

	hash := md5.Sum([]byte(url))
	file := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
	if global.PathExists(file) {
		if err := os.Remove(file); err != nil {
			log.Warnf("删除缓存文件 %v 时出现错误: %v", file, err)
			return Failed(100, "DELETE_FILE_ERROR", err.Error())
		}
	}
	r := download.Request{URL: url, Header: h}
	if err := r.WriteToFileMultiThreading(file, threadCount); err != nil {
		log.Warnf("下载链接 %v 时出现错误: %v", url, err)
		return Failed(100, "DOWNLOAD_FILE_ERROR", err.Error())
	}
	abs, _ := filepath.Abs(file)
	return OK(global.MSG{
		"file": abs,
	})
}

// CQGetForwardMessage 获取合并转发消息
//
// https://git.io/Jtz1F
// @route(get_forward_msg)
// @rename(res_id->"[message_id\x2Cid].0")
func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG {
	m := bot.Client.GetForwardMessage(resID)
	if m == nil {
		return Failed(100, "MSG_NOT_FOUND", "消息不存在")
	}

	var transformNodes func(nodes []*message.ForwardNode) []global.MSG
	transformNodes = func(nodes []*message.ForwardNode) []global.MSG {
		r := make([]global.MSG, len(nodes))
		for i, n := range nodes {
			bot.checkMedia(n.Message, 0)
			content := ToFormattedMessage(n.Message, message.Source{SourceType: message.SourceGroup})
			if len(n.Message) == 1 {
				if forward, ok := n.Message[0].(*message.ForwardMessage); ok {
					content = transformNodes(forward.Nodes)
				}
			}
			r[i] = global.MSG{
				"sender": global.MSG{
					"user_id":  n.SenderId,
					"nickname": n.SenderName,
				},
				"time":     n.Time,
				"content":  content,
				"group_id": n.GroupId,
			}
		}
		return r
	}
	return OK(global.MSG{
		"messages": transformNodes(m.Nodes),
	})
}

// CQGetMessage 获取消息
//
// https://git.io/Jtz1b
// @route(get_msg)
func (bot *CQBot) CQGetMessage(messageID int32) global.MSG {
	msg, err := db.GetMessageByGlobalID(messageID)
	if err != nil {
		log.Warnf("获取消息时出现错误: %v", err)
		return Failed(100, "MSG_NOT_FOUND", "消息不存在")
	}
	m := global.MSG{
		"message_id":    msg.GetGlobalID(),
		"message_id_v2": msg.GetID(),
		"message_type":  msg.GetType(),
		"real_id":       msg.GetAttribute().MessageSeq,
		"message_seq":   msg.GetAttribute().MessageSeq,
		"group":         msg.GetType() == "group",
		"sender": global.MSG{
			"user_id":  msg.GetAttribute().SenderUin,
			"nickname": msg.GetAttribute().SenderName,
		},
		"time": msg.GetAttribute().Timestamp,
	}
	switch o := msg.(type) {
	case *db.StoredGroupMessage:
		m["group_id"] = o.GroupCode
		m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourceGroup, false), message.Source{SourceType: message.SourceGroup, PrimaryID: o.GroupCode})
	case *db.StoredPrivateMessage:
		m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourcePrivate, false), message.Source{SourceType: message.SourcePrivate})
	}
	return OK(m)
}

// CQGetGuildMessage 获取频道消息
// @route(get_guild_msg)
func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) global.MSG {
	source, seq := decodeGuildMessageID(messageID)
	if source.SourceType == 0 {
		log.Warnf("获取消息时出现错误: 无效消息ID")
		return Failed(100, "INVALID_MESSAGE_ID", "无效消息ID")
	}
	m := global.MSG{
		"message_id": messageID,
		"message_source": func() string {
			if source.SourceType == message.SourceGuildDirect {
				return "direct"
			}
			return "channel"
		}(),
		"message_seq": seq,
		"guild_id":    fU64(uint64(source.PrimaryID)),
		"reactions":   []int{},
	}
	// nolint: exhaustive
	switch source.SourceType {
	case message.SourceGuildChannel:
		m["channel_id"] = fU64(uint64(source.SecondaryID))
		if noCache {
			pull, err := bot.Client.GuildService.PullGuildChannelMessage(uint64(source.PrimaryID), uint64(source.SecondaryID), seq, seq)
			if err != nil {
				log.Warnf("获取消息时出现错误: %v", err)
				return Failed(100, "API_ERROR", err.Error())
			}
			if len(m) == 0 {
				log.Warnf("获取消息时出现错误: 消息不存在")
				return Failed(100, "MSG_NOT_FOUND", "消息不存在")
			}
			m["time"] = pull[0].Time
			m["sender"] = global.MSG{
				"user_id":  pull[0].Sender.TinyId,
				"tiny_id":  fU64(pull[0].Sender.TinyId),
				"nickname": pull[0].Sender.Nickname,
			}
			m["message"] = ToFormattedMessage(pull[0].Elements, source)
			m["reactions"] = convertReactions(pull[0].Reactions)
			bot.InsertGuildChannelMessage(pull[0])
		} else {
			channelMsgByDB, err := db.GetGuildChannelMessageByID(messageID)
			if err != nil {
				log.Warnf("获取消息时出现错误: %v", err)
				return Failed(100, "MSG_NOT_FOUND", "消息不存在")
			}
			m["time"] = channelMsgByDB.Attribute.Timestamp
			m["sender"] = global.MSG{
				"user_id":  channelMsgByDB.Attribute.SenderTinyID,
				"tiny_id":  fU64(channelMsgByDB.Attribute.SenderTinyID),
				"nickname": channelMsgByDB.Attribute.SenderName,
			}
			m["message"] = ToFormattedMessage(bot.ConvertContentMessage(channelMsgByDB.Content, message.SourceGuildChannel, false), source)
		}
	case message.SourceGuildDirect:
		// todo(mrs4s): 支持 direct 消息
		m["tiny_id"] = fU64(uint64(source.SecondaryID))
	}
	return OK(m)
}

// CQGetGroupSystemMessages 扩展API-获取群文件系统消息
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E7%B3%BB%E7%BB%9F%E6%B6%88%E6%81%AF
// @route(get_group_system_msg)
func (bot *CQBot) CQGetGroupSystemMessages() global.MSG {
	msg, err := bot.Client.GetGroupSystemMessages()
	if err != nil {
		log.Warnf("获取群系统消息失败: %v", err)
		return Failed(100, "SYSTEM_MSG_API_ERROR", err.Error())
	}
	return OK(msg)
}

// CQGetGroupMessageHistory 获取群消息历史记录
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%B6%88%E6%81%AF%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95
// @route(get_group_msg_history)
// @rename(seq->message_seq)
func (bot *CQBot) CQGetGroupMessageHistory(groupID int64, seq int64) global.MSG {
	if g := bot.Client.FindGroup(groupID); g == nil {
		return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
	}
	if seq == 0 {
		g, err := bot.Client.GetGroupInfo(groupID)
		if err != nil {
			return Failed(100, "GROUP_INFO_API_ERROR", err.Error())
		}
		seq = g.LastMsgSeq
	}
	msg, err := bot.Client.GetGroupMessages(groupID, int64(math.Max(float64(seq-19), 1)), seq)
	if err != nil {
		log.Warnf("获取群历史消息失败: %v", err)
		return Failed(100, "MESSAGES_API_ERROR", err.Error())
	}
	source := message.Source{
		SourceType: message.SourcePrivate,
		PrimaryID:  0,
	}
	ms := make([]*event, 0, len(msg))
	for _, m := range msg {
		bot.checkMedia(m.Elements, groupID)
		id := bot.InsertGroupMessage(m, source)
		t := bot.formatGroupMessage(m)
		t.Others["message_id"] = id
		ms = append(ms, t)
	}
	return OK(global.MSG{
		"messages": ms,
	})
}

// CQGetOnlineClients 扩展API-获取当前账号在线客户端列表
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E8%B4%A6%E5%8F%B7%E5%9C%A8%E7%BA%BF%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%88%97%E8%A1%A8
// @route(get_online_clients)
func (bot *CQBot) CQGetOnlineClients(noCache bool) global.MSG {
	if noCache {
		if err := bot.Client.RefreshStatus(); err != nil {
			log.Warnf("刷新客户端状态时出现问题 %v", err)
			return Failed(100, "REFRESH_STATUS_ERROR", err.Error())
		}
	}
	d := make([]global.MSG, 0, len(bot.Client.OnlineClients))
	for _, oc := range bot.Client.OnlineClients {
		d = append(d, global.MSG{
			"app_id":      oc.AppId,
			"device_name": oc.DeviceName,
			"device_kind": oc.DeviceKind,
		})
	}
	return OK(global.MSG{
		"clients": d,
	})
}

// CQCanSendImage 检查是否可以发送图片(此处永远返回true)
//
// https://git.io/Jtz1N
// @route11(can_send_image)
func (bot *CQBot) CQCanSendImage() global.MSG {
	return OK(global.MSG{"yes": true})
}

// CQCanSendRecord 检查是否可以发送语音(此处永远返回true)
//
// https://git.io/Jtz1x
// @route11(can_send_record)
func (bot *CQBot) CQCanSendRecord() global.MSG {
	return OK(global.MSG{"yes": true})
}

// CQOcrImage 扩展API-图片OCR
//
// https://docs.go-cqhttp.org/api/#%E5%9B%BE%E7%89%87-ocr
// @route(ocr_image,".ocr_image")
// @rename(image_id->image)
func (bot *CQBot) CQOcrImage(imageID string) global.MSG {
	// TODO: fix this
	var elem msg.Element
	elem.Type = "image"
	elem.Data = []msg.Pair{{K: "file", V: imageID}}
	img, err := bot.makeImageOrVideoElem(elem, false, message.SourceGroup)
	if err != nil {
		log.Warnf("load image error: %v", err)
		return Failed(100, "LOAD_FILE_ERROR", err.Error())
	}
	rsp, err := bot.Client.ImageOcr(img)
	if err != nil {
		log.Warnf("ocr image error: %v", err)
		return Failed(100, "OCR_API_ERROR", err.Error())
	}
	return OK(rsp)
}

// CQSetGroupPortrait 扩展API-设置群头像
//
// https://docs.go-cqhttp.org/api/#%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%A4%B4%E5%83%8F
// @route(set_group_portrait)
func (bot *CQBot) CQSetGroupPortrait(groupID int64, file, cache string) global.MSG {
	if g := bot.Client.FindGroup(groupID); g != nil {
		img, err := global.FindFile(file, cache, global.ImagePath)
		if err != nil {
			log.Warnf("set group portrait error: %v", err)
			return Failed(100, "LOAD_FILE_ERROR", err.Error())
		}
		g.UpdateGroupHeadPortrait(img)
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQSetGroupAnonymousBan 群组匿名用户禁言
//
// https://git.io/Jtz1p
// @route(set_group_anonymous_ban)
// @rename(flag->"[anonymous_flag\x2Canonymous.flag].0")
func (bot *CQBot) CQSetGroupAnonymousBan(groupID int64, flag string, duration int32) global.MSG {
	if flag == "" {
		return Failed(100, "INVALID_FLAG", "无效的flag")
	}
	if g := bot.Client.FindGroup(groupID); g != nil {
		id, nick, ok := strings.Cut(flag, "|")
		if !ok {
			return Failed(100, "INVALID_FLAG", "无效的flag")
		}
		if err := g.MuteAnonymous(id, nick, duration); err != nil {
			log.Warnf("anonymous ban error: %v", err)
			return Failed(100, "CALL_API_ERROR", err.Error())
		}
		return OK(nil)
	}
	return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}

// CQGetStatus 获取运行状态
//
// https://git.io/JtzMe
// @route(get_status)
func (bot *CQBot) CQGetStatus(spec *onebot.Spec) global.MSG {
	if spec.Version == 11 {
		return OK(global.MSG{
			"app_initialized": true,
			"app_enabled":     true,
			"plugins_good":    nil,
			"app_good":        true,
			"online":          bot.Client.Online.Load(),
			"good":            bot.Client.Online.Load(),
			"stat":            bot.Client.GetStatistics(),
		})
	}
	return OK(global.MSG{
		"online": bot.Client.Online.Load(),
		"good":   bot.Client.Online.Load(),
		"stat":   bot.Client.GetStatistics(),
	})
}

// CQSetEssenceMessage 扩展API-设置精华消息
//
// https://docs.go-cqhttp.org/api/#%E8%AE%BE%E7%BD%AE%E7%B2%BE%E5%8D%8E%E6%B6%88%E6%81%AF
// @route(set_essence_msg)
func (bot *CQBot) CQSetEssenceMessage(messageID int32) global.MSG {
	msg, err := db.GetGroupMessageByGlobalID(messageID)
	if err != nil {
		return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
	}
	if err := bot.Client.SetEssenceMessage(msg.GroupCode, msg.Attribute.MessageSeq, msg.Attribute.InternalID); err != nil {
		log.Warnf("设置精华消息 %v 失败: %v", messageID, err)
		return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error())
	}
	return OK(nil)
}

// CQDeleteEssenceMessage 扩展API-移出精华消息
//
// https://docs.go-cqhttp.org/api/#%E7%A7%BB%E5%87%BA%E7%B2%BE%E5%8D%8E%E6%B6%88%E6%81%AF
// @route(delete_essence_msg)
func (bot *CQBot) CQDeleteEssenceMessage(messageID int32) global.MSG {
	msg, err := db.GetGroupMessageByGlobalID(messageID)
	if err != nil {
		return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
	}
	if err := bot.Client.DeleteEssenceMessage(msg.GroupCode, msg.Attribute.MessageSeq, msg.Attribute.InternalID); err != nil {
		log.Warnf("删除精华消息 %v 失败: %v", messageID, err)
		return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error())
	}
	return OK(nil)
}

// CQGetEssenceMessageList 扩展API-获取精华消息列表
//
// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96%E7%B2%BE%E5%8D%8E%E6%B6%88%E6%81%AF%E5%88%97%E8%A1%A8
// @route(get_essence_msg_list)
func (bot *CQBot) CQGetEssenceMessageList(groupID int64) global.MSG {
	g := bot.Client.FindGroup(groupID)
	if g == nil {
		return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
	}
	msgList, err := bot.Client.GetGroupEssenceMsgList(groupID)
	if err != nil {
		return Failed(100, "GET_ESSENCE_LIST_FOUND", err.Error())
	}
	list := make([]global.MSG, 0, len(msgList))
	for _, m := range msgList {
		msg := global.MSG{
			"sender_nick":   m.SenderNick,
			"sender_time":   m.SenderTime,
			"operator_time": m.AddDigestTime,
			"operator_nick": m.AddDigestNick,
			"sender_id":     m.SenderUin,
			"operator_id":   m.AddDigestUin,
		}
		msg["message_id"] = db.ToGlobalID(groupID, int32(m.MessageID))
		list = append(list, msg)
	}
	return OK(list)
}

// CQCheckURLSafely 扩展API-检查链接安全性
//
// https://docs.go-cqhttp.org/api/#%E6%A3%80%E6%9F%A5%E9%93%BE%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7
// @route(check_url_safely)
func (bot *CQBot) CQCheckURLSafely(url string) global.MSG {
	return OK(global.MSG{
		"level": bot.Client.CheckUrlSafely(url),
	})
}

// CQGetVersionInfo 获取版本信息
//
// https://git.io/JtwUs
// @route11(get_version_info)
func (bot *CQBot) CQGetVersionInfo() global.MSG {
	wd, _ := os.Getwd()
	return OK(global.MSG{
		"app_name":                   "go-cqhttp",
		"app_version":                base.Version,
		"app_full_name":              fmt.Sprintf("go-cqhttp-%s_%s_%s-%s", base.Version, runtime.GOOS, runtime.GOARCH, runtime.Version()),
		"protocol_version":           "v11",
		"coolq_directory":            wd,
		"coolq_edition":              "pro",
		"go_cqhttp":                  true,
		"plugin_version":             "4.15.0",
		"plugin_build_number":        99,
		"plugin_build_configuration": "release",
		"runtime_version":            runtime.Version(),
		"runtime_os":                 runtime.GOOS,
		"version":                    base.Version,
		"protocol_name":              bot.Client.Device().Protocol,
	})
}

// CQGetModelShow 获取在线机型
//
// https://club.vip.qq.com/onlinestatus/set
// @route(_get_model_show)
func (bot *CQBot) CQGetModelShow(model string) global.MSG {
	variants, err := bot.Client.GetModelShow(model)
	if err != nil {
		return Failed(100, "GET_MODEL_SHOW_API_ERROR", "无法获取在线机型")
	}
	a := make([]global.MSG, 0, len(variants))
	for _, v := range variants {
		a = append(a, global.MSG{
			"model_show": v.ModelShow,
			"need_pay":   v.NeedPay,
		})
	}
	return OK(global.MSG{
		"variants": a,
	})
}

// CQSendGroupSign 群打卡
//
// https://club.vip.qq.com/onlinestatus/set
// @route(send_group_sign)
func (bot *CQBot) CQSendGroupSign(groupID int64) global.MSG {
	bot.Client.SendGroupSign(groupID)
	return OK(nil)
}

// CQSetModelShow 设置在线机型
//
// https://club.vip.qq.com/onlinestatus/set
// @route(_set_model_show)
func (bot *CQBot) CQSetModelShow(model, modelShow string) global.MSG {
	err := bot.Client.SetModelShow(model, modelShow)
	if err != nil {
		return Failed(100, "SET_MODEL_SHOW_API_ERROR", "无法设置在线机型")
	}
	return OK(nil)
}

// CQMarkMessageAsRead 标记消息已读
// @route(mark_msg_as_read)
// @rename(msg_id->message_id)
func (bot *CQBot) CQMarkMessageAsRead(msgID int32) global.MSG {
	m, err := db.GetMessageByGlobalID(msgID)
	if err != nil {
		return Failed(100, "MSG_NOT_FOUND", "消息不存在")
	}
	switch o := m.(type) {
	case *db.StoredGroupMessage:
		bot.Client.MarkGroupMessageReaded(o.GroupCode, int64(o.Attribute.MessageSeq))
		return OK(nil)
	case *db.StoredPrivateMessage:
		bot.Client.MarkPrivateMessageReaded(o.SessionUin, o.Attribute.Timestamp)
	}
	return OK(nil)
}

// CQSetQQProfile 设置 QQ 资料
//
// @route(set_qq_profile)
func (bot *CQBot) CQSetQQProfile(nickname, company, email, college, personalNote gjson.Result) global.MSG {
	u := client.NewProfileDetailUpdate()

	fi := func(f gjson.Result, do func(value string) client.ProfileDetailUpdate) {
		if f.Exists() {
			do(f.String())
		}
	}

	fi(nickname, u.Nick)
	fi(company, u.Company)
	fi(email, u.Email)
	fi(college, u.College)
	fi(personalNote, u.PersonalNote)
	bot.Client.UpdateProfile(u)
	return OK(nil)
}

// CQReloadEventFilter 重载事件过滤器
//
// @route(reload_event_filter)
func (bot *CQBot) CQReloadEventFilter(file string) global.MSG {
	filter.Add(file)
	return OK(nil)
}

// CQGetSupportedActions 获取支持的动作列表
//
// @route(get_supported_actions)
func (bot *CQBot) CQGetSupportedActions(spec *onebot.Spec) global.MSG {
	return OK(spec.SupportedActions)
}

// OK 生成成功返回值
func OK(data any) global.MSG {
	return global.MSG{"data": data, "retcode": 0, "status": "ok", "message": ""}
}

// Failed 生成失败返回值
func Failed(code int, msg ...string) global.MSG {
	m, w := "", ""
	if len(msg) > 0 {
		m = msg[0]
	}
	if len(msg) > 1 {
		w = msg[1]
	}
	return global.MSG{"data": nil, "retcode": code, "msg": m, "wording": w, "message": w, "status": "failed"}
}

func limitedString(str string) string {
	limited := [14]rune{10: ' ', 11: '.', 12: '.', 13: '.'}
	i := 0
	for _, r := range str {
		if i >= 10 {
			break
		}
		limited[i] = r
		i++
	}
	if i != 10 {
		return str
	}
	return string(limited[:])
}


================================================
FILE: coolq/api_v12.go
================================================
package coolq

import (
	"runtime"

	"github.com/tidwall/gjson"

	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/internal/base"
)

// CQGetVersion 获取版本信息 OneBotV12
//
// https://git.io/JtwUs
// @route12(get_version)
func (bot *CQBot) CQGetVersion() global.MSG {
	return OK(global.MSG{
		"impl":            "go_cqhttp",
		"platform":        "qq",
		"version":         base.Version,
		"onebot_version":  12,
		"runtime_version": runtime.Version(),
		"runtime_os":      runtime.GOOS,
	})
}

// CQSendMessageV12 发送消息
//
// @route12(send_message)
// @rename(m->message)
func (bot *CQBot) CQSendMessageV12(groupID, userID, detailType string, m gjson.Result) global.MSG { // nolint
	// TODO: implement
	return OK(nil)
}


================================================
FILE: coolq/bot.go
================================================
package coolq

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"image/png"
	"os"
	"runtime/debug"
	"strings"
	"sync"
	"time"

	"github.com/Mrs4s/MiraiGo/binary"
	"github.com/Mrs4s/MiraiGo/client"
	"github.com/Mrs4s/MiraiGo/message"
	"github.com/Mrs4s/MiraiGo/utils"
	"github.com/RomiChan/syncx"
	"github.com/pkg/errors"
	"github.com/segmentio/asm/base64"
	log "github.com/sirupsen/logrus"
	"golang.org/x/image/webp"

	"github.com/Mrs4s/go-cqhttp/db"
	"github.com/Mrs4s/go-cqhttp/global"
	"github.com/Mrs4s/go-cqhttp/internal/base"
	"github.com/Mrs4s/go-cqhttp/internal/mime"
	"github.com/Mrs4s/go-cqhttp/internal/msg"
	"github.com/Mrs4s/go-cqhttp/pkg/onebot"
)

// CQBot CQBot结构体,存储Bot实例相关配置
type CQBot struct {
	Client *client.QQClient

	lock   sync.RWMutex
	events []func(*Event)

	friendReqCache   syncx.Map[string, *client.NewFriendRequest]
	tempSessionCache syncx.Map[int64, *client.TempSessionInfo]
	nextTokenCache   *utils.Cache[*guildMemberPageToken]
}

// Event 事件
type Event struct {
	once   sync.Once
	Raw    *event
	buffer *bytes.Buffer
}

func (e *Event) marshal() {
	if e.buffer == nil {
		e.buffer = global.NewBuffer()
	}
	_ = json.NewEncoder(e.buffer).Encode(e.Raw)
}

// JSONBytes return byes of json by lazy marshalling.
func (e *Event) JSONBytes() []byte {
	e.once.Do(e.marshal)
	return e.buffer.Bytes()
}

// JSONString return string of json without extra allocation
// by lazy marshalling.
func (e *Event) JSONString() string {
	e.once.Do(e.marshal)
	return utils.B2S(e.buffer.Bytes())
}

// NewQQBot 初始化一个QQBot实例
func NewQQBot(cli *client.QQClient) *CQBot {
	bot := &CQBot{
		Client:         cli,
		nextTokenCache: utils.NewCache[*guildMemberPageToken](time.Second * 10),
	}
	bot.Client.PrivateMessageEvent.Subscribe(bot.privateMessageEvent)
	bot.Client.GroupMessageEvent.Subscribe(bot.groupMessageEvent)
	if base.ReportSelfMessage {
		bot.Client.SelfPrivateMessageEvent.Subscribe(bot.privateMessageEvent)
		bot.Client.SelfGroupMessageEvent.Subscribe(bot.groupMessageEvent)
	}
	bot.Client.TempMessageEvent.Subscribe(bot.tempMessageEvent)
	bot.Client.GuildService.OnGuildChannelMessage(bot.guildChannelMessageEvent)
	bot.Client.GuildService.OnGuildMessageReactionsUpdated(bot.guildMessageReactionsUpdatedEvent)
	bot.Client.GuildService.OnGuildMessageRecalled(bot.guildChannelMessageRecalledEvent)
	bot.Client.GuildService.OnGuildChannelUpdated(bot.guildChannelUpdatedEvent)
	bot.Client.GuildService.OnGuildChannelCreated(bot.guildChannelCreatedEvent)
	bot.Client.GuildService.OnGuildChannelDestroyed(bot.guildChannelDestroyedEvent)
	bot.Client.GroupMuteEvent.Subscribe(bot.groupMutedEvent)
	bot.Client.GroupMessageRecalledEvent.Subscribe(bot.groupRecallEvent)
	bot.Client.GroupNotifyEvent.Subscribe(bot.groupNotifyEvent)
	bot.Client.FriendNotifyEvent.Subscribe(bot.friendNotifyEvent)
	bot.Client.MemberSpecialTitleUpdatedEvent.Subscribe(bot.memberTitleUpdatedEvent)
	bot.Client.FriendMessageRecalledEvent.Subscribe(bot.friendRecallEvent)
	bot.Client.OfflineFileEvent.Subscribe(bot.offlineFileEvent)
	bot.Client.GroupJoinEvent.Subscribe(bot.joinGroupEvent)
	bot.Client.GroupLeaveEvent.Subscribe(bot.leaveGroupEvent)
	bot.Client.GroupMemberJoinEvent.Subscribe(bot.memberJoinEvent)
	bot.Client.GroupMemberLeaveEvent.Subscribe(bot.memberLeaveEvent)
	bot.Client.GroupMemberPermissionChangedEvent.Subscribe(bot.memberPermissionChangedEvent)
	bot.Client.MemberCardUpdatedEvent.Subscribe(bot.memberCardUpdatedEvent)
	bot.Client.NewFriendRequestEvent.Subscribe(bot.friendRequestEvent)
	bot.Client.NewFriendEvent.Subscribe(bot.friendAddedEvent)
	bot.Client.GroupInvitedEvent.Subscribe(bot.groupInvitedEvent)
	bot.Client.UserWantJoinGroupEvent.Subscribe(bot.groupJoinReqEvent)
	bot.Client.OtherClientStatusChangedEvent.Subscribe(bot.otherClientStatusChangedEvent)
	bot.Client.GroupDigestEvent.Subscribe(bot.groupEssenceMsg)
	go func() {
		if base.HeartbeatInterval == 0 {
			log.Warn("警告: 心跳功能已关闭,若非预期,请检查配置文件。")
			return
		}
		t := time.NewTicker(base.HeartbeatInterval)
		for {
			<-t.C
			bot.dispatchEvent("meta_event/heartbeat", global.MSG{
				"status":   bot.CQGetStatus(onebot.V11)["data"],
				"interval": base.HeartbeatInterval.Milliseconds(),
			})
		}
	}()
	return bot
}

// OnEventPush 注册事件上报函数
func (bot *CQBot) OnEventPush(f func(e *Event)) {
	bot.lock.Lock()
	bot.events = append(bot.events, f)
	bot.lock.Unlock()
}

type worker struct {
	wg sync.WaitGroup
}

func (w *worker) do(f func()) {
	w.wg.Add(1)
	go func() {
		defer w.wg.Done()
		f()
	}()
}

func (w *worker) wait() {
	w.wg.Wait()
}

// uploadLocalImage 上传本地图片
func (bot *CQBot) uploadLocalImage(target message.Source, img *msg.LocalImage) (message.IMessageElement, error) {
	if img.File != "" {
		f, err := os.Open(img.File)
		if err != nil {
			return nil, errors.Wrap(err, "open image error")
		}
		defer func() { _ = f.Close() }()
		img.Stream = f
	}
	mt, ok := mime.CheckImage(img.Stream)
	if !ok {
		return nil, errors.New("image type error: " + mt)
	}
	if mt == "image/webp" && base.ConvertWebpImage {
		img0, err := webp.Decode(img.Stream)
		if err != nil {
			return nil, errors.Wrap(err, "decode webp error")
		}
		stream := bytes.NewBuffer(nil)
		err = png.Encode(stream, img0)
		if err != nil {
			return nil, errors.Wrap(err, "encode png error")
		}
		img.Stream = bytes.NewReader(stream.Bytes())
	}
	i, err := bot.Client.UploadImage(target, img.Stream)
	if err != nil {
		return nil, err
	}
	switch i := i.(type) {
	case *message.GroupImageElement:
		i.Flash = img.Flash
		i.EffectID = img.EffectID
	case *message.FriendImageElement:
		i.Flash = img.Flash
	}
	return i, err
}

// uploadLocalVideo 上传本地短视频至群聊
func (bot *CQBot) uploadLocalVideo(target message.Source, v *msg.LocalVideo) (*message.ShortVideoElement, error) {
	video, err := os.Open(v.File)
	if err != nil {
		return nil, err
	}
	defer func() { _ = video.Close() }()
	return bot.Client.UploadShortVideo(target, video, v.Thumb)
}

func removeLocalElement(elements []message.IMessageElement) []message.IMessageElement {
	var j int
	for i, e := range elements {
		switch e.(type) {
		case *msg.LocalImage, *msg.LocalVideo:
		case *message.VoiceElement: // 未上传的语音消息, 也删除
		case nil:
		default:
			if j < i {
				elements[j] = e
			}
			j++
		}
	}
	return elements[:j]
}

const uploadFailedTemplate = "警告: %s %d %s上传失败: %v"

func (bot *CQBot) uploadMedia(target message.Source, elements []message.IMessageElement) []message.IMessageElement {
	var w worker
	var source string
	switch target.SourceType { // nolint:exhaustive
	case message.SourceGroup:
		source = "群"
	case message.SourcePrivate:
		source = "私聊"
	case message.SourceGuildChannel:
		source = "频道"
	}

	for i, m := range elements {
		p := &elements[i]
		switch e := m.(type) {
		case *msg.LocalImage:
			w.do(func() {
				m, err := bot.uploadLocalImage(target, e)
				if err != nil {
					log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "图片", err)
				} else {
					*p = m
				}
			})
		case *message.VoiceElement:
			w.do(func() {
				m, err := bot.Client.UploadVoice(target, bytes.NewReader(e.Data))
				if err != nil {
					log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "语音", err)
				} else {
					*p = m
				}
			})
		case *msg.LocalVideo:
			w.do(func() {
				m, err := bot.uploadLocalVideo(target, e)
				if err != nil {
					log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "视频", err)
				} else {
					*p = m
				}
			})
		}
	}
	w.wait()
	return removeLocalElement(elements)
}

// SendGroupMessage 发送群消息
func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) (int32, error) {
	newElem := make([]message.IMessageElement, 0, len(m.Elements))
	group := bot.Client.FindGroup(groupID)
	source := message.Source{
		SourceType: message.SourceGroup,
		PrimaryID:  groupID,
	}
	m.Elements = bot.uploadMedia(source, m.Elements)
	for _, e := range m.Elements {
		switch i := e.(type) {
		case *msg.Poke:
			if group != nil {
				if mem := group.FindMember(i.Target); mem != nil {
					mem.Poke()
				}
			}
			return 0, nil
		case *message.MusicShareElement:
			ret, err := bot.Client.SendGroupMusicShare(groupID, i)
			if err != nil {
				log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupID, err)
				return -1, errors.Wrap(err, "send group music share error")
			}
			return bot.InsertGroupMessage(ret, source), nil
		case *message.AtElement:
			if i.Target == 0 && group.SelfPermission() == client.Member {
				e = message.NewText("@全体成员")
			}
		}
		newElem = append(newElem, e)
	}
	if len(newElem) == 0 {
		log.Warnf("群 %v 消息发送失败: 消息为空.", groupID)
		return -1, errors.New("empty message")
	}
	m.Elements = newElem
	bot.checkMedia(newElem, groupID)
	ret := bot.Client.SendGroupMessage(groupID, m)
	if ret == nil || ret.Id == -1 {
		log.Warnf("群 %v 发送消息失败: 账号可能被风控.", groupID)
		return -1, errors.New("send group message failed: blocked by server")
	}
	return bot.InsertGroupMessage(ret, source), nil
}

// SendPrivateMessage 发送私聊消息
func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.SendingMessage) int32 {
	newElem := make([]message.IMessageElement, 0, len(m.Elements))
	source := message.Source{
		SourceType: message.SourcePrivate,
		PrimaryID:  target,
	}
	m.Elements = bot.uploadMedia(source, m.Elements)
	for _, e := range m.Elements {
		switch i := e.(type) {
		case *msg.Poke:
			bot.Client.SendFriendPoke(i.Target)
			return 0
		case *message.MusicShareElement:
			bot.Client.SendFriendMusicShare(target, i)
			return 0
		}
		newElem = append(newElem, e)
	}
	if len(newElem) == 0 {
		log.Warnf("好友消息发送失败: 消息为空.")
		return -1
	}
	m.Elements = newElem
	bot.checkMedia(newElem, bot.Client.Uin)

	// 单向好友是否存在
	unidirectionalFriendExists := func() bool {
		list, err := bot.Client.GetUnidirectionalFriendList()
		if err != nil {
			return false
		}
		for _, f := range list {
			if f.Uin == target {
				return true
			}
		}
		return false
	}

	session, ok := bot.tempSessionCache.Load(target)
	var id int32 = -1

	switch {
	case bot.Client.FindFriend(target) != nil: // 双向好友
		msg := bot.Client.SendPrivateMessage(target, m)
		if msg != nil {
			id = bot.InsertPrivateMessage(msg, source)
		}
	case ok || groupID != 0: // 临时会话
		if !base.AllowTempSession {
			log.Warnf("发送临时会话消息失败: 已关闭临时会话信息发送功能")
			return -1
		}
		switch {
		case groupID != 0 && bot.Client.FindGroup(groupID) == nil:
			log.Errorf("错误: 找不到群(%v)", groupID)
		case groupID != 0 && !bot.Client.FindGroup(groupID).AdministratorOrOwner():
			log.Errorf("错误: 机器人在群(%v) 为非管理员或群主, 无法主动发起临时会话", groupID)
		case groupID != 0 && bot.Client.FindGroup(groupID).FindMember(target) == nil:
			log.Errorf("错误: 群员(%v) 不在 群(%v), 无法发起临时会话", target, groupID)
		default:
			if session == nil && groupID != 0 {
				msg := bot.Client.SendGroupTempMessage(groupID, target, m)
				//lint:ignore SA9003 there is a todo
				if msg != nil { // nolint
					// todo(Mrs4s)
					// id = bot.InsertTempMessage(target, msg)
				}
				break
			}
			msg, err := session.SendMessage(m)
			if err != nil {
				log.Errorf("发送临时会话消息失败: %v", err)
				break
			}
			//lint:ignore SA9003 there is a todo
			if msg != nil { // nolint
				// todo(Mrs4s)
				// id = bot.InsertTempMessage(target, msg)
			}
		}
	case unidirectionalFriendExists(): // 单向好友
		msg := bot.Client.SendPrivateMessage(target, m)
		if msg != nil {
			id = bot.InsertPrivateMessage(msg, source)
		}
	default:
		nickname := "Unknown"
		if summaryInfo, _ := bot.Client.GetSummaryInfo(target); summaryInfo != nil {
			nickname = summaryInfo.Nickname
		}
		log.Errorf("错误: 请先添加 %v(%v) 为好友", nickname, target)
	}
	return id
}

// SendGuildChannelMessage 发送频道消息
func (bot *CQBot) SendGuildChannelMessage(guildID, channelID uint64, m *message.SendingMessage) string {
	newElem := make([]message.IMessageElement, 0, len(m.Elements))
	source := message.Source{
		SourceType:  message.SourceGuildChannel,
		PrimaryID:   int64(guildID),
		SecondaryID: int64(channelID),
	}
	m.Elements = bot.uploadMedia(source, m.Elements)
	for _, e := range m.Elements {
		switch i := e.(type) {
		case *message.MusicShareElement:
			bot.Client.SendGuildMusicShare(guildID, channelID, i)
			return "-1" // todo: fix this

		case *message.VoiceElement, *msg.Poke:
			log.Warnf("警告: 频道暂不支持发送 %v 消息", i.Type().String())
			continue
		}
		newElem = append(newElem, e)
	}
	if len(newElem) == 0 {
		log.Warnf("频道消息发送失败: 消息为空.")
		return ""
	}
	m.Elements = newElem
	bot.checkMedia(newElem, bot.Client.Uin)
	ret, err := bot.Client.GuildService.SendGuildChannelMessage(guildID, channelID, m)
	if err != nil {
		log.Warnf("频道消息发送失败: %v", err)
		return ""
	}
	// todo: insert db
	return fmt.Sprintf("%v-%v", ret.Id, ret.InternalId)
}

// InsertGroupMessage 群聊消息入数据库
func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage, source message.Source) int32 {
	t := &message.SendingMessage{Elements: m.Elements}
	replyElem := t.FirstOrNil(func(e message.IMessageElement) bool {
		_, ok := e.(*message.ReplyElement)
		return ok
	})
	msg := &db.StoredGroupMessage{
		ID:       encodeMessageID(m.GroupCode, m.Id),
		GlobalID: db.ToGlobalID(m.GroupCode, m.Id),
		SubType:  "normal",
		Attribute: &db.StoredMessageAttribute{
			MessageSeq: m.Id,
			InternalID: m.InternalId,
			SenderUin:  m.Sender.Uin,
			SenderName: m.Sender.DisplayName(),
			Timestamp:  int64(m.Time),
		},
		GroupCode: m.GroupCode,
		AnonymousID: func() string {
			if m.Sender.IsAnonymous() {
				return m.Sender.AnonymousInfo.AnonymousId
			}
			return ""
		}(),
		Content: ToMessageContent(m.Elements, source),
	}
	if replyElem != nil {
		reply := replyElem.(*message.ReplyElement)
		msg.SubType = "quote"
		msg.QuotedInfo = &db.QuotedInfo{
			PrevID:        encodeMessageID(m.GroupCode, reply.ReplySeq),
			PrevGlobalID:  db.ToGlobalID(m.GroupCode, reply.ReplySeq),
			QuotedContent: ToMessageContent(reply.Elements, source),
		}
	}
	if err := db.InsertGroupMessage(msg); err != nil {
		log.Warnf("记录聊天数据时出现错误: %v", err)
		return -1
	}
	return msg.GlobalID
}

// InsertPrivateMessage 私聊消息入数据库
func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage, source message.Source) int32 {
	t := &message.SendingMessage{Elements: m.Elements}
	replyElem := t.FirstOrNil(func(e message.IMessageElement) bool {
		_, ok := e.(*message.ReplyElement)
		return ok
	})
	msg := &db.StoredPrivateMessage{
		ID:       encodeMessageID(m.Sender.Uin, m.Id),
		GlobalID: db.ToGlobalID(m.Sender.Uin, m.Id),
		SubType:  "normal",
		Attribute: &db.StoredMessageAttribute{
			MessageSeq: m.Id,
			InternalID: m.InternalId,
			SenderUin:  m.Sender.Uin,
			SenderName: m.Sender.DisplayName(),
			Timestamp:  int64(m.Time),
		},
		SessionUin: func() int64 {
			if m.Sender.Uin == m.Self {
				return m.Target
			}
			return m.Sender.Uin
		}(),
		TargetUin: m.Target,
		Content:   ToMessageContent(m.Elements, source),
	}
	if replyElem != nil {
		reply := replyElem.(*message.ReplyElement)
		msg.SubType = "quote"
		msg.QuotedInfo = &db.QuotedInfo{
			PrevID:        encodeMessageID(reply.Sender, reply.ReplySeq),
			PrevGlobalID:  db.ToGlobalID(reply.Sender, reply.ReplySeq),
			QuotedContent: ToMessageContent(reply.Elements, source),
		}
	}
	if err := db.InsertPrivateMessage(msg); err != nil {
		log.Warnf("记录聊天数据时出现错误: %v", err)
		return -1
	}
	return msg.GlobalID
}

/*
// InsertTempMessage 临时消息入数据库
func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 {
	val := global.MSG{
		"message-id": m.Id,
		// FIXME(InsertTempMessage) InternalId missing
		"from-group": m.GroupCode,
		"group-name": m.GroupName,
		"target":     target,
		"sender":     m.Sender,
		"time":       int32(time.Now().Unix()),
		"message":    ToStringMessage(m.Elements, 0, true),
	}
	id := db.ToGlobalID(m.Sender.Uin, m.Id)
	if bot.db != nil {
		buf := global.NewBuffer()
		defer global.PutBuffer(buf)
		if err := gob.NewEncoder(buf).Encode(val); err != nil {
			log.Warnf("记录聊天数据时出现错误: %v", err)
			return -1
		}
		if err := bot.db.Put(binary.ToBytes(id), buf.Bytes(), nil); err != nil {
			log.Warnf("记录聊天数据时出现错误: %v", err)
			return -1
		}
	}
	return id
}
*/

// InsertGuildChannelMessage 频道消息入数据库
func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMessage) string {
	id := encodeGuildMessageID(m.GuildId, m.ChannelId, m.Id, message.SourceGuildChannel)
	source := message.Source{
		SourceType: message.SourceGuildChannel,
		PrimaryID:  int64(m.Sender.TinyId),
	}
	msg := &db.StoredGuildChannelMessage{
		ID: id,
		Attribute: &db.StoredGuildMessageAttribute{
			MessageSeq:   m.Id,
			InternalID:   m.InternalId,
			SenderTinyID: m.Sender.TinyId,
			SenderName:   m.Sender.Nickname,
			Timestamp:    m.Time,
		},
		GuildID:   m.GuildId,
		ChannelID: m.ChannelId,
		Content:   ToMessageContent(m.Elements, source),
	}
	if err := db.InsertGuildChannelMessage(msg); err != nil {
		log.Warnf("记录聊天数据时出现错误: %v", err)
		return ""
	}
	return msg.ID
}

func (bot *CQBot) event(typ string, others global.MSG) *event {
	ev := new(event)
	post, detail, ok := strings.Cut(typ, "/")
	ev.PostType = post
	ev.DetailType = detail
	if ok {
		detail, sub, _ := strings.Cut(detail, "/")
		ev.DetailType = detail
		ev.SubType = sub
	}
	ev.Time = time.Now().Unix()
	ev.SelfID = bot.Client.Uin
	ev.Others = others
	return ev
}

func (bot *CQBot) dispatchEvent(typ string, others global.MSG) {
	bot.dispatch(bot.event(typ, others))
}

func (bot *CQBot) dispatch(ev *event) {
	bot.lock.RLock()
	defer bot.lock.RUnlock()

	event := &Event{Raw: ev}
	wg := sync.WaitGroup{}
	wg.Add(len(bot.events))
	for _, f := range bot.events {
		go func(fn func(*Event)) {
			defer func() {
				if pan := recover(); pan != nil {
					log.Warnf("处理事件 %v 时出现错误: %v \n%s", event.JSONString(), pan, debug.Stack())
				}
				wg.Done()
			}()

			start := time.Now()
			fn(event)
			end := time.Now()
			if end.Sub(start) > time.Second*5 {
				log.Debugf("警告: 事件处理耗时超过 5 秒 (%v), 请检查应用是否有堵塞.", end.Sub(start))
			}
		}(f)
	}
	wg.Wait()
	if event.buffer != nil {
		global.PutBuffer(event.buffer)
	}
}

func formatGroupName(group *client.GroupInfo) string {
	return fmt.Sprintf("%s(%d)", group.Name, group.Code)
}

func formatMemberName(mem *client.GroupMemberInfo) string {
	if mem == nil {
		return "未知"
	}
	return fmt.Sprintf("%s(%d)", mem.DisplayName(), mem.Uin)
}

// encodeMessageID 临时先这样, 暂时用不上
func encodeMessageID(target int64, seq int32) string {
	return hex.EncodeToString(binary.NewWriterF(func(w *binary.Writer) {
		w.WriteUInt64(uint64(target))
		w.WriteUInt32(uint32(seq))
	}))
}

// encodeGuildMessageID 将频道信息编码为字符串
// 当信息来源为 Channel 时 primaryID 为 guildID , subID 为 channelID
// 当信息来源为 Direct 时 primaryID 为 guildID , subID 为 tinyID
func encodeGuildMessageID(primaryID, subID, seq uint64, source message.SourceType) string {
	return base64.StdEncoding.EncodeToString(binary.NewWriterF(func(w *binary.Writer) {
		w.WriteByte(byte(source))
		w.WriteUInt64(primaryID)
		w.WriteUInt64(subID)
		w.WriteUInt64(seq)
	}))
}

func decodeGuildMessageID(id string) (source message.Source, seq uint64) {
	b, _ := base64.StdEncoding.DecodeString(id)
	if len(b) < 25 {
		return
	}
	r := binary.NewReader(b)
	source = message.Source{
		SourceType:  message.SourceType(r.ReadByte()),
		PrimaryID:   r.ReadInt64(),
		SecondaryID: r.ReadInt64(),
	}
	seq = uint64(r.ReadInt64())
	return
}


================================================
FILE: coolq/converter.go
================================================
package coolq

import (
	"strconv"
	"strings"

	"github.com/Mrs4s/MiraiGo/client"
	"github.com/Mrs4s/MiraiGo/message"
	"github.com/Mrs4s/MiraiGo/topic"
	log "github.com/sirupsen/logrus"

	"github.com/Mrs4s/go-cqhttp/global"
)

func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) global.MSG {
	sex := "unknown"
	if m.Gender == 1 { // unknown = 0xff
		sex = "female"
	} else if m.Gender == 0 {
		sex = "male"
	}
	role := "member"
	switch m.Permission { // nolint:exhaustive
	case client.Owner:
		role = "owner"
	case client.Administrator:
		role = "admin"
	}
	return global.MSG{
		"group_id":          groupID,
		"user_id":           m.Uin,
		"nickname":          m.Nickname,
		"card":              m.CardName,
		"sex":               sex,
		"age":               0,
		"area":              "",
		"join_time":         m.JoinTime,
		"last_sent_time":    m.LastSpeakTime,
		"shut_up_timestamp": m.ShutUpTimestamp,
		"level":             strconv.FormatInt(int64(m.Level), 10),
		"role":              role,
		"unfriendly":        false,
		"title":             m.SpecialTitle,
		"title_expire_time": 0,
		"card_changeable":   false,
	}
}

func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) {
	for _, mem := range m {
		r = append(r, global.MSG{
			"tiny_id":   fU64(mem.TinyId),
			"title":     mem.Title,
			"nickname":  mem.Nickname,
			"role_id":   fU64(mem.Role),
			"role_name": mem.RoleName,
		})
	}
	return
}

func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) *event {
	source := message.Source{
		SourceType: message.SourceGroup,
		PrimaryID:  m.GroupCode,
	}
	cqm := toStringMessage(m.Elements, source)
	typ := "message/group/normal"
	if m.Sender.Uin == bot.Client.Uin {
		typ = "message_sent/group/normal"
	}
	gm := global.MSG{
		"anonymous":   nil,
		"font":        0,
		"group_id":    m.GroupCode,
		"message":     ToFormattedMessage(m.Elements, source),
		"message_seq": m.Id,
		"raw_message": cqm,
		"sender": global.MSG{
			"age":     0,
			"area":    "",
			"level":   "",
			"sex":     "unknown",
			"user_id": m.Sender.Uin,
		},
		"user_id": m.Sender.Uin,
	}
	if m.Sender.IsAnonymous() {
		gm["anonymous"] = global.MSG{
			"flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
			"id":   m.Sender.Uin,
			"name": m.Sender.AnonymousInfo.AnonymousNick,
		}
		gm["sender"].(global.MSG)["nickname"] = "匿名消息"
		typ = "message/group/anonymous"
	} else {
		group := bot.Client.FindGroup(m.GroupCode)
		mem := group.FindMember(m.Sender.Uin)
		if mem == nil {
			log.Warnf("获取 %v 成员信息失败,尝试刷新成员列表", m.Sender.Uin)
			t, err := bot.Client.GetGroupMembers(group)
			if err != nil {
				log.Warnf("刷新群 %v 成员列表失败: %v", group.Uin, err)
				return nil
			}
			group.Members = t
			mem = group.FindMember(m.Sender.Uin)
			if mem == nil {
				return nil
			}
		}
		ms := gm["sender"].(global.MSG)
		role := "member"
		switch mem.Permission { // nolint:exhaustive
		case client.Owner
Download .txt
gitextract_q284w6qz/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yaml
│   │   └── feat--.md
│   └── workflows/
│       ├── build_docker_image.yml
│       ├── ci.yml
│       ├── close_pr.yml
│       ├── golint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd/
│   ├── api-generator/
│   │   ├── main.go
│   │   └── supported.go
│   └── gocq/
│       ├── login.go
│       ├── main.go
│       └── qsign.go
├── coolq/
│   ├── api.go
│   ├── api_v12.go
│   ├── bot.go
│   ├── converter.go
│   ├── cqcode.go
│   ├── doc.go
│   ├── event.go
│   └── feed.go
├── db/
│   ├── database.go
│   ├── leveldb/
│   │   ├── const.go
│   │   ├── leveldb.go
│   │   ├── reader.go
│   │   ├── structs.go
│   │   └── writer.go
│   ├── mongodb/
│   │   └── mongodb.go
│   ├── multidb.go
│   └── sqlite3/
│       ├── model.go
│       └── sqlite3.go
├── docker-entrypoint.sh
├── docs/
│   ├── EventFilter.md
│   ├── QA.md
│   ├── README.md
│   ├── adminApi.md
│   ├── config.md
│   ├── cqhttp.md
│   ├── file.md
│   ├── guild.md
│   ├── quick_start.md
│   └── slider.md
├── global/
│   ├── all_test.go
│   ├── buffer.go
│   ├── codec.go
│   ├── doc.go
│   ├── fs.go
│   ├── log_hook.go
│   ├── net.go
│   ├── param.go
│   ├── signal.go
│   ├── signal_unix.go
│   ├── signal_windows.go
│   └── terminal/
│       ├── doc.go
│       ├── double_click.go
│       ├── double_click_windows.go
│       ├── quick_edit.go
│       ├── quick_edit_windows.go
│       ├── title.go
│       ├── title_windows.go
│       ├── vt100.go
│       └── vt100_windows.go
├── go.mod
├── go.sum
├── internal/
│   ├── base/
│   │   ├── feature.go
│   │   ├── flag.go
│   │   └── version.go
│   ├── cache/
│   │   └── cache.go
│   ├── download/
│   │   └── download.go
│   ├── mime/
│   │   └── mime.go
│   ├── msg/
│   │   ├── element.go
│   │   ├── element_test.go
│   │   ├── local.go
│   │   ├── parse.go
│   │   └── parse_test.go
│   ├── param/
│   │   └── param.go
│   ├── selfdiagnosis/
│   │   └── diagnoses.go
│   └── selfupdate/
│       ├── update.go
│       ├── update_others.go
│       └── update_windows.go
├── main.go
├── modules/
│   ├── api/
│   │   ├── api.go
│   │   └── caller.go
│   ├── config/
│   │   ├── config.go
│   │   ├── config_test.go
│   │   └── default_config.yml
│   ├── filter/
│   │   ├── filter.go
│   │   └── middlewares.go
│   ├── pprof/
│   │   └── pprof.go
│   ├── servers/
│   │   └── servers.go
│   └── silk/
│       ├── codec.go
│       ├── codec_unsupported.go
│       └── stubs.go
├── pkg/
│   └── onebot/
│       ├── attr.go
│       ├── kind_string.go
│       ├── onebot.go
│       ├── spec.go
│       ├── supported.go
│       └── value.go
├── scripts/
│   ├── bootstrap
│   └── upload_dist.sh
├── server/
│   ├── daemon.go
│   ├── doc.go
│   ├── http.go
│   ├── http_test.go
│   ├── middlewares.go
│   ├── scf.go
│   └── websocket.go
└── winres/
    ├── .gitignore
    ├── gen/
    │   └── json.go
    └── init.go
Download .txt
SYMBOL INDEX (722 symbols across 81 files)

FILE: cmd/api-generator/main.go
  type Param (line 24) | type Param struct
  type Router (line 30) | type Router struct
  type generator (line 38) | type generator struct
    method WriteString (line 48) | func (g *generator) WriteString(s string) {
    method writef (line 52) | func (g *generator) writef(format string, a ...any) {
    method header (line 56) | func (g *generator) header() {
    method genRouter (line 61) | func (g *generator) genRouter(routers []Router) {
    method router (line 88) | func (g *generator) router(router Router, pathVersion int) {
  constant PathAll (line 43) | PathAll = 0
  constant PathV11 (line 44) | PathV11 = 11
  constant PathV12 (line 45) | PathV12 = 12
  function conv (line 137) | func conv(v, t string) string {
  function main (line 160) | func main() {
  function unquote (line 272) | func unquote(s string) string {
  function parseMap (line 282) | func parseMap(input string, sep string) map[string]string {
  function match (line 296) | func match(text string) (string, string) {
  function snakecase (line 313) | func snakecase(s string) string {
  function convDefault (line 328) | func convDefault(s string, t string) string {
  function typeName (line 344) | func typeName(x ast.Node) string {

FILE: cmd/api-generator/supported.go
  method genSupported (line 5) | func (g *generator) genSupported(routers []Router) {
  constant supportedTemplete (line 35) | supportedTemplete = `

FILE: cmd/gocq/login.go
  function readLine (line 26) | func readLine() (str string) {
  function readLineTimeout (line 32) | func readLineTimeout(t time.Duration) {
  function readIfTTY (line 46) | func readIfTTY(de string) (str string) {
  function commonLogin (line 60) | func commonLogin() error {
  function printQRCode (line 68) | func printQRCode(imgData []byte) {
  function qrcodeLogin (line 96) | func qrcodeLogin() error {
  function loginResponseProcessor (line 144) | func loginResponseProcessor(res *client.LoginResponse) error {
  function getTicket (line 224) | func getTicket(u string) string {
  function fetchCaptcha (line 252) | func fetchCaptcha(id string) string {

FILE: cmd/gocq/main.go
  function InitBase (line 53) | func InitBase() {
  function PrepareData (line 79) | func PrepareData() {
  function LoginInteract (line 119) | func LoginInteract() {
  function WaitSignal (line 421) | func WaitSignal() {
  function PasswordHashEncrypt (line 431) | func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
  function PasswordHashDecrypt (line 446) | func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]by...
  function newClient (line 461) | func newClient() *client.QQClient {
  function getRemoteLatestProtocolVersion (line 489) | func getRemoteLatestProtocolVersion(protocolType int) ([]byte, error) {
  type protocolLogger (line 501) | type protocolLogger struct
    method Info (line 505) | func (p protocolLogger) Info(format string, arg ...any) {
    method Warning (line 509) | func (p protocolLogger) Warning(format string, arg ...any) {
    method Debug (line 513) | func (p protocolLogger) Debug(format string, arg ...any) {
    method Error (line 517) | func (p protocolLogger) Error(format string, arg ...any) {
    method Dump (line 521) | func (p protocolLogger) Dump(data []byte, format string, arg ...any) {
  constant fromProtocol (line 503) | fromProtocol = "Protocol -> "

FILE: cmd/gocq/qsign.go
  type currentSignServer (line 27) | type currentSignServer
    method get (line 29) | func (c *currentSignServer) get() *config.SignServer {
    method set (line 37) | func (c *currentSignServer) set(server *config.SignServer) {
  type errconut (line 45) | type errconut
    method hasOver (line 47) | func (ec *errconut) hasOver(count uintptr) bool {
    method inc (line 51) | func (ec *errconut) inc() {
  function getAvaliableSignServer (line 58) | func getAvaliableSignServer() (*config.SignServer, error) {
  function isServerAvaliable (line 86) | func isServerAvaliable(signServer string) bool {
  function asyncCheckServer (line 99) | func asyncCheckServer(servers []config.SignServer) *config.SignServer {
  function requestSignServer (line 132) | func requestSignServer(method string, url string, headers map[string]str...
  function energy (line 162) | func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) {
  function signSubmit (line 187) | func signSubmit(uin string, cmd string, callbackID int64, buffer []byte,...
  function signCallback (line 212) | func signCallback(uin string, results []gjson.Result, t string) {
  function signRequset (line 232) | func signRequset(seq uint64, uin string, cmd string, qua string, buff []...
  function signRegister (line 255) | func signRegister(uin int64, androidID, guid []byte, qimei36, key string) {
  function signRefreshToken (line 278) | func signRefreshToken(uin string) error {
  function sign (line 300) | func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (...
  function signServerDestroy (line 360) | func signServerDestroy(uin string) error {
  function signVersion (line 383) | func signVersion() (signServer string, version string, err error) {
  function signStartRefreshToken (line 395) | func signStartRefreshToken(interval int64) {

FILE: coolq/api.go
  type guildMemberPageToken (line 36) | type guildMemberPageToken struct
  method CQGetLoginInfo (line 54) | func (bot *CQBot) CQGetLoginInfo() global.MSG {
  method CQGetQiDianAccountInfo (line 60) | func (bot *CQBot) CQGetQiDianAccountInfo() global.MSG {
  method CQGetGuildServiceProfile (line 73) | func (bot *CQBot) CQGetGuildServiceProfile() global.MSG {
  method CQGetGuildList (line 83) | func (bot *CQBot) CQGetGuildList() global.MSG {
  method CQGetGuildMetaByGuest (line 108) | func (bot *CQBot) CQGetGuildMetaByGuest(guildID uint64) global.MSG {
  method CQGetGuildChannelList (line 129) | func (bot *CQBot) CQGetGuildChannelList(guildID uint64, noCache bool) gl...
  method CQGetGuildMembers (line 151) | func (bot *CQBot) CQGetGuildMembers(guildID uint64, nextToken string) gl...
  method CQGetGuildMemberProfile (line 195) | func (bot *CQBot) CQGetGuildMemberProfile(guildID, userID uint64) global...
  method CQGetGuildRoles (line 222) | func (bot *CQBot) CQGetGuildRoles(guildID uint64) global.MSG {
  method CQCreateGuildRole (line 246) | func (bot *CQBot) CQCreateGuildRole(guildID uint64, name string, color u...
  method CQDeleteGuildRole (line 265) | func (bot *CQBot) CQDeleteGuildRole(guildID uint64, roleID uint64) globa...
  method CQSetGuildMemberRole (line 276) | func (bot *CQBot) CQSetGuildMemberRole(guildID uint64, set bool, roleID ...
  method CQModifyRoleInGuild (line 293) | func (bot *CQBot) CQModifyRoleInGuild(guildID uint64, roleID uint64, nam...
  method CQGetTopicChannelFeeds (line 304) | func (bot *CQBot) CQGetTopicChannelFeeds(guildID, channelID uint64) glob...
  method CQGetFriendList (line 332) | func (bot *CQBot) CQGetFriendList(spec *onebot.Spec) global.MSG {
  method CQGetUnidirectionalFriendList (line 347) | func (bot *CQBot) CQGetUnidirectionalFriendList() global.MSG {
  method CQDeleteUnidirectionalFriend (line 368) | func (bot *CQBot) CQDeleteUnidirectionalFriend(uin int64) global.MSG {
  method CQDeleteFriend (line 389) | func (bot *CQBot) CQDeleteFriend(uin int64) global.MSG {
  method CQGetGroupList (line 404) | func (bot *CQBot) CQGetGroupList(noCache bool, spec *onebot.Spec) global...
  method CQGetGroupInfo (line 426) | func (bot *CQBot) CQGetGroupInfo(groupID int64, noCache bool, spec *oneb...
  method CQGetGroupMemberList (line 467) | func (bot *CQBot) CQGetGroupMemberList(groupID int64, noCache bool) glob...
  method CQGetGroupMemberInfo (line 491) | func (bot *CQBot) CQGetGroupMemberInfo(groupID, userID int64, noCache bo...
  method CQGetGroupFileSystemInfo (line 517) | func (bot *CQBot) CQGetGroupFileSystemInfo(groupID int64) global.MSG {
  method CQGetGroupRootFiles (line 530) | func (bot *CQBot) CQGetGroupRootFiles(groupID int64) global.MSG {
  method CQGetGroupFilesByFolderID (line 551) | func (bot *CQBot) CQGetGroupFilesByFolderID(groupID int64, folderID stri...
  method CQGetGroupFileURL (line 573) | func (bot *CQBot) CQGetGroupFileURL(groupID int64, fileID string, busID ...
  method CQUploadGroupFile (line 587) | func (bot *CQBot) CQUploadGroupFile(groupID int64, file, name, folder st...
  method CQUploadPrivateFile (line 610) | func (bot *CQBot) CQUploadPrivateFile(userID int64, file, name string) g...
  method CQGroupFileCreateFolder (line 635) | func (bot *CQBot) CQGroupFileCreateFolder(groupID int64, parentID, name ...
  method CQGroupFileDeleteFolder (line 652) | func (bot *CQBot) CQGroupFileDeleteFolder(groupID int64, id string) glob...
  method CQGroupFileDeleteFile (line 669) | func (bot *CQBot) CQGroupFileDeleteFile(groupID int64, id string, busID ...
  method CQGetWordSlices (line 686) | func (bot *CQBot) CQGetWordSlices(content string) global.MSG {
  method CQSendMessage (line 701) | func (bot *CQBot) CQSendMessage(groupID, userID int64, m gjson.Result, m...
  method CQSendForwardMessage (line 719) | func (bot *CQBot) CQSendForwardMessage(groupID, userID int64, m gjson.Re...
  method CQSendGroupMessage (line 738) | func (bot *CQBot) CQSendGroupMessage(groupID int64, m gjson.Result, auto...
  method CQSendGuildChannelMessage (line 784) | func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m...
  method uploadForwardElement (line 834) | func (bot *CQBot) uploadForwardElement(m gjson.Result, target int64, sou...
  method CQSendGroupForwardMessage (line 970) | func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Resul...
  method CQSendPrivateForwardMessage (line 1000) | func (bot *CQBot) CQSendPrivateForwardMessage(userID int64, m gjson.Resu...
  method CQSendPrivateMessage (line 1025) | func (bot *CQBot) CQSendPrivateMessage(userID int64, groupID int64, m gj...
  method CQSetGroupCard (line 1052) | func (bot *CQBot) CQSetGroupCard(groupID, userID int64, card string) glo...
  method CQSetGroupSpecialTitle (line 1067) | func (bot *CQBot) CQSetGroupSpecialTitle(groupID, userID int64, title st...
  method CQSetGroupName (line 1082) | func (bot *CQBot) CQSetGroupName(groupID int64, name string) global.MSG {
  method CQGetGroupMemo (line 1092) | func (bot *CQBot) CQGetGroupMemo(groupID int64) global.MSG {
  method CQSetGroupMemo (line 1106) | func (bot *CQBot) CQSetGroupMemo(groupID int64, msg, img string) global....
  method CQDelGroupMemo (line 1134) | func (bot *CQBot) CQDelGroupMemo(groupID int64, fid string) global.MSG {
  method CQSetGroupKick (line 1153) | func (bot *CQBot) CQSetGroupKick(groupID int64, userID int64, msg string...
  method CQSetGroupBan (line 1173) | func (bot *CQBot) CQSetGroupBan(groupID, userID int64, duration uint32) ...
  method CQSetGroupWholeBan (line 1194) | func (bot *CQBot) CQSetGroupWholeBan(groupID int64, enable bool) global....
  method CQSetGroupLeave (line 1206) | func (bot *CQBot) CQSetGroupLeave(groupID int64) global.MSG {
  method CQGetAtAllRemain (line 1218) | func (bot *CQBot) CQGetAtAllRemain(groupID int64) global.MSG {
  method CQProcessFriendRequest (line 1234) | func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) glob...
  method CQProcessGroupRequest (line 1253) | func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, ap...
  method CQDeleteMessage (line 1298) | func (bot *CQBot) CQDeleteMessage(messageID int32) global.MSG {
  method CQSetGroupAdmin (line 1330) | func (bot *CQBot) CQSetGroupAdmin(groupID, userID int64, enable bool) gl...
  method CQSetGroupAnonymous (line 1354) | func (bot *CQBot) CQSetGroupAnonymous(groupID int64, enable bool) global...
  method CQGetGroupHonorInfo (line 1367) | func (bot *CQBot) CQGetGroupHonorInfo(groupID int64, t string) global.MSG {
  method CQGetStrangerInfo (line 1435) | func (bot *CQBot) CQGetStrangerInfo(userID int64) global.MSG {
  method CQHandleQuickOperation (line 1465) | func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result...
  method CQGetImage (line 1555) | func (bot *CQBot) CQGetImage(file string) global.MSG {
  method CQDownloadFile (line 1597) | func (bot *CQBot) CQDownloadFile(url string, headers gjson.Result, threa...
  method CQGetForwardMessage (line 1641) | func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG {
  method CQGetMessage (line 1679) | func (bot *CQBot) CQGetMessage(messageID int32) global.MSG {
  method CQGetGuildMessage (line 1710) | func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) glob...
  method CQGetGroupSystemMessages (line 1776) | func (bot *CQBot) CQGetGroupSystemMessages() global.MSG {
  method CQGetGroupMessageHistory (line 1790) | func (bot *CQBot) CQGetGroupMessageHistory(groupID int64, seq int64) glo...
  method CQGetOnlineClients (line 1827) | func (bot *CQBot) CQGetOnlineClients(noCache bool) global.MSG {
  method CQCanSendImage (line 1851) | func (bot *CQBot) CQCanSendImage() global.MSG {
  method CQCanSendRecord (line 1859) | func (bot *CQBot) CQCanSendRecord() global.MSG {
  method CQOcrImage (line 1868) | func (bot *CQBot) CQOcrImage(imageID string) global.MSG {
  method CQSetGroupPortrait (line 1890) | func (bot *CQBot) CQSetGroupPortrait(groupID int64, file, cache string) ...
  method CQSetGroupAnonymousBan (line 1908) | func (bot *CQBot) CQSetGroupAnonymousBan(groupID int64, flag string, dur...
  method CQGetStatus (line 1930) | func (bot *CQBot) CQGetStatus(spec *onebot.Spec) global.MSG {
  method CQSetEssenceMessage (line 1953) | func (bot *CQBot) CQSetEssenceMessage(messageID int32) global.MSG {
  method CQDeleteEssenceMessage (line 1969) | func (bot *CQBot) CQDeleteEssenceMessage(messageID int32) global.MSG {
  method CQGetEssenceMessageList (line 1985) | func (bot *CQBot) CQGetEssenceMessageList(groupID int64) global.MSG {
  method CQCheckURLSafely (line 2014) | func (bot *CQBot) CQCheckURLSafely(url string) global.MSG {
  method CQGetVersionInfo (line 2024) | func (bot *CQBot) CQGetVersionInfo() global.MSG {
  method CQGetModelShow (line 2048) | func (bot *CQBot) CQGetModelShow(model string) global.MSG {
  method CQSendGroupSign (line 2069) | func (bot *CQBot) CQSendGroupSign(groupID int64) global.MSG {
  method CQSetModelShow (line 2078) | func (bot *CQBot) CQSetModelShow(model, modelShow string) global.MSG {
  method CQMarkMessageAsRead (line 2089) | func (bot *CQBot) CQMarkMessageAsRead(msgID int32) global.MSG {
  method CQSetQQProfile (line 2107) | func (bot *CQBot) CQSetQQProfile(nickname, company, email, college, pers...
  method CQReloadEventFilter (line 2128) | func (bot *CQBot) CQReloadEventFilter(file string) global.MSG {
  method CQGetSupportedActions (line 2136) | func (bot *CQBot) CQGetSupportedActions(spec *onebot.Spec) global.MSG {
  function OK (line 2141) | func OK(data any) global.MSG {
  function Failed (line 2146) | func Failed(code int, msg ...string) global.MSG {
  function limitedString (line 2157) | func limitedString(str string) string {

FILE: coolq/api_v12.go
  method CQGetVersion (line 16) | func (bot *CQBot) CQGetVersion() global.MSG {
  method CQSendMessageV12 (line 31) | func (bot *CQBot) CQSendMessageV12(groupID, userID, detailType string, m...

FILE: coolq/bot.go
  type CQBot (line 34) | type CQBot struct
    method OnEventPush (line 128) | func (bot *CQBot) OnEventPush(f func(e *Event)) {
    method uploadLocalImage (line 151) | func (bot *CQBot) uploadLocalImage(target message.Source, img *msg.Loc...
    method uploadLocalVideo (line 191) | func (bot *CQBot) uploadLocalVideo(target message.Source, v *msg.Local...
    method uploadMedia (line 219) | func (bot *CQBot) uploadMedia(target message.Source, elements []messag...
    method SendGroupMessage (line 268) | func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMe...
    method SendPrivateMessage (line 314) | func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *m...
    method SendGuildChannelMessage (line 411) | func (bot *CQBot) SendGuildChannelMessage(guildID, channelID uint64, m...
    method InsertGroupMessage (line 447) | func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage, source m...
    method InsertPrivateMessage (line 490) | func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage, sour...
    method InsertGuildChannelMessage (line 563) | func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMes...
    method event (line 589) | func (bot *CQBot) event(typ string, others global.MSG) *event {
    method dispatchEvent (line 605) | func (bot *CQBot) dispatchEvent(typ string, others global.MSG) {
    method dispatch (line 609) | func (bot *CQBot) dispatch(ev *event) {
  type Event (line 46) | type Event struct
    method marshal (line 52) | func (e *Event) marshal() {
    method JSONBytes (line 60) | func (e *Event) JSONBytes() []byte {
    method JSONString (line 67) | func (e *Event) JSONString() string {
  function NewQQBot (line 73) | func NewQQBot(cli *client.QQClient) *CQBot {
  type worker (line 134) | type worker struct
    method do (line 138) | func (w *worker) do(f func()) {
    method wait (line 146) | func (w *worker) wait() {
  function removeLocalElement (line 200) | func removeLocalElement(elements []message.IMessageElement) []message.IM...
  constant uploadFailedTemplate (line 217) | uploadFailedTemplate = "警告: %s %d %s上传失败: %v"
  function formatGroupName (line 639) | func formatGroupName(group *client.GroupInfo) string {
  function formatMemberName (line 643) | func formatMemberName(mem *client.GroupMemberInfo) string {
  function encodeMessageID (line 651) | func encodeMessageID(target int64, seq int32) string {
  function encodeGuildMessageID (line 661) | func encodeGuildMessageID(primaryID, subID, seq uint64, source message.S...
  function decodeGuildMessageID (line 670) | func decodeGuildMessageID(id string) (source message.Source, seq uint64) {

FILE: coolq/converter.go
  function convertGroupMemberInfo (line 15) | func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) gl...
  function convertGuildMemberInfo (line 49) | func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) {
  method formatGroupMessage (line 62) | func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) *event {
  function convertChannelInfo (line 130) | func convertChannelInfo(c *client.ChannelInfo) global.MSG {
  function convertChannelFeedInfo (line 154) | func convertChannelFeedInfo(f *topic.Feed) global.MSG {
  function convertReactions (line 196) | func convertReactions(reactions []*message.GuildMessageEmojiReaction) (r...
  function toStringMessage (line 211) | func toStringMessage(m []message.IMessageElement, source message.Source)...
  function fU64 (line 220) | func fU64(v uint64) string {

FILE: coolq/cqcode.go
  constant maxImageSize (line 42) | maxImageSize = 1024 * 1024 * 30
  constant maxVideoSize (line 43) | maxVideoSize = 1024 * 1024 * 100
  function replyID (line 46) | func replyID(r *message.ReplyElement, source message.Source) int32 {
  function toElements (line 65) | func toElements(e []message.IMessageElement, source message.Source) (r [...
  function ToMessageContent (line 279) | func ToMessageContent(e []message.IMessageElement, source message.Source...
  method ConvertStringMessage (line 405) | func (bot *CQBot) ConvertStringMessage(spec *onebot.Spec, raw string, so...
  method ConvertObjectMessage (line 411) | func (bot *CQBot) ConvertObjectMessage(spec *onebot.Spec, m gjson.Result...
  method ConvertContentMessage (line 420) | func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType...
  method ConvertElements (line 434) | func (bot *CQBot) ConvertElements(spec *onebot.Spec, elems []msg.Element...
  method reply (line 467) | func (bot *CQBot) reply(spec *onebot.Spec, elem msg.Element, sourceType ...
  method voice (line 532) | func (bot *CQBot) voice(elem msg.Element) (m any, err error) {
  method at (line 551) | func (bot *CQBot) at(id, name string) (m any, err error) {
  method convertV11 (line 564) | func (bot *CQBot) convertV11(elem msg.Element) (m any, ok bool, err erro...
  method convertV12 (line 587) | func (bot *CQBot) convertV12(elem msg.Element) (m any, ok bool, err erro...
  method ConvertElement (line 608) | func (bot *CQBot) ConvertElement(spec *onebot.Spec, elem msg.Element, so...
  method makeImageOrVideoElem (line 884) | func (bot *CQBot) makeImageOrVideoElem(elem msg.Element, video bool, sou...
  method readImageCache (line 1007) | func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType...
  method readVideoCache (line 1047) | func (bot *CQBot) readVideoCache(b []byte) message.IMessageElement {
  method makeShowPic (line 1060) | func (bot *CQBot) makeShowPic(elem message.IMessageElement, source strin...

FILE: coolq/event.go
  function ToFormattedMessage (line 24) | func ToFormattedMessage(e []message.IMessageElement, source message.Sour...
  type event (line 33) | type event struct
    method MarshalJSON (line 42) | func (ev *event) MarshalJSON() ([]byte, error) {
  method privateMessageEvent (line 75) | func (bot *CQBot) privateMessageEvent(_ *client.QQClient, m *message.Pri...
  method groupMessageEvent (line 105) | func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.Group...
  method tempMessageEvent (line 139) | func (bot *CQBot) tempMessageEvent(_ *client.QQClient, e *client.TempMes...
  method guildChannelMessageEvent (line 175) | func (bot *CQBot) guildChannelMessageEvent(c *client.QQClient, m *messag...
  method guildMessageReactionsUpdatedEvent (line 206) | func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, ...
  method guildChannelMessageRecalledEvent (line 240) | func (bot *CQBot) guildChannelMessageRecalledEvent(c *client.QQClient, e...
  method guildChannelUpdatedEvent (line 266) | func (bot *CQBot) guildChannelUpdatedEvent(c *client.QQClient, e *client...
  method guildChannelCreatedEvent (line 283) | func (bot *CQBot) guildChannelCreatedEvent(c *client.QQClient, e *client...
  method guildChannelDestroyedEvent (line 303) | func (bot *CQBot) guildChannelDestroyedEvent(c *client.QQClient, e *clie...
  method groupMutedEvent (line 323) | func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMut...
  method groupRecallEvent (line 354) | func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMe...
  method groupNotifyEvent (line 370) | func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyE...
  method friendNotifyEvent (line 418) | func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotify...
  method memberTitleUpdatedEvent (line 434) | func (bot *CQBot) memberTitleUpdatedEvent(c *client.QQClient, e *client....
  method friendRecallEvent (line 445) | func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.Friend...
  method offlineFileEvent (line 461) | func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.Offline...
  method joinGroupEvent (line 477) | func (bot *CQBot) joinGroupEvent(c *client.QQClient, group *client.Group...
  method leaveGroupEvent (line 485) | func (bot *CQBot) leaveGroupEvent(c *client.QQClient, e *client.GroupLea...
  method memberPermissionChangedEvent (line 494) | func (bot *CQBot) memberPermissionChangedEvent(_ *client.QQClient, e *cl...
  method memberCardUpdatedEvent (line 505) | func (bot *CQBot) memberCardUpdatedEvent(_ *client.QQClient, e *client.M...
  method memberJoinEvent (line 515) | func (bot *CQBot) memberJoinEvent(_ *client.QQClient, e *client.MemberJo...
  method memberLeaveEvent (line 520) | func (bot *CQBot) memberLeaveEvent(_ *client.QQClient, e *client.MemberL...
  method friendRequestEvent (line 529) | func (bot *CQBot) friendRequestEvent(_ *client.QQClient, e *client.NewFr...
  method friendAddedEvent (line 540) | func (bot *CQBot) friendAddedEvent(_ *client.QQClient, e *client.NewFrie...
  method groupInvitedEvent (line 548) | func (bot *CQBot) groupInvitedEvent(_ *client.QQClient, e *client.GroupI...
  method groupJoinReqEvent (line 560) | func (bot *CQBot) groupJoinReqEvent(_ *client.QQClient, e *client.UserJo...
  method otherClientStatusChangedEvent (line 572) | func (bot *CQBot) otherClientStatusChangedEvent(_ *client.QQClient, e *c...
  method groupEssenceMsg (line 588) | func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDig...
  method groupIncrease (line 623) | func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) *...
  method groupDecrease (line 631) | func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *clie...
  method checkMedia (line 651) | func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) {

FILE: coolq/feed.go
  function FeedContentsToArrayMessage (line 10) | func FeedContentsToArrayMessage(contents []topic.IFeedRichContentElement...

FILE: db/database.go
  type Database (line 12) | type Database interface
  type StoredMessage (line 33) | type StoredMessage interface
  type StoredGroupMessage (line 42) | type StoredGroupMessage struct
    method GetID (line 106) | func (m *StoredGroupMessage) GetID() string                         { ...
    method GetType (line 107) | func (m *StoredGroupMessage) GetType() string                       { ...
    method GetGlobalID (line 108) | func (m *StoredGroupMessage) GetGlobalID() int32                    { ...
    method GetAttribute (line 109) | func (m *StoredGroupMessage) GetAttribute() *StoredMessageAttribute { ...
    method GetContent (line 110) | func (m *StoredGroupMessage) GetContent() []global.MSG              { ...
  type StoredPrivateMessage (line 54) | type StoredPrivateMessage struct
    method GetID (line 112) | func (m *StoredPrivateMessage) GetID() string                         ...
    method GetType (line 113) | func (m *StoredPrivateMessage) GetType() string                       ...
    method GetGlobalID (line 114) | func (m *StoredPrivateMessage) GetGlobalID() int32                    ...
    method GetAttribute (line 115) | func (m *StoredPrivateMessage) GetAttribute() *StoredMessageAttribute ...
    method GetContent (line 116) | func (m *StoredPrivateMessage) GetContent() []global.MSG              ...
  type StoredGuildChannelMessage (line 66) | type StoredGuildChannelMessage struct
  type StoredMessageAttribute (line 76) | type StoredMessageAttribute struct
  type StoredGuildMessageAttribute (line 85) | type StoredGuildMessageAttribute struct
  type QuotedInfo (line 94) | type QuotedInfo struct
  function ToGlobalID (line 102) | func ToGlobalID(code int64, msgID int32) int32 {

FILE: db/leveldb/const.go
  constant dataVersion (line 3) | dataVersion = 1
  constant group (line 6) | group        = 0x0
  constant private (line 7) | private      = 0x1
  constant guildChannel (line 8) | guildChannel = 0x2
  type coder (line 11) | type coder
  constant coderNil (line 14) | coderNil coder = iota
  constant coderInt (line 15) | coderInt
  constant coderUint (line 16) | coderUint
  constant coderInt32 (line 17) | coderInt32
  constant coderUint32 (line 18) | coderUint32
  constant coderInt64 (line 19) | coderInt64
  constant coderUint64 (line 20) | coderUint64
  constant coderString (line 21) | coderString
  constant coderMSG (line 22) | coderMSG
  constant coderArrayMSG (line 23) | coderArrayMSG
  constant coderStruct (line 24) | coderStruct

FILE: db/leveldb/leveldb.go
  type database (line 16) | type database struct
    method Open (line 36) | func (ldb *database) Open() error {
    method GetMessageByGlobalID (line 48) | func (ldb *database) GetMessageByGlobalID(id int32) (_ db.StoredMessag...
    method GetGroupMessageByGlobalID (line 72) | func (ldb *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGr...
    method GetPrivateMessageByGlobalID (line 84) | func (ldb *database) GetPrivateMessageByGlobalID(id int32) (*db.Stored...
    method GetGuildChannelMessageByID (line 96) | func (ldb *database) GetGuildChannelMessageByID(id string) (*db.Stored...
    method InsertGroupMessage (line 118) | func (ldb *database) InsertGroupMessage(msg *db.StoredGroupMessage) er...
    method InsertPrivateMessage (line 126) | func (ldb *database) InsertPrivateMessage(msg *db.StoredPrivateMessage...
    method InsertGuildChannelMessage (line 134) | func (ldb *database) InsertGuildChannelMessage(msg *db.StoredGuildChan...
  type config (line 21) | type config struct
  function init (line 25) | func init() {

FILE: db/leveldb/reader.go
  type intReader (line 14) | type intReader struct
    method varint (line 26) | func (r *intReader) varint() int64 {
    method uvarint (line 31) | func (r *intReader) uvarint() uint64 {
  function newIntReader (line 19) | func newIntReader(s string) intReader {
  type reader (line 38) | type reader struct
    method coder (line 44) | func (r *reader) coder() coder    { o, _ := r.data.ReadByte(); return ...
    method varint (line 45) | func (r *reader) varint() int64   { return r.data.varint() }
    method uvarint (line 46) | func (r *reader) uvarint() uint64 { return r.data.uvarint() }
    method int32 (line 47) | func (r *reader) int32() int32    { return int32(r.varint()) }
    method int64 (line 48) | func (r *reader) int64() int64    { return r.varint() }
    method uint64 (line 49) | func (r *reader) uint64() uint64  { return r.uvarint() }
    method string (line 55) | func (r *reader) string() string {
    method msg (line 68) | func (r *reader) msg() global.MSG {
    method arrayMsg (line 78) | func (r *reader) arrayMsg() []global.MSG {
    method obj (line 87) | func (r *reader) obj() any {
  function newReader (line 114) | func newReader(data string) (*reader, error) {

FILE: db/leveldb/structs.go
  method writeStoredGroupMessage (line 5) | func (w *writer) writeStoredGroupMessage(x *db.StoredGroupMessage) {
  method readStoredGroupMessage (line 21) | func (r *reader) readStoredGroupMessage() *db.StoredGroupMessage {
  method writeStoredPrivateMessage (line 38) | func (w *writer) writeStoredPrivateMessage(x *db.StoredPrivateMessage) {
  method readStoredPrivateMessage (line 54) | func (r *reader) readStoredPrivateMessage() *db.StoredPrivateMessage {
  method writeStoredGuildChannelMessage (line 71) | func (w *writer) writeStoredGuildChannelMessage(x *db.StoredGuildChannel...
  method readStoredGuildChannelMessage (line 85) | func (r *reader) readStoredGuildChannelMessage() *db.StoredGuildChannelM...
  method writeStoredMessageAttribute (line 100) | func (w *writer) writeStoredMessageAttribute(x *db.StoredMessageAttribut...
  method readStoredMessageAttribute (line 113) | func (r *reader) readStoredMessageAttribute() *db.StoredMessageAttribute {
  method writeStoredGuildMessageAttribute (line 127) | func (w *writer) writeStoredGuildMessageAttribute(x *db.StoredGuildMessa...
  method readStoredGuildMessageAttribute (line 140) | func (r *reader) readStoredGuildMessageAttribute() *db.StoredGuildMessag...
  method writeQuotedInfo (line 154) | func (w *writer) writeQuotedInfo(x *db.QuotedInfo) {
  method readQuotedInfo (line 165) | func (r *reader) readQuotedInfo() *db.QuotedInfo {

FILE: db/leveldb/writer.go
  type intWriter (line 9) | type intWriter struct
    method varint (line 13) | func (w *intWriter) varint(x int64) {
    method uvarint (line 17) | func (w *intWriter) uvarint(x uint64) {
  type writer (line 47) | type writer struct
    method coder (line 59) | func (w *writer) coder(o coder)    { w.data.WriteByte(byte(o)) }
    method varint (line 60) | func (w *writer) varint(x int64)   { w.data.varint(x) }
    method uvarint (line 61) | func (w *writer) uvarint(x uint64) { w.data.uvarint(x) }
    method nil (line 62) | func (w *writer) nil()             { w.coder(coderNil) }
    method int (line 63) | func (w *writer) int(i int)        { w.varint(int64(i)) }
    method uint (line 64) | func (w *writer) uint(i uint)      { w.uvarint(uint64(i)) }
    method int32 (line 65) | func (w *writer) int32(i int32)    { w.varint(int64(i)) }
    method uint32 (line 66) | func (w *writer) uint32(i uint32)  { w.uvarint(uint64(i)) }
    method int64 (line 67) | func (w *writer) int64(i int64)    { w.varint(i) }
    method uint64 (line 68) | func (w *writer) uint64(i uint64)  { w.uvarint(i) }
    method string (line 70) | func (w *writer) string(s string) {
    method msg (line 84) | func (w *writer) msg(m global.MSG) {
    method arrayMsg (line 92) | func (w *writer) arrayMsg(a []global.MSG) {
    method obj (line 99) | func (w *writer) obj(o any) {
    method bytes (line 135) | func (w *writer) bytes() []byte {
  function newWriter (line 53) | func newWriter() *writer {

FILE: db/mongodb/mongodb.go
  type database (line 15) | type database struct
    method Open (line 48) | func (m *database) Open() error {
    method GetMessageByGlobalID (line 57) | func (m *database) GetMessageByGlobalID(id int32) (db.StoredMessage, e...
    method GetGroupMessageByGlobalID (line 64) | func (m *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGrou...
    method GetPrivateMessageByGlobalID (line 73) | func (m *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPr...
    method GetGuildChannelMessageByID (line 82) | func (m *database) GetGuildChannelMessageByID(id string) (*db.StoredGu...
    method InsertGroupMessage (line 91) | func (m *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
    method InsertPrivateMessage (line 97) | func (m *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) ...
    method InsertGuildChannelMessage (line 103) | func (m *database) InsertGuildChannelMessage(msg *db.StoredGuildChanne...
  type config (line 22) | type config struct
  constant MongoGroupMessageCollection (line 29) | MongoGroupMessageCollection        = "group-messages"
  constant MongoPrivateMessageCollection (line 30) | MongoPrivateMessageCollection      = "private-messages"
  constant MongoGuildChannelMessageCollection (line 31) | MongoGuildChannelMessageCollection = "guild-channel-messages"
  function init (line 34) | func init() {

FILE: db/multidb.go
  function Register (line 22) | func Register(name string, init func(yaml.Node) Database) {
  function Init (line 30) | func Init() {
  function Open (line 42) | func Open() error {
  function GetMessageByGlobalID (line 52) | func GetMessageByGlobalID(id int32) (StoredMessage, error) {
  function GetGroupMessageByGlobalID (line 59) | func GetGroupMessageByGlobalID(id int32) (*StoredGroupMessage, error) {
  function GetPrivateMessageByGlobalID (line 66) | func GetPrivateMessageByGlobalID(id int32) (*StoredPrivateMessage, error) {
  function GetGuildChannelMessageByID (line 73) | func GetGuildChannelMessageByID(id string) (*StoredGuildChannelMessage, ...
  function InsertGroupMessage (line 80) | func InsertGroupMessage(m *StoredGroupMessage) error {
  function InsertPrivateMessage (line 89) | func InsertPrivateMessage(m *StoredPrivateMessage) error {
  function InsertGuildChannelMessage (line 98) | func InsertGuildChannelMessage(m *StoredGuildChannelMessage) error {

FILE: db/sqlite3/model.go
  constant Sqlite3GroupMessageTableName (line 4) | Sqlite3GroupMessageTableName          = "grpmsg"
  constant Sqlite3MessageAttributeTableName (line 5) | Sqlite3MessageAttributeTableName      = "msgattr"
  constant Sqlite3GuildMessageAttributeTableName (line 6) | Sqlite3GuildMessageAttributeTableName = "gmsgattr"
  constant Sqlite3QuotedInfoTableName (line 7) | Sqlite3QuotedInfoTableName            = "quoinf"
  constant Sqlite3PrivateMessageTableName (line 8) | Sqlite3PrivateMessageTableName        = "privmsg"
  constant Sqlite3GuildChannelMessageTableName (line 9) | Sqlite3GuildChannelMessageTableName   = "guildmsg"
  constant Sqlite3UinInfoTableName (line 10) | Sqlite3UinInfoTableName               = "uininf"
  constant Sqlite3TinyInfoTableName (line 11) | Sqlite3TinyInfoTableName              = "tinyinf"
  type StoredMessageAttribute (line 15) | type StoredMessageAttribute struct
  type StoredGuildMessageAttribute (line 24) | type StoredGuildMessageAttribute struct
  type QuotedInfo (line 33) | type QuotedInfo struct
  type UinInfo (line 41) | type UinInfo struct
  type TinyInfo (line 47) | type TinyInfo struct
  type StoredGroupMessage (line 53) | type StoredGroupMessage struct
  type StoredPrivateMessage (line 65) | type StoredPrivateMessage struct
  type StoredGuildChannelMessage (line 77) | type StoredGuildChannelMessage struct

FILE: db/sqlite3/sqlite3.go
  type database (line 23) | type database struct
    method Open (line 51) | func (s *database) Open() error {
    method GetMessageByGlobalID (line 131) | func (s *database) GetMessageByGlobalID(id int32) (db.StoredMessage, e...
    method GetGroupMessageByGlobalID (line 138) | func (s *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGrou...
    method GetPrivateMessageByGlobalID (line 190) | func (s *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPr...
    method GetGuildChannelMessageByID (line 242) | func (s *database) GetGuildChannelMessageByID(id string) (*db.StoredGu...
    method InsertGroupMessage (line 299) | func (s *database) InsertGroupMessage(msg *db.StoredGroupMessage) error {
    method InsertPrivateMessage (line 380) | func (s *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) ...
    method InsertGuildChannelMessage (line 461) | func (s *database) InsertGuildChannelMessage(msg *db.StoredGuildChanne...
  type config (line 30) | type config struct
  function init (line 35) | func init() {

FILE: global/all_test.go
  function TestVersionNameCompare (line 10) | func TestVersionNameCompare(t *testing.T) {

FILE: global/buffer.go
  function NewBuffer (line 10) | func NewBuffer() *bytes.Buffer {
  function PutBuffer (line 15) | func PutBuffer(buf *bytes.Buffer) {

FILE: global/codec.go
  function EncoderSilk (line 16) | func EncoderSilk(data []byte) ([]byte, error) {
  function EncodeMP4 (line 34) | func EncodeMP4(src string, dst string) error { //        -y 覆盖文件
  function ExtractCover (line 51) | func ExtractCover(src string, target string) error {

FILE: global/fs.go
  constant ImagePath (line 25) | ImagePath = "data/images"
  constant VoicePath (line 27) | VoicePath = "data/voices"
  constant VideoPath (line 29) | VideoPath = "data/videos"
  constant VersionsPath (line 31) | VersionsPath = "data/versions"
  constant CachePath (line 33) | CachePath = "data/cache"
  constant DumpsPath (line 35) | DumpsPath = "dumps"
  constant HeaderAmr (line 37) | HeaderAmr = "#!AMR"
  constant HeaderSilk (line 39) | HeaderSilk = "\x02#!SILK_V3"
  function PathExists (line 43) | func PathExists(path string) bool {
  function ReadAllText (line 49) | func ReadAllText(path string) string {
  function WriteAllText (line 59) | func WriteAllText(path, text string) error {
  function Check (line 64) | func Check(err error, deleteSession bool) {
  function IsAMRorSILK (line 74) | func IsAMRorSILK(b []byte) bool {
  function FindFile (line 80) | func FindFile(file, cache, p string) (data []byte, err error) {
  function DelFile (line 128) | func DelFile(path string) bool {
  function ReadAddrFile (line 141) | func ReadAddrFile(path string) []netip.AddrPort {

FILE: global/log_hook.go
  type LocalHook (line 17) | type LocalHook struct
    method Levels (line 26) | func (hook *LocalHook) Levels() []logrus.Level {
    method ioWrite (line 33) | func (hook *LocalHook) ioWrite(entry *logrus.Entry) error {
    method pathWrite (line 46) | func (hook *LocalHook) pathWrite(entry *logrus.Entry) error {
    method Fire (line 68) | func (hook *LocalHook) Fire(entry *logrus.Entry) error {
    method SetFormatter (line 84) | func (hook *LocalHook) SetFormatter(consoleFormatter, fileFormatter lo...
    method SetWriter (line 97) | func (hook *LocalHook) SetWriter(writer io.Writer) {
    method SetPath (line 104) | func (hook *LocalHook) SetPath(path string) {
  function NewLocalHook (line 111) | func NewLocalHook(args any, consoleFormatter, fileFormatter logrus.Forma...
  function GetLogLevel (line 135) | func GetLogLevel(level string) []logrus.Level {
  type LogFormat (line 173) | type LogFormat struct
    method Format (line 178) | func (f LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
  constant colorCodePanic (line 204) | colorCodePanic = "\x1b[1;31m"
  constant colorCodeFatal (line 205) | colorCodeFatal = "\x1b[1;31m"
  constant colorCodeError (line 206) | colorCodeError = "\x1b[31m"
  constant colorCodeWarn (line 207) | colorCodeWarn  = "\x1b[33m"
  constant colorCodeInfo (line 208) | colorCodeInfo  = "\x1b[37m"
  constant colorCodeDebug (line 209) | colorCodeDebug = "\x1b[32m"
  constant colorCodeTrace (line 210) | colorCodeTrace = "\x1b[36m"
  constant colorReset (line 211) | colorReset     = "\x1b[0m"
  function GetLogLevelColorCode (line 215) | func GetLogLevelColorCode(level logrus.Level) string {

FILE: global/net.go
  function QQMusicSongInfo (line 12) | func QQMusicSongInfo(id string) (gjson.Result, error) {
  function NeteaseMusicSongInfo (line 21) | func NeteaseMusicSongInfo(id string) (gjson.Result, error) {

FILE: global/param.go
  function VersionNameCompare (line 26) | func VersionNameCompare(current, remote string) bool {

FILE: global/signal.go
  function dumpStack (line 21) | func dumpStack() {

FILE: global/signal_unix.go
  function SetupMainSignalHandler (line 14) | func SetupMainSignalHandler() <-chan struct{} {

FILE: global/signal_windows.go
  function SetupMainSignalHandler (line 26) | func SetupMainSignalHandler() <-chan struct{} {

FILE: global/terminal/double_click.go
  function RunningByDoubleClick (line 6) | func RunningByDoubleClick() bool {
  function NoMoreDoubleClick (line 11) | func NoMoreDoubleClick() error {

FILE: global/terminal/double_click_windows.go
  function RunningByDoubleClick (line 14) | func RunningByDoubleClick() bool {
  function NoMoreDoubleClick (line 29) | func NoMoreDoubleClick() error {
  function boxW (line 60) | func boxW(hwnd uintptr, caption, title string, flags uint) int {
  function getConsoleWindows (line 74) | func getConsoleWindows() (hWnd uintptr) {
  function toHighDPI (line 80) | func toHighDPI() {

FILE: global/terminal/quick_edit.go
  function RestoreInputMode (line 6) | func RestoreInputMode() error {
  function DisableQuickEdit (line 11) | func DisableQuickEdit() error {

FILE: global/terminal/quick_edit_windows.go
  function RestoreInputMode (line 12) | func RestoreInputMode() error {
  function DisableQuickEdit (line 21) | func DisableQuickEdit() error {

FILE: global/terminal/title.go
  function SetTitle (line 13) | func SetTitle() {

FILE: global/terminal/title_windows.go
  function setConsoleTitle (line 14) | func setConsoleTitle(title string) error {
  function SetTitle (line 27) | func SetTitle() {

FILE: global/terminal/vt100.go
  function EnableVT100 (line 6) | func EnableVT100() error {

FILE: global/terminal/vt100_windows.go
  function EnableVT100 (line 10) | func EnableVT100() error {

FILE: internal/base/feature.go
  function encodeSilk (line 13) | func encodeSilk(_ []byte, _ string) ([]byte, error) {
  function resampleSilk (line 17) | func resampleSilk(data []byte) []byte {

FILE: internal/base/flag.go
  function Parse (line 61) | func Parse() {
  function Init (line 77) | func Init() {
  function Help (line 125) | func Help() {

FILE: internal/base/version.go
  function init (line 8) | func init() {

FILE: internal/cache/cache.go
  type Cache (line 18) | type Cache struct
    method Insert (line 23) | func (c *Cache) Insert(md5, data []byte) {
    method Get (line 28) | func (c *Cache) Get(md5 []byte) []byte {
    method Delete (line 34) | func (c *Cache) Delete(md5 []byte) {
  function Init (line 39) | func Init() {

FILE: internal/download/download.go
  function newClient (line 42) | func newClient(t time.Duration) *http.Client {
  constant UserAgent (line 63) | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3...
  function SetTimeout (line 78) | func SetTimeout(t time.Duration) {
  type Request (line 87) | type Request struct
    method WithTimeout (line 66) | func (r Request) WithTimeout(t time.Duration) *Request {
    method client (line 96) | func (r Request) client() *http.Client {
    method do (line 106) | func (r Request) do() (*http.Response, error) {
    method body (line 123) | func (r Request) body() (io.ReadCloser, error) {
    method Bytes (line 142) | func (r Request) Bytes() ([]byte, error) {
    method JSON (line 153) | func (r Request) JSON() (gjson.Result, error) {
    method WriteToFile (line 181) | func (r Request) WriteToFile(path string) error {
    method WriteToFileMultiThreading (line 192) | func (r Request) WriteToFileMultiThreading(path string, thread int) er...
  function writeToFile (line 170) | func writeToFile(reader io.ReadCloser, path string) error {
  type gzipCloser (line 338) | type gzipCloser struct
    method Read (line 356) | func (g *gzipCloser) Read(p []byte) (n int, err error) {
    method Close (line 361) | func (g *gzipCloser) Close() error {
  function gzipReadCloser (line 344) | func gzipReadCloser(reader io.ReadCloser) (io.ReadCloser, error) {

FILE: internal/mime/mime.go
  constant limit (line 12) | limit = 4 * 1024
  function scan (line 14) | func scan(r io.ReadSeeker) string {
  function CheckImage (line 25) | func CheckImage(r io.ReadSeeker) (t string, ok bool) {
  function CheckAudio (line 41) | func CheckAudio(r io.ReadSeeker) (string, bool) {

FILE: internal/msg/element.go
  function EscapeText (line 19) | func EscapeText(s string) string {
  function EscapeValue (line 60) | func EscapeValue(value string) string {
  function UnescapeText (line 70) | func UnescapeText(content string) string {
  function UnescapeValue (line 84) | func UnescapeValue(content string) string {
  type Pair (line 92) | type Pair struct
  type Element (line 98) | type Element struct
    method Get (line 104) | func (e *Element) Get(k string) string {
    method CQCode (line 114) | func (e *Element) CQCode() string {
    method WriteCQCodeTo (line 121) | func (e *Element) WriteCQCodeTo(sb *strings.Builder) {
    method MarshalJSON (line 138) | func (e *Element) MarshalJSON() ([]byte, error) {
  constant hex (line 159) | hex = "0123456789abcdef"
  function QuoteJSON (line 162) | func QuoteJSON(s string) string {

FILE: internal/msg/element_test.go
  function jsonMarshal (line 8) | func jsonMarshal(s string) string {
  function TestQuoteJSON (line 16) | func TestQuoteJSON(t *testing.T) {

FILE: internal/msg/local.go
  type Poke (line 10) | type Poke struct
    method Type (line 15) | func (e *Poke) Type() message.ElementType {
  type LocalImage (line 21) | type LocalImage struct
    method Type (line 31) | func (e *LocalImage) Type() message.ElementType {
  type LocalVideo (line 36) | type LocalVideo struct
    method Type (line 42) | func (e *LocalVideo) Type() message.ElementType {

FILE: internal/msg/parse.go
  function ParseObject (line 8) | func ParseObject(m gjson.Result) (r []Element) {
  function text (line 32) | func text(txt string) Element {
  function ParseString (line 45) | func ParseString(raw string) (r []Element) {

FILE: internal/msg/parse_test.go
  function TestParseString (line 13) | func TestParseString(_ *testing.T) {
  function BenchmarkParseString (line 25) | func BenchmarkParseString(b *testing.B) {
  function BenchmarkParseObject (line 32) | func BenchmarkParseObject(b *testing.B) {
  constant bText (line 39) | bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&987...
  function BenchmarkCQCodeEscapeText (line 41) | func BenchmarkCQCodeEscapeText(b *testing.B) {
  function TestCQCodeEscapeText (line 48) | func TestCQCodeEscapeText(t *testing.T) {

FILE: internal/param/param.go
  function EnsureBool (line 22) | func EnsureBool(p any, defaultVal bool) bool {
  function SplitURL (line 63) | func SplitURL(s string) []string {

FILE: internal/selfdiagnosis/diagnoses.go
  function NetworkDiagnosis (line 10) | func NetworkDiagnosis(c *client.QQClient) {
  function DNSDiagnosis (line 74) | func DNSDiagnosis() {
  function EnvironmentDiagnosis (line 79) | func EnvironmentDiagnosis() {

FILE: internal/selfupdate/update.go
  function readLine (line 24) | func readLine() (str string) {
  function lastVersion (line 31) | func lastVersion() (string, error) {
  function CheckUpdate (line 40) | func CheckUpdate() {
  function binaryName (line 59) | func binaryName() string {
  function checksum (line 71) | func checksum(github, version string) []byte {
  function wait (line 93) | func wait() {
  function SelfUpdate (line 100) | func SelfUpdate(github string) {
  type writeSumCounter (line 139) | type writeSumCounter struct
    method Write (line 145) | func (wc *writeSumCounter) Write(p []byte) (int, error) {
  function logn (line 154) | func logn(n, b float64) float64 {
  function humanBytes (line 158) | func humanBytes(s uint64) string {
  function fromStream (line 174) | func fromStream(updateWith io.Reader) (err error, errRecover error) {

FILE: internal/selfupdate/update_others.go
  function update (line 16) | func update(url string, sum []byte) error {

FILE: internal/selfupdate/update_windows.go
  function update (line 13) | func update(url string, sum []byte) error {

FILE: main.go
  function main (line 16) | func main() {

FILE: modules/api/api.go
  method call (line 11) | func (c *Caller) call(action string, spec *onebot.Spec, p Getter) global...

FILE: modules/api/caller.go
  type Getter (line 15) | type Getter interface
  type Handler (line 20) | type Handler
  type Caller (line 23) | type Caller struct
    method Call (line 29) | func (c *Caller) Call(action string, spec *onebot.Spec, p Getter) glob...
    method Use (line 39) | func (c *Caller) Use(middlewares ...Handler) {
  function NewCaller (line 44) | func NewCaller(bot *coolq.CQBot) *Caller {

FILE: modules/config/config.go
  type Reconnect (line 22) | type Reconnect struct
  type Account (line 30) | type Account struct
  type SignServer (line 49) | type SignServer struct
  type Config (line 56) | type Config struct
  type Server (line 90) | type Server struct
  function Parse (line 96) | func Parse(path string) *Config {
  function AddServer (line 114) | func AddServer(s *Server) {
  function generateConfig (line 119) | func generateConfig() {
  function expand (line 154) | func expand(s string, mapping func(string) string) string {

FILE: modules/config/config_test.go
  function Test_expand (line 8) | func Test_expand(t *testing.T) {

FILE: modules/filter/filter.go
  type Filter (line 12) | type Filter interface
  type operationNode (line 16) | type operationNode struct
  type notOperator (line 22) | type notOperator struct
    method Eval (line 34) | func (op *notOperator) Eval(payload gjson.Result) bool {
  function newNotOp (line 26) | func newNotOp(argument gjson.Result) Filter {
  type andOperator (line 39) | type andOperator struct
    method Eval (line 76) | func (op *andOperator) Eval(payload gjson.Result) bool {
  function newAndOp (line 43) | func newAndOp(argument gjson.Result) Filter {
  type orOperator (line 96) | type orOperator struct
    method Eval (line 113) | func (op *orOperator) Eval(payload gjson.Result) bool {
  function newOrOp (line 100) | func newOrOp(argument gjson.Result) Filter {
  type eqOperator (line 125) | type eqOperator struct
    method Eval (line 134) | func (op *eqOperator) Eval(payload gjson.Result) bool {
  function newEqOp (line 129) | func newEqOp(argument gjson.Result) Filter {
  type neqOperator (line 139) | type neqOperator struct
    method Eval (line 148) | func (op *neqOperator) Eval(payload gjson.Result) bool {
  function newNeqOp (line 143) | func newNeqOp(argument gjson.Result) Filter {
  type inOperator (line 153) | type inOperator struct
    method Eval (line 176) | func (op *inOperator) Eval(payload gjson.Result) bool {
  function newInOp (line 158) | func newInOp(argument gjson.Result) Filter {
  type containsOperator (line 190) | type containsOperator struct
    method Eval (line 202) | func (op *containsOperator) Eval(payload gjson.Result) bool {
  function newContainOp (line 194) | func newContainOp(argument gjson.Result) Filter {
  type regexOperator (line 207) | type regexOperator struct
    method Eval (line 219) | func (op *regexOperator) Eval(payload gjson.Result) bool {
  function newRegexOp (line 211) | func newRegexOp(argument gjson.Result) Filter {
  function Generate (line 224) | func Generate(opName string, argument gjson.Result) Filter {

FILE: modules/filter/middlewares.go
  function Add (line 17) | func Add(file string) {
  function Find (line 38) | func Find(file string) Filter {

FILE: modules/pprof/pprof.go
  constant pprofDefault (line 19) | pprofDefault = `  # pprof 性能分析服务器, 一般情况下不需要启用.
  type pprofServer (line 30) | type pprofServer struct
  function init (line 36) | func init() {
  function runPprof (line 44) | func runPprof(_ *coolq.CQBot, node yaml.Node) {
  function init (line 73) | func init() {

FILE: modules/servers/servers.go
  function Register (line 17) | func Register(name string, proc func(*coolq.CQBot, yaml.Node)) {
  function RegisterCustom (line 26) | func RegisterCustom(name string, proc func(*coolq.CQBot)) {
  function Run (line 35) | func Run(bot *coolq.CQBot) {

FILE: modules/silk/codec.go
  constant silkCachePath (line 20) | silkCachePath = "data/cache"
  function encode (line 23) | func encode(record []byte, tempName string) (silkWav []byte, err error) {
  function resample (line 62) | func resample(data []byte) []byte {

FILE: modules/silk/codec_unsupported.go
  function encode (line 9) | func encode(record []byte, tempName string) ([]byte, error) {
  function resample (line 14) | func resample(data []byte) []byte {

FILE: modules/silk/stubs.go
  function init (line 8) | func init() {

FILE: pkg/onebot/attr.go
  type Attr (line 12) | type Attr struct
    method String (line 76) | func (a Attr) String() string {
  function String (line 18) | func String(key, value string) Attr {
  function Int64 (line 23) | func Int64(key string, value int64) Attr {
  function Int (line 29) | func Int(key string, value int) Attr {
  function Uint64 (line 34) | func Uint64(key string, v uint64) Attr {
  function Float64 (line 39) | func Float64(key string, v float64) Attr {
  function Bool (line 44) | func Bool(key string, v bool) Attr {
  function Time (line 50) | func Time(key string, v time.Time) Attr {
  function Duration (line 55) | func Duration(key string, v time.Duration) Attr {
  function Group (line 66) | func Group(key string, as ...Attr) Attr {
  function Any (line 72) | func Any(key string, value any) Attr {

FILE: pkg/onebot/kind_string.go
  function _ (line 7) | func _() {
  constant _Kind_name (line 22) | _Kind_name = "AnyBoolDurationFloat64Int64StringTimeUint64Group"
  method String (line 26) | func (i Kind) String() string {

FILE: pkg/onebot/onebot.go
  type Self (line 6) | type Self struct
  type Request (line 14) | type Request struct
  type Response (line 23) | type Response struct
  type Event (line 34) | type Event struct

FILE: pkg/onebot/spec.go
  type Spec (line 9) | type Spec struct
    method ConvertID (line 27) | func (s *Spec) ConvertID(id any) any {

FILE: pkg/onebot/value.go
  type Value (line 18) | type Value struct
    method Kind (line 52) | func (v Value) Kind() Kind {
    method Any (line 198) | func (v Value) Any() any {
    method String (line 229) | func (v Value) String() string {
    method str (line 237) | func (v Value) str() string {
    method Int64 (line 243) | func (v Value) Int64() int64 {
    method Uint64 (line 252) | func (v Value) Uint64() uint64 {
    method Bool (line 261) | func (v Value) Bool() bool {
    method bool (line 268) | func (v Value) bool() bool {
    method Duration (line 274) | func (v Value) Duration() time.Duration {
    method duration (line 282) | func (v Value) duration() time.Duration {
    method Float64 (line 288) | func (v Value) Float64() float64 {
    method float (line 296) | func (v Value) float() float64 {
    method Time (line 302) | func (v Value) Time() time.Time {
    method time (line 309) | func (v Value) time() time.Time {
    method Group (line 319) | func (v Value) Group() []Attr {
    method group (line 326) | func (v Value) group() []Attr {
    method append (line 332) | func (v Value) append(dst []byte) []byte {
  type stringptr (line 25) | type stringptr
  type groupptr (line 26) | type groupptr
  type Kind (line 32) | type Kind
  constant KindAny (line 36) | KindAny Kind = iota
  constant KindBool (line 37) | KindBool
  constant KindDuration (line 38) | KindDuration
  constant KindFloat64 (line 39) | KindFloat64
  constant KindInt64 (line 40) | KindInt64
  constant KindString (line 41) | KindString
  constant KindTime (line 42) | KindTime
  constant KindUint64 (line 43) | KindUint64
  constant KindGroup (line 44) | KindGroup
  type kind (line 49) | type kind
  function StringValue (line 72) | func StringValue(value string) Value {
  function IntValue (line 77) | func IntValue(v int) Value {
  function Int64Value (line 82) | func Int64Value(v int64) Value {
  function Uint64Value (line 87) | func Uint64Value(v uint64) Value {
  function Float64Value (line 92) | func Float64Value(v float64) Value {
  function BoolValue (line 97) | func BoolValue(v bool) Value {
  type timeLocation (line 107) | type timeLocation
  function TimeValue (line 111) | func TimeValue(v time.Time) Value {
  function DurationValue (line 123) | func DurationValue(v time.Duration) Value {
  function GroupValue (line 129) | func GroupValue(as ...Attr) Value {
  function AnyValue (line 148) | func AnyValue(v any) Value {

FILE: server/daemon.go
  function Daemon (line 18) | func Daemon() {
  function savePid (line 51) | func savePid(path string, data string) error {

FILE: server/doc.go
  function init (line 7) | func init() {

FILE: server/http.go
  type HTTPServer (line 36) | type HTTPServer struct
  type httpServerPost (line 52) | type httpServerPost struct
  type httpServer (line 59) | type httpServer struct
    method ServeHTTP (line 156) | func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *ht...
  type HTTPClient (line 66) | type HTTPClient struct
    method Run (line 321) | func (c HTTPClient) Run() {
    method onBotPushEvent (line 352) | func (c *HTTPClient) onBotPushEvent(e *coolq.Event) {
  type httpCtx (line 78) | type httpCtx struct
    method get (line 118) | func (h *httpCtx) get(pattern string, join bool) gjson.Result {
    method Get (line 148) | func (h *httpCtx) Get(s string) gjson.Result {
  constant httpDefault (line 84) | httpDefault = `
  function init (line 105) | func init() {
  function mayJSONParam (line 111) | func mayJSONParam(p string) bool {
  function checkAuth (line 221) | func checkAuth(req *http.Request, token string) int {
  function puint64Operator (line 246) | func puint64Operator(p *uint64, def uint64) uint64 {
  function runHTTP (line 254) | func runHTTP(bot *coolq.CQBot, node yaml.Node) {

FILE: server/http_test.go
  function TestHttpCtx_Get (line 11) | func TestHttpCtx_Get(t *testing.T) {

FILE: server/middlewares.go
  type MiddleWares (line 18) | type MiddleWares struct
  function rateLimit (line 28) | func rateLimit(frequency float64, bucketSize int) api.Handler {
  function longPolling (line 36) | func longPolling(bot *coolq.CQBot, maxSize int) api.Handler {

FILE: server/scf.go
  type lambdaClient (line 24) | type lambdaClient struct
    method next (line 180) | func (c *lambdaClient) next() *http.Request {
  type lambdaResponse (line 32) | type lambdaResponse struct
  type lambdaResponseWriter (line 39) | type lambdaResponseWriter struct
    method Write (line 45) | func (l *lambdaResponseWriter) Write(p []byte) (n int, err error) {
    method Header (line 49) | func (l *lambdaResponseWriter) Header() http.Header {
    method flush (line 53) | func (l *lambdaResponseWriter) flush() error {
    method WriteHeader (line 76) | func (l *lambdaResponseWriter) WriteHeader(statusCode int) {
  function runLambda (line 83) | func runLambda(bot *coolq.CQBot, node yaml.Node) {
  type lambdaInvoke (line 147) | type lambdaInvoke struct
  constant lambdaDefault (line 158) | lambdaDefault = `  # LambdaServer 配置
  type LambdaServer (line 166) | type LambdaServer struct
  function init (line 173) | func init() {

FILE: server/websocket.go
  type webSocketServer (line 31) | type webSocketServer struct
    method event (line 362) | func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) {
    method api (line 390) | func (s *webSocketServer) api(w http.ResponseWriter, r *http.Request) {
    method any (line 412) | func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
    method listenAPI (line 444) | func (s *webSocketServer) listenAPI(c *wsConn) {
    method onBotPushEvent (line 497) | func (s *webSocketServer) onBotPushEvent(e *coolq.Event) {
  type websocketClient (line 44) | type websocketClient struct
    method connect (line 238) | func (c *websocketClient) connect(typ, addr string, conptr **wsConn) {
    method listenAPI (line 308) | func (c *websocketClient) listenAPI(typ, url string, conn *wsConn) {
    method onBotPushEvent (line 339) | func (c *websocketClient) onBotPushEvent(typ, url string, conn **wsCon...
  type wsConn (line 56) | type wsConn struct
    method WriteText (line 62) | func (c *wsConn) WriteText(b []byte) error {
    method Close (line 69) | func (c *wsConn) Close() error {
    method handleRequest (line 468) | func (c *wsConn) handleRequest(_ *coolq.CQBot, payload []byte) {
  constant wsDefault (line 79) | wsDefault = `  # 正向WS设置
  constant wsReverseDefault (line 87) | wsReverseDefault = `  # 反向WS设置
  type WebsocketServer (line 103) | type WebsocketServer struct
  type WebsocketReverse (line 113) | type WebsocketReverse struct
  function init (line 123) | func init() {
  function runWSServer (line 135) | func runWSServer(b *coolq.CQBot, node yaml.Node) {
  function runWSClient (line 179) | func runWSClient(b *coolq.CQBot, node yaml.Node) {
  function resolveURI (line 220) | func resolveURI(addr string) (network, address string) {

FILE: winres/gen/json.go
  constant js (line 15) | js = `{
  constant timeformat (line 78) | timeformat = `2006-01-02T15:04:05+08:00`
  function main (line 80) | func main() {
Condensed preview — 118 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (587K chars).
[
  {
    "path": ".dockerignore",
    "chars": 58,
    "preview": ".gitlab-ci.yml\n.dockerignore\nDockerfile\nREADME.md\nLICENSE\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yaml",
    "chars": 3639,
    "preview": "name: 回报错误\ndescription: 在使用 go-cqhttp 的过程中遇到了错误\ntitle: '[Bug]: '\nlabels: [ \"bug?\" ]\n\nbody:\n  # User's README and agreeme"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feat--.md",
    "chars": 162,
    "preview": "---\nname: 新功能提议\nabout: 提出新功能\ntitle: ''\nlabels: feature request\nassignees: ''\n\n---\n\n**环境信息**\n<!-- 请尽量填写 -->\ngo-cqhttp版本: "
  },
  {
    "path": ".github/workflows/build_docker_image.yml",
    "chars": 2317,
    "preview": "name: Build And Push Docker Image\n\non:\n  push:\n    branches:\n      - 'master'\n      - 'dev'\n    # Sequence of patterns m"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1666,
    "preview": "name: CI\n\non: [push, pull_request,workflow_dispatch]\n\nenv:\n  BINARY_PREFIX: \"go-cqhttp_\"\n  BINARY_SUFFIX: \"\"\n  COMMIT_ID"
  },
  {
    "path": ".github/workflows/close_pr.yml",
    "chars": 699,
    "preview": "name: Check and Close Invalid PR\n\non:\n  pull_request_target:\n    types: [opened, reopened]\n\njobs:\n  # This workflow clos"
  },
  {
    "path": ".github/workflows/golint.yml",
    "chars": 1083,
    "preview": "name: Lint\n\non: [push,pull_request,workflow_dispatch]\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    s"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 962,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - nam"
  },
  {
    "path": ".gitignore",
    "chars": 199,
    "preview": "vendor/\n.idea\n.vscode\nconfig.hjson\nconfig.yml\nsession.token\ndevice.json\ndata/\nlogs/\ninternal/btree/*.lock\ninternal/btree"
  },
  {
    "path": ".golangci.yml",
    "chars": 1667,
    "preview": "linters-settings:\n  errcheck:\n    ignore: fmt:.*,io/ioutil:^Read.*\n    ignoretests: true\n\n  goimports:\n    local-prefixe"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 1926,
    "preview": "env:\n  - GO111MODULE=on\nbefore:\n  hooks:\n    - go mod tidy\n    - go install github.com/tc-hib/go-winres@latest\n    - go "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1211,
    "preview": "# Contributing to go-cqhttp\n\n想要成为 go-cqhttp 的 Contributor? Awesome!\n\n这个页面提供了一些 Tips ,可能对您的开发提供一些帮助.\n\n## 开发环境准备\n\ngo-cqhtt"
  },
  {
    "path": "Dockerfile",
    "chars": 902,
    "preview": "FROM golang:1.20-alpine AS builder\n\nRUN go env -w GO111MODULE=auto \\\n  && go env -w CGO_ENABLED=0 \\\n  && go env -w GOPRO"
  },
  {
    "path": "LICENSE",
    "chars": 34523,
    "preview": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C)"
  },
  {
    "path": "README.md",
    "chars": 16779,
    "preview": "<p align=\"center\">\n  <a href=\"https://ishkong.github.io/go-cqhttp-docs/\">\n    <img src=\"winres/icon.png\" width=\"200\" hei"
  },
  {
    "path": "cmd/api-generator/main.go",
    "chars": 7821,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/format\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\""
  },
  {
    "path": "cmd/api-generator/supported.go",
    "chars": 833,
    "preview": "package main\n\nimport \"html/template\"\n\nfunc (g *generator) genSupported(routers []Router) {\n\tvar v11, v12 []string // for"
  },
  {
    "path": "cmd/gocq/login.go",
    "chars": 5943,
    "preview": "package gocq\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Mrs4s/Mirai"
  },
  {
    "path": "cmd/gocq/main.go",
    "chars": 15231,
    "preview": "// Package gocq 程序的主体部分\npackage gocq\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t"
  },
  {
    "path": "cmd/gocq/qsign.go",
    "chars": 12151,
    "preview": "package gocq\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\""
  },
  {
    "path": "coolq/api.go",
    "chars": 64318,
    "preview": "package coolq\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"r"
  },
  {
    "path": "coolq/api_v12.go",
    "chars": 731,
    "preview": "package coolq\n\nimport (\n\t\"runtime\"\n\n\t\"github.com/tidwall/gjson\"\n\n\t\"github.com/Mrs4s/go-cqhttp/global\"\n\t\"github.com/Mrs4s"
  },
  {
    "path": "coolq/bot.go",
    "chars": 19178,
    "preview": "package coolq\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\"\n"
  },
  {
    "path": "coolq/converter.go",
    "chars": 5811,
    "preview": "package coolq\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/Mrs4s/MiraiGo/client\"\n\t\"github.com/Mrs4s/MiraiGo/message\"\n\t\""
  },
  {
    "path": "coolq/cqcode.go",
    "chars": 31826,
    "preview": "package coolq\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/url\"\n\t\"os\"\n\t\"pa"
  },
  {
    "path": "coolq/doc.go",
    "chars": 67,
    "preview": "// Package coolq 包含CQBot实例,CQ码处理,消息发送,消息处理等的相关函数与结构体\npackage coolq\n"
  },
  {
    "path": "coolq/event.go",
    "chars": 22985,
    "preview": "package coolq\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/Mrs4s/MiraiG"
  },
  {
    "path": "coolq/feed.go",
    "chars": 1196,
    "preview": "package coolq\n\nimport (\n\t\"github.com/Mrs4s/MiraiGo/topic\"\n\n\t\"github.com/Mrs4s/go-cqhttp/global\"\n)\n\n// FeedContentsToArra"
  },
  {
    "path": "db/database.go",
    "chars": 4701,
    "preview": "package db\n\nimport (\n\t\"fmt\"\n\t\"hash/crc32\"\n\n\t\"github.com/Mrs4s/go-cqhttp/global\"\n)\n\ntype (\n\t// Database 数据库操作接口定义\n\tDataba"
  },
  {
    "path": "db/leveldb/const.go",
    "chars": 332,
    "preview": "package leveldb\n\nconst dataVersion = 1\n\nconst (\n\tgroup        = 0x0\n\tprivate      = 0x1\n\tguildChannel = 0x2\n)\n\ntype code"
  },
  {
    "path": "db/leveldb/leveldb.go",
    "chars": 3286,
    "preview": "package leveldb\n\nimport (\n\t\"path\"\n\n\t\"github.com/Mrs4s/MiraiGo/binary\"\n\t\"github.com/Mrs4s/MiraiGo/utils\"\n\t\"github.com/pkg"
  },
  {
    "path": "db/leveldb/reader.go",
    "chars": 2951,
    "preview": "package leveldb\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/Mrs4s/g"
  },
  {
    "path": "db/leveldb/structs.go",
    "chars": 3799,
    "preview": "package leveldb\n\nimport \"github.com/Mrs4s/go-cqhttp/db\"\n\nfunc (w *writer) writeStoredGroupMessage(x *db.StoredGroupMessa"
  },
  {
    "path": "db/leveldb/writer.go",
    "chars": 2955,
    "preview": "package leveldb\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/Mrs4s/go-cqhttp/global\"\n)\n\ntype intWriter struct {\n\tbytes.Buffer\n}\n\nfun"
  },
  {
    "path": "db/mongodb/mongodb.go",
    "chars": 3376,
    "preview": "package mongodb\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mong"
  },
  {
    "path": "db/multidb.go",
    "chars": 2417,
    "preview": "package db\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/Mrs4s/go-cqhttp/internal/base\"\n)\n\n// bac"
  },
  {
    "path": "db/sqlite3/model.go",
    "chars": 2045,
    "preview": "package sqlite3\n\nconst (\n\tSqlite3GroupMessageTableName          = \"grpmsg\"\n\tSqlite3MessageAttributeTableName      = \"msg"
  },
  {
    "path": "db/sqlite3/sqlite3.go",
    "chars": 15867,
    "preview": "package sqlite3\n\nimport (\n\t\"encoding/base64\"\n\t\"hash/crc64\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\tsql \"github.com/Fl"
  },
  {
    "path": "docker-entrypoint.sh",
    "chars": 497,
    "preview": "#!/bin/sh\n\nUSER=abc\n\necho \"---Setup Timezone to ${TZ}---\"\necho \"${TZ}\" > /etc/timezone\necho \"---Checking if UID: ${UID} "
  },
  {
    "path": "docs/EventFilter.md",
    "chars": 3719,
    "preview": "# 事件过滤器\n\n在配置文件填写对应通信方式的 `middlewares.filter` 即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则不会启用事件过滤器。\n事件过滤器会"
  },
  {
    "path": "docs/QA.md",
    "chars": 192,
    "preview": "# 常见问题\n\n> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.\n\n### Q: 为什么"
  },
  {
    "path": "docs/README.md",
    "chars": 227,
    "preview": "# 文档\n\n> 文档目前依旧保留以便往前兼容\n\\\n下面的文档更易读以及人性化, 强烈建议您查看下面提供的文档\n\n目前文档已移动到位于 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp"
  },
  {
    "path": "docs/adminApi.md",
    "chars": 4119,
    "preview": "# 管理 API\n\n> 支持跨域\n\n## 公共参数\n\n参数:\n\n| 参数名       | 类型   | 说明                        |\n| ------------ | ------ | -------------"
  },
  {
    "path": "docs/config.md",
    "chars": 5470,
    "preview": "# 配置\n\n> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留, 所以内容可能有不足.\n\ngo-cqhttp 包含"
  },
  {
    "path": "docs/cqhttp.md",
    "chars": 35172,
    "preview": "# 拓展API\n\n由于部分 api 原版 CQHTTP 并未实现,go-cqhttp 修改并增加了一些拓展 api \n\n> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/"
  },
  {
    "path": "docs/file.md",
    "chars": 1369,
    "preview": "# 文件\n\ngo-cqhttp 默认生成的文件树如下所示:\n\n```\n.\n├── go-cqhttp\n├── config.yml\n├── device.json\n├── logs\n│   └── xx-xx-xx.log\n└── data"
  },
  {
    "path": "docs/guild.md",
    "chars": 10951,
    "preview": "# 频道相关API\n\n> 注意: QQ频道功能目前还在测试阶段, go-cqhttp 也在适配的初期阶段, 以下 `API` `Event` 的字段名可能存在错误并均有可能在后续版本修改/添加/删除.\n> 目前仅供开发者测试以及适配使用\n\n"
  },
  {
    "path": "docs/quick_start.md",
    "chars": 3113,
    "preview": "#  开始\n\n欢迎来到 go-cqhttp 文档 目前还在咕\n\n> 注意, 最新文档已经移动到 [go-cqhttp-docs](https://github.com/ishkong/go-cqhttp-docs), 当前文档只做兼容性保留"
  },
  {
    "path": "docs/slider.md",
    "chars": 1993,
    "preview": "# 滑块验证码\n\n> 该文档已过期, 最新版本下可直接使用手机扫描二维码通过验证.\n\n由于TX最新的限制, 所有协议在陌生设备/IP登录时都有可能被要求通过滑块验证码, 否则将会出现 `当前上网环境异常` 的错误. 目前我们准备了两个临时方"
  },
  {
    "path": "global/all_test.go",
    "chars": 903,
    "preview": "package global\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestVersionNameCompare(t *"
  },
  {
    "path": "global/buffer.go",
    "chars": 321,
    "preview": "package global\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/Mrs4s/MiraiGo/binary\" // 和 MiraiGo 共用同一 buffer 池\n)\n\n// NewBuffer 从池中获取新 "
  },
  {
    "path": "global/codec.go",
    "chars": 1416,
    "preview": "package global\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/"
  },
  {
    "path": "global/doc.go",
    "chars": 87,
    "preview": "// Package global 包含文件下载,视频音频编码,本地文件缓存处理,消息过滤器,调用速率限制,gocq主配置等的相关函数与结构体\npackage global\n"
  },
  {
    "path": "global/fs.go",
    "chars": 3603,
    "preview": "package global\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"runtim"
  },
  {
    "path": "global/log_hook.go",
    "chars": 5301,
    "preview": "package global\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/mattn/go-colora"
  },
  {
    "path": "global/net.go",
    "chars": 992,
    "preview": "package global\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/tidwall/gjson\"\n\n\t\"github.com/Mrs4s/go-cqhttp/internal/download\"\n)\n\n// QQMu"
  },
  {
    "path": "global/param.go",
    "chars": 980,
    "preview": "package global\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// MSG 消息Map\ntype MSG = map[string]a"
  },
  {
    "path": "global/signal.go",
    "chars": 926,
    "preview": "package global\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n"
  },
  {
    "path": "global/signal_unix.go",
    "chars": 650,
    "preview": "//go:build !windows\n// +build !windows\n\npackage global\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n)\n\n// SetupMainSi"
  },
  {
    "path": "global/signal_windows.go",
    "chars": 1888,
    "preview": "//go:build windows\n// +build windows\n\npackage global\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"s"
  },
  {
    "path": "global/terminal/doc.go",
    "chars": 90,
    "preview": "// Package terminal 包含用于检测在windows下是否通过双击运行go-cqhttp, 禁用快速编辑, 启用VT100的函数\npackage terminal\n"
  },
  {
    "path": "global/terminal/double_click.go",
    "chars": 245,
    "preview": "//go:build !windows\n\npackage terminal\n\n// RunningByDoubleClick 检查是否通过双击直接运行,非Windows系统永远返回false\nfunc RunningByDoubleClic"
  },
  {
    "path": "global/terminal/double_click_windows.go",
    "chars": 2570,
    "preview": "package terminal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/pkg/errors\"\n)\n\n//"
  },
  {
    "path": "global/terminal/quick_edit.go",
    "chars": 222,
    "preview": "//go:build !windows\n\npackage terminal\n\n// RestoreInputMode 还原输入模式,非Windows系统永远返回nil\nfunc RestoreInputMode() error {\n\tret"
  },
  {
    "path": "global/terminal/quick_edit_windows.go",
    "chars": 1012,
    "preview": "package terminal\n\nimport (\n\t\"os\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nvar inputmode uint32\n\n// RestoreInputMode 还原输入模式\nfunc R"
  },
  {
    "path": "global/terminal/title.go",
    "chars": 262,
    "preview": "//go:build !windows\n\npackage terminal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/Mrs4s/go-cqhttp/internal/base\"\n)\n\n// SetTit"
  },
  {
    "path": "global/terminal/title_windows.go",
    "chars": 584,
    "preview": "package terminal\n\nimport (\n\t\"fmt\"\n\t\"syscall\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/Mrs4s/go-cqhtt"
  },
  {
    "path": "global/terminal/vt100.go",
    "chars": 123,
    "preview": "//go:build !windows\n\npackage terminal\n\n// EnableVT100 启用颜色、控制字符,非Windows系统永远返回nil\nfunc EnableVT100() error {\n\treturn nil"
  },
  {
    "path": "global/terminal/vt100_windows.go",
    "chars": 433,
    "preview": "package terminal\n\nimport (\n\t\"os\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n// EnableVT100 启用颜色、控制字符\nfunc EnableVT100() error {\n\tst"
  },
  {
    "path": "go.mod",
    "chars": 2816,
    "preview": "module github.com/Mrs4s/go-cqhttp\n\ngo 1.20\n\nrequire (\n\tgithub.com/FloatTech/sqlite v1.6.3\n\tgithub.com/Microsoft/go-winio"
  },
  {
    "path": "go.sum",
    "chars": 19726,
    "preview": "github.com/FloatTech/sqlite v1.6.3 h1:MQkqBNlkPuCoKQQgoNLuTL/2Ci3tBTFAnVYBdD0Wy4M=\ngithub.com/FloatTech/sqlite v1.6.3/go"
  },
  {
    "path": "internal/base/feature.go",
    "chars": 344,
    "preview": "package base\n\nimport (\n\t\"github.com/pkg/errors\"\n)\n\n// silk encode features\nvar (\n\tEncodeSilk   = encodeSilk   // 编码 Silk"
  },
  {
    "path": "internal/base/flag.go",
    "chars": 4320,
    "preview": "// Package base provides base config for go-cqhttp\npackage base\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tlog \"github.com"
  },
  {
    "path": "internal/base/version.go",
    "chars": 239,
    "preview": "package base\n\nimport \"runtime/debug\"\n\n// Version go-cqhttp的版本信息,在编译时使用ldflags进行覆盖\nvar Version = \"unknown\"\n\nfunc init() {"
  },
  {
    "path": "internal/cache/cache.go",
    "chars": 964,
    "preview": "// Package cache impl the cache for gocq\npackage cache\n\nimport (\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/syndtr/g"
  },
  {
    "path": "internal/download/download.go",
    "chars": 8113,
    "preview": "// Package download provide download utility functions\npackage download\n\nimport (\n\t\"bufio\"\n\t\"compress/gzip\"\n\t\"crypto/tls"
  },
  {
    "path": "internal/mime/mime.go",
    "chars": 985,
    "preview": "// Package mime 提供MIME检查功能\npackage mime\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/Mrs4s/go-cqhttp/internal/ba"
  },
  {
    "path": "internal/msg/element.go",
    "chars": 4849,
    "preview": "// Package msg 提供了go-cqhttp消息中间表示,CQ码处理等等\npackage msg\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/Mrs4s/"
  },
  {
    "path": "internal/msg/element_test.go",
    "chars": 440,
    "preview": "package msg\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nfunc jsonMarshal(s string) string {\n\tb, err := json.Marshal(s)\n\tif "
  },
  {
    "path": "internal/msg/local.go",
    "chars": 685,
    "preview": "package msg\n\nimport (\n\t\"io\"\n\n\t\"github.com/Mrs4s/MiraiGo/message\"\n)\n\n// Poke 拍一拍\ntype Poke struct {\n\tTarget int64\n}\n\n// T"
  },
  {
    "path": "internal/msg/parse.go",
    "chars": 1719,
    "preview": "package msg\n\nimport (\n\t\"github.com/tidwall/gjson\"\n)\n\n// ParseObject 将消息JSON对象转为消息元素数组\nfunc ParseObject(m gjson.Result) ("
  },
  {
    "path": "internal/msg/parse_test.go",
    "chars": 1650,
    "preview": "package msg\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/Mrs4s/MiraiGo/utils\"\n\t\"github.com/stretchr/testify/asse"
  },
  {
    "path": "internal/param/param.go",
    "chars": 1576,
    "preview": "// Package param provide some util for param parse\npackage param\n\nimport (\n\t\"math\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"githu"
  },
  {
    "path": "internal/selfdiagnosis/diagnoses.go",
    "chars": 2901,
    "preview": "// Package selfdiagnosis 自我诊断相关\npackage selfdiagnosis\n\nimport (\n\t\"github.com/Mrs4s/MiraiGo/client\"\n\tlog \"github.com/siru"
  },
  {
    "path": "internal/selfupdate/update.go",
    "chars": 5557,
    "preview": "// Package selfupdate 版本升级检查和自更新\npackage selfupdate\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"m"
  },
  {
    "path": "internal/selfupdate/update_others.go",
    "chars": 804,
    "preview": "//go:build !windows\n\npackage selfupdate\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\""
  },
  {
    "path": "internal/selfupdate/update_windows.go",
    "chars": 685,
    "preview": "package selfupdate\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// update go-cqhttp"
  },
  {
    "path": "main.go",
    "chars": 659,
    "preview": "// Package main\npackage main\n\nimport (\n\t\"github.com/Mrs4s/go-cqhttp/cmd/gocq\"\n\t\"github.com/Mrs4s/go-cqhttp/global/termin"
  },
  {
    "path": "modules/api/api.go",
    "chars": 12305,
    "preview": "// Code generated by cmd/api-generator. DO NOT EDIT.\n\npackage api\n\nimport (\n\t\"github.com/Mrs4s/go-cqhttp/coolq\"\n\t\"github"
  },
  {
    "path": "modules/api/caller.go",
    "chars": 1113,
    "preview": "// Package api implements the API route for servers.\npackage api\n\nimport (\n\t\"github.com/tidwall/gjson\"\n\n\t\"github.com/Mrs"
  },
  {
    "path": "modules/config/config.go",
    "chars": 4429,
    "preview": "// Package config 包含go-cqhttp操作配置文件的相关函数\npackage config\n\nimport (\n\t\"bufio\"\n\t_ \"embed\" // embed the default config file\n\t"
  },
  {
    "path": "modules/config/config_test.go",
    "chars": 947,
    "preview": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc Test_expand(t *testing.T) {\n\tnullStringMapping := func(_ string) "
  },
  {
    "path": "modules/config/default_config.yml",
    "chars": 3797,
    "preview": "# go-cqhttp 默认配置文件\n\naccount: # 账号相关\n  uin: 1233456 # QQ账号\n  password: '' # 密码为空时使用扫码登录\n  encrypt: false  # 是否开启密码加密\n  st"
  },
  {
    "path": "modules/filter/filter.go",
    "chars": 5504,
    "preview": "// Package filter implements an event filter for go-cqhttp\npackage filter\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/t"
  },
  {
    "path": "modules/filter/middlewares.go",
    "chars": 782,
    "preview": "package filter\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/tidwall/gjson\"\n)\n\nvar (\n\tfilters     "
  },
  {
    "path": "modules/pprof/pprof.go",
    "chars": 1773,
    "preview": "// Package pprof provide pprof server of go-cqhttp\npackage pprof\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"os\"\n\t\""
  },
  {
    "path": "modules/servers/servers.go",
    "chars": 913,
    "preview": "// Package servers provide servers register\npackage servers\n\nimport (\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/Mrs4s/go-cqhttp/"
  },
  {
    "path": "modules/silk/codec.go",
    "chars": 1780,
    "preview": "//go:build (linux || (windows && !arm && !arm64) || darwin) && (386 || amd64 || arm || arm64) && !race && !nosilk\n// +bu"
  },
  {
    "path": "modules/silk/codec_unsupported.go",
    "chars": 489,
    "preview": "//go:build (!arm && !arm64 && !amd64 && !386) || (!windows && !linux && !darwin) || (windows && arm) || (windows && arm6"
  },
  {
    "path": "modules/silk/stubs.go",
    "chars": 169,
    "preview": "// Package silk Silk编码核心模块\npackage silk\n\nimport (\n\t\"github.com/Mrs4s/go-cqhttp/internal/base\"\n)\n\nfunc init() {\n\tbase.Enc"
  },
  {
    "path": "pkg/onebot/attr.go",
    "chars": 1932,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "pkg/onebot/kind_string.go",
    "chars": 803,
    "preview": "// Code generated by \"stringer -type=Kind -trimprefix=Kind\"; DO NOT EDIT.\n\npackage onebot\n\nimport \"strconv\"\n\nfunc _() {\n"
  },
  {
    "path": "pkg/onebot/onebot.go",
    "chars": 983,
    "preview": "package onebot\n\n// Self 机器人自身标识\n//\n// https://12.onebot.dev/connect/data-protocol/basic-types/#_10\ntype Self struct {\n\tP"
  },
  {
    "path": "pkg/onebot/spec.go",
    "chars": 669,
    "preview": "// Package onebot defines onebot protocol struct and some spec info.\npackage onebot\n\nimport \"fmt\"\n\n//go:generate go run "
  },
  {
    "path": "pkg/onebot/supported.go",
    "chars": 3768,
    "preview": "// Code generated by cmd/api-generator. DO NOT EDIT.\n\npackage onebot\n\nvar supportedV11 = []string{\n\t\".get_word_slices\",\n"
  },
  {
    "path": "pkg/onebot/value.go",
    "chars": 8669,
    "preview": "// Copyright 2022 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "scripts/bootstrap",
    "chars": 215,
    "preview": "#!/usr/bin/env bash\nfunction index.main_handler() {\n    echo \"Start GOCQHTTP~~~\"\n    cp -f config.yml /tmp/config.yml\n  "
  },
  {
    "path": "scripts/upload_dist.sh",
    "chars": 542,
    "preview": "#!/bin/bash\n\nif [ \"$GITHUB_ACTIONS\" != \"true\" ]; then\n    echo \"This script is only meant to be run in GitHub Actions.\"\n"
  },
  {
    "path": "server/daemon.go",
    "chars": 1026,
    "preview": "package server\n\n// daemon 功能写在这,目前仅支持了-d 作为后台运行参数,stop,start,restart这些功能目前看起来并不需要,可以通过api控制,后续需要的话再补全。\n\nimport (\n\t\"os\"\n\t"
  },
  {
    "path": "server/doc.go",
    "chars": 307,
    "preview": "// Package server 包含HTTP,WebSocket,反向WebSocket请求处理的相关函数与结构体\npackage server\n\nimport \"github.com/Mrs4s/go-cqhttp/modules/s"
  },
  {
    "path": "server/http.go",
    "chars": 10616,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding"
  },
  {
    "path": "server/http_test.go",
    "chars": 723,
    "preview": "package server\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/tidwall/gjson\"\n)\n\nfun"
  },
  {
    "path": "server/middlewares.go",
    "chars": 2216,
    "preview": "package server\n\nimport (\n\t\"container/list\"\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Mrs4s/go-cqhttp/coolq\"\n\t\"github.com/"
  },
  {
    "path": "server/scf.go",
    "chars": 4972,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\""
  },
  {
    "path": "server/websocket.go",
    "chars": 12832,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"runtime/deb"
  },
  {
    "path": "winres/.gitignore",
    "chars": 11,
    "preview": "winres.json"
  },
  {
    "path": "winres/gen/json.go",
    "chars": 2753,
    "preview": "// Package main generates winres.json\npackage main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gith"
  },
  {
    "path": "winres/init.go",
    "chars": 105,
    "preview": "// Package winres 生成windows资源\npackage winres\n\n//go:generate go run github.com/Mrs4s/go-cqhttp/winres/gen\n"
  }
]

About this extraction

This page contains the full source code of the Mrs4s/go-cqhttp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 118 files (520.7 KB), approximately 176.5k tokens, and a symbol index with 722 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!