diff --git a/.env b/.env
new file mode 100644
index 00000000..920b3df9
--- /dev/null
+++ b/.env
@@ -0,0 +1,8 @@
+# DB_TYPE=mysql
+# DB_DSN="root:root@tcp(127.0.0.1:3306)/nokoti?charset=utf8mb4&parseTime=True&loc=Local"
+
+# DB_TYPE=postgres
+# DB_DSN="postgres://postgres:pinenut666@localhost:5432/nokoti?sslmode=disable"
+# SQLITE IS DIFFERENT
+DB_TYPE=sqlite
+# DATA_DIR="./data/default"
diff --git a/.github/workflows/cleanup-caches.yml b/.github/workflows/cleanup-caches.yml
new file mode 100644
index 00000000..58a8611e
--- /dev/null
+++ b/.github/workflows/cleanup-caches.yml
@@ -0,0 +1,29 @@
+name: Cleanup Caches from Pull Request
+on:
+ pull_request:
+ types:
+ - closed
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cleanup
+ run: |
+ gh extension install actions/gh-actions-cache
+
+ echo "Fetching list of cache key"
+ cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
+
+ ## Setting this to not fail the workflow while deleting cache keys.
+ set +e
+ echo "Deleting caches..."
+ for cacheKey in $cacheKeysForPR
+ do
+ gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
+ done
+ echo "Done"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ REPO: ${{ github.repository }}
+ BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
new file mode 100644
index 00000000..a5f56aba
--- /dev/null
+++ b/.github/workflows/reviewdog.yml
@@ -0,0 +1,39 @@
+name: Review Dog
+
+on:
+ pull_request_target:
+ paths:
+ - '**.go'
+ - 'go.mod'
+ - '.github/workflows/reviewdog.yml'
+
+jobs:
+ review-dog:
+ permissions:
+ checks: write
+ contents: read
+ pull-requests: write
+ name: Review Dog
+ runs-on: ubuntu-latest
+ steps:
+ - name: Code
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ ref: ${{ github.head_ref }}
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.22'
+ - run: go get
+ - run: go generate ./...
+ - name: Set Up GolangCI-Lint
+ run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0
+ - uses: reviewdog/action-setup@v1
+ with:
+ reviewdog_version: latest
+ - name: Run golangci-lint & reviewdog
+ env:
+ REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ golangci-lint run | reviewdog -reporter=github-pr-review -f=golangci-lint -filter-mode=nofilter -fail-on-error
\ No newline at end of file
diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml
new file mode 100644
index 00000000..62cf5b77
--- /dev/null
+++ b/.github/workflows/test-and-lint.yml
@@ -0,0 +1,37 @@
+name: Test & Lint
+
+on:
+ push:
+ paths:
+ - '**.go'
+ - 'go.mod'
+ - 'test-and-lint.yml'
+ pull_request:
+ paths:
+ - '**.go'
+ - 'go.mod'
+ - 'test-and-lint.yml'
+
+jobs:
+ test-and-lint:
+ name: Test & Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Code
+ uses: actions/checkout@v4
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.22'
+ - run: go get
+ - run: go generate ./...
+
+ - run: go test -v -race ./...
+ - run: go vet ./...
+
+ - name: GolangCI-Lint
+ uses: golangci/golangci-lint-action@v6
+ if: github.event_name != 'pull_request'
+ with:
+ version: 'v1.61.0'
+ args: '--timeout 9999s'
\ No newline at end of file
diff --git a/.github/workflows/test_and_lint.yml b/.github/workflows/test_and_lint.yml
deleted file mode 100644
index a2f3f22a..00000000
--- a/.github/workflows/test_and_lint.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-on:
- push:
- paths:
- - '**.go'
- - 'go.mod'
- - '.github/workflows/test_and_lint.yml'
- pull_request:
- paths:
- - '**.go'
- - 'go.mod'
- - '.github/workflows/test_and_lint.yml'
-
-name: Test & Lint
-
-jobs:
- test-and-lint:
- runs-on: ubuntu-latest
- steps:
- - name: Code
- uses: actions/checkout@v4
- - name: Install Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.20'
- cache: false
- - run: go get
- - run: go generate ./...
-
- - run: go test -v ./...
- - run: go vet ./...
-
- - name: GolangCI-Lint
- uses: golangci/golangci-lint-action@v3
- if: github.event.name == 'pull_request'
- with:
- version: 'v1.55.2'
- args: '--timeout 9999s'
- only-new-issues: true
- skip-pkg-cache: true
- skip-build-cache: true
-
- - name: GolangCI-Lint
- uses: golangci/golangci-lint-action@v3
- if: github.event.name != 'pull_request'
- with:
- version: 'v1.55.2'
- args: '--timeout 9999s'
- skip-pkg-cache: true
- skip-build-cache: true
diff --git a/.gitignore b/.gitignore
index 61484bea..701fbea2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,5 @@ _help_cache
.vscode/
!.vscode/settings.json
+!.vscode/extensions.json
+sealdice-lock.lock
diff --git a/.golangci.yml b/.golangci.yml
index 5de02ea0..c08655b0 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,7 +1,7 @@
# This code is licensed under the terms of the MIT license https://opensource.org/license/mit
# Copyright (c) 2021 Marat Reymers
-## Golden config for golangci-lint v1.54.2
+## Golden config for golangci-lint v1.60.3
#
# This is the best config for golangci-lint based on my experience and opinion.
# It is very strict, but not extremely strict.
@@ -12,7 +12,6 @@ run:
# Default: 1m
timeout: 3m
-
# This file contains only configs which differ from defaults.
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
linters-settings:
@@ -54,9 +53,11 @@ linters-settings:
- G101
- G501
- G505
+ - G115
exhaustruct:
- # List of regular expressions to exclude struct packages and names from check.
+ # List of regular expressions to exclude struct packages and their names from checks.
+ # Regular expressions must match complete canonical struct package/name/structname.
# Default: []
exclude:
# std libs
@@ -128,25 +129,6 @@ linters-settings:
# Default: ""
local-prefixes: sealdice-core
- gomnd:
- # List of function patterns to exclude from analysis.
- # Values always ignored: `time.Date`,
- # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
- # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
- # Default: []
- ignored-functions:
- - flag.Arg
- - flag.Duration.*
- - flag.Float.*
- - flag.Int.*
- - flag.Uint.*
- - os.Chmod
- - os.Mkdir.*
- - os.OpenFile
- - os.WriteFile
- - prometheus.ExponentialBuckets.*
- - prometheus.LinearBuckets
-
gomodguard:
blocked:
# List of blocked modules.
@@ -162,8 +144,8 @@ linters-settings:
reason: "satori's package is not maintained"
- github.com/gofrs/uuid:
recommendations:
- - github.com/google/uuid
- reason: "gofrs' package is not go module"
+ - github.com/gofrs/uuid/v5
+ reason: "gofrs' package was not go module before v5"
govet:
# Enable all analyzers.
@@ -174,7 +156,6 @@ linters-settings:
# Default: []
disable:
- fieldalignment # too strict
- # - shadow # TOOOOOO noisy
# Settings per analyzer.
settings:
shadow:
@@ -182,6 +163,31 @@ linters-settings:
# Default: false
strict: false
+ inamedparam:
+ # Skips check for interface methods with only a single parameter.
+ # Default: false
+ skip-single-param: true
+
+ mnd:
+ # List of function patterns to exclude from analysis.
+ # Values always ignored: `time.Date`,
+ # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
+ # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
+ # Default: []
+ ignored-functions:
+ - args.Error
+ - flag.Arg
+ - flag.Duration.*
+ - flag.Float.*
+ - flag.Int.*
+ - flag.Uint.*
+ - os.Chmod
+ - os.Mkdir.*
+ - os.OpenFile
+ - os.WriteFile
+ - prometheus.ExponentialBuckets.*
+ - prometheus.LinearBuckets
+
nakedret:
# Make an issue if func has more lines of code than this setting, and it has naked returns.
# Default: 30
@@ -190,7 +196,7 @@ linters-settings:
nolintlint:
# Exclude following linters from requiring an explanation.
# Default: []
- allow-no-explanation: [ funlen, gocognit, lll ]
+ allow-no-explanation: [funlen, gocognit, lll]
# Enable to require an explanation of nonzero length after each nolint directive.
# Default: false
require-explanation: false
@@ -199,18 +205,102 @@ linters-settings:
require-specific: false
allow-unused: true
+ perfsprint:
+ # Optimizes into strings concatenation.
+ # Default: true
+ strconcat: false
+
rowserrcheck:
# database/sql is always checked
# Default: []
packages:
- github.com/jmoiron/sqlx
+ sloglint:
+ # Enforce not using global loggers.
+ # Values:
+ # - "": disabled
+ # - "all": report all global loggers
+ # - "default": report only the default slog logger
+ # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
+ # Default: ""
+ no-global: "default"
+ # Enforce using methods that accept a context.
+ # Values:
+ # - "": disabled
+ # - "all": report all contextless calls
+ # - "scope": report only if a context exists in the scope of the outermost function
+ # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
+ # Default: ""
+ context: "scope"
+
tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
# Default: false
all: true
+ stylecheck:
+ # STxxxx checks in https://staticcheck.io/docs/configuration/options/#checks
+ # Default: ["*"]
+ checks:
+ ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
+ # https://staticcheck.io/docs/configuration/options/#dot_import_whitelist
+ # Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"]
+ dot-import-whitelist:
+ - fmt
+ # https://staticcheck.io/docs/configuration/options/#initialisms
+ # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
+ initialisms:
+ [
+ "ACL",
+ "ASCII",
+ "CPU",
+ "CSS",
+ "DNS",
+ "EOF",
+ "GUID",
+ "HTML",
+ "HTTP",
+ "HTTPS",
+ "ID",
+ "IP",
+ "JSON",
+ "QPS",
+ "RAM",
+ "RPC",
+ "SLA",
+ "SMTP",
+ "SQL",
+ "SSH",
+ "TCP",
+ "TLS",
+ "TTL",
+ "UDP",
+ "UI",
+ "GID",
+ "UID",
+ "UUID",
+ "URI",
+ "UTF8",
+ "VM",
+ "XML",
+ "XMPP",
+ "XSRF",
+ "XSS",
+ "SIP",
+ "RTP",
+ "AMQP",
+ "DB",
+ "TS",
+ ]
+ # https://staticcheck.io/docs/configuration/options/#http_status_code_whitelist
+ # Default: ["200", "400", "404", "500"]
+ http-status-code-whitelist: ["200", "400", "404", "500"]
+
+ forbidigo:
+ exclude-godoc-examples: true
+ analyze-types: true
linters:
disable-all: true
@@ -228,53 +318,64 @@ linters:
- asciicheck # checks that your code does not contain non-ASCII identifiers
- bidichk # checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- #TooMany - cyclop # checks function and package cyclomatic complexity
- # NOTE(Xiangze Li): disabled due to existing bad code
- #- dupl # tool for code clone detection
+ - canonicalheader # checks whether net/http.Header uses canonical header
+ - copyloopvar # detects places where loop variables are copied (Go 1.22+)
+ # - cyclop # checks function and package cyclomatic complexity
+ # - dupl # tool for code clone detection
- durationcheck # checks for two durations multiplied together
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
- - execinquery # checks query string in Query function which reads your Go src files and warning it finds
- exhaustive # checks exhaustiveness of enum switch statements
- - exportloopref # checks for pointers to enclosing loop variables
- #- forbidigo # forbids identifiers
- #- funlen # tool for detection of long functions
+ - fatcontext # detects nested contexts in loops
+ - forbidigo # forbids identifiers
+ # - funlen # tool for detection of long functions
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
+ # - gochecknoglobals # checks that no global variables exist
- gochecknoinits # checks that no init functions are present in Go code
- #TooMany - gocognit # computes and checks the cognitive complexity of functions
- #TODO - goconst # finds repeated strings that could be replaced by a constant
+ - gochecksumtype # checks exhaustiveness on Go "sum types"
+ # - gocognit # computes and checks the cognitive complexity of functions
+ # - goconst # finds repeated strings that could be replaced by a constant
- gocritic # provides diagnostics that check for bugs, performance and style issues
- #TooMany - gocyclo # computes and checks the cyclomatic complexity of functions
- #- godot # checks if comments end in a period
+ # - gocyclo # computes and checks the cyclomatic complexity of functions
+ # - godot # checks if comments end in a period
- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
- #- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
+ # - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
- goprintffuncname # checks that printf-like functions are named with f at the end
- gosec # inspects source code for security problems
- #TooMany - lll # reports long lines
+ - intrange # finds places where for loops could make use of an integer range
+ # - lll # reports long lines
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
- makezero # finds slice declarations with non-zero initial length
- # - mirror # reports wrong mirror patterns of bytes/strings usage
- #FP - musttag # enforces field tags in (un)marshaled structs
- #- nakedret # finds naked returns in functions greater than a specified function length
+ - mirror # reports wrong mirror patterns of bytes/strings usage
+ # - mnd # detects magic numbers
+ # - musttag # enforces field tags in (un)marshaled structs
+ # - nakedret # finds naked returns in functions greater than a specified function length
- nestif # reports deeply nested if statements
- nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
- #- noctx # finds sending http request without context.Context
+ # - noctx # finds sending http request without context.Context
- nolintlint # reports ill-formed or insufficient nolint directives
- #- nonamedreturns # reports all named returns
+ # - nonamedreturns # reports all named returns
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
+ - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
- predeclared # finds code that shadows one of Go's predeclared identifiers
- promlinter # checks Prometheus metrics naming via promlint
+ - protogetter # reports direct reads from proto message fields when getters should be used
- reassign # checks that package variables are not reassigned
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
- #- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
- #- stylecheck # is a replacement for golint
+ # - rowserrcheck # checks whether Err of rows is checked successfully
+ - sloglint # ensure consistent code style when using log/slog
+ - spancheck # checks for mistakes with OpenTelemetry/Census spans
+ - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
+ - stylecheck # is a replacement for golint
- tenv # detects using os.Setenv instead of t.Setenv since Go1.17
- testableexamples # checks if examples are testable (have an expected output)
+ - testifylint # checks usage of github.com/stretchr/testify
+ # - testpackage # makes you use a separate _test package
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
- unconvert # removes unnecessary type conversions
- #return values - unparam # reports unused function parameters
+ # - unparam # reports unused function parameters
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
- wastedassign # finds wasted assignment statements
- whitespace # detects leading and trailing whitespace
@@ -286,7 +387,7 @@ linters:
#- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
#- godox # detects FIXME, TODO and other comment keywords
#- goheader # checks is file header matches to pattern
- #- gomnd # detects magic numbers
+ #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
#- interfacebloat # checks the number of methods inside an interface
#- ireturn # accept interfaces, return concrete types
#- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
@@ -294,7 +395,6 @@ linters:
#- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
#- wrapcheck # checks that errors returned from external packages are wrapped
#- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
- #- rowserrcheck # checks whether Err of rows is checked successfully
## disabled
#- containedctx # detects struct contained context.Context field
@@ -302,10 +402,11 @@ linters:
#- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
#- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
#- dupword # [useless without config] checks for duplicate words in the source code
+ #- err113 # [too strict] checks the errors handling expressions
#- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
+ #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds
+ #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables
#- forcetypeassert # [replaced by errcheck] finds forced type assertions
- #- gochecknoglobals # checks that no global variables exist
- #- goerr113 # [too strict] checks the errors handling expressions
#- gofmt # [replaced by goimports] checks whether code was gofmt-ed
#- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
#- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
@@ -316,23 +417,9 @@ linters:
#- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
#- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
#- tagliatelle # checks the struct tags
- #- testpackage # makes you use a separate _test package
#- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
#- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
- ## deprecated
- #- deadcode # [deprecated, replaced by unused] finds unused code
- #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized
- #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
- #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible
- #- interfacer # [deprecated] suggests narrower interface types
- #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted
- #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name
- #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs
- #- structcheck # [deprecated, replaced by unused] finds unused struct fields
- #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants
-
-
issues:
# Maximum count of issues with the same text.
# Set to 0 to disable.
@@ -341,9 +428,9 @@ issues:
exclude-rules:
- source: "(noinspection|TODO)"
- linters: [ godot ]
+ linters: [godot]
- source: "//noinspection"
- linters: [ gocritic ]
+ linters: [gocritic]
- path: "_test\\.go"
linters:
- bodyclose
diff --git a/.reviewdog.yml b/.reviewdog.yml
new file mode 100644
index 00000000..e3f29d13
--- /dev/null
+++ b/.reviewdog.yml
@@ -0,0 +1,10 @@
+# reviewdog.yml
+runner:
+ golint-by-project-conf:
+ cmd: golint $(go list ./... | grep -v /vendor/)
+ format: golint
+ level: warning
+ govet-by-project-conf:
+ cmd: go vet
+ format: govet
+ level: error
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..4d56811d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["golang.go", "task.vscode-task"]
+}
diff --git a/Taskfile.yml b/Taskfile.yml
new file mode 100644
index 00000000..f9cba7b7
--- /dev/null
+++ b/Taskfile.yml
@@ -0,0 +1,36 @@
+# https://taskfile.dev
+
+version: '3'
+
+tasks:
+ install:
+ cmds:
+ - go mod download
+ - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
+ - go install golang.org/x/tools/cmd/goimports@latest
+ - go install github.com/pointlander/peg@v1.0.1
+ - go generate ./...
+
+ run:
+ deps: ['test-and-lint']
+ cmds:
+ - go run .
+ build:
+ deps: ['test-and-lint']
+ cmds:
+ - task: build-only
+
+ test-and-lint:
+ deps: ['test', 'lint']
+ test:
+ cmds:
+ - go test ./...
+ - go vet ./...
+ lint:
+ cmds:
+ - goimports -w .
+ - golangci-lint run
+
+ build-only:
+ cmds:
+ - go build .
diff --git a/api/api_bind.go b/api/api_bind.go
index 2bbeeed6..60b7947c 100644
--- a/api/api_bind.go
+++ b/api/api_bind.go
@@ -166,7 +166,7 @@ func forceStop(c echo.Context) error {
for _, i := range diceManager.Dice {
if i.IsAlreadyLoadConfig {
- i.BanList.SaveChanged(i)
+ i.Config.BanList.SaveChanged(i)
i.AttrsManager.CheckForSave()
i.Save(true)
for _, j := range i.ExtList {
@@ -190,7 +190,11 @@ func forceStop(c echo.Context) error {
dbData := d.DBData
if dbData != nil {
d.DBData = nil
- _ = dbData.Close()
+ db, err := dbData.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
@@ -201,7 +205,11 @@ func forceStop(c echo.Context) error {
dbLogs := d.DBLogs
if dbLogs != nil {
d.DBLogs = nil
- _ = dbLogs.Close()
+ db, err := dbLogs.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
@@ -213,7 +221,11 @@ func forceStop(c echo.Context) error {
if cm != nil && cm.DB != nil {
dbCensor := cm.DB
cm.DB = nil
- _ = dbCensor.Close()
+ db, err := dbCensor.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
}
@@ -597,7 +609,7 @@ func Bind(e *echo.Echo, _myDice *dice.DiceManager) {
e.POST(prefix+"/im_connections/del", ImConnectionsDel)
e.POST(prefix+"/im_connections/set_enable", ImConnectionsSetEnable)
e.POST(prefix+"/im_connections/set_data", ImConnectionsSetData)
- e.POST(prefix+"/im_connections/set_sign_server", ImConnectionsRWSignServerUrl)
+ e.GET(prefix+"/im_connections/get_lgr_signinfo", ImConnectionsGetSignInfo)
e.POST(prefix+"/im_connections/gocqhttpRelogin", ImConnectionsGocqhttpRelogin)
e.POST(prefix+"/im_connections/walleQRelogin", ImConnectionsWalleQRelogin)
e.GET(prefix+"/im_connections/gocq_config_download.zip", ImConnectionsGocqConfigDownload)
@@ -626,8 +638,10 @@ func Bind(e *echo.Echo, _myDice *dice.DiceManager) {
e.GET(prefix+"/dice/cmdList", DiceAllCommand)
e.POST(prefix+"/dice/upload_to_upgrade", DiceNewVersionUpload)
- e.POST(prefix+"/dice/config/vm-version-for-reply-set", vmVersionForReplySet)
- e.POST(prefix+"/dice/config/vm-version-for-deck-set", vmVersionForDeckSet)
+ e.GET(prefix+"/dice/public/info", dicePublicInfo)
+ e.POST(prefix+"/dice/public/set", dicePublicSet)
+
+ e.POST(prefix+"/dice/config/vm-version-set", vmVersionSet)
e.POST(prefix+"/signin", doSignIn)
e.GET(prefix+"/signin/salt", doSignInGetSalt)
diff --git a/api/ban.go b/api/ban.go
index fc7b1855..ef637c99 100644
--- a/api/ban.go
+++ b/api/ban.go
@@ -20,7 +20,7 @@ func banConfigGet(c echo.Context) error {
return c.JSON(http.StatusForbidden, nil)
}
- return c.JSON(http.StatusOK, myDice.BanList)
+ return c.JSON(http.StatusOK, myDice.Config.BanList)
}
func banConfigSet(c echo.Context) error {
@@ -42,23 +42,25 @@ func banConfigSet(c echo.Context) error {
v.ThresholdBan = 200
}
- myDice.BanList.BanBehaviorRefuseReply = v.BanBehaviorRefuseReply
- myDice.BanList.BanBehaviorRefuseInvite = v.BanBehaviorRefuseInvite
- myDice.BanList.BanBehaviorQuitLastPlace = v.BanBehaviorQuitLastPlace
- myDice.BanList.BanBehaviorQuitPlaceImmediately = v.BanBehaviorQuitPlaceImmediately
- myDice.BanList.BanBehaviorQuitIfAdmin = v.BanBehaviorQuitIfAdmin
- myDice.BanList.ScoreReducePerMinute = v.ScoreReducePerMinute
- myDice.BanList.ThresholdWarn = v.ThresholdWarn
- myDice.BanList.ThresholdBan = v.ThresholdBan
- myDice.BanList.ScoreGroupMuted = v.ScoreGroupMuted
- myDice.BanList.ScoreGroupKicked = v.ScoreGroupKicked
- myDice.BanList.ScoreTooManyCommand = v.ScoreTooManyCommand
-
- myDice.BanList.JointScorePercentOfGroup = v.JointScorePercentOfGroup
- myDice.BanList.JointScorePercentOfInviter = v.JointScorePercentOfInviter
+ config := &myDice.Config
+ config.BanList.BanBehaviorRefuseReply = v.BanBehaviorRefuseReply
+ config.BanList.BanBehaviorRefuseInvite = v.BanBehaviorRefuseInvite
+ config.BanList.BanBehaviorQuitLastPlace = v.BanBehaviorQuitLastPlace
+ config.BanList.BanBehaviorQuitPlaceImmediately = v.BanBehaviorQuitPlaceImmediately
+ config.BanList.BanBehaviorQuitIfAdmin = v.BanBehaviorQuitIfAdmin
+ config.BanList.BanBehaviorQuitIfAdminSilentIfNotAdmin = v.BanBehaviorQuitIfAdminSilentIfNotAdmin
+ config.BanList.ScoreReducePerMinute = v.ScoreReducePerMinute
+ config.BanList.ThresholdWarn = v.ThresholdWarn
+ config.BanList.ThresholdBan = v.ThresholdBan
+ config.BanList.ScoreGroupMuted = v.ScoreGroupMuted
+ config.BanList.ScoreGroupKicked = v.ScoreGroupKicked
+ config.BanList.ScoreTooManyCommand = v.ScoreTooManyCommand
+
+ config.BanList.JointScorePercentOfGroup = v.JointScorePercentOfGroup
+ config.BanList.JointScorePercentOfInviter = v.JointScorePercentOfInviter
myDice.MarkModified()
- return c.JSON(http.StatusOK, myDice.BanList)
+ return c.JSON(http.StatusOK, myDice.Config.BanList)
}
func banMapList(c echo.Context) error {
@@ -75,7 +77,7 @@ func banMapDeleteOne(c echo.Context) error {
if err != nil {
return c.String(430, err.Error())
}
- myDice.BanList.DeleteByID(myDice, v.ID)
+ (&myDice.Config).BanList.DeleteByID(myDice, v.ID)
return c.JSON(http.StatusOK, nil)
}
@@ -92,7 +94,7 @@ func banMapAddOne(c echo.Context) error {
return c.String(430, err.Error())
}
if v.Rank == dice.BanRankBanned {
- score := myDice.BanList.ThresholdBan
+ score := myDice.Config.BanList.ThresholdBan
reason := "骰主后台设置"
if len(v.Reasons) > 0 {
reason = v.Reasons[0]
@@ -102,7 +104,7 @@ func banMapAddOne(c echo.Context) error {
platform := strings.Replace(prefix, "-Group", "", 1)
for _, i := range myDice.ImSession.EndPoints {
if i.Platform == platform && i.Enable {
- v2 := myDice.BanList.AddScoreBase(v.ID, score, "海豹后台", reason, &dice.MsgContext{Dice: myDice, EndPoint: i})
+ v2 := (&myDice.Config).BanList.AddScoreBase(v.ID, score, "海豹后台", reason, &dice.MsgContext{Dice: myDice, EndPoint: i})
if v2 != nil {
if v.Name != "" {
v2.Name = v.Name
@@ -112,7 +114,7 @@ func banMapAddOne(c echo.Context) error {
}
}
if v.Rank == dice.BanRankTrusted {
- myDice.BanList.SetTrustByID(v.ID, "海豹后台", "骰主后台设置")
+ (&myDice.Config).BanList.SetTrustByID(v.ID, "海豹后台", "骰主后台设置")
}
return c.JSON(http.StatusOK, nil)
@@ -132,7 +134,7 @@ func banMapAddOne(c echo.Context) error {
// return c.String(430, err.Error())
// }
//
-// myDice.BanList.LoadMapFromJSON(v.data)
+// (&myDice.Config).BanList.LoadMapFromJSON(v.data)
// return c.JSON(http.StatusOK, nil)
//}
@@ -199,9 +201,9 @@ func banImport(c echo.Context) error {
}
}
item.Reasons = newReasons
- myDice.BanList.Map.Store(item.ID, item)
+ (&myDice.Config).BanList.Map.Store(item.ID, item)
}
- myDice.BanList.SaveChanged(myDice)
+ (&myDice.Config).BanList.SaveChanged(myDice)
return Success(&c, Response{})
}
diff --git a/api/censor.go b/api/censor.go
index c3a10e67..093ad5b5 100644
--- a/api/censor.go
+++ b/api/censor.go
@@ -3,7 +3,6 @@ package api
import (
"bufio"
"encoding/json"
- "fmt"
"io"
"mime/multipart"
"net/http"
@@ -20,6 +19,7 @@ import (
"sealdice-core/dice"
"sealdice-core/dice/censor"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
)
func check(c echo.Context) (bool, error) {
@@ -29,7 +29,7 @@ func check(c echo.Context) (bool, error) {
if dm.JustForTest {
return false, Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
- if !myDice.EnableCensor {
+ if !myDice.Config.EnableCensor {
return false, Error(&c, "未启用拦截引擎", Response{})
}
if myDice.CensorManager.IsLoading {
@@ -47,10 +47,11 @@ func censorRestart(c echo.Context) error {
}
myDice.NewCensorManager()
- myDice.EnableCensor = true
+ (&myDice.Config).EnableCensor = true
+ myDice.MarkModified()
return Success(&c, Response{
- "enable": myDice.EnableCensor,
+ "enable": myDice.Config.EnableCensor,
"isLoading": myDice.CensorManager.IsLoading,
})
}
@@ -61,8 +62,16 @@ func censorStop(c echo.Context) error {
return err
}
- myDice.EnableCensor = false
- _ = myDice.CensorManager.DB.Close()
+ (&myDice.Config).EnableCensor = false
+ myDice.MarkModified()
+ db, err2 := myDice.CensorManager.DB.DB()
+ if err2 != nil {
+ return Error(&c, "关闭拦截引擎失败", Response{})
+ }
+ err = db.Close()
+ if err != nil {
+ return err
+ }
myDice.CensorManager = nil
return Success(&c, Response{})
@@ -74,23 +83,24 @@ func censorGetStatus(c echo.Context) error {
isLoading = myDice.CensorManager.IsLoading
}
return Success(&c, Response{
- "enable": myDice.EnableCensor,
+ "enable": myDice.Config.EnableCensor,
"isLoading": isLoading,
})
}
func censorGetConfig(c echo.Context) error {
+ config := myDice.Config
levelConfig := map[string]LevelConfig{
- "notice": getLevelConfig(censor.Notice, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores),
- "caution": getLevelConfig(censor.Caution, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores),
- "warning": getLevelConfig(censor.Warning, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores),
- "danger": getLevelConfig(censor.Danger, myDice.CensorThresholds, myDice.CensorHandlers, myDice.CensorScores),
+ "notice": getLevelConfig(censor.Notice, config.CensorThresholds, config.CensorHandlers, config.CensorScores),
+ "caution": getLevelConfig(censor.Caution, config.CensorThresholds, config.CensorHandlers, config.CensorScores),
+ "warning": getLevelConfig(censor.Warning, config.CensorThresholds, config.CensorHandlers, config.CensorScores),
+ "danger": getLevelConfig(censor.Danger, config.CensorThresholds, config.CensorHandlers, config.CensorScores),
}
return Success(&c, Response{
- "mode": myDice.CensorMode,
- "caseSensitive": myDice.CensorCaseSensitive,
- "matchPinyin": myDice.CensorMatchPinyin,
- "filterRegex": myDice.CensorFilterRegexStr,
+ "mode": config.CensorMode,
+ "caseSensitive": config.CensorCaseSensitive,
+ "matchPinyin": config.CensorMatchPinyin,
+ "filterRegex": config.CensorFilterRegexStr,
"levelConfig": levelConfig,
})
}
@@ -149,10 +159,11 @@ func censorSetConfig(c echo.Context) error {
jsonMap := make(map[string]interface{})
err = json.NewDecoder(c.Request().Body).Decode(&jsonMap)
if err != nil {
- fmt.Println(err)
+ log.Error("censorSetConfig", err)
return c.JSON(http.StatusInternalServerError, err)
}
+ config := &myDice.Config
if val, ok := jsonMap["filterRegex"]; ok {
filterRegex, ok := val.(string)
if ok {
@@ -160,25 +171,25 @@ func censorSetConfig(c echo.Context) error {
if err != nil {
return Error(&c, "过滤字符正则不是合法的正则表达式", Response{})
}
- myDice.CensorFilterRegexStr = filterRegex
+ config.CensorFilterRegexStr = filterRegex
}
}
if val, ok := jsonMap["mode"]; ok {
mode, ok := val.(float64)
if ok {
- myDice.CensorMode = dice.CensorMode(mode)
+ config.CensorMode = dice.CensorMode(mode)
}
}
if val, ok := jsonMap["caseSensitive"]; ok {
caseSensitive, ok := val.(bool)
if ok {
- myDice.CensorCaseSensitive = caseSensitive
+ config.CensorCaseSensitive = caseSensitive
}
}
if val, ok := jsonMap["matchPinyin"]; ok {
matchPinyin, ok := val.(bool)
if ok {
- myDice.CensorMatchPinyin = matchPinyin
+ config.CensorMatchPinyin = matchPinyin
}
}
if val, ok := jsonMap["levelConfig"]; ok { //nolint:nestif
@@ -212,7 +223,7 @@ func censorSetConfig(c echo.Context) error {
if ok {
if val, ok = confMap["threshold"]; ok {
threshold := val.(float64)
- myDice.CensorThresholds[level] = int(threshold)
+ config.CensorThresholds[level] = int(threshold)
}
if val, ok = confMap["handlers"]; ok {
handlers := stringConvert(val)
@@ -220,7 +231,7 @@ func censorSetConfig(c echo.Context) error {
}
if val, ok = confMap["score"]; ok {
score := val.(float64)
- myDice.CensorScores[level] = int(score)
+ config.CensorScores[level] = int(score)
}
}
}
@@ -259,7 +270,7 @@ func setLevelHandlers(level censor.Level, handlers []string) {
handlerVal = newHandlerVal(handlerVal, dice.BanInviter, newHandlers)
handlerVal = newHandlerVal(handlerVal, dice.AddScore, newHandlers)
- myDice.CensorHandlers[level] = handlerVal
+ (&myDice.Config).CensorHandlers[level] = handlerVal
}
func newHandlerVal(val uint8, handle dice.CensorHandler, newHandlers map[dice.CensorHandler]bool) uint8 {
@@ -434,7 +445,7 @@ func censorDeleteWordFiles(c echo.Context) error {
}{}
err = c.Bind(&v)
if err != nil {
- fmt.Println(err)
+ log.Error("censorDeleteWordFiles", err)
return c.JSON(http.StatusInternalServerError, err)
}
@@ -509,7 +520,7 @@ func censorGetLogPage(c echo.Context) error {
v := model.QueryCensorLog{}
err = c.Bind(&v)
if err != nil {
- fmt.Println(err)
+ log.Error("censorGetLogPage", err)
return c.JSON(http.StatusInternalServerError, err)
}
if v.PageNum < 1 {
diff --git a/api/conn.go b/api/conn.go
index deac6260..ceca01dc 100644
--- a/api/conn.go
+++ b/api/conn.go
@@ -2,7 +2,6 @@ package api
import (
"encoding/base64"
- "fmt"
"net/http"
"sort"
"strconv"
@@ -11,6 +10,7 @@ import (
"github.com/labstack/echo/v4"
"sealdice-core/dice"
+ log "sealdice-core/utils/kratos"
)
func ImConnections(c echo.Context) error {
@@ -137,46 +137,18 @@ func ImConnectionsSetData(c echo.Context) error {
return c.JSON(http.StatusNotFound, nil)
}
-func ImConnectionsRWSignServerUrl(c echo.Context) error {
+func ImConnectionsGetSignInfo(c echo.Context) error {
if !doAuth(c) {
return c.JSON(http.StatusForbidden, nil)
}
- if dm.JustForTest {
- return c.JSON(http.StatusOK, map[string]interface{}{
- "testMode": true,
- })
- }
- v := struct {
- ID string `form:"id" json:"id"`
- SignServerUrl string `form:"signServerUrl" json:"signServerUrl"`
- W bool `form:"w" json:"w"`
- SignServerVersion string `form:"signServerVersion" json:"signServerVersion"`
- }{}
-
- err := c.Bind(&v)
+ data, err := dice.LagrangeGetSignInfo(myDice)
if err != nil {
- myDice.Save(false)
- return c.JSON(http.StatusNotFound, nil)
+ return Error(&c, "读取SignInfo失败", Response{})
}
- for _, i := range myDice.ImSession.EndPoints {
- if i.ID != v.ID {
- continue
- }
- if i.ProtocolType == "onebot" {
- pa := i.Adapter.(*dice.PlatformAdapterGocq)
- if pa.BuiltinMode == "lagrange" {
- signServerUrl, signServerVersion := dice.RWLagrangeSignServerUrl(myDice, i, v.SignServerUrl, v.W, v.SignServerVersion)
- if signServerUrl != "" {
- return Success(&c, Response{
- "signServerUrl": signServerUrl,
- "signServerVersion": signServerVersion,
- })
- }
- }
- }
- }
- return Error(&c, "读取signServerUrl字段失败", Response{})
+ return Success(&c, Response{
+ "data": data,
+ })
}
func ImConnectionsDel(c echo.Context) error {
@@ -207,7 +179,7 @@ func ImConnectionsDel(c echo.Context) error {
myDice.ImSession.EndPoints = append(myDice.ImSession.EndPoints[:index], myDice.ImSession.EndPoints[index+1:]...)
if i.ProtocolType == "onebot" {
pa := i.Adapter.(*dice.PlatformAdapterGocq)
- if pa.BuiltinMode == "lagrange" {
+ if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" {
dice.BuiltinQQServeProcessKillBase(myDice, i, true)
// 经测试,若不延时,可能导致清理对应目录失败(原因:文件被占用)
time.Sleep(1 * time.Second)
@@ -437,7 +409,7 @@ func ImConnectionsGocqhttpRelogin(c echo.Context) error {
if err == nil {
for _, i := range myDice.ImSession.EndPoints {
if i.ID == v.ID {
- fmt.Print("!!! relogin ", v.ID)
+ log.Warnf("relogin %s", v.ID)
i.Adapter.DoRelogin()
return c.JSON(http.StatusOK, nil)
}
@@ -967,8 +939,9 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error {
v := struct {
Account string `yaml:"account" json:"account"`
- SignServerUrl string `yaml:"signServerUrl" json:"signServerUrl"`
+ SignServerName string `yaml:"signServerName" json:"signServerName"`
SignServerVersion string `yaml:"signServerVersion" json:"signServerVersion"`
+ IsGocq bool `yaml:"isGocq" json:"isGocq"`
}{}
err := c.Bind(&v)
if err == nil {
@@ -977,7 +950,7 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error {
return nil
}
- conn := dice.NewLagrangeConnectInfoItem(v.Account)
+ conn := dice.NewLagrangeConnectInfoItem(v.Account, v.IsGocq)
conn.UserID = dice.FormatDiceIDQQ(uid)
conn.Session = myDice.ImSession
pa := conn.Adapter.(*dice.PlatformAdapterGocq)
@@ -990,9 +963,11 @@ func ImConnectionsAddBuiltinLagrange(c echo.Context) error {
if err != nil {
return err
}
+ pa.SignServerName = v.SignServerName
+ pa.SignServerVer = v.SignServerVersion
dice.LagrangeServe(myDice, conn, dice.LagrangeLoginInfo{
UIN: uin,
- SignServerUrl: v.SignServerUrl,
+ SignServerName: v.SignServerName,
SignServerVersion: v.SignServerVersion,
IsAsyncRun: true,
})
diff --git a/api/deck.go b/api/deck.go
index 9264b566..7f676466 100644
--- a/api/deck.go
+++ b/api/deck.go
@@ -92,19 +92,22 @@ func deckEnable(c echo.Context) error {
}
v := struct {
- Index int `json:"index"`
- Enable bool `json:"enable"`
+ Filename string `json:"filename"`
+ Enable bool `json:"enable"`
}{}
err := c.Bind(&v)
if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.DeckList) {
- myDice.DeckList[v.Index].Enable = v.Enable
- myDice.MarkModified()
+ for _, deck := range myDice.DeckList {
+ if deck.Filename == v.Filename {
+ deck.Enable = v.Enable
+ myDice.MarkModified()
+ break
+ }
}
}
- return c.JSON(http.StatusOK, myDice.BanList)
+ return c.JSON(http.StatusOK, myDice.Config.BanList)
}
func deckDelete(c echo.Context) error {
@@ -118,14 +121,17 @@ func deckDelete(c echo.Context) error {
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.DeckList) {
- dice.DeckDelete(myDice, myDice.DeckList[v.Index])
- myDice.MarkModified()
+ if err == nil && v.Filename != "" {
+ for _, deck := range myDice.DeckList {
+ if deck.Filename == v.Filename {
+ dice.DeckDelete(myDice, deck)
+ myDice.MarkModified()
+ break
+ }
}
}
@@ -140,23 +146,25 @@ func deckCheckUpdate(c echo.Context) error {
return Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.DeckList) {
- deck := myDice.DeckList[v.Index]
- oldDeck, newDeck, tempFileName, err := myDice.DeckCheckUpdate(deck)
- if err != nil {
- return Error(&c, err.Error(), Response{})
+ if err == nil && v.Filename != "" {
+ for _, deck := range myDice.DeckList {
+ if deck.Filename == v.Filename {
+ oldDeck, newDeck, tempFileName, err := myDice.DeckCheckUpdate(deck)
+ if err != nil {
+ return Error(&c, err.Error(), Response{})
+ }
+ return Success(&c, Response{
+ "old": oldDeck,
+ "new": newDeck,
+ "format": deck.FileFormat,
+ "filename": deck.Filename,
+ "tempFileName": tempFileName,
+ })
}
- return Success(&c, Response{
- "old": oldDeck,
- "new": newDeck,
- "format": deck.FileFormat,
- "tempFileName": tempFileName,
- })
}
}
return Success(&c, Response{})
@@ -170,18 +178,21 @@ func deckUpdate(c echo.Context) error {
return Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
TempFileName string `json:"tempFileName"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.DeckList) {
- err := myDice.DeckUpdate(myDice.DeckList[v.Index], v.TempFileName)
- if err != nil {
- return Error(&c, err.Error(), Response{})
+ if err == nil && v.Filename != "" {
+ for _, deck := range myDice.DeckList {
+ if deck.Filename == v.Filename {
+ err := myDice.DeckUpdate(deck, v.TempFileName)
+ if err != nil {
+ return Error(&c, err.Error(), Response{})
+ }
+ myDice.MarkModified()
+ break
}
- myDice.MarkModified()
}
}
return Success(&c, Response{})
diff --git a/api/dice_config.go b/api/dice_config.go
index 03928129..2b102ffd 100644
--- a/api/dice_config.go
+++ b/api/dice_config.go
@@ -2,7 +2,6 @@ package api
import (
"encoding/json"
- "fmt"
"net/http"
"strconv"
"strings"
@@ -13,64 +12,21 @@ import (
"sealdice-core/dice"
"sealdice-core/utils"
+ log "sealdice-core/utils/kratos"
)
type DiceConfigInfo struct {
- // 注:form其实不需要
- CommandPrefix []string `json:"commandPrefix" form:"commandPrefix"` // 指令前缀
- DiceMasters []string `json:"diceMasters" form:"diceMasters"` // 骰主设置,需要格式: 平台:帐号
- NoticeIds []string `json:"noticeIds"` // 通知设置,需要格式: 平台:帐号
- OnlyLogCommandInGroup bool `json:"onlyLogCommandInGroup" form:"onlyLogCommandInGroup"` // 日志中仅记录命令
- OnlyLogCommandInPrivate bool `json:"onlyLogCommandInPrivate" form:"onlyLogCommandInPrivate"` // 日志中仅记录命令
- WorkInQQChannel bool `json:"workInQQChannel"` // 在QQ频道中开启
- MessageDelayRangeStart float64 `json:"messageDelayRangeStart" form:"messageDelayRangeStart"` // 指令延迟区间
- MessageDelayRangeEnd float64 `json:"messageDelayRangeEnd" form:"messageDelayRangeEnd"`
- UIPassword string `json:"uiPassword" form:"uiPassword"`
- HelpDocEngineType int `json:"helpDocEngineType"`
- MasterUnlockCode string `json:"masterUnlockCode" form:"masterUnlockCode"`
- ServeAddress string `json:"serveAddress" form:"serveAddress"`
- MasterUnlockCodeTime int64 `json:"masterUnlockCodeTime"`
- LogPageItemLimit int64 `json:"logPageItemLimit"`
- FriendAddComment string `json:"friendAddComment"`
- QQChannelAutoOn bool `json:"QQChannelAutoOn"`
- QQChannelLogMessage bool `json:"QQChannelLogMessage"`
- RefuseGroupInvite bool `json:"refuseGroupInvite"` // 拒绝群组邀请
-
- QuitInactiveThreshold float64 `json:"quitInactiveThreshold"` // 退出不活跃群组阈值(天)
- QuitInactiveBatchSize int64 `json:"quitInactiveBatchSize"` // 退出不活跃群组的批量大小
- QuitInactiveBatchWait int64 `json:"quitInactiveBatchWait"` // 退出不活跃群组的批量等待时间(分)
-
- DefaultCocRuleIndex string `json:"defaultCocRuleIndex"` // 默认coc index
- MaxExecuteTime string `json:"maxExecuteTime"` // 最大骰点次数
- MaxCocCardGen string `json:"maxCocCardGen"` // 最大coc制卡数
-
- ExtDefaultSettings []*dice.ExtDefaultSettingItem `yaml:"extDefaultSettings" json:"extDefaultSettings"` // 新群扩展按此顺序加载
- BotExtFreeSwitch bool `json:"botExtFreeSwitch"`
- TrustOnlyMode bool `json:"trustOnlyMode"`
- AliveNoticeEnable bool `json:"aliveNoticeEnable"`
- AliveNoticeValue string `json:"aliveNoticeValue"`
- ReplyDebugMode bool `json:"replyDebugMode"`
-
- CustomReplyConfigEnable bool `json:"customReplyConfigEnable"` // 是否开启reply
-
- LogSizeNoticeEnable bool `json:"logSizeNoticeEnable"` // 开启日志数量提示
- LogSizeNoticeCount int `json:"logSizeNoticeCount"` // 日志数量提示阈值,默认500
-
- TextCmdTrustOnly bool `json:"textCmdTrustOnly"` // text命令只允许信任用户和master
- IgnoreUnaddressedBotCmd bool `json:"ignoreUnaddressedBotCmd"` // 不响应群聊裸bot指令
- QQEnablePoke bool `json:"QQEnablePoke"` // QQ允许戳一戳
- PlayerNameWrapEnable bool `json:"playerNameWrapEnable"` // 玩家名外框
-
- MailEnable bool `json:"mailEnable"`
- MailFrom string `json:"mailFrom"` // 邮箱来源
- MailPassword string `json:"mailPassword"` // 邮箱密钥/密码
- MailSMTP string `json:"mailSmtp"` // 邮箱 smtp 地址
-
- RateLimitEnabled bool `json:"rateLimitEnabled"` // 是否开启限速
- PersonalReplenishRate string `json:"personalReplenishRate"` // 个人自定义速率
- PersonalReplenishBurst int64 `json:"personalBurst"` // 个人自定义上限
- GroupReplenishRate string `json:"groupReplenishRate"` // 群组自定义速率
- GroupReplenishBurst int64 `json:"groupBurst"` // 群组自定义上限
+ dice.Config
+
+ CommandPrefix []string `json:"commandPrefix" form:"commandPrefix"`
+ DiceMasters []string `json:"diceMasters" form:"diceMasters"`
+ UIPassword string `json:"uiPassword" form:"uiPassword"`
+ LogPageItemLimit int64 `json:"logPageItemLimit"`
+ DefaultCocRuleIndex string `json:"defaultCocRuleIndex"` // 默认coc index
+ MaxExecuteTime string `json:"maxExecuteTime"` // 最大骰点次数
+ MaxCocCardGen string `json:"maxCocCardGen"` // 最大coc制卡数
+ ServerAddress string `json:"serveAddress" form:"serveAddress"`
+ HelpDocEngineType int `json:"helpDocEngineType"`
}
func DiceConfig(c echo.Context) error {
@@ -83,93 +39,53 @@ func DiceConfig(c echo.Context) error {
password = "------"
}
- limit := myDice.UILogLimit
+ limit := myDice.Config.UILogLimit
if limit == 0 {
limit = 100
}
myDice.UnlockCodeUpdate(false)
- cocRule := strconv.FormatInt(myDice.DefaultCocRuleIndex, 10)
- if myDice.DefaultCocRuleIndex == 11 {
+ cocRule := strconv.FormatInt(myDice.Config.DefaultCocRuleIndex, 10)
+ if myDice.Config.DefaultCocRuleIndex == 11 {
cocRule = "dg"
}
- maxExec := strconv.FormatInt(myDice.MaxExecuteTime, 10)
+ maxExec := strconv.FormatInt(myDice.Config.MaxExecuteTime, 10)
- maxCard := strconv.FormatInt(myDice.MaxCocCardGen, 10)
+ maxCard := strconv.FormatInt(myDice.Config.MaxCocCardGen, 10)
emailPasswordMasked := ""
- if myDice.MailPassword != "" {
+ if myDice.Config.MailPassword != "" {
emailPasswordMasked = "******"
}
// 过滤掉未加载的: 包括关闭的和已经删除的
// TODO(Xiangze Li): 如果前端能支持区分显示未加载插件的配置(.loaded字段), 这里就的过滤就可以去掉
- extDefaultSettings := make([]*dice.ExtDefaultSettingItem, 0, len(myDice.ExtDefaultSettings))
- for _, i := range myDice.ExtDefaultSettings {
+ extDefaultSettings := make([]*dice.ExtDefaultSettingItem, 0, len(myDice.Config.ExtDefaultSettings))
+ for _, i := range myDice.Config.ExtDefaultSettings {
if i.Loaded {
extDefaultSettings = append(extDefaultSettings, i)
}
}
info := DiceConfigInfo{
- CommandPrefix: myDice.CommandPrefix,
- DiceMasters: myDice.DiceMasters,
- NoticeIds: myDice.NoticeIDs,
- OnlyLogCommandInPrivate: myDice.OnlyLogCommandInPrivate,
- OnlyLogCommandInGroup: myDice.OnlyLogCommandInGroup,
- MessageDelayRangeStart: myDice.MessageDelayRangeStart,
- MessageDelayRangeEnd: myDice.MessageDelayRangeEnd,
- UIPassword: password,
- MasterUnlockCode: myDice.MasterUnlockCode,
- MasterUnlockCodeTime: myDice.MasterUnlockCodeTime,
- WorkInQQChannel: myDice.WorkInQQChannel,
- LogPageItemLimit: limit,
- FriendAddComment: myDice.FriendAddComment,
- QQChannelAutoOn: myDice.QQChannelAutoOn,
- QQChannelLogMessage: myDice.QQChannelLogMessage,
- RefuseGroupInvite: myDice.RefuseGroupInvite,
- RateLimitEnabled: myDice.RateLimitEnabled,
-
- ExtDefaultSettings: extDefaultSettings,
- DefaultCocRuleIndex: cocRule,
+ Config: myDice.Config,
- QuitInactiveThreshold: myDice.QuitInactiveThreshold.Hours() / 24,
- QuitInactiveBatchSize: myDice.QuitInactiveBatchSize,
- QuitInactiveBatchWait: myDice.QuitInactiveBatchWait,
-
- BotExtFreeSwitch: myDice.BotExtFreeSwitch,
- TrustOnlyMode: myDice.TrustOnlyMode,
- AliveNoticeEnable: myDice.AliveNoticeEnable,
- AliveNoticeValue: myDice.AliveNoticeValue,
- ReplyDebugMode: myDice.ReplyDebugMode,
-
- ServeAddress: myDice.Parent.ServeAddress,
- HelpDocEngineType: myDice.Parent.HelpDocEngineType,
-
- // 1.0 正式
- LogSizeNoticeEnable: myDice.LogSizeNoticeEnable,
- LogSizeNoticeCount: myDice.LogSizeNoticeCount,
- CustomReplyConfigEnable: myDice.CustomReplyConfigEnable,
-
- // 1.2
- TextCmdTrustOnly: myDice.TextCmdTrustOnly,
- QQEnablePoke: myDice.QQEnablePoke,
- PlayerNameWrapEnable: myDice.PlayerNameWrapEnable,
-
- // 1.3?
- MailEnable: myDice.MailEnable,
- MailFrom: myDice.MailFrom,
- MailPassword: emailPasswordMasked,
- MailSMTP: myDice.MailSMTP,
- MaxExecuteTime: maxExec,
- MaxCocCardGen: maxCard,
- PersonalReplenishRate: myDice.PersonalReplenishRateStr,
- PersonalReplenishBurst: myDice.PersonalBurst,
- GroupReplenishRate: myDice.GroupReplenishRateStr,
- GroupReplenishBurst: myDice.GroupBurst,
- IgnoreUnaddressedBotCmd: myDice.IgnoreUnaddressedBotCmd,
+ CommandPrefix: myDice.CommandPrefix,
+ DiceMasters: myDice.DiceMasters,
+ UIPassword: password,
+ LogPageItemLimit: limit,
+ DefaultCocRuleIndex: cocRule,
+ ServerAddress: myDice.Parent.ServeAddress,
+ HelpDocEngineType: myDice.Parent.HelpDocEngineType,
+ MaxExecuteTime: maxExec,
+ MaxCocCardGen: maxCard,
}
+ info.ExtDefaultSettings = extDefaultSettings
+ info.DefaultCocRuleIndex = cocRule
+ info.MailPassword = emailPasswordMasked
+ info.Config.QuitInactiveThresholdDays = info.QuitInactiveThreshold.Hours() / 24
+
return c.JSON(http.StatusOK, info)
}
@@ -193,7 +109,7 @@ func DiceConfigSet(c echo.Context) error {
}
if err != nil {
- fmt.Println(err)
+ log.Error("DiceConfigSet", err)
return c.JSON(http.StatusOK, nil)
}
if val, ok := jsonMap["commandPrefix"]; ok {
@@ -217,8 +133,9 @@ func DiceConfigSet(c echo.Context) error {
myDice.DiceMasters = masters
}
+ config := &myDice.Config
if val, ok := jsonMap["noticeIds"]; ok {
- myDice.NoticeIDs = stringConvert(val)
+ config.NoticeIDs = stringConvert(val)
}
if val, ok := jsonMap["defaultCocRuleIndex"]; ok { //nolint:nestif
@@ -226,12 +143,12 @@ func DiceConfigSet(c echo.Context) error {
if ok {
valStr = strings.TrimSpace(valStr)
if strings.EqualFold(valStr, "dg") {
- myDice.DefaultCocRuleIndex = 11
+ config.DefaultCocRuleIndex = 11
} else {
- myDice.DefaultCocRuleIndex, err = strconv.ParseInt(valStr, 10, 64)
+ config.DefaultCocRuleIndex, err = strconv.ParseInt(valStr, 10, 64)
if err == nil {
- if myDice.DefaultCocRuleIndex > 5 || myDice.DefaultCocRuleIndex < 0 {
- myDice.DefaultCocRuleIndex = 0
+ if config.DefaultCocRuleIndex > 5 || config.DefaultCocRuleIndex < 0 {
+ config.DefaultCocRuleIndex = dice.DefaultConfig.DefaultCocRuleIndex
}
}
}
@@ -245,7 +162,7 @@ func DiceConfigSet(c echo.Context) error {
var valInt int64
valInt, err = strconv.ParseInt(valStr, 10, 64)
if err == nil && valInt > 0 {
- myDice.MaxExecuteTime = valInt
+ config.MaxExecuteTime = valInt
} /* else {
Should return some error?
} */
@@ -259,7 +176,7 @@ func DiceConfigSet(c echo.Context) error {
var valInt int64
valInt, err = strconv.ParseInt(valStr, 10, 64)
if err == nil && valInt > 0 {
- myDice.MaxCocCardGen = valInt
+ config.MaxCocCardGen = valInt
} /* else {
Should return some error?
} */
@@ -271,7 +188,7 @@ func DiceConfigSet(c echo.Context) error {
if ok {
customBurst := int64(valStr)
if customBurst >= 1 {
- myDice.PersonalBurst = customBurst
+ config.PersonalBurst = customBurst
}
}
}
@@ -282,8 +199,8 @@ func DiceConfigSet(c echo.Context) error {
valStr = strings.TrimSpace(valStr)
newRate, errParse := utils.ParseRate(valStr)
if errParse == nil && newRate != rate.Limit(0) {
- myDice.PersonalReplenishRate = newRate
- myDice.PersonalReplenishRateStr = valStr
+ config.PersonalReplenishRate = newRate
+ config.PersonalReplenishRateStr = valStr
}
}
}
@@ -293,7 +210,7 @@ func DiceConfigSet(c echo.Context) error {
if ok {
customBurst := int64(valStr)
if customBurst >= 1 {
- myDice.GroupBurst = customBurst
+ config.GroupBurst = customBurst
}
}
}
@@ -304,54 +221,54 @@ func DiceConfigSet(c echo.Context) error {
valStr = strings.TrimSpace(valStr)
newRate, errParse := utils.ParseRate(valStr)
if errParse == nil && newRate != rate.Limit(0) {
- myDice.GroupReplenishRate = newRate
- myDice.GroupReplenishRateStr = valStr
+ config.GroupReplenishRate = newRate
+ config.GroupReplenishRateStr = valStr
}
}
}
if val, ok := jsonMap["onlyLogCommandInGroup"]; ok {
- myDice.OnlyLogCommandInGroup = val.(bool)
+ config.OnlyLogCommandInGroup = val.(bool)
}
if val, ok := jsonMap["onlyLogCommandInPrivate"]; ok {
- myDice.OnlyLogCommandInPrivate = val.(bool)
+ config.OnlyLogCommandInPrivate = val.(bool)
}
if val, ok := jsonMap["refuseGroupInvite"]; ok {
- myDice.RefuseGroupInvite = val.(bool)
+ config.RefuseGroupInvite = val.(bool)
}
if val, ok := jsonMap["workInQQChannel"]; ok {
- myDice.WorkInQQChannel = val.(bool)
+ config.WorkInQQChannel = val.(bool)
}
if val, ok := jsonMap["QQChannelLogMessage"]; ok {
- myDice.QQChannelLogMessage = val.(bool)
+ config.QQChannelLogMessage = val.(bool)
}
if val, ok := jsonMap["QQChannelAutoOn"]; ok {
- myDice.QQChannelAutoOn = val.(bool)
+ config.QQChannelAutoOn = val.(bool)
}
if val, ok := jsonMap["botExtFreeSwitch"]; ok {
- myDice.BotExtFreeSwitch = val.(bool)
+ config.BotExtFreeSwitch = val.(bool)
}
if val, ok := jsonMap["rateLimitEnabled"]; ok {
- myDice.RateLimitEnabled = val.(bool)
+ config.RateLimitEnabled = val.(bool)
}
if val, ok := jsonMap["trustOnlyMode"]; ok {
- myDice.TrustOnlyMode = val.(bool)
+ config.TrustOnlyMode = val.(bool)
}
aliveNoticeMod := false
if val, ok := jsonMap["aliveNoticeEnable"]; ok {
- myDice.AliveNoticeEnable = val.(bool)
+ config.AliveNoticeEnable = val.(bool)
aliveNoticeMod = true
}
if val, ok := jsonMap["aliveNoticeValue"]; ok {
- myDice.AliveNoticeValue = val.(string)
+ config.AliveNoticeValue = val.(string)
aliveNoticeMod = true
}
if aliveNoticeMod {
@@ -373,10 +290,10 @@ func DiceConfigSet(c echo.Context) error {
if f < 0 {
f = 0
}
- if myDice.MessageDelayRangeEnd < f {
- myDice.MessageDelayRangeEnd = f
+ if config.MessageDelayRangeEnd < f {
+ config.MessageDelayRangeEnd = f
}
- myDice.MessageDelayRangeStart = f
+ config.MessageDelayRangeStart = f
}
}
@@ -387,14 +304,14 @@ func DiceConfigSet(c echo.Context) error {
f = 0
}
- if f >= myDice.MessageDelayRangeStart {
- myDice.MessageDelayRangeEnd = f
+ if f >= config.MessageDelayRangeStart {
+ config.MessageDelayRangeEnd = f
}
}
}
if val, ok := jsonMap["friendAddComment"]; ok {
- myDice.FriendAddComment = strings.TrimSpace(val.(string))
+ config.FriendAddComment = strings.TrimSpace(val.(string))
}
if val, ok := jsonMap["uiPassword"]; ok {
@@ -409,7 +326,7 @@ func DiceConfigSet(c echo.Context) error {
var items []*dice.ExtDefaultSettingItem
err := json.Unmarshal(data, &items)
if err == nil {
- myDice.ExtDefaultSettings = items
+ config.ExtDefaultSettings = items
myDice.ApplyExtDefaultSettings()
}
}
@@ -434,57 +351,57 @@ func DiceConfigSet(c echo.Context) error {
// }
if val, ok := jsonMap["logSizeNoticeEnable"]; ok {
- myDice.LogSizeNoticeEnable = val.(bool)
+ config.LogSizeNoticeEnable = val.(bool)
}
if val, ok := jsonMap["logSizeNoticeCount"]; ok {
count, ok := val.(float64)
if ok {
- myDice.LogSizeNoticeCount = int(count)
+ config.LogSizeNoticeCount = int(count)
}
if !ok {
if v, ok := val.(string); ok {
v2, _ := strconv.ParseInt(v, 10, 64)
- myDice.LogSizeNoticeCount = int(v2)
+ config.LogSizeNoticeCount = int(v2)
}
}
- if myDice.LogSizeNoticeCount == 0 {
+ if config.LogSizeNoticeCount == 0 {
// 不能为零
- myDice.LogSizeNoticeCount = 500
+ config.LogSizeNoticeCount = dice.DefaultConfig.LogSizeNoticeCount
}
}
if val, ok := jsonMap["customReplyConfigEnable"]; ok {
- myDice.CustomReplyConfigEnable = val.(bool)
+ config.CustomReplyConfigEnable = val.(bool)
}
if val, ok := jsonMap["textCmdTrustOnly"]; ok {
- myDice.TextCmdTrustOnly = val.(bool)
+ config.TextCmdTrustOnly = val.(bool)
}
if val, ok := jsonMap["ignoreUnaddressedBotCmd"]; ok {
- myDice.IgnoreUnaddressedBotCmd = val.(bool)
+ config.IgnoreUnaddressedBotCmd = val.(bool)
}
if val, ok := jsonMap["QQEnablePoke"]; ok {
- myDice.QQEnablePoke = val.(bool)
+ config.QQEnablePoke = val.(bool)
}
if val, ok := jsonMap["playerNameWrapEnable"]; ok {
- myDice.PlayerNameWrapEnable = val.(bool)
+ config.PlayerNameWrapEnable = val.(bool)
}
if val, ok := jsonMap["mailEnable"]; ok {
- myDice.MailEnable = val.(bool)
+ config.MailEnable = val.(bool)
}
if val, ok := jsonMap["mailFrom"]; ok {
- myDice.MailFrom = val.(string)
+ config.MailFrom = val.(string)
}
if val, ok := jsonMap["mailPassword"]; ok {
- myDice.MailPassword = val.(string)
+ config.MailPassword = val.(string)
}
if val, ok := jsonMap["mailSmtp"]; ok {
- myDice.MailSMTP = val.(string)
+ config.MailSMTP = val.(string)
}
if val, ok := jsonMap["quitInactiveThreshold"]; ok {
@@ -492,14 +409,14 @@ func DiceConfigSet(c echo.Context) error {
switch v := val.(type) {
case string:
if vv, err := strconv.ParseFloat(v, 64); err == nil {
- myDice.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * vv)
+ config.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * vv)
set = true
}
case float64:
- myDice.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * v)
+ config.QuitInactiveThreshold = time.Duration(float64(24*time.Hour) * v)
set = true
case int64:
- myDice.QuitInactiveThreshold = 24 * time.Hour * time.Duration(v)
+ config.QuitInactiveThreshold = 24 * time.Hour * time.Duration(v)
set = true
default:
// ignore
@@ -512,7 +429,7 @@ func DiceConfigSet(c echo.Context) error {
if val, ok := jsonMap["quitInactiveBatchSize"]; ok {
if v, ok := val.(float64); ok {
if vv := int64(v); vv > 0 {
- myDice.QuitInactiveBatchSize = vv
+ config.QuitInactiveBatchSize = vv
}
}
}
@@ -520,7 +437,7 @@ func DiceConfigSet(c echo.Context) error {
if val, ok := jsonMap["quitInactiveBatchWait"]; ok {
if v, ok := val.(float64); ok {
if vv := int64(v); vv > 0 {
- myDice.QuitInactiveBatchWait = vv
+ config.QuitInactiveBatchWait = vv
}
}
}
@@ -546,7 +463,7 @@ func DiceMailTest(c echo.Context) error {
return Success(&c, Response{})
}
-func vmVersionForExtSetBase(c echo.Context, callback func(val string)) error {
+func vmVersionSet(c echo.Context) error {
if !doAuth(c) {
return c.JSON(http.StatusForbidden, nil)
}
@@ -554,29 +471,42 @@ func vmVersionForExtSetBase(c echo.Context, callback func(val string)) error {
return Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
- var data struct {
+ var req []struct {
+ Type string `json:"type"`
Value string `json:"value"`
}
- err := c.Bind(&data)
+ err := c.Bind(&req)
if err != nil {
return Error(&c, err.Error(), nil)
}
+ if len(req) == 0 {
+ return Error(&c, "缺少设置vm版本的参数", Response{})
+ }
- callback(data.Value)
+ var failTypes []string
+ for _, data := range req {
+ if data.Type == "" || data.Value == "" {
+ failTypes = append(failTypes, data.Type)
+ continue
+ }
+ switch data.Type {
+ case dice.VMVersionReply:
+ (&myDice.Config).VMVersionForReply = data.Value
+ case dice.VMVersionDeck:
+ (&myDice.Config).VMVersionForDeck = data.Value
+ case dice.VMVersionCustomText:
+ (&myDice.Config).VMVersionForCustomText = data.Value
+ case dice.VmVersionMsg:
+ (&myDice.Config).VMVersionForMsg = data.Value
+ default:
+ failTypes = append(failTypes, data.Type)
+ }
+ }
myDice.MarkModified()
- myDice.Parent.Save()
- return Success(&c, Response{})
-}
-
-func vmVersionForReplySet(c echo.Context) error {
- return vmVersionForExtSetBase(c, func(val string) {
- myDice.VMVersionForReply = val
- })
-}
+ myDice.Save(false)
-func vmVersionForDeckSet(c echo.Context) error {
- return vmVersionForExtSetBase(c, func(val string) {
- myDice.VMVersionForDeck = val
+ return Success(&c, Response{
+ "failTypes": failTypes,
})
}
diff --git a/api/dice_public.go b/api/dice_public.go
new file mode 100644
index 00000000..6ac1ee1c
--- /dev/null
+++ b/api/dice_public.go
@@ -0,0 +1,48 @@
+package api
+
+import (
+ "net/http"
+ "time"
+
+ "sealdice-core/dice"
+
+ "github.com/labstack/echo/v4"
+)
+
+func dicePublicInfo(c echo.Context) error {
+ if !doAuth(c) {
+ return c.JSON(http.StatusForbidden, nil)
+ }
+
+ return c.JSON(http.StatusOK, map[string]interface{}{
+ "endpoints": myDice.ImSession.EndPoints,
+ "config": myDice.Config.PublicDiceConfig})
+}
+func dicePublicSet(c echo.Context) error {
+ if !doAuth(c) {
+ return c.JSON(http.StatusForbidden, nil)
+ }
+ v := struct {
+ Config dice.PublicDiceConfig `json:"config"`
+ Selected []map[string]interface{} `json:"selected"`
+ }{}
+ err := c.Bind(&v)
+ if err != nil {
+ return Error(&c, err.Error(), Response{})
+ }
+ myDice.Config.PublicDiceConfig = v.Config
+ for _, i := range myDice.ImSession.EndPoints {
+ i.IsPublic = false
+ for _, s := range v.Selected {
+ if id, ok := s["id"].(string); ok && i.ID == id {
+ i.IsPublic = true
+ }
+ }
+ }
+ myDice.PublicDiceInfoRegister()
+ myDice.PublicDiceEndpointRefresh()
+ myDice.PublicDiceSetupTick()
+ myDice.LastUpdatedTime = time.Now().Unix()
+ myDice.Save(false)
+ return Success(&c, Response{})
+}
diff --git a/api/helpdoc.go b/api/helpdoc.go
index df4bd9eb..4838b83e 100644
--- a/api/helpdoc.go
+++ b/api/helpdoc.go
@@ -47,7 +47,6 @@ func helpDocReload(c echo.Context) error {
dm.Help.Close()
dm.InitHelp()
- dm.AddHelpWithDice(dm.Dice[0])
return Success(&c, Response{})
}
return Error(&c, "帮助文档正在重新装载", Response{})
diff --git a/api/js.go b/api/js.go
index 231e8269..04532ec1 100644
--- a/api/js.go
+++ b/api/js.go
@@ -26,7 +26,7 @@ func jsExec(c echo.Context) error {
"testMode": true,
})
}
- if !myDice.JsEnable {
+ if !myDice.Config.JsEnable {
resp := c.JSON(200, map[string]interface{}{
"result": false,
"err": "js扩展支持已关闭",
@@ -86,7 +86,7 @@ func jsGetRecord(c echo.Context) error {
if !doAuth(c) {
return c.JSON(http.StatusForbidden, nil)
}
- if !myDice.JsEnable {
+ if !myDice.Config.JsEnable {
resp := c.JSON(200, map[string]interface{}{
"outputs": []string{},
})
@@ -109,7 +109,7 @@ func jsDelete(c echo.Context) error {
"testMode": true,
})
}
- if !myDice.JsEnable {
+ if !myDice.Config.JsEnable {
resp := c.JSON(200, map[string]interface{}{
"result": false,
"err": "js扩展支持已关闭",
@@ -118,13 +118,16 @@ func jsDelete(c echo.Context) error {
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.JsScriptList) {
- dice.JsDelete(myDice, myDice.JsScriptList[v.Index])
+ if err == nil && v.Filename != "" {
+ for _, js := range myDice.JsScriptList {
+ if js.Filename == v.Filename {
+ dice.JsDelete(myDice, js)
+ break
+ }
}
}
@@ -179,7 +182,6 @@ func jsUpload(c echo.Context) error {
// fmt.Println("????", filepath.Join("./data/decks", file.Filename))
file.Filename = strings.ReplaceAll(file.Filename, "/", "_")
file.Filename = strings.ReplaceAll(file.Filename, "\\", "_")
- fmt.Println("XXXX", filepath.Join(myDice.BaseConfig.DataDir, "scripts", file.Filename))
dst, err := os.Create(filepath.Join(myDice.BaseConfig.DataDir, "scripts", file.Filename))
if err != nil {
return err
@@ -200,7 +202,7 @@ func jsList(c echo.Context) error {
if !doAuth(c) {
return c.JSON(http.StatusForbidden, nil)
}
- if !myDice.JsEnable {
+ if !myDice.Config.JsEnable {
resp := c.JSON(200, []*dice.JsScriptInfo{})
return resp
}
@@ -231,7 +233,7 @@ func jsShutdown(c echo.Context) error {
})
}
- if myDice.JsEnable {
+ if myDice.Config.JsEnable {
myDice.JsShutdown()
}
@@ -243,7 +245,7 @@ func jsShutdown(c echo.Context) error {
func jsStatus(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"result": true,
- "status": myDice.JsEnable,
+ "status": myDice.Config.JsEnable,
})
}
@@ -304,24 +306,27 @@ func jsCheckUpdate(c echo.Context) error {
return Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.JsScriptList) {
- jsScript := myDice.JsScriptList[v.Index]
- oldJs, newJs, tempFileName, errUpdate := myDice.JsCheckUpdate(jsScript)
- if errUpdate != nil {
- return Error(&c, errUpdate.Error(), Response{})
+ if err == nil && v.Filename != "" {
+ for _, jsScript := range myDice.JsScriptList {
+ if jsScript.Filename == v.Filename {
+ oldJs, newJs, tempFileName, errUpdate := myDice.JsCheckUpdate(jsScript)
+ if errUpdate != nil {
+ return Error(&c, errUpdate.Error(), Response{})
+ }
+ return Success(&c, Response{
+ "old": oldJs,
+ "new": newJs,
+ "format": "javascript",
+ "filename": jsScript.Filename,
+ "tempFileName": tempFileName,
+ })
}
- return Success(&c, Response{
- "old": oldJs,
- "new": newJs,
- "format": "javascript",
- "tempFileName": tempFileName,
- })
}
+ return Error(&c, "未找到脚本", Response{})
}
return Success(&c, Response{})
}
@@ -333,23 +338,26 @@ func jsUpdate(c echo.Context) error {
if dm.JustForTest {
return Error(&c, "展示模式不支持该操作", Response{"testMode": true})
}
- if !myDice.JsEnable {
+ if !myDice.Config.JsEnable {
return Error(&c, "js扩展支持已关闭", Response{})
}
v := struct {
- Index int `json:"index"`
+ Filename string `json:"filename"`
TempFileName string `json:"tempFileName"`
}{}
err := c.Bind(&v)
- if err == nil {
- if v.Index >= 0 && v.Index < len(myDice.JsScriptList) {
- err = myDice.JsUpdate(myDice.JsScriptList[v.Index], v.TempFileName)
- if err != nil {
- return Error(&c, err.Error(), Response{})
+ if err == nil && v.Filename != "" {
+ for _, jsScript := range myDice.JsScriptList {
+ if jsScript.Filename == v.Filename {
+ err = myDice.JsUpdate(jsScript, v.TempFileName)
+ if err != nil {
+ return Error(&c, err.Error(), Response{})
+ }
+ myDice.MarkModified()
+ break
}
- myDice.MarkModified()
}
}
return Success(&c, Response{})
diff --git a/api/news.go b/api/news.go
index 2a405472..681c71ed 100644
--- a/api/news.go
+++ b/api/news.go
@@ -29,7 +29,7 @@ func getNews(c echo.Context) error {
}
return c.JSON(http.StatusOK, map[string]interface{}{
"result": true,
- "checked": mark == myDice.NewsMark,
+ "checked": mark == myDice.Config.NewsMark,
"news": news,
"newsMark": mark,
})
@@ -55,7 +55,7 @@ func checkNews(c echo.Context) error {
err := c.Bind(&v)
if err == nil {
- myDice.NewsMark = v.NewsMark
+ (&myDice.Config).NewsMark = v.NewsMark
myDice.MarkModified()
return c.JSON(http.StatusOK, map[string]interface{}{
"result": true,
diff --git a/api/reply.go b/api/reply.go
index fdc4bb1d..3f71cc93 100644
--- a/api/reply.go
+++ b/api/reply.go
@@ -202,7 +202,7 @@ func customReplyDebugModeGet(c echo.Context) error {
}
return c.JSON(http.StatusOK, map[string]interface{}{
- "value": myDice.ReplyDebugMode,
+ "value": myDice.Config.ReplyDebugMode,
})
}
@@ -219,9 +219,9 @@ func customReplyDebugModeSet(c echo.Context) error {
return c.String(430, err.Error())
}
- myDice.ReplyDebugMode = v.Value
+ myDice.Config.ReplyDebugMode = v.Value
myDice.MarkModified()
return c.JSON(http.StatusOK, map[string]interface{}{
- "value": myDice.ReplyDebugMode,
+ "value": myDice.Config.ReplyDebugMode,
})
}
diff --git a/api/resource.go b/api/resource.go
index 63bfb44d..11265287 100644
--- a/api/resource.go
+++ b/api/resource.go
@@ -2,7 +2,7 @@ package api
import (
"bytes"
- "fmt"
+ "errors"
"io"
"io/fs"
"mime/multipart"
@@ -246,7 +246,7 @@ func ensurePathSafe(path string) error {
if !strings.HasPrefix(abs, imagesPath) &&
!strings.HasPrefix(abs, audiosPath) &&
!strings.HasPrefix(abs, videosPath) {
- return fmt.Errorf("invalid path")
+ return errors.New("invalid path")
}
return nil
}
diff --git a/api/story_log.go b/api/story_log.go
index fab7fc7f..4553e31a 100644
--- a/api/story_log.go
+++ b/api/story_log.go
@@ -9,12 +9,13 @@ import (
"sealdice-core/dice"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
)
func storyGetInfo(c echo.Context) error {
info, err := model.LogGetInfo(myDice.DBLogs)
if err != nil {
- fmt.Println(err)
+ log.Error("storyGetInfo", err)
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, info)
@@ -27,7 +28,7 @@ func storyGetLogs(c echo.Context) error {
}
logs, err := model.LogGetLogs(myDice.DBLogs)
if err != nil {
- fmt.Println(err)
+ log.Error("storyGetLogs", err)
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, logs)
@@ -70,7 +71,7 @@ func storyGetItems(c echo.Context) error {
}
lines, err := model.LogGetAllLines(myDice.DBLogs, c.QueryParam("groupId"), c.QueryParam("name"))
if err != nil {
- fmt.Println(err)
+ log.Error("storyGetItems", err)
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, lines)
@@ -84,7 +85,7 @@ func storyGetItemPage(c echo.Context) error {
v := model.QueryLogLinePage{}
err := c.Bind(&v)
if err != nil {
- fmt.Println(err)
+ log.Error("storyGetItemPage", err)
return c.JSON(http.StatusInternalServerError, err)
}
if v.PageNum < 1 {
@@ -96,7 +97,7 @@ func storyGetItemPage(c echo.Context) error {
lines, err := model.LogGetLinePage(myDice.DBLogs, &v)
if err != nil {
- fmt.Println(err)
+ log.Error("storyGetItemPage", err)
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, lines)
@@ -109,12 +110,12 @@ func storyDelLog(c echo.Context) error {
v := &model.LogInfo{}
err := c.Bind(&v)
if err != nil {
- fmt.Println(err)
+ log.Error("storyDelLog", err)
return c.JSON(http.StatusInternalServerError, err)
}
is := model.LogDelete(myDice.DBLogs, v.GroupID, v.Name)
if !is {
- fmt.Println(err)
+ log.Error("storyDelLog", "failed to delete")
return c.JSON(http.StatusInternalServerError, false)
}
return c.JSON(http.StatusOK, true)
@@ -128,7 +129,7 @@ func storyUploadLog(c echo.Context) error {
_ = c.Bind(&v)
unofficial, url, err := logSendToBackend(v.GroupID, v.Name)
if err != nil {
- fmt.Println(err)
+ log.Error("storyUploadLog", err)
return c.JSON(http.StatusInternalServerError, err.Error())
}
ret := fmt.Sprintf("跑团日志已上传服务器,链接如下:
%s", url)
diff --git a/api/utils.go b/api/utils.go
index 566861fe..e1a01456 100644
--- a/api/utils.go
+++ b/api/utils.go
@@ -5,7 +5,6 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
- "log"
"net/http"
"os"
"path/filepath"
@@ -17,6 +16,7 @@ import (
"github.com/monaco-io/request"
"sealdice-core/dice"
+ log "sealdice-core/utils/kratos"
)
type Response map[string]interface{}
@@ -39,7 +39,7 @@ func Int64ToBytes(i int64) []byte {
}
func doAuth(c echo.Context) bool {
- token := c.Request().Header.Get("token")
+ token := c.Request().Header.Get("token") //nolint:canonicalheader // private header
if token == "" {
token = c.QueryParam("token")
}
@@ -74,11 +74,11 @@ func GetHexData(c echo.Context, method string, name string) (value []byte, finis
return value, false
}
-var times = 0
+var getAvatarCounter = 0
func getGithubAvatar(c echo.Context) error {
- times++
- if times > 500 {
+ getAvatarCounter++
+ if getAvatarCounter > 500 {
// 请求次数过多
return c.JSON(http.StatusNotFound, "")
}
@@ -110,10 +110,10 @@ func packGocqConfig(relWorkDir string) *bytes.Buffer {
zipWriter := zip.NewWriter(buf)
if err := compressFile(filepath.Join(rootPath, "config.yml"), "config.yml", zipWriter); err != nil {
- log.Println(err)
+ log.Error(err)
}
if err := compressFile(filepath.Join(rootPath, "device.json"), "device.json", zipWriter); err != nil {
- log.Println(err)
+ log.Error(err)
}
_ = compressFile(filepath.Join(rootPath, "data/versions/1.json"), "data/versions/6.json", zipWriter)
_ = compressFile(filepath.Join(rootPath, "data/versions/6.json"), "data/versions/6.json", zipWriter)
@@ -148,11 +148,12 @@ func checkUidExists(c echo.Context, uid string) bool {
var relWorkDir string
if pa.BuiltinMode == "lagrange" {
relWorkDir = "extra/lagrange-qq" + uid
+ } else if pa.BuiltinMode == "lagrange-gocq" {
+ relWorkDir = "extra/lagrange-gocq-qq" + uid
} else {
// 默认为gocq
relWorkDir = "extra/go-cqhttp-qq" + uid
}
- fmt.Println(relWorkDir, i.RelWorkDir)
if relWorkDir == i.RelWorkDir {
// 不允许工作路径重复
_ = c.JSON(CodeAlreadyExists, i)
@@ -169,7 +170,45 @@ func checkUidExists(c echo.Context, uid string) bool {
return false
}
-var timeout = 5 * time.Second
+const (
+ checkTimes = 3
+ checkTimeout = 5 * time.Second
+)
+
+func checkHTTPConnectivity(url string) bool {
+ client := http.Client{
+ Timeout: checkTimeout,
+ }
+ rsChan := make(chan bool, checkTimes)
+ once := func(wg *sync.WaitGroup, url string) {
+ defer wg.Done()
+ resp, err := client.Get(url)
+ log.Debugf("check http connectivity, url=%s", url)
+ if err == nil {
+ _ = resp.Body.Close()
+ rsChan <- true
+ } else {
+ log.Debugf("url can't be connected, error: %s", err)
+ rsChan <- false
+ }
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(checkTimes)
+ for range checkTimes {
+ go once(&wg, url)
+ }
+ go func() {
+ wg.Wait()
+ close(rsChan)
+ }()
+
+ ok := true
+ for res := range rsChan {
+ ok = ok && res
+ }
+ return ok
+}
func checkNetworkHealth(c echo.Context) error {
total := 5 // baidu, seal, sign, google, github
@@ -178,25 +217,20 @@ func checkNetworkHealth(c echo.Context) error {
wg.Add(total)
rsChan := make(chan string, 5)
- checkHTTPConnectivity := func(target string, urls []string) {
+ checkUrls := func(target string, urls []string) {
defer wg.Done()
- client := http.Client{
- Timeout: timeout,
- }
for _, url := range urls {
- resp, err := client.Get(url)
- if err == nil {
- _ = resp.Body.Close()
+ if checkHTTPConnectivity(url) {
rsChan <- target
break
}
}
}
- go checkHTTPConnectivity("baidu", []string{"https://baidu.com"})
- go checkHTTPConnectivity("seal", dice.BackendUrls)
- go checkHTTPConnectivity("sign", []string{"https://sign.lagrangecore.org/api/sign/ping"})
- go checkHTTPConnectivity("google", []string{"https://google.com"})
- go checkHTTPConnectivity("github", []string{"https://github.com"})
+ go checkUrls("baidu", []string{"https://baidu.com"})
+ go checkUrls("seal", dice.BackendUrls)
+ go checkUrls("sign", []string{"https://sign.lagrangecore.org/api/sign/ping"})
+ go checkUrls("google", []string{"https://google.com"})
+ go checkUrls("github", []string{"https://github.com"})
go func() {
wg.Wait()
diff --git a/dice/builtin_commands.go b/dice/builtin_commands.go
index 1cce0bce..06379c7f 100644
--- a/dice/builtin_commands.go
+++ b/dice/builtin_commands.go
@@ -13,11 +13,12 @@ import (
"time"
"github.com/golang-module/carbon"
- "github.com/samber/lo"
-
"github.com/juliangruber/go-intersect"
cp "github.com/otiai10/copy"
+ "github.com/samber/lo"
ds "github.com/sealdice/dicescript"
+
+ "sealdice-core/dice/docengine"
)
/** 这几条指令不能移除 */
@@ -75,7 +76,7 @@ func (d *Dice) registerCoreCommands() {
if reason == "" {
reason = "骰主指令"
}
- d.BanList.AddScoreBase(uid, d.BanList.ThresholdBan, "骰主指令", reason, ctx)
+ (&d.Config).BanList.AddScoreBase(uid, (&d.Config).BanList.ThresholdBan, "骰主指令", reason, ctx)
ReplyToSender(ctx, msg, fmt.Sprintf("已将用户/群组 %s 加入黑名单,原因: %s", uid, reason))
case "rm", "del":
uid = getID()
@@ -83,7 +84,7 @@ func (d *Dice) registerCoreCommands() {
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
}
- item, ok := d.BanList.GetByID(uid)
+ item, ok := (&d.Config).BanList.GetByID(uid)
if !ok || (item.Rank != BanRankBanned && item.Rank != BanRankTrusted && item.Rank != BanRankWarn) {
ReplyToSender(ctx, msg, "找不到用户/群组")
break
@@ -99,14 +100,14 @@ func (d *Dice) registerCoreCommands() {
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
}
- d.BanList.SetTrustByID(uid, "骰主指令", "骰主指令")
+ (&d.Config).BanList.SetTrustByID(uid, "骰主指令", "骰主指令")
ReplyToSender(ctx, msg, fmt.Sprintf("已将用户/群组 %s 加入信任列表", uid))
case "list", "show":
// ban/warn/trust
var extra, text string
extra = cmdArgs.GetArgN(2)
- d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
+ (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
if v.Rank == BanRankNormal {
return true
}
@@ -133,7 +134,7 @@ func (d *Dice) registerCoreCommands() {
break
}
- v, exists := d.BanList.Map.Load(targetID)
+ v, exists := (&d.Config).BanList.Map.Load(targetID)
if !exists {
ReplyToSender(ctx, msg, fmt.Sprintf("所查询的<%s>情况:正常(0)", targetID))
break
@@ -229,10 +230,12 @@ func (d *Dice) registerCoreCommands() {
var (
useGroupSearch bool
group string
+ text string = cmdArgs.CleanArgs
)
- if _group := cmdArgs.GetArgN(1); strings.HasPrefix(_group, "#") {
+ if rawGroup := cmdArgs.GetArgN(1); strings.HasPrefix(rawGroup, "#") {
useGroupSearch = true
- fakeGroup := strings.TrimPrefix(_group, "#")
+ fakeGroup := strings.TrimPrefix(rawGroup, "#")
+ text = strings.TrimPrefix(text, rawGroup+" ")
// 转换 group 别名
if _g, ok := d.Parent.Help.GroupAliases[fakeGroup]; ok {
@@ -248,6 +251,7 @@ func (d *Dice) registerCoreCommands() {
var id string
if cmdArgs.GetKwarg("rand") != nil || cmdArgs.GetKwarg("随机") != nil {
+ // FIXME: byd WHAT IS THAT
_id := rand.Uint64()%d.Parent.Help.CurID + 1
id = strconv.FormatUint(_id, 10)
}
@@ -268,8 +272,10 @@ func (d *Dice) registerCoreCommands() {
}
if id != "" {
- text, exists := d.Parent.Help.TextMap.Load(id)
- if exists {
+ text, err := d.Parent.Help.searchEngine.GetItemByID(id)
+ if err == nil {
+ // Copied from 支援换行符 By Fripine #963
+ text.Content = ctx.TranslateSplit(text.Content)
content := d.Parent.Help.GetContent(text, 0)
ReplyToSender(ctx, msg, fmt.Sprintf("词条: %s:%s\n%s", text.PackageName, text.Title, content))
} else {
@@ -278,14 +284,16 @@ func (d *Dice) registerCoreCommands() {
return CmdExecuteResult{Matched: true, Solved: true}
}
- var val string
- if useGroupSearch {
- val = cmdArgs.GetArgN(2)
- } else {
- val = cmdArgs.GetArgN(1)
- }
- if val == "" {
- return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
+ { // 判断是否关键字缺失
+ var val string
+ if useGroupSearch {
+ val = cmdArgs.GetArgN(2)
+ } else {
+ val = cmdArgs.GetArgN(1)
+ }
+ if val == "" {
+ return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
+ }
}
numLimit := 4
@@ -305,8 +313,6 @@ func (d *Dice) registerCoreCommands() {
}
}
- text := strings.TrimPrefix(cmdArgs.CleanArgs, "#"+group+" ")
-
if numLimit <= 0 {
numLimit = 1
} else if numLimit > 10 {
@@ -319,6 +325,7 @@ func (d *Dice) registerCoreCommands() {
// 未指定搜索分组时,取当前群指定的分组
group = ctx.Group.DefaultHelpGroup
}
+ // 进行结果搜索
search, total, pgStart, pgEnd, err := d.Parent.Help.Search(ctx, text, false, numLimit, page, group)
if err != nil {
ReplyToSender(ctx, msg, groupStr+"搜索故障: "+err.Error())
@@ -334,20 +341,25 @@ func (d *Dice) registerCoreCommands() {
}
hasSecond := len(search.Hits) >= 2
- best, ok := d.Parent.Help.TextMap.Load(search.Hits[0].ID)
- if !ok {
- d.Logger.Errorf("加载d.Parent.Help.TextMap.Load(search.Hits[0].ID)->(%s)的数据出现错误!", search.Hits[0].ID)
- ReplyToSender(ctx, msg, "未找到搜索结果,出现数据加载错误!")
- return CmdExecuteResult{Matched: true, Solved: true}
+ // 准备接下来读取这里面的Fields
+ bestRaw := search.Hits[0].Fields
+ best := &docengine.HelpTextItem{
+ Group: fmt.Sprintf("%v", bestRaw["group"]),
+ From: fmt.Sprintf("%v", bestRaw["from"]),
+ Title: fmt.Sprintf("%v", bestRaw["title"]),
+ Content: fmt.Sprintf("%v", bestRaw["content"]),
+ PackageName: fmt.Sprintf("%v", bestRaw["package"]),
+ // 这俩是什么东西?!
+ // KeyWords: "",
+ // RelatedExt: nil,
}
others := ""
for _, i := range search.Hits {
- t, ok := d.Parent.Help.TextMap.Load(i.ID)
- if !ok {
- d.Logger.Errorf("加载d.Parent.Help.TextMap.Load(search.Hits[0].ID)->(%s)的数据出现错误!", search.Hits[0].ID)
- ReplyToSender(ctx, msg, "未找到搜索结果,出现数据加载错误!")
- return CmdExecuteResult{Matched: true, Solved: true}
+ t := &docengine.HelpTextItem{
+ Group: fmt.Sprintf("%v", i.Fields["group"]),
+ Title: fmt.Sprintf("%v", i.Fields["title"]),
+ PackageName: fmt.Sprintf("%v", i.Fields["package"]),
}
if t.Group != "" && t.Group != HelpBuiltinGroup {
others += fmt.Sprintf("[%s][%s]【%s:%s】 匹配度%.2f\n", i.ID, t.Group, t.PackageName, t.Title, i.Score)
@@ -375,6 +387,8 @@ func (d *Dice) registerCoreCommands() {
var bestResult string
if showBest {
+ // Copied from 支援换行符 By Fripine #963
+ best.Content = ctx.TranslateSplit(best.Content)
content := d.Parent.Help.GetContent(best, 0)
bestResult = fmt.Sprintf("最优先结果%s:\n词条: %s:%s\n%s\n\n", groupStr, best.PackageName, best.Title, content)
}
@@ -428,7 +442,6 @@ func (d *Dice) registerCoreCommands() {
dm.Help.Close()
dm.InitHelp()
- dm.AddHelpWithDice(dm.Dice[0])
ReplyToSender(ctx, msg, "帮助文档已经重新装载")
} else {
ReplyToSender(ctx, msg, "帮助文档正在重新装载,请稍后...")
@@ -466,11 +479,16 @@ func (d *Dice) registerCoreCommands() {
search, _, _, _, err := d.Parent.Help.Search(ctx, cmdArgs.CleanArgs, true, 1, 1, "")
if err == nil {
if len(search.Hits) > 0 {
- a, ok := d.Parent.Help.TextMap.Load(search.Hits[0].ID)
- if !ok {
- d.Logger.Error("HELPDOC:读取ID对应的信息出现问题")
- ReplyToSender(ctx, msg, "HELPDOC:读取ID对应的信息出现问题")
- return CmdExecuteResult{Matched: true, Solved: true}
+ a := &docengine.HelpTextItem{
+ Group: fmt.Sprintf("%v", search.Hits[0].Fields["group"]),
+ From: fmt.Sprintf("%v", search.Hits[0].Fields["from"]),
+ Title: fmt.Sprintf("%v", search.Hits[0].Fields["title"]),
+ // Edited. Original change from 支援换行符 By Fripine #963
+ Content: ctx.TranslateSplit(fmt.Sprintf("%v", search.Hits[0].Fields["content"])),
+ PackageName: fmt.Sprintf("%v", search.Hits[0].Fields["package"]),
+ // 这俩是什么东西?!
+ KeyWords: "",
+ RelatedExt: nil,
}
content := d.Parent.Help.GetContent(a, 0)
ReplyToSender(ctx, msg, fmt.Sprintf("%s:%s\n%s", a.PackageName, a.Title, content))
@@ -495,7 +513,7 @@ func (d *Dice) registerCoreCommands() {
if inGroup {
// 不响应裸指令选项
- if len(cmdArgs.At) < 1 && ctx.Dice.IgnoreUnaddressedBotCmd {
+ if len(cmdArgs.At) < 1 && ctx.Dice.Config.IgnoreUnaddressedBotCmd {
return CmdExecuteResult{Matched: true, Solved: false}
}
// 不响应at其他人
@@ -527,7 +545,7 @@ func (d *Dice) registerCoreCommands() {
}
if cmdArgs.IsArgEqual(1, "on") {
- if !(msg.Platform == "QQ-CH" || ctx.Dice.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) {
+ if !(msg.Platform == "QQ-CH" || ctx.Dice.Config.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者"))
return CmdExecuteResult{Matched: true, Solved: true}
}
@@ -550,7 +568,7 @@ func (d *Dice) registerCoreCommands() {
return CmdExecuteResult{Matched: true, Solved: true}
} else if cmdArgs.IsArgEqual(1, "off") {
- if !(msg.Platform == "QQ-CH" || ctx.Dice.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) {
+ if !(msg.Platform == "QQ-CH" || ctx.Dice.Config.BotExtFreeSwitch || ctx.PrivilegeLevel >= 40) {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者"))
return CmdExecuteResult{Matched: true, Solved: true}
}
@@ -1006,7 +1024,7 @@ func (d *Dice) registerCoreCommands() {
dm.UpdateCheckRequestChan <- 1
// 等待获取新版本,最多10s
- for i := 0; i < 5; i++ {
+ for range 5 {
time.Sleep(2 * time.Second)
if dm.AppVersionOnline != nil {
break
@@ -1036,11 +1054,11 @@ func (d *Dice) registerCoreCommands() {
ret := <-dm.UpdateDownloadedChan
if ctx.IsPrivate {
- ctx.Dice.UpgradeWindowID = msg.Sender.UserID
+ ctx.Dice.Config.UpgradeWindowID = msg.Sender.UserID
} else {
- ctx.Dice.UpgradeWindowID = ctx.Group.GroupID
+ ctx.Dice.Config.UpgradeWindowID = ctx.Group.GroupID
}
- ctx.Dice.UpgradeEndpointID = ctx.EndPoint.ID
+ ctx.Dice.Config.UpgradeEndpointID = ctx.EndPoint.ID
ctx.Dice.Save(true)
bakFn, _ := ctx.Dice.Parent.Backup(BackupSelectionAll, false)
@@ -1103,7 +1121,6 @@ func (d *Dice) registerCoreCommands() {
dm.IsHelpReloading = true
dm.Help.Close()
dm.InitHelp()
- dm.AddHelpWithDice(dice)
ReplyToSender(ctx, msg, "帮助文档已重载")
} else {
ReplyToSender(ctx, msg, "帮助文档正在重新装载")
@@ -1207,7 +1224,7 @@ func (d *Dice) registerCoreCommands() {
}
ctx.SystemTemplate = ctx.Group.GetCharTemplate(ctx.Dice)
- if ctx.Dice.CommandCompatibleMode {
+ if ctx.Dice.Config.CommandCompatibleMode {
if (cmdArgs.Command == "rd" || cmdArgs.Command == "rhd" || cmdArgs.Command == "rdh") && len(cmdArgs.Args) >= 1 {
if m, _ := regexp.MatchString(`^\d|优势|劣势|\+|-`, cmdArgs.CleanArgs); m {
if cmdArgs.IsSpaceBeforeArgs {
@@ -1348,12 +1365,12 @@ func (d *Dice) registerCoreCommands() {
if cmdArgs.SpecialExecuteTimes > 1 {
VarSetValueInt64(ctx, "$t次数", int64(cmdArgs.SpecialExecuteTimes))
- if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.MaxExecuteTime) {
+ if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告"))
return CmdExecuteResult{Matched: true, Solved: true}
}
var texts []string
- for i := 0; i < cmdArgs.SpecialExecuteTimes; i++ {
+ for range cmdArgs.SpecialExecuteTimes {
ret := rollOne()
if ret != nil {
return *ret
@@ -1501,7 +1518,7 @@ func (d *Dice) registerCoreCommands() {
if cmdArgs.IsArgEqual(1, "list") {
showList()
} else if cmdArgs.IsArgEqual(last, "on") {
- if !ctx.Dice.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 {
+ if !ctx.Dice.Config.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者"))
return CmdExecuteResult{Matched: true, Solved: true}
}
@@ -1524,7 +1541,7 @@ func (d *Dice) registerCoreCommands() {
var extNames []string
var conflictsAll []string
- for index := 0; index < len(cmdArgs.Args); index++ {
+ for index := range len(cmdArgs.Args) {
extName := strings.ToLower(cmdArgs.Args[index])
if i := d.ExtFind(extName); i != nil {
extNames = append(extNames, extName)
@@ -1544,14 +1561,14 @@ func (d *Dice) registerCoreCommands() {
ReplyToSender(ctx, msg, text)
}
} else if cmdArgs.IsArgEqual(last, "off") {
- if !ctx.Dice.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 {
+ if !ctx.Dice.Config.BotExtFreeSwitch && ctx.PrivilegeLevel < 40 {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_无权限_非master/管理/邀请者"))
return CmdExecuteResult{Matched: true, Solved: true}
}
var closed []string
var notfound []string
- for index := 0; index < len(cmdArgs.Args); index++ {
+ for index := range len(cmdArgs.Args) {
extName := cmdArgs.Args[index]
extName = d.ExtAliasToName(extName)
ei := ctx.Group.ExtInactiveByName(extName)
@@ -1917,7 +1934,7 @@ func (d *Dice) registerCoreCommands() {
setCurPlayerName(b)
}
attrs.LastModifiedTime = time.Now().Unix()
- attrs.SaveToDB(am.db, nil) // 直接保存
+ attrs.SaveToDB(am.db) // 直接保存
ReplyToSender(ctx, msg, "操作完成")
} else {
ReplyToSender(ctx, msg, "此角色名已存在")
diff --git a/dice/censor/trie.go b/dice/censor/trie.go
index 6810ff72..8cdd043c 100644
--- a/dice/censor/trie.go
+++ b/dice/censor/trie.go
@@ -57,7 +57,7 @@ func (t *trie) Match(text string) (sensitiveWords map[string]Level) {
sensitiveWords = map[string]Level{}
chars := []rune(text)
- for i := 0; i < len(chars); i++ {
+ for i := range chars {
cur := t.root.findChild(chars[i])
if cur == nil {
continue
diff --git a/dice/config.go b/dice/config.go
index a10a3591..8c218c27 100644
--- a/dice/config.go
+++ b/dice/config.go
@@ -14,11 +14,11 @@ import (
"time"
wr "github.com/mroth/weightedrand"
- "golang.org/x/time/rate"
+ "github.com/samber/lo"
"gopkg.in/yaml.v3"
"sealdice-core/dice/model"
- "sealdice-core/utils"
+ log "sealdice-core/utils/kratos"
)
// type TextTemplateWithWeight = map[string]map[string]uint
@@ -78,74 +78,94 @@ type ConfigItem struct {
}
func (i *ConfigItem) UnmarshalJSON(data []byte) error {
- raw := map[string]any{}
+ raw := map[string]json.RawMessage{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
- var ok bool
- if i.Key, ok = raw["key"].(string); !ok {
- return errors.New("'key' must be a string")
+ if err := json.Unmarshal(raw["key"], &i.Key); err != nil {
+ return fmt.Errorf("ConfigItem: unmarshal 'key' failed as %w", err)
}
- if i.Type, ok = raw["type"].(string); !ok {
- return errors.New("'type' must be a string")
+ if err := json.Unmarshal(raw["type"], &i.Type); err != nil {
+ return fmt.Errorf("ConfigItem (%s): unmarshal 'type' failed as %w", i.Key, err)
}
- if i.Description, ok = raw["description"].(string); !ok {
- return errors.New("'description' must be a string")
+ if err := json.Unmarshal(raw["description"], &i.Description); err != nil {
+ return fmt.Errorf("ConfigItem (%s): unmarshal 'description' failed as %w", i.Key, err)
}
if v, ok := raw["deprecated"]; ok {
- if i.Deprecated, ok = v.(bool); !ok {
- return errors.New("'deprecated' must be a bool")
+ if err := json.Unmarshal(v, &i.Deprecated); err != nil {
+ return fmt.Errorf("ConfigItem (%s): unmarshal 'deprecated' failed as %w", i.Key, err)
}
}
switch i.Type {
- case "string", "bool", "float":
- i.DefaultValue = raw["defaultValue"]
- i.Value = raw["value"]
+ case "string", "task:cron", "task:daily":
+ var stringVal string
+ if err := json.Unmarshal(raw["defaultValue"], &stringVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
+ }
+ i.DefaultValue = stringVal
+ if err := json.Unmarshal(raw["value"], &stringVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
+ }
+ i.Value = stringVal
+ case "bool":
+ var boolVal bool
+ if err := json.Unmarshal(raw["defaultValue"], &boolVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
+ }
+ i.DefaultValue = boolVal
+ if err := json.Unmarshal(raw["value"], &boolVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
+ }
+ i.Value = boolVal
+ case "float":
+ var floatVal float64
+ if err := json.Unmarshal(raw["defaultValue"], &floatVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
+ }
+ i.DefaultValue = floatVal
+ if err := json.Unmarshal(raw["value"], &floatVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
+ }
+ i.Value = floatVal
case "int":
- // 2024.08.09 1.4.6首发版本unmarshal产生类型报错修复
- if v, ok := raw["defaultValue"].(float64); ok {
- i.DefaultValue = int64(v)
- } else if v, ok := raw["defaultValue"].(int64); ok {
- i.DefaultValue = v
+ var intVal int64
+ if err := json.Unmarshal(raw["defaultValue"], &intVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
}
- if v, ok := raw["value"]; ok {
- if v2, ok := v.(float64); ok {
- i.Value = int64(v2)
- } else if v2, ok := v.(int64); ok {
- i.Value = v2
- }
+ i.DefaultValue = intVal
+ if err := json.Unmarshal(raw["value"], &intVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
}
+ i.Value = intVal
case "template":
- {
- v := raw["defaultValue"].([]interface{})
- strarr := make([]string, len(v))
- for i, vv := range v {
- strarr[i] = vv.(string)
- }
- i.DefaultValue = strarr
+ var templateVal []string
+ if err := json.Unmarshal(raw["defaultValue"], &templateVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
}
- if v, ok := raw["value"]; ok {
- vv := v.([]interface{})
- strarr := make([]string, len(vv))
- for i, vv := range vv {
- strarr[i] = vv.(string)
- }
- i.Value = strarr
+ i.DefaultValue = templateVal
+ if err := json.Unmarshal(raw["value"], &templateVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
}
+ i.Value = templateVal
case "option":
- i.DefaultValue = raw["defaultValue"]
- i.Value = raw["value"]
- v := raw["option"].([]interface{})
- strarr := make([]string, len(v))
- for i, vv := range v {
- strarr[i] = vv.(string)
+ var stringVal string
+ var optionVal []string
+ if err := json.Unmarshal(raw["defaultValue"], &stringVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'defaultValue' failed as %w", i.Key, i.Type, err)
}
- i.Option = strarr
+ i.DefaultValue = stringVal
+ if err := json.Unmarshal(raw["value"], &stringVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'value' failed as %w", i.Key, i.Type, err)
+ }
+ i.Value = stringVal
+ if err := json.Unmarshal(raw["option"], &optionVal); err != nil {
+ return fmt.Errorf("ConfigItem (%s-%s): unmarshal 'option' failed as %w", i.Key, i.Type, err)
+ }
+ i.Option = optionVal
default:
- return errors.New("unsupported type " + i.Type)
+ return errors.New("ConfigItem.UnmarshalJSON: unsupported type " + i.Type)
}
-
return nil
}
@@ -326,16 +346,16 @@ func (cm *ConfigManager) getConfig(pluginName, key string) *ConfigItem {
func (cm *ConfigManager) ResetConfigToDefault(pluginName, key string) {
cm.lock.Lock()
defer cm.lock.Unlock()
- fmt.Println("try reset config to default", pluginName, key)
+ log.Debug("try reset config to default", pluginName, key)
plugin, ok := cm.Plugins[pluginName]
if !ok {
- fmt.Println("plugin not found", pluginName)
+ log.Debug("plugin not found", pluginName)
return
}
configItem, exists := plugin.Configs[key]
if exists {
- fmt.Println("reset config to default", pluginName, key)
+ log.Debug("reset config to default", pluginName, key)
configItem.Value = configItem.DefaultValue
plugin.Configs[key] = configItem
if strings.HasPrefix(configItem.Type, "task:") {
@@ -755,9 +775,19 @@ func setupBaseTextTemplate(d *Dice) {
"制卡_分隔符": {
{`\n`, 1},
},
+ "检定_单项结果文本": {
+ {`{$t检定过程文本} = {$t检定结果}`, 1},
+ },
"检定": {
{`{$t玩家}的"{$t技能}"检定(DND5E)结果为: {$t检定过程文本} = {$t检定结果}`, 1},
},
+ "检定_多轮": {
+ {`对{$t玩家}的"{$t技能}"进行了{$t次数}次检定(DND5E),结果为:\n{$t结果文本}`, 1},
+ },
+ "检定_轮数过多警告": {
+ {`你真的需要这么多轮检定?{核心:骰子名字}将对你提高警惕!`, 1},
+ {`不支持连续检定{$t次数}次,{核心:骰子名字}觉得这太多了。`, 1},
+ },
},
"核心": {
"骰子名字": {
@@ -1003,6 +1033,9 @@ func setupBaseTextTemplate(d *Dice) {
"鸽子理由": guguReason,
},
"其它": {
+ "抽牌_牌堆列表": {
+ {"载入并开启的牌堆:\n{$t牌堆列表}", 1},
+ },
"抽牌_列表": {
{"{$t原始列表}", 1},
},
@@ -1463,9 +1496,18 @@ func setupBaseTextTemplate(d *Dice) {
SubType: ".dndx",
ExtraText: "带属性名",
},
+ "检定_单项结果文本": {
+ SubType: ".rc",
+ },
"检定": {
SubType: ".rc 力量",
},
+ "检定_多轮": {
+ SubType: ".rc 3#力量",
+ },
+ "检定_轮数过多警告": {
+ SubType: ".rc 30#",
+ },
},
"核心": {
"骰子名字": {
@@ -1724,6 +1766,9 @@ func setupBaseTextTemplate(d *Dice) {
},
},
"其它": {
+ "抽牌_牌堆列表": {
+ SubType: ".draw list",
+ },
"抽牌_列表": {
SubType: ".draw keys",
},
@@ -2057,156 +2102,32 @@ func getNumVal(i interface{}) uint {
}
func (d *Dice) loads() {
+ config := NewConfig(d)
data, err := os.ReadFile(filepath.Join(d.BaseConfig.DataDir, "serve.yaml"))
-
- // 配置这块弄得比较屎,有机会换个方案。。。
- // TODO(Xiangze Li): 不管谁都好 赶紧重写吧, 谁能想起来加了配置还要在这里添一行才能Load出来哇
if err == nil { //nolint:nestif
+ err3 := config.LoadYamlConfig(data)
+ if err3 != nil {
+ d.Logger.Error("serve.yaml parse failed")
+ panic(err3)
+ }
+
+ // 有一些配置项被用 jsbind 导出了,只能先留在 Dice 不迁移了
dNew := Dice{}
err2 := yaml.Unmarshal(data, &dNew)
if err2 != nil {
d.Logger.Error("serve.yaml parse failed")
panic(err2)
}
- d.CommandCompatibleMode = true // 一直为true即可
d.ImSession.EndPoints = dNew.ImSession.EndPoints
- d.CommandPrefix = dNew.CommandPrefix
d.DiceMasters = dNew.DiceMasters
- d.VersionCode = dNew.VersionCode
- d.MessageDelayRangeStart = dNew.MessageDelayRangeStart
- d.MessageDelayRangeEnd = dNew.MessageDelayRangeEnd
- d.WorkInQQChannel = dNew.WorkInQQChannel
- d.QQChannelLogMessage = dNew.QQChannelLogMessage
- d.QQChannelAutoOn = dNew.QQChannelAutoOn
- d.QQEnablePoke = dNew.QQEnablePoke
- d.TextCmdTrustOnly = dNew.TextCmdTrustOnly
- d.IgnoreUnaddressedBotCmd = dNew.IgnoreUnaddressedBotCmd
- d.UILogLimit = dNew.UILogLimit
- d.FriendAddComment = dNew.FriendAddComment
- d.NoticeIDs = dNew.NoticeIDs
- d.ExtDefaultSettings = dNew.ExtDefaultSettings
- d.CustomReplyConfigEnable = dNew.CustomReplyConfigEnable
- d.RefuseGroupInvite = dNew.RefuseGroupInvite
- d.DefaultCocRuleIndex = dNew.DefaultCocRuleIndex
- d.UpgradeWindowID = dNew.UpgradeWindowID
- d.UpgradeEndpointID = dNew.UpgradeEndpointID
- d.BotExtFreeSwitch = dNew.BotExtFreeSwitch
- d.RateLimitEnabled = dNew.RateLimitEnabled
- d.TrustOnlyMode = dNew.TrustOnlyMode
- d.AliveNoticeEnable = dNew.AliveNoticeEnable
- d.AliveNoticeValue = dNew.AliveNoticeValue
- d.ReplyDebugMode = dNew.ReplyDebugMode
- d.LogSizeNoticeCount = dNew.LogSizeNoticeCount
- d.LogSizeNoticeEnable = dNew.LogSizeNoticeEnable
- d.PlayerNameWrapEnable = dNew.PlayerNameWrapEnable
- d.MailEnable = dNew.MailEnable
- d.MailFrom = dNew.MailFrom
- d.MailPassword = dNew.MailPassword
- d.MailSMTP = dNew.MailSMTP
- d.JsEnable = dNew.JsEnable
- d.DisabledJsScripts = dNew.DisabledJsScripts
- d.NewsMark = dNew.NewsMark
-
- d.QuitInactiveThreshold = dNew.QuitInactiveThreshold
- d.QuitInactiveBatchSize = dNew.QuitInactiveBatchSize
- if d.QuitInactiveBatchSize == 0 {
- d.QuitInactiveBatchSize = 10
- }
- d.QuitInactiveBatchWait = dNew.QuitInactiveBatchWait
- if d.QuitInactiveBatchWait == 0 {
- d.QuitInactiveBatchWait = 30
- }
-
- d.EnableCensor = dNew.EnableCensor
- d.CensorMode = dNew.CensorMode
- d.CensorThresholds = dNew.CensorThresholds
- d.CensorHandlers = dNew.CensorHandlers
- d.CensorScores = dNew.CensorScores
- d.CensorCaseSensitive = dNew.CensorCaseSensitive
- d.CensorMatchPinyin = dNew.CensorMatchPinyin
- d.CensorFilterRegexStr = dNew.CensorFilterRegexStr
-
- d.VMVersionForDeck = dNew.VMVersionForDeck
- d.VMVersionForReply = dNew.VMVersionForReply
-
- if d.VMVersionForDeck == "" {
- d.VMVersionForDeck = "v2"
- }
-
- if d.VMVersionForReply == "" {
- d.VMVersionForReply = "v1"
- }
-
- if dNew.BanList != nil {
- d.BanList.BanBehaviorRefuseReply = dNew.BanList.BanBehaviorRefuseReply
- d.BanList.BanBehaviorRefuseInvite = dNew.BanList.BanBehaviorRefuseInvite
- d.BanList.BanBehaviorQuitLastPlace = dNew.BanList.BanBehaviorQuitLastPlace
- d.BanList.BanBehaviorQuitPlaceImmediately = dNew.BanList.BanBehaviorQuitPlaceImmediately
- d.BanList.BanBehaviorQuitIfAdmin = dNew.BanList.BanBehaviorQuitIfAdmin
-
- d.BanList.ScoreReducePerMinute = dNew.BanList.ScoreReducePerMinute
-
- d.BanList.ThresholdWarn = dNew.BanList.ThresholdWarn
- d.BanList.ThresholdBan = dNew.BanList.ThresholdBan
- d.BanList.ScoreGroupMuted = dNew.BanList.ScoreGroupMuted
- d.BanList.ScoreGroupKicked = dNew.BanList.ScoreGroupKicked
- d.BanList.ScoreTooManyCommand = dNew.BanList.ScoreTooManyCommand
-
- d.BanList.JointScorePercentOfGroup = dNew.BanList.JointScorePercentOfGroup
- d.BanList.JointScorePercentOfInviter = dNew.BanList.JointScorePercentOfInviter
- }
-
- d.MaxExecuteTime = dNew.MaxExecuteTime
- if d.MaxExecuteTime == 0 {
- d.MaxExecuteTime = 12
- }
-
- d.MaxCocCardGen = dNew.MaxCocCardGen
- if d.MaxCocCardGen == 0 {
- d.MaxCocCardGen = 5
- }
-
- d.PersonalReplenishRateStr = dNew.PersonalReplenishRateStr
- if d.PersonalReplenishRateStr == "" {
- d.PersonalReplenishRateStr = "@every 3s"
- d.PersonalReplenishRate = rate.Every(time.Second * 3)
- } else {
- if parsed, errParse := utils.ParseRate(d.PersonalReplenishRateStr); errParse == nil {
- d.PersonalReplenishRate = parsed
- } else {
- d.Logger.Errorf("解析PersonalReplenishRate失败: %v", errParse)
- d.PersonalReplenishRateStr = "@every 3s"
- d.PersonalReplenishRate = rate.Every(time.Second * 3)
- }
- }
-
- d.PersonalBurst = dNew.PersonalBurst
- if d.PersonalBurst == 0 {
- d.PersonalBurst = 3
+ if len(d.DiceMasters) == 0 {
+ d.DiceMasters = DefaultConfig.DiceMasters
}
-
- d.GroupReplenishRateStr = dNew.GroupReplenishRateStr
- if d.GroupReplenishRateStr == "" {
- d.GroupReplenishRateStr = "@every 3s"
- d.GroupReplenishRate = rate.Every(time.Second * 3)
- } else {
- if parsed, errParse := utils.ParseRate(d.GroupReplenishRateStr); errParse == nil {
- d.GroupReplenishRate = parsed
- } else {
- d.Logger.Errorf("解析GroupReplenishRate失败: %v", errParse)
- d.GroupReplenishRateStr = "@every 3s"
- d.GroupReplenishRate = rate.Every(time.Second * 3)
- }
- }
-
- d.GroupBurst = dNew.GroupBurst
- if d.GroupBurst == 0 {
- d.GroupBurst = 3
- }
-
- if d.DiceMasters == nil || len(d.DiceMasters) == 0 {
- d.DiceMasters = []string{"UI:1001"}
+ d.CommandPrefix = dNew.CommandPrefix
+ if len(d.CommandPrefix) == 0 {
+ d.CommandPrefix = DefaultConfig.CommandPrefix
}
+ d.DeckList = dNew.DeckList
var newDiceMasters []string
for _, i := range d.DiceMasters {
if i != "<平台,如QQ>:<帐号,如QQ号>" {
@@ -2252,6 +2173,9 @@ func (d *Dice) loads() {
d.ImSession.ServiceAtNew.Range(func(_ string, groupInfo *GroupInfo) bool {
// Pinenutn: ServiceAtNew重构
var tmp []*ExtInfo
+ groupInfo.ExtListSnapshot = lo.Map(groupInfo.ActivatedExtList, func(item *ExtInfo, index int) string {
+ return item.Name
+ })
for _, i := range groupInfo.ActivatedExtList {
if m[i.Name] != nil {
tmp = append(tmp, m[i.Name])
@@ -2277,22 +2201,7 @@ func (d *Dice) loads() {
return true
})
- if d.VersionCode != 0 && d.VersionCode < 10000 {
- d.CustomReplyConfigEnable = false
- }
-
- if d.VersionCode != 0 && d.VersionCode < 10001 {
- d.AliveNoticeValue = "@every 3h"
- }
-
- if d.VersionCode != 0 && d.VersionCode < 10003 {
- d.Logger.Infof("进行配置文件版本升级: %d -> %d", d.VersionCode, 10003)
- d.LogSizeNoticeCount = 500
- d.LogSizeNoticeEnable = true
- d.CustomReplyConfigEnable = true
- }
-
- if d.VersionCode != 0 && d.VersionCode < 10005 {
+ if config.VersionCode != 0 && config.VersionCode < 10005 {
d.RunAfterLoaded = append(d.RunAfterLoaded, func() {
d.Logger.Info("正在自动升级自定义文案文件")
for index, text := range d.TextMapRaw["核心"]["昵称_重置"] {
@@ -2315,10 +2224,10 @@ func (d *Dice) loads() {
}
// 1.2 版本
- if d.VersionCode != 0 && d.VersionCode < 10200 {
- d.TextCmdTrustOnly = true
- d.QQEnablePoke = true
- d.PlayerNameWrapEnable = true
+ if config.VersionCode != 0 && config.VersionCode < 10200 {
+ config.TextCmdTrustOnly = DefaultConfig.TextCmdTrustOnly
+ config.QQEnablePoke = DefaultConfig.QQEnablePoke
+ config.PlayerNameWrapEnable = DefaultConfig.PlayerNameWrapEnable
isUI1001Master := false
for _, i := range d.DiceMasters {
@@ -2346,7 +2255,7 @@ func (d *Dice) loads() {
}
// 1.2 版本
- if d.VersionCode != 0 && d.VersionCode < 10203 {
+ if config.VersionCode != 0 && config.VersionCode < 10203 {
d.RunAfterLoaded = append(d.RunAfterLoaded, func() {
// 更正写反的部分
d.Logger.Info("正在自动升级自定义文案文件")
@@ -2362,8 +2271,8 @@ func (d *Dice) loads() {
}
// 1.3 版本
- if d.VersionCode != 0 && d.VersionCode < 10300 {
- d.JsEnable = true
+ if config.VersionCode != 0 && config.VersionCode < 10300 {
+ config.JsEnable = DefaultConfig.JsEnable
d.RunAfterLoaded = append(d.RunAfterLoaded, func() {
// 更正写反的部分
@@ -2394,21 +2303,23 @@ func (d *Dice) loads() {
d.SaveText()
})
+ d.Config = config
+
// 1.4.5 版本 - 覆写lagrange配置
- for _, i := range d.ImSession.EndPoints {
- if i.ProtocolType == "onebot" {
- pa := i.Adapter.(*PlatformAdapterGocq)
- if pa.BuiltinMode == "lagrange" {
- signServerUrl, signServerVersion := RWLagrangeSignServerUrl(d, i, "sealdice", false, "25765")
- if signServerUrl != "" {
- // 版本为空,覆写为 "25765"
- if signServerVersion == "" {
- RWLagrangeSignServerUrl(d, i, "sealdice", true, "25765")
- }
- }
- }
- }
- }
+ // for _, i := range d.ImSession.EndPoints {
+ // if i.ProtocolType == "onebot" {
+ // pa := i.Adapter.(*PlatformAdapterGocq)
+ // if pa.BuiltinMode == "lagrange" {
+ // signServerUrl, signServerVersion := RWLagrangeSignServerUrl(d, i, "sealdice", false, "30366")
+ // if signServerUrl != "" {
+ // // 版本为空,覆写为 "30366"
+ // if signServerVersion == "" {
+ // RWLagrangeSignServerUrl(d, i, "sealdice", true, "30366")
+ // }
+ // }
+ // }
+ // }
+ // }
// 设置全局群名缓存和用户名缓存
dm := d.Parent
@@ -2422,34 +2333,12 @@ func (d *Dice) loads() {
})
d.Logger.Info("serve.yaml loaded")
} else {
- // 这里是没有加载到配置文件,所以写默认设置项
- d.WorkInQQChannel = true
- d.CustomReplyConfigEnable = false
- d.AliveNoticeValue = "@every 3h"
d.Logger.Info("serve.yaml not found")
-
- d.LogSizeNoticeCount = 500
- d.LogSizeNoticeEnable = true
-
- // 1.2
- d.QQEnablePoke = true
- d.TextCmdTrustOnly = true
- d.PlayerNameWrapEnable = true
- d.DiceMasters = []string{"UI:1001"}
-
- // 1.3
- d.JsEnable = true
-
- // 1.4
- d.MaxExecuteTime = 12
- d.MaxCocCardGen = 5
-
- d.QuitInactiveBatchSize = 10
- d.QuitInactiveBatchWait = 30
-
- // 1.5
- d.VMVersionForDeck = "v2"
- d.VMVersionForReply = "v1"
+ // 这里是没有加载到配置文件,所以写默认设置项
+ d.DeckList = config.DeckList
+ d.CommandPrefix = config.CommandPrefix
+ d.DiceMasters = config.DiceMasters
+ d.Config = config
}
_ = model.BanItemList(d.DBData, func(id string, banUpdatedAt int64, data []byte) {
@@ -2457,7 +2346,7 @@ func (d *Dice) loads() {
err := json.Unmarshal(data, &v)
if err == nil {
v.BanUpdatedAt = banUpdatedAt
- d.BanList.Map.Store(id, &v)
+ (&d.Config).BanList.Map.Store(id, &v)
}
})
@@ -2466,21 +2355,7 @@ func (d *Dice) loads() {
i.AdapterSetup()
}
- if d.NoticeIDs == nil {
- d.NoticeIDs = []string{}
- }
-
- if len(d.CommandPrefix) == 0 {
- d.CommandPrefix = []string{
- "!",
- ".",
- "。",
- "/",
- }
- }
-
- d.VersionCode = 10300 // TODO: 记得修改!!!
- d.LogWriter.LogLimit = d.UILogLimit
+ d.LogWriter.LogLimit = d.Config.UILogLimit
// 设置扩展选项
d.ApplyExtDefaultSettings()
@@ -2518,7 +2393,7 @@ func (d *Dice) loadAdvanced() {
func (d *Dice) SaveText() {
buf, err := yaml.Marshal(d.TextMapRaw)
if err != nil {
- fmt.Println(err)
+ log.Error("Dice.SaveText", err)
} else {
newFn := filepath.Join(d.BaseConfig.DataDir, "configs/text-template.yaml")
bakFn := filepath.Join(d.BaseConfig.DataDir, "configs/text-template.yaml.bak")
@@ -2536,7 +2411,7 @@ func (d *Dice) SaveText() {
func (d *Dice) ApplyExtDefaultSettings() {
// 遍历两个列表
exts1 := map[string]*ExtDefaultSettingItem{}
- for _, i := range d.ExtDefaultSettings {
+ for _, i := range d.Config.ExtDefaultSettings {
exts1[i.Name] = i
}
@@ -2549,7 +2424,7 @@ func (d *Dice) ApplyExtDefaultSettings() {
for k, v := range exts2 {
if _, exists := exts1[k]; !exists {
item := &ExtDefaultSettingItem{Name: k, AutoActive: v.AutoActive, DisabledCommand: map[string]bool{}}
- d.ExtDefaultSettings = append(d.ExtDefaultSettings, item)
+ (&d.Config).ExtDefaultSettings = append((&d.Config).ExtDefaultSettings, item)
exts1[k] = item
}
}
@@ -2597,7 +2472,24 @@ func (d *Dice) ApplyExtDefaultSettings() {
func (d *Dice) Save(isAuto bool) {
if d.LastUpdatedTime != 0 {
- a, err1 := yaml.Marshal(d)
+ totalConf := &struct {
+ // copy from Dice
+ ImSession *IMSession `yaml:"imSession" jsbind:"imSession" json:"-"`
+ DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息
+ CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导
+ DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号
+
+ Config `yaml:",inline"`
+ }{
+ // 这些都是由于导出到 goja 无法拆分的字段
+ d.ImSession,
+ d.DeckList,
+ d.CommandPrefix,
+ d.DiceMasters,
+
+ d.Config,
+ }
+ a, err1 := yaml.Marshal(totalConf)
advancedData, err2 := yaml.Marshal(d.AdvancedConfig)
if err1 == nil && err2 == nil {
@@ -2605,7 +2497,7 @@ func (d *Dice) Save(isAuto bool) {
err2 := os.WriteFile(filepath.Join(d.BaseConfig.DataDir, "advanced.yaml"), advancedData, 0o644)
if err1 == nil && err2 == nil {
now := time.Now()
- d.LastSavedTime = &now
+ d.Config.LastSavedTime = &now
if isAuto {
d.Logger.Info("自动保存")
} else {
@@ -2613,11 +2505,11 @@ func (d *Dice) Save(isAuto bool) {
}
d.LastUpdatedTime = 0
} else if err1 != nil && err2 != nil {
- d.Logger.Errorln("保存 serve.yaml 和 advanced.yaml 出错", err2)
+ d.Logger.Error("保存 serve.yaml 和 advanced.yaml 出错", err2)
} else if err1 != nil {
- d.Logger.Errorln("保存 serve.yaml 出错", err1)
+ d.Logger.Error("保存 serve.yaml 出错", err1)
} else {
- d.Logger.Errorln("保存 advanced.yaml 出错", err2)
+ d.Logger.Error("保存 advanced.yaml 出错", err2)
}
}
}
@@ -2629,7 +2521,12 @@ func (d *Dice) Save(isAuto bool) {
if groupInfo.Players != nil {
groupInfo.Players.Range(func(key string, value *GroupPlayerInfo) bool {
if value.UpdatedAtTime != 0 {
- _ = model.GroupPlayerInfoSave(d.DBData, groupInfo.GroupID, key, (*model.GroupPlayerInfoBase)(value))
+ // 解离数据库层的操作到调用处,设置对应的信息
+ now := int(time.Now().Unix())
+ value.UserID = key
+ value.GroupID = groupInfo.GroupID
+ value.UpdatedAt = now // 更新当前时间为 UpdatedAt
+ _ = model.GroupPlayerInfoSave(d.DBData, (*model.GroupPlayerInfoBase)(value))
value.UpdatedAtTime = 0
}
return true
@@ -2654,7 +2551,7 @@ func (d *Dice) Save(isAuto bool) {
// 保存黑名单数据
// TODO: 增加更新时间检测
- // model.BanMapSet(d.DBData, d.BanList.MapToJSON())
+ // model.BanMapSet(d.DBData, d.Config.BanList.MapToJSON())
// endpoint数据额外更新到数据库
for _, ep := range d.ImSession.EndPoints {
diff --git a/dice/dice.go b/dice/dice.go
index 960040de..c098ae7a 100644
--- a/dice/dice.go
+++ b/dice/dice.go
@@ -14,20 +14,18 @@ import (
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/require"
"github.com/go-creed/sat"
- "github.com/jmoiron/sqlx"
wr "github.com/mroth/weightedrand"
"github.com/robfig/cron/v3"
ds "github.com/sealdice/dicescript"
"github.com/tidwall/buntdb"
- "go.uber.org/zap"
-
rand2 "golang.org/x/exp/rand"
"golang.org/x/exp/slices"
- "golang.org/x/time/rate"
+ "gorm.io/gorm"
- "sealdice-core/dice/censor"
"sealdice-core/dice/logger"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
+ "sealdice-core/utils/public_dice"
)
type CmdExecuteResult struct {
@@ -104,10 +102,10 @@ type ExtInfo struct {
OnLoad func() `yaml:"-" json:"-" jsbind:"onLoad"`
}
-type DiceConfig struct { //nolint:revive
- Name string `yaml:"name"` // 名称,默认为default
- DataDir string `yaml:"dataDir"` // 数据路径,为./data/{name},例如data/default
- IsLogPrint bool `yaml:"isLogPrint"` // 是否在控制台打印log
+// RootConfig TODO:历史遗留问题,由于不输出DICE日志效果过差,已经抹除日志输出选项,剩余两个选项,私以为可以想办法也抹除掉。
+type RootConfig struct { //nolint:revive
+ Name string `yaml:"name"` // 名称,默认为default
+ DataDir string `yaml:"dataDir"` // 数据路径,为./data/{name},例如data/default
}
type ExtDefaultSettingItem struct {
@@ -127,71 +125,29 @@ func (x ExtDefaultSettingItemSlice) Less(i, _ int) bool { return x[i].Name == "c
func (x ExtDefaultSettingItemSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
type Dice struct {
- ImSession *IMSession `yaml:"imSession" jsbind:"imSession"`
- CmdMap CmdMapCls `yaml:"-" json:"-"`
- ExtList []*ExtInfo `yaml:"-"`
- RollParser *DiceRollParser `yaml:"-"`
- CommandCompatibleMode bool `yaml:"commandCompatibleMode"`
- LastSavedTime *time.Time `yaml:"lastSavedTime"`
- LastUpdatedTime int64 `yaml:"-"`
- TextMap map[string]*wr.Chooser `yaml:"-"`
- BaseConfig DiceConfig `yaml:"-"`
- DBData *sqlx.DB `yaml:"-"` // 数据库对象
- DBLogs *sqlx.DB `yaml:"-"` // 数据库对象
- Logger *zap.SugaredLogger `yaml:"-"` // 日志
- LogWriter *logger.WriterX `yaml:"-"` // 用于api的log对象
- IsDeckLoading bool `yaml:"-"` // 正在加载中
- DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息
- CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导
- DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号
- NoticeIDs []string `yaml:"noticeIds"` // 通知ID
- OnlyLogCommandInGroup bool `yaml:"onlyLogCommandInGroup"` // 日志中仅记录命令
- OnlyLogCommandInPrivate bool `yaml:"onlyLogCommandInPrivate"` // 日志中仅记录命令
- VersionCode int `json:"versionCode"` // 版本ID(配置文件)
- MessageDelayRangeStart float64 `yaml:"messageDelayRangeStart"` // 指令延迟区间
- MessageDelayRangeEnd float64 `yaml:"messageDelayRangeEnd"`
- WorkInQQChannel bool `yaml:"workInQQChannel"`
- QQChannelAutoOn bool `yaml:"QQChannelAutoOn"` // QQ频道中自动开启(默认不开)
- QQChannelLogMessage bool `yaml:"QQChannelLogMessage"` // QQ频道中记录消息(默认不开)
- QQEnablePoke bool `yaml:"QQEnablePoke"` // 启用戳一戳
- TextCmdTrustOnly bool `yaml:"textCmdTrustOnly"` // 只允许信任用户或master使用text指令
- IgnoreUnaddressedBotCmd bool `yaml:"ignoreUnaddressedBotCmd"` // 不响应群聊裸bot指令
- UILogLimit int64 `yaml:"UILogLimit"`
- FriendAddComment string `yaml:"friendAddComment"` // 加好友验证信息
- MasterUnlockCode string `yaml:"-"` // 解锁码,每20分钟变化一次,使用后立即变化
- MasterUnlockCodeTime int64 `yaml:"-"`
- CustomReplyConfigEnable bool `yaml:"customReplyConfigEnable"`
- CustomReplyConfig []*ReplyConfig `yaml:"-"`
- RefuseGroupInvite bool `yaml:"refuseGroupInvite"` // 拒绝加入新群
- UpgradeWindowID string `yaml:"upgradeWindowId"` // 执行升级指令的窗口
- UpgradeEndpointID string `yaml:"upgradeEndpointId"` // 执行升级指令的端点
- BotExtFreeSwitch bool `yaml:"botExtFreeSwitch"` // 允许任意人员开关: 否则邀请者、群主、管理员、master有权限
- TrustOnlyMode bool `yaml:"trustOnlyMode"` // 只有信任的用户/master可以拉群和使用
- AliveNoticeEnable bool `yaml:"aliveNoticeEnable"` // 定时通知
- AliveNoticeValue string `yaml:"aliveNoticeValue"` // 定时通知间隔
- ReplyDebugMode bool `yaml:"replyDebugMode"` // 回复调试
- PlayerNameWrapEnable bool `yaml:"playerNameWrapEnable"` // 启用玩家名称外框
-
- RateLimitEnabled bool `yaml:"rateLimitEnabled"` // 启用频率限制 (刷屏限制)
- PersonalReplenishRateStr string `yaml:"personalReplenishRate"` // 个人刷屏警告速率,字符串格式
- PersonalReplenishRate rate.Limit `yaml:"-"` // 个人刷屏警告速率
- GroupReplenishRateStr string `yaml:"groupReplenishRate"` // 群组刷屏警告速率,字符串格式
- GroupReplenishRate rate.Limit `yaml:"-"` // 群组刷屏警告速率
- PersonalBurst int64 `yaml:"personalBurst"` // 个人自定义上限
- GroupBurst int64 `yaml:"groupBurst"` // 群组自定义上限
-
- QuitInactiveThreshold time.Duration `yaml:"quitInactiveThreshold"` // 退出不活跃群组的时间阈值
- quitInactiveCronEntry cron.EntryID
- QuitInactiveBatchSize int64 `yaml:"quitInactiveBatchSize"` // 退出不活跃群组的批量大小
- QuitInactiveBatchWait int64 `yaml:"quitInactiveBatchWait"` // 退出不活跃群组的批量等待时间(分)
-
- DefaultCocRuleIndex int64 `yaml:"defaultCocRuleIndex" jsbind:"defaultCocRuleIndex"` // 默认coc index
- MaxExecuteTime int64 `yaml:"maxExecuteTime" jsbind:"maxExecuteTime"` // 最大骰点次数
- MaxCocCardGen int64 `yaml:"maxCocCardGen" jsbind:"maxCocCardGen"` // 最大coc制卡数
-
- ExtDefaultSettings []*ExtDefaultSettingItem `yaml:"extDefaultSettings"` // 新群扩展按此顺序加载
-
- BanList *BanListInfo `yaml:"banList"` //
+ // 由于被导出的原因,暂时不迁移至 config
+ ImSession *IMSession `yaml:"imSession" jsbind:"imSession" json:"-"`
+
+ CmdMap CmdMapCls `yaml:"-" json:"-"`
+ ExtList []*ExtInfo `yaml:"-"`
+ RollParser *DiceRollParser `yaml:"-"`
+ LastUpdatedTime int64 `yaml:"-"`
+ TextMap map[string]*wr.Chooser `yaml:"-"`
+ BaseConfig BaseConfig `yaml:"-"`
+ DBData *gorm.DB `yaml:"-"` // 数据库对象
+ DBLogs *gorm.DB `yaml:"-"` // 数据库对象
+ Logger *log.Helper `yaml:"-"` // 日志
+ LogWriter *log.WriterX `yaml:"-"` // 用于api的log对象
+ IsDeckLoading bool `yaml:"-"` // 正在加载中
+
+ // 由于被导出的原因,暂时不迁移至 config
+ DeckList []*DeckInfo `yaml:"deckList" jsbind:"deckList"` // 牌堆信息
+ CommandPrefix []string `yaml:"commandPrefix" jsbind:"commandPrefix"` // 指令前导
+ DiceMasters []string `yaml:"diceMasters" jsbind:"diceMasters"` // 骰主设置,需要格式: 平台:帐号
+
+ MasterUnlockCode string `yaml:"-" json:"masterUnlockCode"` // 解锁码,每20分钟变化一次,使用后立即变化
+ MasterUnlockCodeTime int64 `yaml:"-" json:"masterUnlockCodeTime"`
+ CustomReplyConfig []*ReplyConfig `yaml:"-" json:"-"`
TextMapRaw TextTemplateWithWeightDict `yaml:"-"`
TextMapHelpInfo TextTemplateWithHelpDict `yaml:"-"`
@@ -200,20 +156,18 @@ type Dice struct {
ConfigManager *ConfigManager `yaml:"-"`
Parent *DiceManager `yaml:"-"`
- CocExtraRules map[int]*CocRuleInfo `yaml:"-" json:"cocExtraRules"`
- Cron *cron.Cron `yaml:"-" json:"-"`
- AliveNoticeEntry cron.EntryID `yaml:"-" json:"-"`
- JsEnable bool `yaml:"jsEnable" json:"jsEnable"`
- DisabledJsScripts map[string]bool `yaml:"disabledJsScripts" json:"disabledJsScripts"` // 作为set
- JsPrinter *PrinterFunc `yaml:"-" json:"-"`
- JsRequire *require.RequireModule `yaml:"-" json:"-"`
- JsLoop *eventloop.EventLoop `yaml:"-" json:"-"`
- JsScriptList []*JsScriptInfo `yaml:"-" json:"-"`
- JsScriptCron *cron.Cron `yaml:"-" json:"-"`
- JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"`
+ CocExtraRules map[int]*CocRuleInfo `yaml:"-" json:"cocExtraRules"`
+ Cron *cron.Cron `yaml:"-" json:"-"`
+ AliveNoticeEntry cron.EntryID `yaml:"-" json:"-"`
+ JsPrinter *PrinterFunc `yaml:"-" json:"-"`
+ JsRequire *require.RequireModule `yaml:"-" json:"-"`
+
+ JsLoop *eventloop.EventLoop `yaml:"-" json:"-"`
+ JsScriptList []*JsScriptInfo `yaml:"-" json:"-"`
+ JsScriptCron *cron.Cron `yaml:"-" json:"-"`
+ JsScriptCronLock *sync.Mutex `yaml:"-" json:"-"`
// 重载使用的互斥锁
JsReloadLock sync.Mutex `yaml:"-" json:"-"`
-
// 内置脚本摘要表,用于判断内置脚本是否有更新
JsBuiltinDigestSet map[string]bool `yaml:"-" json:"-"`
// 当前在加载的脚本路径,用于关联 jsScriptInfo 和 ExtInfo
@@ -224,75 +178,26 @@ type Dice struct {
RunAfterLoaded []func() `yaml:"-" json:"-"`
- LogSizeNoticeEnable bool `yaml:"logSizeNoticeEnable"` // 开启日志数量提示
- LogSizeNoticeCount int `yaml:"LogSizeNoticeCount"` // 日志数量提示阈值,默认500
-
- IsAlreadyLoadConfig bool `yaml:"-"` // 如果在loads前崩溃,那么不写入配置,防止覆盖为空的
deckCommandItemsList DeckCommandListItems // 牌堆key信息,辅助作为模糊搜索使用
UIEndpoint *EndPointInfo `yaml:"-" json:"-"` // UI Endpoint
- MailEnable bool `json:"mailEnable" yaml:"mailEnable"` // 是否启用
- MailFrom string `json:"mailFrom" yaml:"mailFrom"` // 邮箱来源
- MailPassword string `json:"mailPassword" yaml:"mailPassword"` // 邮箱密钥/密码
- MailSMTP string `json:"mailSmtp" yaml:"mailSmtp"` // 邮箱 smtp 地址
-
- NewsMark string `json:"newsMark" yaml:"newsMark"` // 已读新闻的md5
-
- EnableCensor bool `json:"enableCensor" yaml:"enableCensor"` // 启用敏感词审查
- CensorManager *CensorManager `json:"-" yaml:"-"`
- CensorMode CensorMode `json:"censorMode" yaml:"censorMode"`
- CensorThresholds map[censor.Level]int `json:"censorThresholds" yaml:"censorThresholds"` // 敏感词阈值
- CensorHandlers map[censor.Level]uint8 `json:"censorHandlers" yaml:"censorHandlers"`
- CensorScores map[censor.Level]int `json:"censorScores" yaml:"censorScores"` // 敏感词怒气值
- CensorCaseSensitive bool `json:"censorCaseSensitive" yaml:"censorCaseSensitive"` // 敏感词大小写敏感
- CensorMatchPinyin bool `json:"censorMatchPinyin" yaml:"censorMatchPinyin"` // 敏感词匹配拼音
- CensorFilterRegexStr string `json:"censorFilterRegexStr" yaml:"censorFilterRegexStr"` // 敏感词过滤字符正则
-
- VMVersionForReply string `json:"VMVersionForReply" yaml:"VMVersionForReply"` // 自定义回复使用的vm版本
- VMVersionForDeck string `json:"VMVersionForDeck" yaml:"VMVersionForDeck"` // 牌堆使用的vm版本
+ CensorManager *CensorManager `json:"-" yaml:"-"`
AttrsManager *AttrsManager `json:"-" yaml:"-"`
- AdvancedConfig AdvancedConfig `json:"-" yaml:"-"`
-
- ContainerMode bool `yaml:"-" json:"-"` // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq
-}
+ Config Config `json:"-" yaml:"-"`
-type CensorMode int
+ AdvancedConfig AdvancedConfig `json:"-" yaml:"-"`
-const (
- OnlyOutputReply CensorMode = iota
- OnlyInputCommand
- AllInput
-)
+ PublicDice *public_dice.PublicDiceClient `json:"-" yaml:"-"`
+ PublicDiceTimerId cron.EntryID `json:"-" yaml:"-"`
-const (
- // SendWarning 发送警告
- SendWarning CensorHandler = iota
- // SendNotice 向通知列表/邮件发送通知
- SendNotice
- // BanUser 拉黑用户
- BanUser
- // BanGroup 拉黑群
- BanGroup
- // BanInviter 拉黑邀请人
- BanInviter
- // AddScore 增加怒气值
- AddScore
-)
+ ContainerMode bool `yaml:"-" json:"-"` // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq
-var CensorHandlerText = map[CensorHandler]string{
- SendWarning: "SendWarning",
- SendNotice: "SendNotice",
- BanUser: "BanUser",
- BanGroup: "BanGroup",
- BanInviter: "BanInviter",
- AddScore: "AddScore",
+ IsAlreadyLoadConfig bool `yaml:"-"` // 如果在loads前崩溃,那么不写入配置,防止覆盖为空的
}
-type CensorHandler int
-
func (d *Dice) MarkModified() {
d.LastUpdatedTime = time.Now().Unix()
}
@@ -314,7 +219,7 @@ func (d *Dice) Init() {
_ = os.MkdirAll(filepath.Join(d.BaseConfig.DataDir, "extra"), 0o755)
_ = os.MkdirAll(filepath.Join(d.BaseConfig.DataDir, "scripts"), 0o755)
- log := logger.Init(filepath.Join(d.BaseConfig.DataDir, "record.log"), d.BaseConfig.Name, d.BaseConfig.IsLogPrint)
+ log := logger.Init()
d.Logger = log.Logger
d.LogWriter = log.WX
@@ -324,21 +229,20 @@ func (d *Dice) Init() {
d.CocExtraRules = map[int]*CocRuleInfo{}
var err error
- d.DBData, d.DBLogs, err = model.SQLiteDBInit(d.BaseConfig.DataDir)
+ d.DBData, d.DBLogs, err = model.DatabaseInit()
if err != nil {
- fmt.Println(err)
d.Logger.Errorf("Failed to init database: %v", err)
}
d.AttrsManager = &AttrsManager{}
d.AttrsManager.Init(d)
- d.BanList = &BanListInfo{Parent: d}
- d.BanList.Init()
+ (&d.Config).BanList = &BanListInfo{Parent: d}
+ (&d.Config).BanList.Init()
initVerify()
- d.CommandCompatibleMode = true
+ d.BaseConfig.CommandCompatibleMode = true
// Pinenutn: 预先初始化对应的SyncMap
d.ImSession = &IMSession{}
d.ImSession.Parent = d
@@ -355,16 +259,18 @@ func (d *Dice) Init() {
d.RegisterBuiltinExt()
d.loads()
d.loadAdvanced()
- d.BanList.Loads()
- d.BanList.AfterLoads()
+ (&d.Config).BanList.Loads()
+ (&d.Config).BanList.AfterLoads()
d.IsAlreadyLoadConfig = true
- if d.EnableCensor {
+ if d.Config.EnableCensor {
d.NewCensorManager()
}
+ go d.PublicDiceSetup()
+
// 创建js运行时
- if d.JsEnable {
+ if d.Config.JsEnable {
d.Logger.Info("js扩展支持:开启")
d.JsInit()
} else {
@@ -456,20 +362,20 @@ func (d *Dice) Init() {
go refreshGroupInfo()
d.ApplyAliveNotice()
- if d.JsEnable {
+ if d.Config.JsEnable {
d.JsBuiltinDigestSet = make(map[string]bool)
d.JsLoadScripts()
} else {
d.Logger.Info("js扩展支持已关闭,跳过js脚本的加载")
}
- if d.UpgradeWindowID != "" {
+ if d.Config.UpgradeWindowID != "" {
go func() {
defer ErrorLogAndContinue(d)
var ep *EndPointInfo
for _, _ep := range d.ImSession.EndPoints {
- if _ep.ID == d.UpgradeEndpointID {
+ if _ep.ID == d.Config.UpgradeEndpointID {
ep = _ep
break
}
@@ -491,16 +397,16 @@ func (d *Dice) Init() {
// 可以了,发送消息
ctx := &MsgContext{Dice: d, EndPoint: ep, Session: d.ImSession}
- isGroup := strings.Contains(d.UpgradeWindowID, "-Group:")
+ isGroup := strings.Contains(d.Config.UpgradeWindowID, "-Group:")
if isGroup {
- ReplyGroup(ctx, &Message{GroupID: d.UpgradeWindowID}, text)
+ ReplyGroup(ctx, &Message{GroupID: d.Config.UpgradeWindowID}, text)
} else {
- ReplyPerson(ctx, &Message{Sender: SenderBase{UserID: d.UpgradeWindowID}}, text)
+ ReplyPerson(ctx, &Message{Sender: SenderBase{UserID: d.Config.UpgradeWindowID}}, text)
}
d.Logger.Infof("升级完成,当前版本: %s", VERSION.String())
- d.UpgradeWindowID = ""
- d.UpgradeEndpointID = ""
+ (&d.Config).UpgradeWindowID = ""
+ (&d.Config).UpgradeEndpointID = ""
d.MarkModified()
d.Save(false)
break
@@ -567,7 +473,7 @@ func (d *Dice) _ExprTextBaseV1(buffer string, ctx *MsgContext, flags RollExtraFl
// 隐藏的内置字符串符号 \x1e
val, detail, err := d._ExprEvalBaseV1("\x1e"+buffer+"\x1e", ctx, flags)
if err != nil {
- fmt.Println("脚本执行出错: ", buffer, "->", err)
+ log.Warnf("脚本执行出错: %s -> %v", buffer, err)
}
if err == nil && (val.TypeID == VMTypeString || val.TypeID == VMTypeNone) {
@@ -710,8 +616,8 @@ func (d *Dice) ApplyAliveNotice() {
if d.Cron != nil && d.AliveNoticeEntry != 0 {
d.Cron.Remove(d.AliveNoticeEntry)
}
- if d.AliveNoticeEnable {
- entry, err := d.Cron.AddFunc(d.AliveNoticeValue, func() {
+ if d.Config.AliveNoticeEnable {
+ entry, err := d.Cron.AddFunc((&d.Config).AliveNoticeValue, func() {
d.NoticeForEveryEndpoint(fmt.Sprintf("存活, D100=%d", DiceRoll64(100)), false)
})
if err == nil {
@@ -723,9 +629,10 @@ func (d *Dice) ApplyAliveNotice() {
}
}
-// GameSystemTemplateAdd 应用一个角色模板
-func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool {
- if _, exists := d.GameSystemMap.Load(tmpl.Name); !exists {
+// GameSystemTemplateAddEx 应用一个角色模板
+func (d *Dice) GameSystemTemplateAddEx(tmpl *GameSystemTemplate, overwrite bool) bool {
+ _, exists := d.GameSystemMap.Load(tmpl.Name)
+ if !exists || overwrite {
d.GameSystemMap.Store(tmpl.Name, tmpl)
// sn 从这里读取
// set 时从这里读取对应System名字的模板
@@ -744,6 +651,11 @@ func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool {
return false
}
+// GameSystemTemplateAdd 应用一个角色模板,当已存在时返回false
+func (d *Dice) GameSystemTemplateAdd(tmpl *GameSystemTemplate) bool {
+ return d.GameSystemTemplateAddEx(tmpl, false)
+}
+
// var randSource = rand2.NewSource(uint64(time.Now().Unix()))
var randSource = &rand2.PCGSource{}
@@ -784,25 +696,124 @@ func ErrorLogAndContinue(d *Dice) {
}
var chsS2T = sat.DefaultDict()
+var taskId cron.EntryID
+var quitMutex sync.Mutex
func (d *Dice) ResetQuitInactiveCron() {
+ // TODO: 这里加锁是否有必要?
+ quitMutex.Lock()
+ defer quitMutex.Unlock()
dm := d.Parent
- if d.quitInactiveCronEntry > 0 {
- dm.Cron.Remove(d.quitInactiveCronEntry)
- d.quitInactiveCronEntry = 0
- }
-
- if d.QuitInactiveThreshold > 0 {
- var err error
- d.quitInactiveCronEntry, err = dm.Cron.AddFunc("0 4 * * *", func() {
- thr := time.Now().Add(-d.QuitInactiveThreshold)
- hint := thr.Add(d.QuitInactiveThreshold / 10) // 进入退出判定线的9/10开始提醒
- d.ImSession.LongTimeQuitInactiveGroup(thr, hint,
- int(d.QuitInactiveBatchWait),
- int(d.QuitInactiveBatchSize))
+ if d.Config.quitInactiveCronEntry > 0 {
+ dm.Cron.Remove(d.Config.quitInactiveCronEntry)
+ (&d.Config).quitInactiveCronEntry = DefaultConfig.quitInactiveCronEntry
+ }
+ // 如果退群功能开启,那么设定退群的Cron
+ if d.Config.QuitInactiveThreshold > 0 {
+ duration := time.Duration(d.Config.QuitInactiveBatchWait) * time.Minute
+ // 每隔上面的退群时间,执行一次函数
+ if taskId != 0 {
+ dm.Cron.Remove(taskId)
+ }
+ taskId = dm.Cron.Schedule(cron.Every(duration), cron.FuncJob(func() {
+ thr := time.Now().Add(-d.Config.QuitInactiveThreshold)
+ // 进入退出判定线的9/10开始提醒, 但是目前来看,原版退群只有一个提示,提示会被大量刷屏然后消失不见。同时并没有告知对应的群
+ // 或许也不应该告知对应的群,因为群可能被解散了,大量告知容易出问题?
+ // hint := thr.Add(d.Config.QuitInactiveThreshold / 10)
+ d.ImSession.LongTimeQuitInactiveGroupReborn(thr, int(d.Config.QuitInactiveBatchSize))
+ }))
+ d.Logger.Infof("退群功能已启动,每 %s 执行一次退群判定", duration.String())
+ }
+}
+
+func (d *Dice) PublicDiceEndpointRefresh() {
+ cfg := &d.Config.PublicDiceConfig
+
+ var endpointItems []*public_dice.Endpoint
+ for _, i := range d.ImSession.EndPoints {
+ if !i.IsPublic {
+ continue
+ }
+ endpointItems = append(endpointItems, &public_dice.Endpoint{
+ Platform: i.Platform,
+ UID: i.UserID,
+ IsOnline: i.State == 1,
})
- if err != nil {
- d.Logger.Errorf("创建自动清理群聊cron任务失败: %v", err)
+ }
+
+ _, code := d.PublicDice.EndpointUpdate(&public_dice.EndpointUpdateRequest{
+ DiceID: cfg.ID,
+ Endpoints: endpointItems,
+ }, GenerateVerificationKeyForPublicDice)
+ if code != 200 {
+ log.Warn("[公骰]无法通过服务器校验,不再进行更新")
+ return
+ }
+}
+
+func (d *Dice) PublicDiceInfoRegister() {
+ cfg := &d.Config.PublicDiceConfig
+ pd, code := d.PublicDice.Register(&public_dice.RegisterRequest{
+ ID: cfg.ID,
+ Name: cfg.Name,
+ Brief: cfg.Brief,
+ Note: cfg.Note,
+ }, GenerateVerificationKeyForPublicDice)
+ if code != 200 {
+ log.Warn("[公骰]无法通过服务器校验,不再进行骰号注册")
+ return
+ }
+ // ID为空时才将注册好的ID覆写配置
+ if pd.Item.ID != "" && cfg.ID == "" {
+ cfg.ID = pd.Item.ID
+ }
+}
+
+func (d *Dice) PublicDiceSetupTick() {
+ cfg := &d.Config.PublicDiceConfig
+
+ doTickUpdate := func() {
+ if !cfg.Enable {
+ d.Cron.Remove(d.PublicDiceTimerId)
+ return
+ }
+ var tickEndpointItems []*public_dice.TickEndpoint
+ for _, i := range d.ImSession.EndPoints {
+ if !i.IsPublic {
+ continue
+ }
+ tickEndpointItems = append(tickEndpointItems, &public_dice.TickEndpoint{
+ UID: i.UserID,
+ IsOnline: i.State == 1,
+ })
}
+ d.PublicDice.TickUpdate(&public_dice.TickUpdateRequest{
+ ID: cfg.ID,
+ Endpoints: tickEndpointItems,
+ }, GenerateVerificationKeyForPublicDice)
+ }
+
+ if d.PublicDiceTimerId != 0 {
+ d.Cron.Remove(d.PublicDiceTimerId)
+ }
+
+ go func() {
+ // 20s后进行第一次调用,此后3min进行一次更新
+ time.Sleep(20 * time.Second)
+ doTickUpdate()
+ }()
+
+ d.PublicDiceTimerId, _ = d.Cron.AddFunc("@every 3m", doTickUpdate)
+}
+
+func (d *Dice) PublicDiceSetup() {
+ d.PublicDice = public_dice.NewClient("https://dice.weizaima.com", "")
+
+ cfg := &d.Config.PublicDiceConfig
+ if !cfg.Enable {
+ return
}
+ d.PublicDiceInfoRegister()
+ d.PublicDiceEndpointRefresh()
+ d.PublicDiceSetupTick()
}
diff --git a/dice/dice_attrs_manager.go b/dice/dice_attrs_manager.go
index ba7e4019..85938d43 100644
--- a/dice/dice_attrs_manager.go
+++ b/dice/dice_attrs_manager.go
@@ -1,24 +1,30 @@
package dice
import (
- "database/sql"
+ "context"
"errors"
"fmt"
"time"
- "github.com/jmoiron/sqlx"
ds "github.com/sealdice/dicescript"
- "go.uber.org/zap"
+ "gorm.io/gorm"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
)
type AttrsManager struct {
- db *sqlx.DB
- logger *zap.SugaredLogger
+ db *gorm.DB
+ logger *log.Helper
+ cancel context.CancelFunc
m SyncMap[string, *AttributesItem]
}
+func (am *AttrsManager) Stop() {
+ log.Info("结束数据库保存程序...")
+ am.cancel()
+}
+
// LoadByCtx 获取当前角色,如有绑定,则获取绑定的角色,若无绑定,获取群内默认卡
func (am *AttrsManager) LoadByCtx(ctx *MsgContext) (*AttributesItem, error) {
return am.Load(ctx.Group.GroupID, ctx.Player.UserID)
@@ -149,14 +155,25 @@ func (am *AttrsManager) LoadById(id string) (*AttributesItem, error) {
func (am *AttrsManager) Init(d *Dice) {
am.db = d.DBData
am.logger = d.Logger
+ // 创建一个 context 用于取消 goroutine
+ ctx, cancel := context.WithCancel(context.Background())
+ // 确保程序退出时取消上下文
go func() {
// NOTE(Xiangze Li): 这种不退出的goroutine不利于平稳结束程序
for {
- am.CheckForSave()
- am.CheckAndFreeUnused()
- time.Sleep(15 * time.Second)
+ select {
+ case <-ctx.Done():
+ // 检测到取消信号后退出循环
+ return
+ default:
+ // 正常工作
+ am.CheckForSave()
+ am.CheckAndFreeUnused()
+ time.Sleep(15 * time.Second)
+ }
}
}()
+ am.cancel = cancel
}
func (am *AttrsManager) CheckForSave() (int, int) {
@@ -169,24 +186,18 @@ func (am *AttrsManager) CheckForSave() (int, int) {
return 0, 0
}
- tx, err := db.Begin()
- if err != nil {
- if am.logger != nil {
- am.logger.Errorf("定期写入用户数据出错(创建事务): %v", err)
- }
- return 0, 0
- }
+ tx := db.Begin()
am.m.Range(func(key string, value *AttributesItem) bool {
if !value.IsSaved {
saved += 1
- value.SaveToDB(db, tx)
+ value.SaveToDB(tx)
}
times += 1
return true
})
- err = tx.Commit()
+ err := tx.Commit().Error
if err != nil {
if am.logger != nil {
am.logger.Errorf("定期写入用户数据出错(提交事务): %v", err)
@@ -207,14 +218,22 @@ func (am *AttrsManager) CheckAndFreeUnused() {
prepareToFree := map[string]int{}
currentTime := time.Now().Unix()
+ tx := db.Begin()
am.m.Range(func(key string, value *AttributesItem) bool {
if value.LastUsedTime-currentTime > 60*10 {
prepareToFree[key] = 1
- value.SaveToDB(am.db, nil)
+ // 直接保存
+ value.SaveToDB(tx)
}
return true
})
-
+ err := tx.Commit().Error
+ if err != nil {
+ if am.logger != nil {
+ am.logger.Errorf("定期清理无用用户数据出错(提交事务): %v", err)
+ }
+ _ = tx.Rollback()
+ }
for key := range prepareToFree {
am.m.Delete(key)
}
@@ -279,15 +298,15 @@ type AttributesItem struct {
SheetType string
}
-func (i *AttributesItem) SaveToDB(db *sqlx.DB, tx *sql.Tx) {
+func (i *AttributesItem) SaveToDB(db *gorm.DB) {
// 使用事务写入
rawData, err := ds.NewDictVal(i.valueMap).V().ToJSON()
if err != nil {
return
}
- err = model.AttrsPutById(db, tx, i.ID, rawData, i.Name, i.SheetType)
+ err = model.AttrsPutById(db, i.ID, rawData, i.Name, i.SheetType)
if err != nil {
- fmt.Println("保存数据失败", err.Error())
+ log.Error("保存数据失败", err.Error())
return
}
i.IsSaved = true
diff --git a/dice/dice_backup.go b/dice/dice_backup.go
index 8ce8c138..51e78846 100644
--- a/dice/dice_backup.go
+++ b/dice/dice_backup.go
@@ -2,7 +2,9 @@ package dice
import (
"encoding/json"
+ "errors"
"fmt"
+ "io"
"io/fs"
"os"
"path/filepath"
@@ -16,6 +18,7 @@ import (
"sealdice-core/dice/model"
"sealdice-core/utils"
"sealdice-core/utils/crypto"
+ log "sealdice-core/utils/kratos"
)
const BackupDir = "./backups"
@@ -123,7 +126,7 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error
}
backup := func(d *Dice, fn string) {
- data, err := os.ReadFile(fn)
+ file, err := os.Open(fn)
if err != nil && !strings.Contains(fn, "session.token") {
if d != nil {
d.Logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error())
@@ -132,6 +135,7 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error
}
return
}
+ defer file.Close()
h := &zip.FileHeader{Name: fn, Method: zip.Deflate, Flags: 0x800}
fileWriter, err := writer.CreateHeader(h)
@@ -143,7 +147,15 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error
}
return
}
- _, _ = fileWriter.Write(data)
+
+ _, err = io.Copy(fileWriter, file)
+ if err != nil {
+ if d != nil {
+ d.Logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error())
+ } else {
+ logger.Errorf("备份文件失败: %s, 原因: %s", fn, err.Error())
+ }
+ }
}
backupDir := func(path string, info fs.FileInfo, _ error) error {
@@ -226,20 +238,20 @@ func (dm *DiceManager) Backup(sel BackupSelection, fromAuto bool) (string, error
err := model.FlushWAL(d.DBData)
if err != nil {
- d.Logger.Warnln("备份时data数据库flush出错", err.Error())
+ d.Logger.Errorf("备份时data数据库flush出错 错误为:%v", err.Error())
} else {
backup(d, filepath.Join(dataDir, "data.db"))
}
err = model.FlushWAL(d.DBLogs)
if err != nil {
- d.Logger.Warnln("备份时logs数据库flush出错", err.Error())
+ d.Logger.Errorf("备份时logs数据库flush出错 错误为:%v", err.Error())
} else {
backup(d, filepath.Join(dataDir, "data-logs.db"))
}
if d.CensorManager != nil && d.CensorManager.DB != nil {
err = model.FlushWAL(d.CensorManager.DB)
if err != nil {
- d.Logger.Warnln("备份时censor数据库flush出错", err.Error())
+ d.Logger.Errorf("备份时censor数据库flush出错 %v", err.Error())
} else {
backup(d, filepath.Join(dataDir, "data-censor.db"))
}
@@ -340,7 +352,7 @@ func (dm *DiceManager) BackupClean(fromAuto bool) (err error) {
return nil
}
- // fmt.Println("开始定时清理备份", fromAuto)
+ log.Info("开始清理备份文件")
backupDir, err := os.Open(BackupDir)
if err != nil {
@@ -369,31 +381,42 @@ func (dm *DiceManager) BackupClean(fromAuto bool) (err error) {
sort.Sort(utils.ByModtime(fileInfos))
var fileInfoOld []os.FileInfo
+
+ logMsg := strings.Builder{}
+ logMsg.WriteString(fmt.Sprintf("现有备份文件 %d 个, 清理模式为 ", len(fileInfos)))
+
switch dm.BackupCleanStrategy {
case BackupCleanStrategyByCount:
+ logMsg.WriteString(fmt.Sprintf("保留一定数量(%d)", dm.BackupCleanKeepCount))
if len(fileInfos) > dm.BackupCleanKeepCount {
fileInfoOld = fileInfos[:len(fileInfos)-dm.BackupCleanKeepCount]
}
case BackupCleanStrategyByTime:
threshold := time.Now().Add(-dm.BackupCleanKeepDur)
+ logMsg.WriteString(fmt.Sprintf("保留一定时间(%v, %s)", dm.BackupCleanKeepDur, threshold.Format(time.DateTime)))
idx, _ := sort.Find(len(fileInfos), func(i int) int {
return threshold.Compare(fileInfos[i].ModTime())
})
- fileInfoOld = fileInfos[:idx+1]
+ fileInfoOld = fileInfos[:idx]
default:
// no-op
}
+ logMsg.WriteString(fmt.Sprintf(", 有以下 %d 个将要被删除", len(fileInfoOld)))
+
errDel := []string{}
- for _, fi := range fileInfoOld {
+ for i, fi := range fileInfoOld {
+ logMsg.WriteString(fmt.Sprintf("\n%d. %s", i+1, fi.Name()))
errDelete := os.Remove(filepath.Join(BackupDir, fi.Name()))
if errDelete != nil {
errDel = append(errDel, errDelete.Error())
}
}
+ log.Info(logMsg.String())
+
if len(errDel) > 0 {
- return fmt.Errorf("error(s) occured when deleting files:\n" + strings.Join(errDel, "\n"))
+ return errors.New("error(s) occured when deleting files:\n" + strings.Join(errDel, "\n"))
}
return nil
}
diff --git a/dice/dice_ban.go b/dice/dice_ban.go
index aeb0095e..89068239 100644
--- a/dice/dice_ban.go
+++ b/dice/dice_ban.go
@@ -53,16 +53,17 @@ func (i *BanListInfoItem) toText(_ *Dice) string {
}
type BanListInfo struct {
- Parent *Dice `yaml:"-" json:"-"`
- Map *SyncMap[string, *BanListInfoItem] `yaml:"-" json:"-"`
- BanBehaviorRefuseReply bool `yaml:"banBehaviorRefuseReply" json:"banBehaviorRefuseReply"` // 拉黑行为: 拒绝回复
- BanBehaviorRefuseInvite bool `yaml:"banBehaviorRefuseInvite" json:"banBehaviorRefuseInvite"` // 拉黑行为: 拒绝邀请
- BanBehaviorQuitLastPlace bool `yaml:"banBehaviorQuitLastPlace" json:"banBehaviorQuitLastPlace"` // 拉黑行为: 退出事发群
- BanBehaviorQuitPlaceImmediately bool `yaml:"banBehaviorQuitPlaceImmediately" json:"banBehaviorQuitPlaceImmediately"` // 拉黑行为: 使用时立即退出群
- BanBehaviorQuitIfAdmin bool `yaml:"banBehaviorQuitIfAdmin" json:"banBehaviorQuitIfAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则发出警告信息
- ThresholdWarn int64 `yaml:"thresholdWarn" json:"thresholdWarn"` // 警告阈值
- ThresholdBan int64 `yaml:"thresholdBan" json:"thresholdBan"` // 错误阈值
- AutoBanMinutes int64 `yaml:"autoBanMinutes" json:"autoBanMinutes"` // 自动禁止时长
+ Parent *Dice `yaml:"-" json:"-"`
+ Map *SyncMap[string, *BanListInfoItem] `yaml:"-" json:"-"`
+ BanBehaviorRefuseReply bool `yaml:"banBehaviorRefuseReply" json:"banBehaviorRefuseReply"` // 拉黑行为: 拒绝回复
+ BanBehaviorRefuseInvite bool `yaml:"banBehaviorRefuseInvite" json:"banBehaviorRefuseInvite"` // 拉黑行为: 拒绝邀请
+ BanBehaviorQuitLastPlace bool `yaml:"banBehaviorQuitLastPlace" json:"banBehaviorQuitLastPlace"` // 拉黑行为: 退出事发群
+ BanBehaviorQuitPlaceImmediately bool `yaml:"banBehaviorQuitPlaceImmediately" json:"banBehaviorQuitPlaceImmediately"` // 拉黑行为: 使用时立即退出群
+ BanBehaviorQuitIfAdmin bool `yaml:"banBehaviorQuitIfAdmin" json:"banBehaviorQuitIfAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则发出警告信息
+ BanBehaviorQuitIfAdminSilentIfNotAdmin bool `yaml:"banBehaviorQuitIfAdminSilentIfNotAdmin" json:"banBehaviorQuitIfAdminSilentIfNotAdmin"` // 拉黑行为: 邀请者以上权限使用时立即退群,否则仅拒绝回复
+ ThresholdWarn int64 `yaml:"thresholdWarn" json:"thresholdWarn"` // 警告阈值
+ ThresholdBan int64 `yaml:"thresholdBan" json:"thresholdBan"` // 错误阈值
+ AutoBanMinutes int64 `yaml:"autoBanMinutes" json:"autoBanMinutes"` // 自动禁止时长
ScoreReducePerMinute int64 `yaml:"scoreReducePerMinute" json:"scoreReducePerMinute"` // 每分钟下降
ScoreGroupMuted int64 `yaml:"scoreGroupMuted" json:"scoreGroupMuted"` // 群组禁言
@@ -105,7 +106,7 @@ func (i *BanListInfo) AfterLoads() {
return
}
var toDelete []string
- d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
+ (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
if v.Rank == BanRankNormal || v.Rank == BanRankWarn {
v.Score -= i.ScoreReducePerMinute
if v.Score <= 0 {
@@ -121,7 +122,7 @@ func (i *BanListInfo) AfterLoads() {
_ = model.BanItemDel(d.DBData, j)
}
- d.BanList.SaveChanged(d)
+ (&d.Config).BanList.SaveChanged(d)
})
}
@@ -191,6 +192,7 @@ func (i *BanListInfo) AddScoreBase(uid string, score int64, place string, reason
// 警告: XXX 因为等行为,进入警告列表
// 黑名单: XXX 因为等行为,进入黑名单。将作出以下惩罚:拒绝回复、拒绝邀请、退出事发群
// TODO
+ //nolint:forbidigo // that is a todo
fmt.Println("TODO Alert")
}
@@ -281,7 +283,7 @@ func (i *BanListInfo) NoticeCheck(uid string, place string, oldRank BanRankType,
if ctx != nil {
var isWhiteGroup bool
d := ctx.Dice
- value, exists := d.BanList.Map.Load(place)
+ value, exists := (&d.Config).BanList.Map.Load(place)
if exists {
if value.Rank == BanRankTrusted {
isWhiteGroup = true
@@ -403,7 +405,7 @@ func (d *Dice) GetBanList() []*BanListInfoItem {
}
func (i *BanListInfo) SaveChanged(d *Dice) {
- d.BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
+ (&d.Config).BanList.Map.Range(func(k string, v *BanListInfoItem) bool {
if v.UpdatedAt != 0 {
data, err := json.Marshal(v)
if err == nil {
diff --git a/dice/dice_censor.go b/dice/dice_censor.go
index eff905d5..55f49194 100644
--- a/dice/dice_censor.go
+++ b/dice/dice_censor.go
@@ -1,6 +1,7 @@
package dice
import (
+ "errors"
"fmt"
"io/fs"
"os"
@@ -8,43 +9,78 @@ import (
"sort"
"strings"
- "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
"sealdice-core/dice/censor"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
)
+type CensorMode int
+
+const (
+ OnlyOutputReply CensorMode = iota
+ OnlyInputCommand
+ AllInput
+)
+
+const (
+ // SendWarning 发送警告
+ SendWarning CensorHandler = iota
+ // SendNotice 向通知列表/邮件发送通知
+ SendNotice
+ // BanUser 拉黑用户
+ BanUser
+ // BanGroup 拉黑群
+ BanGroup
+ // BanInviter 拉黑邀请人
+ BanInviter
+ // AddScore 增加怒气值
+ AddScore
+)
+
+var CensorHandlerText = map[CensorHandler]string{
+ SendWarning: "SendWarning",
+ SendNotice: "SendNotice",
+ BanUser: "BanUser",
+ BanGroup: "BanGroup",
+ BanInviter: "BanInviter",
+ AddScore: "AddScore",
+}
+
+type CensorHandler int
+
type CensorManager struct {
IsLoading bool
Parent *Dice
Censor *censor.Censor
- DB *sqlx.DB
+ DB *gorm.DB
SensitiveWordsFiles map[string]*censor.WordFile
}
func (d *Dice) NewCensorManager() {
- db, err := model.SQLiteCensorDBInit(d.BaseConfig.DataDir)
+ db, err := model.CensorDBInit()
if err != nil {
panic(err)
}
cm := CensorManager{
Censor: &censor.Censor{
- CaseSensitive: d.CensorCaseSensitive,
- MatchPinyin: d.CensorMatchPinyin,
- FilterRegexStr: d.CensorFilterRegexStr,
+ CaseSensitive: d.Config.CensorCaseSensitive,
+ MatchPinyin: d.Config.CensorMatchPinyin,
+ FilterRegexStr: d.Config.CensorFilterRegexStr,
},
DB: db,
}
cm.Parent = d
d.CensorManager = &cm
- if d.CensorThresholds == nil {
- d.CensorThresholds = make(map[censor.Level]int)
+ if d.Config.CensorThresholds == nil {
+ (&d.Config).CensorThresholds = make(map[censor.Level]int)
}
- if d.CensorHandlers == nil {
- d.CensorHandlers = make(map[censor.Level]uint8)
+ if d.Config.CensorHandlers == nil {
+ (&d.Config).CensorHandlers = make(map[censor.Level]uint8)
}
- if d.CensorScores == nil {
- d.CensorScores = make(map[censor.Level]int)
+ if d.Config.CensorScores == nil {
+ (&d.Config).CensorScores = make(map[censor.Level]int)
}
cm.Load(d)
}
@@ -60,7 +96,7 @@ func (cm *CensorManager) Load(_ *Dice) {
cm.Parent.Logger.Infof("正在读取敏感词文件:%s\n", path)
fileInfo, e := cm.Censor.PreloadFile(path)
if e != nil {
- fmt.Printf("censor: unable to read %s, %s\n", path, e.Error())
+ log.Errorf("censor: unable to read %s, %v", path, e)
}
if cm.SensitiveWordsFiles == nil {
cm.SensitiveWordsFiles = make(map[string]*censor.WordFile)
@@ -71,14 +107,14 @@ func (cm *CensorManager) Load(_ *Dice) {
})
err := cm.Censor.Load()
if err != nil {
- fmt.Printf("censor: load fail, %s\n", err.Error())
+ log.Errorf("censor: load fail, %v", err)
}
cm.IsLoading = false
}
func (cm *CensorManager) Check(ctx *MsgContext, msg *Message, checkContent string) (*MsgCheckResult, error) {
if cm.IsLoading {
- return nil, fmt.Errorf("censor is loading")
+ return nil, errors.New("censor is loading")
}
res := cm.Censor.Check(checkContent)
if !ctx.Censored && res.HighestLevel > censor.Ignore {
@@ -137,7 +173,7 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se
if !ok {
d.Logger.Warn("Dice CenSor获取GroupInfo失败")
}
- thresholds := d.CensorThresholds
+ thresholds := d.Config.CensorThresholds
// 保证按程度依次降低来处理
var tempLevels censor.Levels
@@ -155,7 +191,7 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se
// 清空此用户该等级计数
model.CensorClearLevelCount(d.CensorManager.DB, msg.Sender.UserID, level)
// 该等级敏感词超过阈值,执行操作
- handler := d.CensorHandlers[level]
+ handler := d.Config.CensorHandlers[level]
levelText := censor.LevelText[level]
if handler&(1<敏感词",
mctx,
@@ -195,9 +231,9 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se
if handler&(1<敏感词",
mctx,
@@ -207,9 +243,9 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se
if handler&(1<敏感词",
mctx,
@@ -217,13 +253,13 @@ func (d *Dice) CensorMsg(mctx *MsgContext, msg *Message, checkContent string, se
}
}
if handler&(1< 0 {
key += "/" + row[1+j]
}
}
content := row[synonymCount+1]
- _ = m.AddItem(HelpTextItem{
+ _ = m.AddItem(docengine.HelpTextItem{
Group: group,
From: path,
Title: key,
@@ -370,7 +358,7 @@ func (m *HelpManager) loadHelpDoc(group string, path string) bool {
// Close the spreadsheet.
if err := f.Close(); err != nil {
- fmt.Println(err)
+ log.Error("HelpManager.loadHelpDoc", err)
}
return true
}
@@ -380,7 +368,7 @@ func (m *HelpManager) loadHelpDoc(group string, path string) bool {
// validateXlsxHeaders 验证 xlsx 格式 helpdoc 的表头是否是 Key Synonym(可能有多列) Content Description Catalogue Tag
func validateXlsxHeaders(headers []string) (int, error) {
if len(headers) < 3 {
- return 0, fmt.Errorf("helpdoc格式错误,缺少必须列 Key Synonym Content")
+ return 0, errors.New("helpdoc格式错误,缺少必须列 Key Synonym Content")
}
var (
@@ -442,205 +430,83 @@ out:
return synonymCount, nil
}
-func (dm *DiceManager) AddHelpWithDice(dice *Dice) {
- m := dm.Help
-
- addCmdMap := func(packageName string, cmdMap CmdMapCls) {
- for k, v := range cmdMap {
- content := v.Help
- if content == "" {
- content = v.ShortHelp
- }
- _ = m.AddItem(HelpTextItem{
- Group: HelpBuiltinGroup,
- Title: k,
- Content: content,
- PackageName: packageName,
- })
+func (m *HelpManager) addCmdMap(packageName string, cmdMap CmdMapCls) error {
+ for k, v := range cmdMap {
+ content := v.Help
+ if content == "" {
+ content = v.ShortHelp
}
- }
-
- addCmdMap("核心指令", dice.CmdMap)
- for _, i := range dice.ExtList {
- _ = m.AddItem(HelpTextItem{
+ err := m.AddItem(docengine.HelpTextItem{
Group: HelpBuiltinGroup,
- Title: i.Name,
- Content: i.GetDescText(i),
- PackageName: "扩展模块",
+ Title: k,
+ Content: content,
+ PackageName: packageName,
})
- addCmdMap(i.Name, i.CmdMap)
- }
- _ = m.AddItemApply()
-}
-
-func (m *HelpManager) AddItem(item HelpTextItem) error {
- data := map[string]string{
- "group": item.Group,
- "from": item.From,
- "title": item.Title,
- "content": item.Content,
- "package": item.PackageName,
- "_type": "helpdoc",
- }
-
- id := m.GetNextID()
- m.TextMap.Store(id, &item)
-
- if m.EngineType == 0 {
- if m.batch == nil {
- m.batch = m.Index.NewBatch()
- }
- if m.batchNum >= 50 {
- err := m.Index.Batch(m.batch)
- if err != nil {
- return err
- }
- m.batch.Reset()
- m.batchNum = 0
+ if err != nil {
+ log.Errorf("AddCmdMapItem err:%v", err)
+ return err
}
-
- m.batchNum++
- return m.batch.Index(id, data)
}
return nil
}
-func (m *HelpManager) AddItemApply() error {
- if m.batch != nil {
- err := m.Index.Batch(m.batch)
- m.batch.Reset()
- m.batch = nil
+func (m *HelpManager) addInternalCmdHelp(cmdMap CmdMapCls) error {
+ err := m.addCmdMap("核心指令", cmdMap)
+ if err != nil {
return err
}
return nil
}
-func (m *HelpManager) searchBleve(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (*bleve.SearchResult, int, int, int, error) {
- // 在标题中查找
- queryTitle := query.NewMatchPhraseQuery(text)
- queryTitle.SetField("title")
-
- titleOrContent := bleve.NewDisjunctionQuery(queryTitle)
-
- // 在正文中查找
- if !titleOnly {
- for _, i := range reSpace.Split(text, -1) {
- queryContent := query.NewMatchPhraseQuery(i)
- queryContent.SetField("content")
- titleOrContent.AddQuery(queryContent)
+func (m *HelpManager) addExternalCmdHelp(ext []*ExtInfo) error {
+ for _, i := range ext {
+ err := m.AddItem(docengine.HelpTextItem{
+ Group: HelpBuiltinGroup,
+ Title: i.Name,
+ Content: i.GetDescText(i),
+ PackageName: "扩展模块",
+ })
+ if err != nil {
+ return err
+ }
+ err = m.addCmdMap(i.Name, i.CmdMap)
+ if err != nil {
+ return err
}
}
+ return nil
+}
- andQuery := bleve.NewConjunctionQuery(titleOrContent)
-
- // 限制查询组
- for _, i := range ctx.Group.HelpPackages {
- queryPack := query.NewMatchPhraseQuery(i)
- queryPack.SetField("package")
- andQuery.AddQuery(queryPack)
- }
-
- // 查询指定文档组
- if group != "" {
- queryPack := query.NewMatchPhraseQuery(group)
- queryPack.SetField("group")
- andQuery.AddQuery(queryPack)
- }
-
- req := bleve.NewSearchRequestOptions(andQuery, pageSize, (pageNum-1)*pageSize, false)
+func (m *HelpManager) AddItem(item docengine.HelpTextItem) error {
+ _, err := m.searchEngine.AddItem(item)
+ return err
+}
- index := m.Index
- res, err := index.Search(req)
+func (m *HelpManager) AddItemApply(end bool) error {
+ err := m.searchEngine.AddItemApply(end)
if err != nil {
- return res, 0, 0, 0, err
+ return err
}
-
- total := int(res.Total)
- pageStart := (pageNum - 1) * pageSize
- pageEnd := pageStart + len(res.Hits)
- return res, total, pageStart, pageEnd, nil
+ return nil
}
-func (m *HelpManager) Search(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (res *bleve.SearchResult, total, pageStart, pageEnd int, err error) {
- if pageSize <= 0 || pageNum <= 0 {
- // 为了使Search的结果完全忠实于分页参数, 而不产生有结果但与分页不相符的情况
- return nil, 0, 0, 0, fmt.Errorf("分页参数错误")
- }
-
- if m.EngineType == 0 {
- return m.searchBleve(ctx, text, titleOnly, pageSize, pageNum, group)
- }
-
- // 不是很好的做法,待优化
- items := HelpTextItems{}
- var idLst []string
-
- m.TextMap.Range(func(id string, v *HelpTextItem) bool {
- items = append(items, v)
- idLst = append(idLst, id)
- return true
- })
-
- hits := search.DocumentMatchCollection{}
- matches := fuzzy.FindFrom(text, items)
-
- total = len(matches)
- pageStart = (pageNum - 1) * pageSize
- pageEnd = pageNum * pageSize
-
- if pageStart < total {
- if pageEnd > total {
- pageEnd = total
- }
-
- for _, i := range matches[pageStart:pageEnd] {
- hits = append(hits, &search.DocumentMatch{
- ID: idLst[i.Index],
- Score: float64(i.Score),
- })
- }
- } else {
- // 分页超出范围, 返回空结果
- pageStart = -1
- pageEnd = -1
- }
-
- return &bleve.SearchResult{
- Status: nil,
- Request: nil,
- Hits: hits,
- Total: uint64(total),
- }, total, pageStart, pageEnd, nil
+func (m *HelpManager) Search(ctx *MsgContext, text string, titleOnly bool, pageSize, pageNum int, group string) (res *docengine.GeneralSearchResult, total, pageStart, pageEnd int, err error) {
+ return m.searchEngine.Search(ctx.Group.HelpPackages, text, titleOnly, pageSize, pageNum, group)
}
func (m *HelpManager) GetSuffixText() string {
- switch m.EngineType {
- case 0:
- return "(本次搜索由全文搜索完成)"
- default:
- return "(本次搜索由快速文档查找完成)"
- }
+ return m.searchEngine.GetSuffixText()
}
func (m *HelpManager) GetPrefixText() string {
- switch m.EngineType {
- case 0:
- return "[全文搜索]"
- default:
- return "[快速文档查找]"
- }
+ return m.searchEngine.GetPrefixText()
}
func (m *HelpManager) GetShowBestOffset() int {
- switch m.EngineType {
- case 0:
- return 1
- default:
- return 15
- }
+ return m.searchEngine.GetShowBestOffset()
}
-func (m *HelpManager) GetContent(item *HelpTextItem, depth int) string {
+func (m *HelpManager) GetContent(item *docengine.HelpTextItem, depth int) string {
if depth > 7 {
return "{递归层数过多,不予显示}"
}
@@ -672,20 +538,15 @@ func (m *HelpManager) GetContent(item *HelpTextItem, depth int) string {
result.WriteString(txt[formattedIdx:left])
formattedIdx = right
name := txt[left+1 : right-1]
- matched := false
- // 注意: 效率更加不高
- m.TextMap.Range(func(key string, v *HelpTextItem) bool {
- if v.Title == name {
- result.WriteString(m.GetContent(v, depth+1))
- matched = true
- return false
- }
- return true
- })
- if !matched {
+ // 搜索TitleOnly,严格匹配Title的情形
+ // 如果查询到对应数据,那么就调用m.GetContent
+ valueResult, err := m.searchEngine.GetHelpTextItemByTermTitle(name)
+ if err != nil {
result.WriteByte('{')
result.WriteString(name)
result.WriteString(" - 未能找到}")
+ } else {
+ result.WriteString(m.GetContent(valueResult, depth+1))
}
}
result.WriteString(txt[formattedIdx:])
@@ -697,45 +558,56 @@ func generateHelpDocKey() string {
return key
}
+// 修改 buildHelpDocTree 函数签名,添加进度参数
func buildHelpDocTree(node *HelpDoc, fn func(d *HelpDoc)) {
- p, err := os.Stat(node.Path)
- if err != nil {
- return
- }
-
- fn(node)
+ // 收集所有节点
+ allNodes := []*HelpDoc{node}
- if !p.IsDir() {
- return
- }
+ for i := 0; i < len(allNodes); i++ {
+ current := allNodes[i]
- subs, err := os.ReadDir(node.Path)
- if err != nil {
- return
- }
+ p, err := os.Stat(current.Path)
+ if err != nil {
+ continue
+ }
- for _, sub := range subs {
- if strings.HasPrefix(sub.Name(), ".") {
+ if !p.IsDir() {
continue
}
- var child HelpDoc
- child.Key = generateHelpDocKey()
- child.Name = sub.Name()
- child.Path = path.Join(node.Path, sub.Name())
- child.Group = node.Group
- child.IsDir = sub.IsDir()
- if sub.IsDir() {
- child.Type = "dir"
- child.Children = make([]*HelpDoc, 0)
- } else {
- child.Type = filepath.Ext(sub.Name())
+
+ subs, err := os.ReadDir(current.Path)
+ if err != nil {
+ continue
}
- fn(&child)
- if sub.IsDir() {
- buildHelpDocTree(&child, fn)
+ current.Children = make([]*HelpDoc, 0)
+
+ for _, sub := range subs {
+ if strings.HasPrefix(sub.Name(), ".") {
+ continue
+ }
+
+ var child HelpDoc
+ child.Key = generateHelpDocKey()
+ child.Name = sub.Name()
+ child.Path = path.Join(current.Path, sub.Name())
+ child.Group = current.Group
+ child.IsDir = sub.IsDir()
+
+ if sub.IsDir() {
+ child.Type = "dir"
+ child.Children = make([]*HelpDoc, 0)
+ } else {
+ child.Type = filepath.Ext(sub.Name())
+ }
+
+ allNodes = append(allNodes, &child)
+ current.Children = append(current.Children, &child)
}
- node.Children = append(node.Children, &child)
+ }
+ for _, current := range allNodes {
+ // 调用处理函数
+ fn(current)
}
}
@@ -922,12 +794,13 @@ func (m *HelpManager) GetHelpItemPage(pageNum, pageSize int, id, group, from, ti
return 0, HelpTextVos{}
}
+ // 如果ID不为空
if id != "" {
- item, ok := m.TextMap.Load(id)
- if ok &&
- strings.Contains(item.Group, group) &&
- strings.Contains(item.From, from) &&
- strings.Contains(item.Title, title) {
+ // 加载对应ID的数据
+ item, err := m.searchEngine.GetItemByID(id)
+ // 若成功
+ if err == nil {
+ // 返回这条数据
vo := HelpTextVo{
Group: item.Group,
From: item.From,
@@ -941,36 +814,25 @@ func (m *HelpManager) GetHelpItemPage(pageNum, pageSize int, id, group, from, ti
}
return 0, HelpTextVos{}
}
- temp := make(HelpTextVos, 0, m.TextMap.Len())
- m.TextMap.Range(func(i string, item *HelpTextItem) bool {
- if strings.Contains(item.Group, group) &&
- strings.Contains(item.From, from) &&
- strings.Contains(item.Title, title) {
- vo := HelpTextVo{
- Group: item.Group,
- From: item.From,
- Title: item.Title,
- Content: item.Content,
- PackageName: item.PackageName,
- KeyWords: item.KeyWords,
- }
- vo.ID, _ = strconv.Atoi(i)
- temp = append(temp, vo)
+ // ID为空的情形,分页查询数据
+ total, result, err := m.searchEngine.PaginateDocuments(pageSize, pageNum, group, from, title)
+ if err != nil {
+ return 0, nil
+ }
+ var items = make(HelpTextVos, 0)
+ for _, item := range result {
+ vo := HelpTextVo{
+ Group: item.Group,
+ From: item.From,
+ Title: item.Title,
+ Content: item.Content,
+ PackageName: item.PackageName,
+ KeyWords: item.KeyWords,
}
- return true
- })
-
- sort.Sort(temp)
-
- start := (pageNum - 1) * pageSize
- end := start + pageSize
- total := len(temp)
- if start >= total {
- return total, HelpTextVos{}
- } else if end < total {
- return total, temp[start:end]
+ vo.ID, _ = strconv.Atoi(id)
+ items = append(items, vo)
}
- return total, temp[start:]
+ return int(total), items
}
// SetDefaultHelpGroup 设置群默认搜索分组
diff --git a/dice/dice_jsvm.go b/dice/dice_jsvm.go
index cc27debd..87439955 100644
--- a/dice/dice_jsvm.go
+++ b/dice/dice_jsvm.go
@@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "runtime/debug"
"strconv"
"strings"
"sync"
@@ -28,12 +29,12 @@ import (
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
"github.com/samber/lo"
- "go.uber.org/zap"
"gopkg.in/elazarl/goproxy.v1"
"gopkg.in/yaml.v3"
"sealdice-core/static"
"sealdice-core/utils/crypto"
+ log "sealdice-core/utils/kratos"
)
var (
@@ -98,7 +99,6 @@ func (d *Dice) JsInit() {
d.JsScriptCron = cron.New()
d.JsScriptCronLock = &sync.Mutex{}
d.JsScriptCron.Start()
-
// 初始化
loop.Run(func(vm *goja.Runtime) {
vm.SetFieldNameMapper(goja.TagFieldNameMapper("jsbind", true))
@@ -121,30 +121,30 @@ func (d *Dice) JsInit() {
ban := vm.NewObject()
_ = seal.Set("ban", ban)
_ = ban.Set("addBan", func(ctx *MsgContext, id string, place string, reason string) {
- d.BanList.AddScoreBase(id, d.BanList.ThresholdBan, place, reason, ctx)
- d.BanList.SaveChanged(d)
+ (&d.Config).BanList.AddScoreBase(id, d.Config.BanList.ThresholdBan, place, reason, ctx)
+ (&d.Config).BanList.SaveChanged(d)
})
_ = ban.Set("addTrust", func(ctx *MsgContext, id string, place string, reason string) {
- d.BanList.SetTrustByID(id, place, reason)
- d.BanList.SaveChanged(d)
+ (&d.Config).BanList.SetTrustByID(id, place, reason)
+ (&d.Config).BanList.SaveChanged(d)
})
_ = ban.Set("remove", func(ctx *MsgContext, id string) {
- _, ok := d.BanList.GetByID(id)
+ _, ok := (&d.Config).BanList.GetByID(id)
if !ok {
return
}
- d.BanList.DeleteByID(d, id)
+ (&d.Config).BanList.DeleteByID(d, id)
})
_ = ban.Set("getList", func() []BanListInfoItem {
var list []BanListInfoItem
- d.BanList.Map.Range(func(key string, value *BanListInfoItem) bool {
+ (&d.Config).BanList.Map.Range(func(key string, value *BanListInfoItem) bool {
list = append(list, *value)
return true
})
return list
})
_ = ban.Set("getUser", func(id string) *BanListInfoItem {
- i, ok := d.BanList.GetByID(id)
+ i, ok := (&d.Config).BanList.GetByID(id)
if !ok {
return nil
}
@@ -203,7 +203,7 @@ func (d *Dice) JsInit() {
// Pinenutn: Range模板 ServiceAtNew重构代码
d.ImSession.ServiceAtNew.Range(func(key string, groupInfo *GroupInfo) bool {
// Pinenutn: ServiceAtNew重构
- groupInfo.ExtActive(ei)
+ groupInfo.ExtActiveBySnapshotOrder(ei, true)
return true
})
})
@@ -511,7 +511,7 @@ func (d *Dice) JsInit() {
if err != nil {
return errors.New("解析失败:" + err.Error())
}
- ret := d.GameSystemTemplateAdd(tmpl)
+ ret := d.GameSystemTemplateAddEx(tmpl, true)
if !ret {
return errors.New("已存在同名模板")
}
@@ -523,7 +523,7 @@ func (d *Dice) JsInit() {
if err != nil {
return errors.New("解析失败:" + err.Error())
}
- ret := d.GameSystemTemplateAdd(tmpl)
+ ret := d.GameSystemTemplateAddEx(tmpl, true)
if !ret {
return errors.New("已存在同名模板")
}
@@ -598,15 +598,27 @@ func (d *Dice) JsInit() {
// `)
_, _ = vm.RunString(`Object.freeze(seal);Object.freeze(seal.deck);Object.freeze(seal.coc);Object.freeze(seal.ext);Object.freeze(seal.vars);`)
})
- loop.Start()
- d.JsEnable = true
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Errorf("JS核心执行异常: %v 堆栈: %v", r, string(debug.Stack()))
+ }
+ }()
+ loop.StartInForeground()
+ }()
+ // loop.Start()
+ (&d.Config).JsEnable = true
d.Logger.Info("已加载JS环境")
+ d.MarkModified()
+ d.Save(false)
}
func (d *Dice) JsShutdown() {
- d.JsEnable = false
+ (&d.Config).JsEnable = false
d.jsClear()
d.Logger.Info("已关闭JS环境")
+ d.MarkModified()
+ d.Save(false)
}
func (d *Dice) jsClear() {
@@ -630,7 +642,7 @@ func (d *Dice) jsClear() {
d.RegisterBuiltinSystemTemplate()
// 关闭js vm
if d.JsLoop != nil {
- d.JsLoop.Stop()
+ d.JsLoop.Terminate()
d.JsLoop = nil
}
}
@@ -774,9 +786,20 @@ func (d *Dice) JsReload() {
d.JsScriptCron.Stop()
d.JsScriptCron = nil
}
+
+ // 记录扩展快照
+ d.ImSession.ServiceAtNew.Range(func(key string, groupInfo *GroupInfo) bool {
+ groupInfo.ExtListSnapshot = lo.Map(groupInfo.ActivatedExtList, func(item *ExtInfo, index int) string {
+ return item.Name
+ })
+ return true
+ })
+
d.JsInit()
_ = d.ConfigManager.Load()
d.JsLoadScripts()
+ d.MarkModified()
+ d.Save(false)
}
// JsExtSettingVacuum 清理已被删除的脚本对应的插件配置
@@ -795,7 +818,7 @@ func (d *Dice) JsExtSettingVacuum() {
}
idxToDel := []int{}
- for k, v := range d.ExtDefaultSettings {
+ for k, v := range d.Config.ExtDefaultSettings {
if !v.ExtItem.IsJsExt {
continue
}
@@ -806,7 +829,7 @@ func (d *Dice) JsExtSettingVacuum() {
for i := len(idxToDel) - 1; i >= 0; i-- {
idx := idxToDel[i]
- d.ExtDefaultSettings = append(d.ExtDefaultSettings[:idx], d.ExtDefaultSettings[idx+1:]...)
+ (&d.Config).ExtDefaultSettings = append((&d.Config).ExtDefaultSettings[:idx], (&d.Config).ExtDefaultSettings[idx+1:]...)
}
panic("DONT USE ME")
@@ -1004,7 +1027,7 @@ func (d *Dice) JsParseMeta(s string, installTime time.Time, rawData []byte, buil
jsInfo.ErrText = strings.Join(errMsg, "\n")
return nil, errors.New(strings.Join(errMsg, "|"))
}
- jsInfo.Enable = !d.DisabledJsScripts[jsInfo.Name]
+ jsInfo.Enable = !(&d.Config).DisabledJsScripts[jsInfo.Name]
return jsInfo, nil
}
@@ -1109,27 +1132,31 @@ func JsDelete(_ *Dice, jsInfo *JsScriptInfo) {
}
func JsEnable(d *Dice, jsInfoName string) {
- delete(d.DisabledJsScripts, jsInfoName)
+ delete((&d.Config).DisabledJsScripts, jsInfoName)
for _, jsInfo := range d.JsScriptList {
if jsInfo.Name == jsInfoName {
jsInfo.Enable = true
}
}
+ d.LastUpdatedTime = time.Now().Unix()
+ d.Save(false)
}
func JsDisable(d *Dice, jsInfoName string) {
- d.DisabledJsScripts[jsInfoName] = true
+ (&d.Config).DisabledJsScripts[jsInfoName] = true
for _, jsInfo := range d.JsScriptList {
if jsInfo.Name == jsInfoName {
jsInfo.Enable = false
}
}
+ d.LastUpdatedTime = time.Now().Unix()
+ d.Save(false)
}
func (d *Dice) JsCheckUpdate(jsScriptInfo *JsScriptInfo) (string, string, string, error) {
// FIXME: dirty, copy from check deck update.
if len(jsScriptInfo.UpdateUrls) == 0 {
- return "", "", "", fmt.Errorf("插件未提供更新链接")
+ return "", "", "", errors.New("插件未提供更新链接")
}
statusCode, newData, err := GetCloudContent(jsScriptInfo.UpdateUrls, jsScriptInfo.Etag)
@@ -1137,10 +1164,10 @@ func (d *Dice) JsCheckUpdate(jsScriptInfo *JsScriptInfo) (string, string, string
return "", "", "", err
}
if statusCode == http.StatusNotModified {
- return "", "", "", fmt.Errorf("插件没有更新")
+ return "", "", "", errors.New("插件没有更新")
}
if statusCode != http.StatusOK {
- return "", "", "", fmt.Errorf("未获取到插件更新")
+ return "", "", "", errors.New("未获取到插件更新")
}
oldData, err := os.ReadFile(jsScriptInfo.Filename)
if err != nil {
@@ -1179,7 +1206,7 @@ func (d *Dice) JsUpdate(jsScriptInfo *JsScriptInfo, tempFileName string) error {
return err
}
if len(newData) == 0 {
- return fmt.Errorf("new data is empty")
+ return errors.New("new data is empty")
}
// 更新插件
err = os.WriteFile(jsScriptInfo.Filename, newData, 0o755)
@@ -1335,7 +1362,7 @@ type JsScriptTask struct {
entryID *cron.EntryID
lock *sync.Mutex
- logger *zap.SugaredLogger
+ logger *log.Helper
}
type JsScriptTaskCtx struct {
@@ -1407,7 +1434,7 @@ func (t *JsScriptTask) reset(expr string) error {
func parseTaskTime(taskTimeStr string) (string, error) {
match := taskTimeRe.MatchString(taskTimeStr)
if !match {
- return "", fmt.Errorf("仅接受 24 小时表示的时间作为每天的执行时间,如 0:05 13:30")
+ return "", errors.New("仅接受 24 小时表示的时间作为每天的执行时间,如 0:05 13:30")
}
time, err := time.Parse("15:04", taskTimeStr)
if err != nil {
diff --git a/dice/dice_jsvm_test.go b/dice/dice_jsvm_test.go
index 621a21de..749c3b71 100644
--- a/dice/dice_jsvm_test.go
+++ b/dice/dice_jsvm_test.go
@@ -247,7 +247,7 @@ func sameScriptInfos(a []*JsScriptInfo, b []*JsScriptInfo) bool {
if len(a) != len(b) {
return false
}
- for i := 0; i < len(a); i++ {
+ for i := range a {
if !sameScriptInfo(a[i], b[i]) {
return false
}
diff --git a/dice/dice_manager.go b/dice/dice_manager.go
index 612a97f5..b5dc0b85 100644
--- a/dice/dice_manager.go
+++ b/dice/dice_manager.go
@@ -1,15 +1,15 @@
package dice
import (
- "fmt"
"os"
"sync/atomic"
"time"
"github.com/dop251/goja_nodejs/require"
"github.com/robfig/cron/v3"
- "go.uber.org/zap"
"gopkg.in/yaml.v3"
+
+ log "sealdice-core/utils/kratos"
)
type VersionInfo struct {
@@ -75,14 +75,14 @@ type DiceManager struct { //nolint:revive
ServiceName string
JustForTest bool
JsRegistry *require.Registry
- UpdateSealdiceByFile func(packName string, log *zap.SugaredLogger) bool // 使用指定压缩包升级海豹,如果出错返回false,如果成功进程会自动结束
+ UpdateSealdiceByFile func(packName string, log *log.Helper) bool // 使用指定压缩包升级海豹,如果出错返回false,如果成功进程会自动结束
ContainerMode bool // 容器模式:禁用内置适配器,不允许使用内置Lagrange和旧的内置Gocq
CleanupFlag atomic.Uint32 // 1 为正在清理,0为普通状态
}
-type DiceConfigs struct { //nolint:revive
- DiceConfigs []DiceConfig `yaml:"diceConfigs"`
+type Configs struct { //nolint:revive
+ DiceConfigs []BaseConfig `yaml:"diceConfigs"`
ServeAddress string `yaml:"serveAddress"`
WebUIAddress string `yaml:"webUIAddress"`
HelpDocEngineType int `yaml:"helpDocEngineType"`
@@ -112,9 +112,12 @@ func (dm *DiceManager) InitHelp() {
dm.IsHelpReloading = true
_ = os.MkdirAll("./data/helpdoc", 0755)
dm.Help = new(HelpManager)
- dm.Help.Parent = dm
- dm.Help.EngineType = dm.HelpDocEngineType
- dm.Help.Load()
+ dm.Help.EngineType = EngineType(dm.HelpDocEngineType)
+ if len(dm.Dice) == 0 {
+ log.Fatalf("Dice实例不存在!")
+ return
+ }
+ dm.Help.Load(dm.Dice[0].CmdMap, dm.Dice[0].ExtList)
dm.IsHelpReloading = false
}
@@ -149,10 +152,10 @@ func (dm *DiceManager) LoadDice() {
return
}
- var dc DiceConfigs
+ var dc Configs
err = yaml.Unmarshal(data, &dc)
if err != nil {
- fmt.Println("读取 data/dice.yaml 发生错误: 配置文件格式不正确")
+ log.Error("读取 data/dice.yaml 发生错误: 配置文件格式不正确", err)
panic(err)
}
@@ -195,7 +198,7 @@ func (dm *DiceManager) LoadDice() {
}
func (dm *DiceManager) Save() {
- var dc DiceConfigs
+ var dc Configs
dc.ServeAddress = dm.ServeAddress
dc.HelpDocEngineType = dm.HelpDocEngineType
dc.UIPasswordSalt = dm.UIPasswordSalt
@@ -236,7 +239,7 @@ func (dm *DiceManager) InitDice() {
g, err := NewProcessExitGroup()
if err != nil {
- fmt.Println("进程组创建失败,若进程崩溃,gocqhttp进程可能需要手动结束。")
+ log.Warn("进程组创建失败,若进程崩溃,gocqhttp进程可能需要手动结束。")
} else {
dm.progressExitGroupWin = g
}
@@ -249,18 +252,14 @@ func (dm *DiceManager) InitDice() {
go func() {
defer func() {
if r := recover(); r != nil {
- fmt.Println("帮助文档加载失败。可能是由于退出程序过快,帮助文档还未加载完成所致", r)
+ log.Warn("帮助文档加载失败。可能是由于退出程序过快,帮助文档还未加载完成所致", r)
if dm.Help != nil {
- fmt.Println("帮助文件加载失败:", dm.Help.LoadingFn)
+ log.Warn("帮助文件加载失败:", dm.Help.LoadingFn)
}
}
}()
-
// 加载帮助
dm.InitHelp()
- if len(dm.Dice) >= 1 {
- dm.AddHelpWithDice(dm.Dice[0])
- }
}()
dm.ResetAutoBackup()
@@ -277,17 +276,15 @@ func (dm *DiceManager) ResetAutoBackup() {
dm.backupEntryID, err = dm.Cron.AddFunc(dm.AutoBackupTime, func() {
errBackup := dm.BackupAuto()
if errBackup != nil {
- fmt.Println("自动备份失败: ", errBackup.Error())
+ log.Errorf("自动备份失败: %v", errBackup)
return
}
if errBackup = dm.BackupClean(true); errBackup != nil {
- fmt.Println("滚动清理备份失败: ", errBackup.Error())
+ log.Errorf("滚动清理备份失败: %v", errBackup)
}
})
if err != nil {
- if len(dm.Dice) > 0 {
- dm.Dice[0].Logger.Errorf("设定的自动备份间隔有误: %v", err.Error())
- }
+ log.Errorf("设定的自动备份间隔有误: %v", err)
return
}
}
@@ -304,14 +301,11 @@ func (dm *DiceManager) ResetBackupClean() {
dm.backupCleanCronID, err = dm.Cron.AddFunc(dm.BackupCleanCron, func() {
errBackup := dm.BackupClean(false)
if errBackup != nil {
- fmt.Println("定时清理备份失败: ", errBackup.Error())
+ log.Errorf("定时清理备份失败: %v", errBackup)
}
})
-
if err != nil {
- if len(dm.Dice) > 0 {
- dm.Dice[0].Logger.Errorf("设定的备份清理cron有误: %q %v", dm.BackupCleanCron, err)
- }
+ log.Errorf("设定的备份清理cron有误: %q %v", dm.BackupCleanCron, err)
return
}
}
@@ -325,9 +319,8 @@ func (dm *DiceManager) TryCreateDefault() {
if len(dm.Dice) == 0 {
defaultDice := new(Dice)
defaultDice.BaseConfig.Name = "default"
- defaultDice.BaseConfig.IsLogPrint = true
- defaultDice.MessageDelayRangeStart = 0.4
- defaultDice.MessageDelayRangeEnd = 0.9
+ defaultDice.Config.MessageDelayRangeStart = DefaultConfig.MessageDelayRangeStart
+ defaultDice.Config.MessageDelayRangeEnd = DefaultConfig.MessageDelayRangeEnd
defaultDice.MarkModified()
defaultDice.ContainerMode = dm.ContainerMode
dm.Dice = append(dm.Dice, defaultDice)
diff --git a/dice/dice_name_generator.go b/dice/dice_name_generator.go
index cc0c0924..45f51e25 100644
--- a/dice/dice_name_generator.go
+++ b/dice/dice_name_generator.go
@@ -9,6 +9,8 @@ import (
wr "github.com/mroth/weightedrand"
"github.com/xuri/excelize/v2"
+
+ log "sealdice-core/utils/kratos"
)
type NamesGenerator struct {
@@ -24,7 +26,7 @@ func (ng *NamesGenerator) Load() {
for _, fn := range []string{"./data/names/names.xlsx", "./data/names/names-dnd.xlsx"} {
f, err := excelize.OpenFile(fn)
if err != nil {
- fmt.Println("加载names信息出错", fn, err)
+ log.Warn("加载names信息出错", fn, err)
continue
}
@@ -53,7 +55,7 @@ func (ng *NamesGenerator) Load() {
}
if err := f.Close(); err != nil {
- fmt.Println(err)
+ log.Error("NamesGenerator.Load", err)
}
}
}
@@ -112,7 +114,7 @@ func (ng *NamesGenerator) NameGenerate(rule string) string {
length = length2
}
- for index := 0; index < length; index++ {
+ for index := range length {
choices = append(choices, wr.NewChoice(index, uint(weightLst[index])))
}
restText = sp[0]
diff --git a/dice/docengine/bleve.go b/dice/docengine/bleve.go
new file mode 100644
index 00000000..492f9f0c
--- /dev/null
+++ b/dice/docengine/bleve.go
@@ -0,0 +1,321 @@
+package docengine
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/blevesearch/bleve/v2"
+ "github.com/blevesearch/bleve/v2/search/query"
+ index "github.com/blevesearch/bleve_index_api"
+
+ log "sealdice-core/utils/kratos"
+)
+
+type BleveSearchEngine struct {
+ Index bleve.Index
+ batch *bleve.Batch
+ batchSize int
+ CurID uint64
+}
+
+var indexDir = "./data/_index"
+var reSpace = regexp.MustCompile(`\s+`)
+
+// getNextID 使用原子操作,避免并发问题
+func (d *BleveSearchEngine) getNextID() string {
+ // 使用原子操作安全递增 CurID
+ nextID := atomic.AddUint64(&d.CurID, 1)
+ return strconv.FormatUint(nextID, 10)
+}
+
+// NewEngine 创建并初始化 BleveSearchEngine
+func NewBleveSearchEngine() (*BleveSearchEngine, error) {
+ engine := &BleveSearchEngine{}
+ err := engine.Init()
+ if err != nil {
+ return nil, err
+ }
+ return engine, nil
+}
+
+func (d *BleveSearchEngine) GetSuffixText() string {
+ return "(本次搜索由全文搜索完成)"
+}
+
+func (d *BleveSearchEngine) GetPrefixText() string {
+ return "[全文搜索]"
+}
+
+func (d *BleveSearchEngine) GetShowBestOffset() int {
+ return 1
+}
+
+func (d *BleveSearchEngine) Init() error {
+ mapping := bleve.NewIndexMapping()
+ docMapping := bleve.NewDocumentMapping()
+ contentFieldMapping := bleve.NewTextFieldMapping()
+ keywordMapping := bleve.NewKeywordFieldMapping()
+ // 注意: 这里group,from,title,package都是keywordMapping,这样就能进行精确搜索。
+ docMapping.AddFieldMappingsAt("group", keywordMapping)
+ docMapping.AddFieldMappingsAt("from", keywordMapping)
+ docMapping.AddFieldMappingsAt("title", contentFieldMapping)
+ // Content才是真正的文档
+ docMapping.AddFieldMappingsAt("content", contentFieldMapping)
+ docMapping.AddFieldMappingsAt("package", keywordMapping)
+ mapping.AddDocumentMapping("helpdoc", docMapping)
+ mapping.TypeField = "_type"
+ i, err := bleve.New(indexDir, mapping)
+ if err != nil {
+ return err
+ }
+ d.Index = i
+ // 初始化ID列表
+ d.CurID = 0
+ // 初始化新的batch
+ d.batch = d.Index.NewBatch()
+ return nil
+}
+
+func (d *BleveSearchEngine) Close() {
+ if d.Index != nil {
+ _ = d.Index.Close()
+ d.Index = nil
+ }
+}
+
+func (d *BleveSearchEngine) GetTotalID() uint64 {
+ return d.CurID
+}
+
+// AddItem 这里引用了dice,其实不妥,应该将它单独拆出来的。
+func (d *BleveSearchEngine) AddItem(item HelpTextItem) (string, error) {
+ // 如果batch为空,初始化一个batch
+ if d.batch == nil {
+ return "", errors.New("已通过end参数执行AddItemApply,不允许新增文档。请检查代码逻辑")
+ }
+ id := d.getNextID()
+ data := map[string]string{
+ "group": item.Group,
+ "from": item.From,
+ "title": item.Title,
+ "content": item.Content,
+ "package": item.PackageName,
+ "_type": "helpdoc",
+ }
+ d.batchSize++
+ // 五十一次执行
+ if d.batchSize >= 50 {
+ err := d.AddItemApply(false)
+ d.batchSize = 0
+ if err != nil {
+ return "", err
+ }
+ }
+ return id, d.batch.Index(id, data)
+}
+
+// AddItemApply 这里认为是真正执行插入文档的逻辑
+// 由于现在已经将执行函数改为了可按文件执行,所以可以按文件进行Apply,这应当不会有太大的量级。
+// end代表是否是最后一次执行,一般用在所有的数据都处理完之后,关闭逻辑的时候使用,如bleve batch重复利用后最后销毁
+func (d *BleveSearchEngine) AddItemApply(end bool) error {
+ if d.batch != nil {
+ // 执行batch
+ err := d.Index.Batch(d.batch)
+ if err != nil {
+ return err
+ }
+ // 如果是最后一批
+ if end {
+ d.batch.Reset()
+ d.batch = nil
+ } else {
+ // 否则仅重置batch
+ d.batch.Reset()
+ }
+ return err
+ }
+ return nil
+}
+
+func (d *BleveSearchEngine) Search(helpPackages []string, text string, titleOnly bool, pageSize, pageNum int, group string) (*GeneralSearchResult, int, int, int, error) {
+ // 在标题中查找
+ queryTitle := query.NewMatchPhraseQuery(text)
+ queryTitle.SetField("title")
+
+ titleOrContent := bleve.NewDisjunctionQuery(queryTitle)
+
+ // 在正文中查找
+ if !titleOnly {
+ for _, i := range reSpace.Split(text, -1) {
+ queryContent := query.NewMatchPhraseQuery(i)
+ queryContent.SetField("content")
+ titleOrContent.AddQuery(queryContent)
+ }
+ }
+
+ andQuery := bleve.NewConjunctionQuery(titleOrContent)
+
+ // 限制查询组
+ for _, i := range helpPackages {
+ queryPack := query.NewMatchPhraseQuery(i)
+ queryPack.SetField("package")
+ andQuery.AddQuery(queryPack)
+ }
+
+ // 查询指定文档组
+ if group != "" {
+ queryPack := query.NewMatchPhraseQuery(group)
+ queryPack.SetField("group")
+ andQuery.AddQuery(queryPack)
+ }
+
+ req := bleve.NewSearchRequestOptions(andQuery, pageSize, (pageNum-1)*pageSize, false)
+ // 设置要被返回的数据
+ req.Fields = []string{"*"}
+ res, err := d.Index.Search(req)
+ if err != nil {
+ return nil, 0, 0, 0, err
+ }
+ var resultList = make(MatchCollection, 0)
+ for _, hit := range res.Hits {
+ result := MatchResult{
+ ID: hit.ID,
+ Fields: hit.Fields,
+ Score: hit.Score,
+ }
+ resultList = append(resultList, &result)
+ }
+ // 转换搜索格式
+ responseResult := GeneralSearchResult{
+ Hits: resultList,
+ Total: res.Total,
+ }
+ total := int(res.Total)
+ pageStart := (pageNum - 1) * pageSize
+ pageEnd := pageStart + len(res.Hits)
+ return &responseResult, total, pageStart, pageEnd, nil
+}
+
+// 下面的代码都应该重构,因为它们返回的不是我们想要的结果
+// PaginateAllDocuments 分页查询所有文档
+// TODO:这里坏了,没有办法用,本来应该是精确匹配NewMatchQuery
+func (d *BleveSearchEngine) PaginateDocuments(pageSize, pageNum int, group, from, title string) (uint64, []*HelpTextItem, error) {
+ var items []*HelpTextItem
+ // 只有Keyword才支持NewTermQuery
+ conjunctionQuery := bleve.NewConjunctionQuery()
+ if group != "" {
+ groupQuery := bleve.NewTermQuery(group)
+ groupQuery.SetField("group")
+ conjunctionQuery.AddQuery(groupQuery)
+ }
+ if from != "" {
+ fromQuery := bleve.NewTermQuery(from)
+ fromQuery.SetField("from")
+ conjunctionQuery.AddQuery(fromQuery)
+ }
+ if title != "" {
+ titleQuery := bleve.NewTermQuery(title)
+ titleQuery.SetField("title")
+ conjunctionQuery.AddQuery(titleQuery)
+ }
+
+ // 计算分页参数
+ fromInt := (pageNum - 1) * pageSize // 起始位置
+ if fromInt < 0 {
+ fromInt = 0
+ }
+ var searchRequest *bleve.SearchRequest
+ // 创建查询请求,设置分页参数
+ if group == "" && from == "" && title == "" {
+ searchRequest = bleve.NewSearchRequestOptions(bleve.NewMatchAllQuery(), pageSize, fromInt, false)
+ } else {
+ searchRequest = bleve.NewSearchRequestOptions(conjunctionQuery, pageSize, fromInt, true)
+ }
+ searchRequest.Fields = []string{"*"} // 设置需要返回的字段
+
+ // 执行查询
+ searchResult, err := d.Index.Search(searchRequest)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ // 处理结果
+ for _, hit := range searchResult.Hits {
+ fields := hit.Fields
+ item := &HelpTextItem{
+ Group: fmt.Sprintf("%v", fields["group"]),
+ From: fmt.Sprintf("%v", fields["from"]),
+ Title: fmt.Sprintf("%v", fields["title"]),
+ Content: fmt.Sprintf("%v", fields["content"]),
+ PackageName: fmt.Sprintf("%v", fields["package"]),
+ KeyWords: "", // 暂时空值
+ RelatedExt: nil, // 暂时空值
+ }
+ items = append(items, item)
+ }
+ return searchResult.Total, items, nil
+}
+
+func (d *BleveSearchEngine) GetItemByID(id string) (*HelpTextItem, error) {
+ document, err := d.Index.Document(id)
+ if err != nil {
+ return nil, err
+ }
+ // 检查是否找到文档
+ if document == nil {
+ return nil, errors.New("未找到匹配的文档")
+ }
+ item := HelpTextItem{}
+ // 看了看源码,意思就是这样访问文档内的所有fields
+ document.VisitFields(func(field index.Field) {
+ name := field.Name()
+ value := string(field.Value())
+ // 这里的代码有点抽象……
+ switch name {
+ case "group":
+ item.Group = value
+ case "from":
+ item.From = value
+ case "title":
+ item.Title = value
+ case "content":
+ item.Content = value
+ case "package":
+ item.PackageName = value
+ // 好像会碰到Type的参数?
+ default:
+ log.Debugf("这是个什么参数 %s", name)
+ }
+ })
+ return &item, nil
+}
+
+// 精确查询title
+func (d *BleveSearchEngine) GetHelpTextItemByTermTitle(title string) (*HelpTextItem, error) {
+ newTermQuery := query.NewTermQuery(title)
+ newTermQuery.SetField("title") // 精确匹配title
+ req := bleve.NewSearchRequest(newTermQuery)
+ req.Fields = []string{"*"}
+ res, err := d.Index.Search(req)
+ if err != nil {
+ return nil, err
+ }
+ // 取出结果
+ if len(res.Hits) > 0 {
+ fields := res.Hits[0].Fields
+ return &HelpTextItem{
+ Group: fmt.Sprintf("%v", fields["group"]),
+ From: fmt.Sprintf("%v", fields["from"]),
+ Title: fmt.Sprintf("%v", fields["title"]),
+ Content: fmt.Sprintf("%v", fields["content"]),
+ PackageName: fmt.Sprintf("%v", fields["package"]),
+ // 这俩是什么东西?!
+ KeyWords: "",
+ RelatedExt: nil,
+ }, nil
+ }
+ return nil, errors.New("查询失败,未查询到数据")
+}
diff --git a/dice/docengine/enter.go b/dice/docengine/enter.go
new file mode 100644
index 00000000..faa5795a
--- /dev/null
+++ b/dice/docengine/enter.go
@@ -0,0 +1,54 @@
+package docengine
+
+type MatchResult struct {
+ ID string `json:"id"`
+ Fields map[string]interface{} `json:"fields"`
+ Score float64 `json:"score"`
+}
+
+type Fields struct {
+}
+
+type MatchCollection []*MatchResult
+
+// GeneralSearchResult Copied from bleve
+type GeneralSearchResult struct {
+ Hits MatchCollection
+ Total uint64
+}
+
+type HelpTextItem struct {
+ Group string
+ From string
+ Title string
+ Content string
+ PackageName string
+ // 这俩玩意有用?
+ KeyWords string
+ RelatedExt []string
+}
+
+// SearchEngine TODO: 进一步优化结构,封装成通用的搜索
+type SearchEngine interface {
+ GetSuffixText() string
+ GetPrefixText() string
+ GetShowBestOffset() int
+ // Init 初始化搜索引擎
+ Init() error
+ // Close 关闭搜索引擎
+ Close()
+ // AddItem 添加文档条目,返回添加文档的ID
+ AddItem(item HelpTextItem) (string, error)
+ // AddItemApply 提交文档条目
+ AddItemApply(end bool) error
+ // Search 搜索文档条目
+ Search(helpPackages []string, text string, titleOnly bool, pageSize, pageNum int, group string) (*GeneralSearchResult, int, int, int, error)
+ // GetHelpTextItemByTermTitle 精确查询title,用于嵌套获取数据的情形
+ GetHelpTextItemByTermTitle(title string) (*HelpTextItem, error)
+ // GetItemByID 通过ID获取Item数据的方案
+ GetItemByID(id string) (*HelpTextItem, error)
+ // PaginateDocuments 分页获取数据
+ PaginateDocuments(pageSize, pageNum int, group, from, title string) (uint64, []*HelpTextItem, error)
+ // GetTotalID 获取当前ID总数,注意,ID必须是顺序排列的
+ GetTotalID() uint64
+}
diff --git a/dice/ext.go b/dice/ext.go
index 3aba0a0f..04d0dd79 100644
--- a/dice/ext.go
+++ b/dice/ext.go
@@ -110,7 +110,7 @@ func GetExtensionDesc(ei *ExtInfo) string {
func (i *ExtInfo) callWithJsCheck(d *Dice, f func()) {
if i.IsJsExt {
- if d.JsEnable {
+ if d.Config.JsEnable {
waitRun := make(chan int, 1)
d.JsLoop.RunOnLoop(func(vm *goja.Runtime) {
defer func() {
@@ -142,9 +142,7 @@ func (i *ExtInfo) StorageInit() error {
// 使用互斥锁保护初始化过程,确保只初始化一次
i.dbMu.Lock()
defer i.dbMu.Unlock()
- d.Logger.Debugf("[扩展]:%s 正在尝试获取锁进行初始化", i.Name)
if i.init {
- d.Logger.Debug("[扩展]:初始化调用,但数据库已经加载")
// 如果已经初始化,则直接返回
return nil
}
diff --git a/dice/ext_coc7.go b/dice/ext_coc7.go
index f6f96f10..f96f5b7f 100644
--- a/dice/ext_coc7.go
+++ b/dice/ext_coc7.go
@@ -420,6 +420,7 @@ func RegisterBuiltinExtCoc7(self *Dice) {
VarSetValueInt64(mctx, "$tD100", outcome)
VarSetValueInt64(mctx, "$t判定值", checkVal)
VarSetValueInt64(mctx, "$tSuccessRank", int64(successRank))
+ VarSetValueStr(mctx, "$t属性表达式文本", expr2Text)
var suffix string
var suffixFull string
@@ -460,24 +461,22 @@ func RegisterBuiltinExtCoc7(self *Dice) {
commandInfoItems = append(commandInfoItems, infoItem)
VarSetValueStr(mctx, "$t检定表达式文本", expr1Text)
- VarSetValueStr(mctx, "$t属性表达式文本", expr2Text)
VarSetValueStr(mctx, "$t检定计算过程", detailWrap)
VarSetValueStr(mctx, "$t计算过程", detailWrap)
SetTempVars(mctx, mctx.Player.Name) // 信息里没有QQ昵称,用这个顶一下
- VarSetValueStr(mctx, "$t结果文本", DiceFormatTmpl(mctx, "COC:检定_单项结果文本"))
return nil
}
var text string
if cmdArgs.SpecialExecuteTimes > 1 {
VarSetValueInt64(mctx, "$t次数", int64(cmdArgs.SpecialExecuteTimes))
- if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.MaxExecuteTime) {
+ if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) {
ReplyToSender(mctx, msg, DiceFormatTmpl(mctx, "COC:检定_轮数过多警告"))
return CmdExecuteResult{Matched: true, Solved: true}
}
texts := []string{}
- for i := 0; i < cmdArgs.SpecialExecuteTimes; i++ {
+ for range cmdArgs.SpecialExecuteTimes {
ret := rollOne(true)
if ret != nil {
return *ret
@@ -599,7 +598,7 @@ func RegisterBuiltinExtCoc7(self *Dice) {
ReplyToSender(ctx, msg, text)
case "details":
help := "当前有coc7规则如下:\n"
- for i := 0; i < 6; i++ {
+ for i := range 6 {
basicStr := strings.ReplaceAll(SetCocRuleText[i], "\n", " ")
help += fmt.Sprintf(".setcoc %d // %s\n", i, basicStr)
}
@@ -907,12 +906,12 @@ func RegisterBuiltinExtCoc7(self *Dice) {
increment int64
newVarValue int64
}
- RuleNotMatch := fmt.Errorf("rule not match")
- FormatMismatch := fmt.Errorf("format mismatch")
- SkillNotEntered := fmt.Errorf("skill not entered")
- SkillTypeError := fmt.Errorf("skill value type error")
- SuccessExprFormatError := fmt.Errorf("success expr format error")
- FailExprFormatError := fmt.Errorf("fail expr format error")
+ RuleNotMatch := errors.New("rule not match")
+ FormatMismatch := errors.New("format mismatch")
+ SkillNotEntered := errors.New("skill not entered")
+ SkillTypeError := errors.New("skill value type error")
+ SuccessExprFormatError := errors.New("success expr format error")
+ FailExprFormatError := errors.New("fail expr format error")
singleRe := regexp.MustCompile(`([a-zA-Z_\p{Han}]+)\s*(\d+)?\s*(\+(([^/]+)/)?\s*(.+))?`)
check := func(skill string) (checkResult enCheckResult) {
checkResult.valid = true
@@ -1520,13 +1519,12 @@ func RegisterBuiltinExtCoc7(self *Dice) {
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
}
}
- if val > ctx.Dice.MaxCocCardGen {
- val = ctx.Dice.MaxCocCardGen
+ if val > ctx.Dice.Config.MaxCocCardGen {
+ val = ctx.Dice.Config.MaxCocCardGen
}
- var i int64
var ss []string
- for i = 0; i < val; i++ {
+ for range val {
result := ctx.EvalFString(`力量:{力量=3d6*5} 敏捷:{敏捷=3d6*5} 意志:{意志=3d6*5}\n体质:{体质=3d6*5} 外貌:{外貌=3d6*5} 教育:{教育=(2d6+6)*5}\n体型:{体型=(2d6+6)*5} 智力:{智力=(2d6+6)*5} 幸运:{幸运=3d6*5}\nHP:{(体质+体型)/10} [{力量+敏捷+意志+体质+外貌+教育+体型+智力}/{力量+敏捷+意志+体质+外貌+教育+体型+智力+幸运}]`, nil)
if result.vm.Error != nil {
break
diff --git a/dice/ext_coc7_template.go b/dice/ext_coc7_template.go
index 00ea42d2..1f290a96 100644
--- a/dice/ext_coc7_template.go
+++ b/dice/ext_coc7_template.go
@@ -2,7 +2,8 @@ package dice
import (
"encoding/json"
- "fmt"
+
+ log "sealdice-core/utils/kratos"
)
var coc7TemplateData = `
@@ -762,7 +763,7 @@ func getCoc7CharTemplate() *GameSystemTemplate {
temp := &GameSystemTemplate{}
err := json.Unmarshal([]byte(coc7TemplateData), temp)
if err != nil {
- fmt.Println("解析模板错误:", err.Error())
+ log.Errorf("解析模板错误: %v", err)
return nil
}
diff --git a/dice/ext_deck.go b/dice/ext_deck.go
index 413491ec..4f71bd55 100644
--- a/dice/ext_deck.go
+++ b/dice/ext_deck.go
@@ -626,7 +626,7 @@ func RegisterBuiltinExtDeck(d *Dice) {
}
if strings.EqualFold(deckName, "list") { //nolint:nestif
- text := "载入并开启的牌堆:\n"
+ text := ""
for _, i := range ctx.Dice.DeckList {
if i.Enable {
author := fmt.Sprintf(" 作者:%s", i.Author)
@@ -640,7 +640,8 @@ func RegisterBuiltinExtDeck(d *Dice) {
text += fmt.Sprintf("- %s 格式: %s%s%s 牌组数量: %d\n", i.Name, i.Format, author, version, count)
}
}
- ReplyToSender(ctx, msg, text)
+ VarSetValueStr(ctx, "$t牌堆列表", text)
+ ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "其它:抽牌_牌堆列表"))
} else if strings.EqualFold(deckName, "desc") {
// 查看详情
text := cmdArgs.GetArgN(2)
@@ -754,7 +755,7 @@ func RegisterBuiltinExtDeck(d *Dice) {
// if times > 5 {
// times = 5
// }
- if times > int(ctx.Dice.MaxExecuteTime) {
+ if times > int(ctx.Dice.Config.MaxExecuteTime) {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告"))
return CmdExecuteResult{Matched: true, Solved: true}
}
@@ -948,7 +949,7 @@ func deckStringFormat(ctx *MsgContext, deckInfo *DeckInfo, s string) (string, er
s = ImageRewrite(s, imgSolve)
s = strings.ReplaceAll(s, "\n", `\n`)
- if ctx.Dice.VMVersionForDeck == "v1" {
+ if ctx.Dice.getTargetVmEngineVersion(VMVersionDeck) == "v1" {
return DiceFormatV1(ctx, s)
} else {
return DiceFormatV2(ctx, s)
@@ -1161,7 +1162,7 @@ func extractExecuteContent(s string) (string, string) {
func (d *Dice) DeckCheckUpdate(deckInfo *DeckInfo) (string, string, string, error) {
if len(deckInfo.UpdateUrls) == 0 {
- return "", "", "", fmt.Errorf("牌堆未提供更新链接")
+ return "", "", "", errors.New("牌堆未提供更新链接")
}
statusCode, newData, err := GetCloudContent(deckInfo.UpdateUrls, deckInfo.Etag)
@@ -1169,10 +1170,10 @@ func (d *Dice) DeckCheckUpdate(deckInfo *DeckInfo) (string, string, string, erro
return "", "", "", err
}
if statusCode == http.StatusNotModified {
- return "", "", "", fmt.Errorf("牌堆没有更新")
+ return "", "", "", errors.New("牌堆没有更新")
}
if statusCode != http.StatusOK {
- return "", "", "", fmt.Errorf("未获取到牌堆更新")
+ return "", "", "", errors.New("未获取到牌堆更新")
}
oldData, err := os.ReadFile(deckInfo.Filename)
@@ -1212,13 +1213,13 @@ func (d *Dice) DeckUpdate(deckInfo *DeckInfo, tempFileName string) error {
return err
}
if len(newData) == 0 {
- return fmt.Errorf("new data is empty")
+ return errors.New("new data is empty")
}
// 更新牌堆
ok := parseDeck(d, tempFileName, newData, deckInfo)
if !ok {
d.Logger.Errorf("牌堆“%s”更新失败,无法解析获取到的牌堆数据", deckInfo.Name)
- return fmt.Errorf("无法解析获取到的牌堆数据")
+ return errors.New("无法解析获取到的牌堆数据")
}
err = os.WriteFile(deckInfo.Filename, newData, 0755)
diff --git a/dice/ext_deck_test.go b/dice/ext_deck_test.go
index fa73c352..ea7300d1 100644
--- a/dice/ext_deck_test.go
+++ b/dice/ext_deck_test.go
@@ -17,6 +17,7 @@ func TestDeck(t *testing.T) {
k := s.Pick().(string)
m[k] += 1
}
+ //nolint:forbidigo // just a test
fmt.Println(m) // map[1:1002 2:988 3:954 4:1013 5:965 6:1016 7:4062]
}
@@ -36,5 +37,6 @@ func TestDeckLast(t *testing.T) {
k = fmt.Sprintf("Last=%s", k)
m[k] += 1
}
+ //nolint:forbidigo // just a test
fmt.Println(m) // map[Last=1:1413 Last=2:1452 Last=3:1420 Last=4:1401 Last=5:1423 Last=6:1460 Last=7:1431]
}
diff --git a/dice/ext_dnd5e.go b/dice/ext_dnd5e.go
index 4dfcb28d..386c92c1 100644
--- a/dice/ext_dnd5e.go
+++ b/dice/ext_dnd5e.go
@@ -72,7 +72,7 @@ func setupConfigDND(_ *Dice) AttributeConfigs {
}
func getPlayerNameTempFunc(mctx *MsgContext) string {
- if mctx.Dice.PlayerNameWrapEnable {
+ if mctx.Dice.Config.PlayerNameWrapEnable {
return fmt.Sprintf("<%s>", mctx.Player.Name)
}
return mctx.Player.Name
@@ -398,26 +398,32 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
".rc <属性> // .rc 力量\n" +
".rc <属性>豁免 // .rc 力量豁免\n" +
".rc <表达式> // .rc 力量+3\n" +
+ ".rc 3# <表达式> // 多重检定\n" +
".rc 优势 <表达式> // .rc 优势 力量+4\n" +
".rc 劣势 <表达式> [<原因>] // .rc 劣势 力量+4 推一下试试\n" +
".rc <表达式> @某人 // 对某人做检定"
cmdRc := &CmdItemInfo{
- Name: "rc",
- ShortHelp: helpRc,
- Help: "DND5E 检定:\n" + helpRc,
- AllowDelegate: true,
+ // Pinenutn: 从这里添加是否检查有多次检定,很隐蔽,通过简单研究cmdArgs是看不出来的,尚未清楚此处逻辑来源
+ EnableExecuteTimesParse: true,
+ Name: "rc",
+ ShortHelp: helpRc,
+ Help: "DND5E 检定:\n" + helpRc,
+ AllowDelegate: true,
Solve: func(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs) CmdExecuteResult {
+ // 获取代骰
mctx := GetCtxProxyFirst(ctx, cmdArgs)
mctx.DelegateText = ctx.DelegateText
mctx.Player.TempValueAlias = &ac.Alias
-
+ // 参数确认
val := cmdArgs.GetArgN(1)
switch val {
case "", "help":
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
default:
+ // 获取参数
restText := cmdArgs.CleanArgs
+ // 检查是否符合要求
re := regexp.MustCompile(`^(优势|劣势|優勢|劣勢)`)
m := re.FindString(restText)
if m != "" {
@@ -425,43 +431,85 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
m = strings.Replace(m, "劣勢", "劣势", 1)
restText = strings.TrimSpace(restText[len(m):])
}
- expr := fmt.Sprintf("D20%s + %s", m, restText)
+ // 准备要处理的函数
+ expr := fmt.Sprintf("d20%s + %s", m, restText)
+ // 初始化VM
mctx.CreateVmIfNotExists()
+ // 获取角色模板
tmpl := mctx.Group.GetCharTemplate(mctx.Dice)
- mctx.Eval(tmpl.PreloadCode, nil)
- mctx.setDndReadForVM(true)
-
- r := mctx.Eval(expr, nil)
- if r.vm.Error != nil {
- ReplyToSender(mctx, msg, "无法解析表达式: "+restText)
+ // 初始化多轮检定结果保存数组
+ textList := make([]string, 0)
+ // 多轮检定判断
+ round := 1
+ if cmdArgs.SpecialExecuteTimes > 1 {
+ round = cmdArgs.SpecialExecuteTimes
+ }
+ // 从COC复制来的轮数检查,同时特判一次的情况,防止完全骰不出去点
+ if cmdArgs.SpecialExecuteTimes > int(ctx.Dice.Config.MaxExecuteTime) && cmdArgs.SpecialExecuteTimes != 1 {
+ VarSetValueStr(ctx, "$t次数", strconv.Itoa(cmdArgs.SpecialExecuteTimes))
+ ReplyToSender(mctx, msg, DiceFormatTmpl(mctx, "DND:检定_轮数过多警告"))
return CmdExecuteResult{Matched: true, Solved: true}
}
- reason := r.vm.RestInput
- if reason == "" {
- reason = restText
- }
- detail := r.vm.GetDetailText()
-
- VarSetValueStr(ctx, "$t技能", reason)
- VarSetValueStr(ctx, "$t检定过程文本", detail)
- VarSetValueStr(ctx, "$t检定结果", r.ToString())
-
- text := DiceFormatTmpl(ctx, "DND:检定")
- // 指令信息
- commandInfo := map[string]interface{}{
+ // commandInfo配置
+ var commandInfo = map[string]interface{}{
"cmd": "rc",
"rule": "dnd5e",
"pcName": mctx.Player.Name,
- "items": []interface{}{
- map[string]interface{}{
- "expr": expr,
- "reason": reason,
- "result": r.Value,
- },
- },
+ // items的赋值转移到下面
+ // "items": []interface{}{},
+ }
+ var commandItems = make([]interface{}, 0)
+ // 循环N轮
+ for range round {
+ // 执行预订的code
+ mctx.Eval(tmpl.PreloadCode, nil)
+ // 为rc设定属性豁免
+ mctx.setDndReadForVM(true)
+ // 执行了一次
+ r := mctx.Eval(expr, nil)
+ // 执行出错就丢出去
+ if r.vm.Error != nil {
+ ReplyToSender(mctx, msg, "无法解析表达式: "+restText)
+ return CmdExecuteResult{Matched: true, Solved: true}
+ }
+ // 拿到执行的结果
+ reason := r.vm.RestInput
+ if reason == "" {
+ reason = restText
+ }
+ detail := r.vm.GetDetailText()
+ // Pinenutn/bugtower100:猜测这里只是格式化的部分,所以如果做多次检定,这个变量保存最后一次就够了
+ VarSetValueStr(ctx, "$t技能", reason)
+ VarSetValueStr(ctx, "$t检定过程文本", detail)
+ VarSetValueStr(ctx, "$t检定结果", r.ToString())
+ // 添加对应结果文本,若只执行一次,则使用DND检定,否则使用单项文本初始化
+ if round == 1 {
+ textList = append(textList, DiceFormatTmpl(ctx, "DND:检定"))
+ } else {
+ textList = append(textList, DiceFormatTmpl(ctx, "DND:检定_单项结果文本"))
+ }
+ // 添加对应commandItems
+ commandItems = append(commandItems, map[string]interface{}{
+ "expr": expr,
+ "reason": reason,
+ "result": r.Value,
+ })
+ }
+ // 拼接文本
+ // 由于循环内保留了最后一次的部分技能文本,所以这里不需要再初始化一次技能
+ var text string
+ if round > 1 {
+ VarSetValueStr(ctx, "$t结果文本", strings.Join(textList, "\n"))
+ VarSetValueStr(ctx, "$t次数", strconv.Itoa(cmdArgs.SpecialExecuteTimes))
+ text = DiceFormatTmpl(ctx, "DND:检定_多轮")
+ } else {
+ // 是单轮检定,不需要组装成多轮的描述
+ text = textList[0]
}
+ // 赋值commandItems
+ commandInfo["items"] = commandItems
+ // 设置对应的Command
mctx.CommandInfo = commandInfo
-
if kw := cmdArgs.GetKwarg("ci"); kw != nil {
info, err := json.Marshal(mctx.CommandInfo)
if err == nil {
@@ -470,9 +518,30 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
text += "\n" + "指令信息无法序列化"
}
}
+
+ isHide := cmdArgs.Command == "rah" || cmdArgs.Command == "rch"
+
+ if isHide {
+ if msg.Platform == "QQ-CH" {
+ ReplyToSender(ctx, msg, "QQ频道内尚不支持暗骰")
+ return CmdExecuteResult{Matched: true, Solved: true}
+ }
+ if ctx.Group != nil {
+ if ctx.IsPrivate {
+ ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:提示_私聊不可用"))
+ } else {
+ ctx.CommandHideFlag = ctx.Group.GroupID
+ prefix := DiceFormatTmpl(ctx, "核心:暗骰_私聊_前缀")
+ ReplyGroup(ctx, msg, DiceFormatTmpl(ctx, "核心:暗骰_群内"))
+ ReplyPerson(ctx, msg, prefix+text)
+ }
+ } else {
+ ReplyToSender(ctx, msg, text)
+ }
+ return CmdExecuteResult{Matched: true, Solved: true}
+ }
ReplyToSender(mctx, msg, text)
}
-
return CmdExecuteResult{Matched: true, Solved: true}
},
}
@@ -1011,7 +1080,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
if m != "" {
restText = strings.TrimSpace(restText[len(m):])
}
- expr := fmt.Sprintf("D20%s%s", m, restText)
+ expr := fmt.Sprintf("d20%s%s", m, restText)
mctx.CreateVmIfNotExists()
mctx.setDndReadForVM(true)
r := mctx.Eval(expr, nil)
@@ -1172,7 +1241,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
if strings.HasPrefix(text, "+") {
// 加值情况1,D20+
- r := ctx.Eval("D20"+text, nil)
+ r := ctx.Eval("d20"+text, nil)
if r.vm.Error != nil {
// 情况1,加值输入错误
return 1, name, val, detail, ""
@@ -1183,7 +1252,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
exprExists = true
} else if strings.HasPrefix(text, "-") {
// 加值情况1.1,D20-
- r := ctx.Eval("D20"+text, nil)
+ r := ctx.Eval("d20"+text, nil)
if r.vm.Error != nil {
// 情况1,加值输入错误
return 1, name, val, detail, ""
@@ -1205,7 +1274,7 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
exprExists = true
} else if strings.HasPrefix(text, "优势") || strings.HasPrefix(text, "劣势") {
// 优势/劣势
- r := ctx.Eval("D20"+text, nil)
+ r := ctx.Eval("d20"+text, nil)
if r.vm.Error != nil {
// 优势劣势输入错误
return 2, name, val, detail, ""
@@ -1234,6 +1303,9 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
text = strings.TrimPrefix(text, ",")
// 情况1,名字是自己
name = mctx.Player.Name
+ // replace any space or \n with _
+ name = strings.ReplaceAll(name, " ", "_")
+ name = strings.ReplaceAll(name, "\n", "_")
// 情况2,名字是自己,没有加值
if !exprExists {
val = int64(ds.Roll(nil, 20, 0))
@@ -1349,61 +1421,67 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
setInitNextRoundVars(ctx, lst, round)
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "DND:先攻_下一回合"))
case "del", "rm":
- names := cmdArgs.Args[1:]
- riList := (RIList{}).LoadByCurGroup(ctx)
- newList := RIList{}
+ tryDeleteMembersInInitList := func(deleteNames []string, riList RIList) (newList RIList, textOut strings.Builder, ok bool) {
+ round, _ := VarGetValueInt64(ctx, "$g回合数")
+ round %= int64(len(riList))
+ toDeleted := map[string]bool{}
+ for _, i := range deleteNames {
+ toDeleted[i] = true
+ }
- round, _ := VarGetValueInt64(ctx, "$g回合数")
- round %= int64(len(riList))
+ delCounter := 0
- toDeleted := map[string]bool{}
- for _, i := range names {
- toDeleted[i] = true
- }
+ preCurrent := 0 // 每有一个在当前单位前面的单位被删除, 当前单位下标需要减 1
+ for index, i := range riList {
+ if !toDeleted[i.name] {
+ newList = append(newList, i)
+ } else {
+ delCounter++
+ textOut.WriteString(fmt.Sprintf("%2d. %s\n", delCounter, i.name))
- textOut := strings.Builder{}
- textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_移除_前缀"))
- delCounter := 0
+ if int64(index) < round {
+ preCurrent++
+ }
+ }
+ }
+ current := *riList[round]
+ currentDeleted := toDeleted[current.name]
- preCurrent := 0 // 每有一个在当前单位前面的单位被删除, 当前单位下标需要减 1
- for index, i := range riList {
- if !toDeleted[i.name] {
- newList = append(newList, i)
- } else {
- delCounter++
- textOut.WriteString(fmt.Sprintf("%2d. %s\n", delCounter, i.name))
+ round -= int64(preCurrent)
+ if round >= int64(len(newList)) {
+ round = 0
+ }
+ VarSetValueInt64(ctx, "$g回合数", round)
- if int64(index) < round {
- preCurrent++
- }
+ if delCounter == 0 {
+ textOut.WriteString("- 没有找到任何单位\n")
+ return newList, textOut, false
}
- }
- current := *riList[round]
- currentDeleted := toDeleted[current.name]
- round -= int64(preCurrent)
- if round >= int64(len(newList)) {
- round = 0
+ newList.SaveToGroup(ctx)
+ if currentDeleted {
+ if len(newList) == 0 {
+ textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_清除列表"))
+ } else {
+ setInitNextRoundVars(ctx, newList, round)
+ // Note(Xiangze Li): 这是为了让回合结束的角色显示为被删除的角色,而不是当前角色的上一个
+ VarSetValueStr(ctx, "$t当前回合角色名", current.name)
+ VarSetValueStr(ctx, "$t当前回合at", AtBuild(current.uid))
+ textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_下一回合"))
+ }
+ }
+ return newList, textOut, true
}
- VarSetValueInt64(ctx, "$g回合数", round)
- if delCounter == 0 {
- textOut.WriteString("- 没有找到任何单位\n")
+ nameWithSpace, _ := cmdArgs.EatPrefixWith("del", "rm")
+ riList := (RIList{}).LoadByCurGroup(ctx)
+ _, textOut, ok := tryDeleteMembersInInitList([]string{nameWithSpace}, riList)
+ if !ok {
+ _, textOut, _ = tryDeleteMembersInInitList(cmdArgs.Args[1:], riList)
}
+ textToSend := DiceFormatTmpl(ctx, "DND:先攻_移除_前缀") + textOut.String()
- newList.SaveToGroup(ctx)
- if currentDeleted {
- if len(newList) == 0 {
- textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_清除列表"))
- } else {
- setInitNextRoundVars(ctx, newList, round)
- // Note(Xiangze Li): 这是为了让回合结束的角色显示为被删除的角色,而不是当前角色的上一个
- VarSetValueStr(ctx, "$t当前回合角色名", current.name)
- VarSetValueStr(ctx, "$t当前回合at", AtBuild(current.uid))
- textOut.WriteString(DiceFormatTmpl(ctx, "DND:先攻_下一回合"))
- }
- }
- ReplyToSender(ctx, msg, textOut.String())
+ ReplyToSender(ctx, msg, textToSend)
case "set":
name := cmdArgs.GetArgN(2)
exists := name != ""
@@ -1473,6 +1551,8 @@ func RegisterBuiltinExtDnd5e(self *Dice) {
"dst": cmdSt,
"rc": cmdRc,
"ra": cmdRc,
+ "rah": cmdRc,
+ "rch": cmdRc,
"drc": cmdRc,
"buff": cmdBuff,
"dbuff": cmdBuff,
diff --git a/dice/ext_exp.go b/dice/ext_exp.go
index e407d3e1..1a295b8e 100644
--- a/dice/ext_exp.go
+++ b/dice/ext_exp.go
@@ -590,7 +590,7 @@ func getCmdStBase(soi CmdStOverrideInfo) *CmdItemInfo {
// 进行简化卡的尝试解析
input := cmdArgs.CleanArgs
- re := regexp.MustCompile(`^(([^\s\-#]{1,25})([-#]))([^=\s\d]+\d+)`)
+ re := regexp.MustCompile(`^(([^\s\-#]{1,25})([-#]))([^=\s\d(\[{\-+]+\d+)`)
matches := re.FindStringSubmatch(input)
if len(matches) > 0 {
flag := matches[3]
@@ -644,7 +644,7 @@ func getCmdStBase(soi CmdStOverrideInfo) *CmdItemInfo {
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
}
- rRestIput := ctx.vm.RestInput
+ rRestIput := mctx.vm.RestInput
// 处理直接设置属性
var text string
diff --git a/dice/ext_fun.go b/dice/ext_fun.go
index bfdfa8aa..1a1eacaa 100644
--- a/dice/ext_fun.go
+++ b/dice/ext_fun.go
@@ -436,7 +436,7 @@ func RegisterBuiltinExtFun(self *Dice) {
} else if val == "help" || val == "" {
return CmdExecuteResult{Matched: true, Solved: true, ShowHelp: true}
} else {
- if self.MailEnable {
+ if self.Config.MailEnable {
_ = ctx.Dice.SendMail(cmdArgs.CleanArgs, MailTypeSendNote)
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:留言_已记录"))
return CmdExecuteResult{Matched: true, Solved: true}
@@ -569,7 +569,7 @@ func RegisterBuiltinExtFun(self *Dice) {
successDegrees := int64(0)
failedCount := int64(0)
var results []string
- for i := int64(0); i < num; i++ {
+ for range num {
v := DiceRoll64(6)
if v >= 5 {
successDegrees++
@@ -738,7 +738,7 @@ func RegisterBuiltinExtFun(self *Dice) {
successDegrees := int64(0)
var results []string
- for i := int64(0); i < diceNum; i++ {
+ for range diceNum {
v := DiceRoll64(10)
if v <= checkVal {
successDegrees++
@@ -1006,15 +1006,26 @@ func RegisterBuiltinExtFun(self *Dice) {
return CmdExecuteResult{Matched: true, Solved: true}
}
- addNum := int64(10)
+ wodAdd := int64(10)
if adding, exists := groupAttrs.LoadX("wodAdd"); exists {
addNumX, _ := adding.ReadInt()
- addNum = int64(addNumX)
+ wodAdd = int64(addNumX)
+ }
+ wodFace := int64(10)
+ if face, exists := groupAttrs.LoadX("wodPoints"); exists {
+ faceNumX, _ := face.ReadInt()
+ wodFace = int64(faceNumX)
+ }
+ wodSucc := int64(8)
+ if succ, exists := groupAttrs.LoadX("wodThreshold"); exists {
+ succNumX, _ := succ.ReadInt()
+ wodSucc = int64(succNumX)
}
- txt := readNumber(cmdArgs.CleanArgs, fmt.Sprintf("a%d", addNum))
+ exprWoDicenum := fmt.Sprintf("a%dk%dm%d", wodAdd, wodSucc, wodFace)
+ txt := readNumber(cmdArgs.CleanArgs, exprWoDicenum)
if txt == "" {
- txt = fmt.Sprintf("10a%d", addNum)
+ txt = "10" + exprWoDicenum
cmdArgs.Args = []string{txt}
}
cmdArgs.CleanArgs = txt
@@ -1031,7 +1042,7 @@ func RegisterBuiltinExtFun(self *Dice) {
ShortHelp: textHelp,
Help: "文本模板指令:\n" + textHelp,
Solve: func(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs) CmdExecuteResult {
- if ctx.Dice.TextCmdTrustOnly {
+ if ctx.Dice.Config.TextCmdTrustOnly {
// 检查master和信任权限
// 拒绝无权限访问
if ctx.PrivilegeLevel < 70 {
@@ -1118,7 +1129,7 @@ func RegisterBuiltinExtFun(self *Dice) {
if m == 0 {
m = int(getDefaultDicePoints(ctx))
}
- if t > int(ctx.Dice.MaxExecuteTime) {
+ if t > int(ctx.Dice.Config.MaxExecuteTime) {
ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告"))
return CmdExecuteResult{Matched: true, Solved: true}
}
@@ -1208,7 +1219,7 @@ func RegisterBuiltinExtFun(self *Dice) {
}
// NOTE(Xiangze Li): 允许创建更多轮数。使用洗牌算法后并不会很重复计算
- // if roulette.Time > int(ctx.Dice.MaxExecuteTime) {
+ // if roulette.Time > int(ctx.Dice.Config.MaxExecuteTime) {
// ReplyToSender(ctx, msg, DiceFormatTmpl(ctx, "核心:骰点_轮数过多警告"))
// return CmdExecuteResult{Matched: true, Solved: true}
// }
@@ -1226,7 +1237,7 @@ func RegisterBuiltinExtFun(self *Dice) {
for i := range allNum {
allNum[i] = i + 1
}
- for idx := 0; idx < roulette.Time; idx++ {
+ for idx := range roulette.Time {
i := int(roulette.Face) - 1 - idx
j := rand.Intn(i + 1)
allNum[i], allNum[j] = allNum[j], allNum[i]
diff --git a/dice/ext_log.go b/dice/ext_log.go
index e39c7b80..31068c11 100644
--- a/dice/ext_log.go
+++ b/dice/ext_log.go
@@ -26,7 +26,11 @@ var ErrGroupCardOverlong = errors.New("群名片长度超过限制")
func SetPlayerGroupCardByTemplate(ctx *MsgContext, tmpl string) (string, error) {
ctx.Player.TempValueAlias = nil // 防止dnd的hp被转为“生命值”
- v := ctx.EvalFString(tmpl, nil)
+ config := ctx.GenDefaultRollVmConfig()
+ config.HookFuncValueStore = func(ctx *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) {
+ return nil, true
+ }
+ v := ctx.EvalFString(tmpl, config)
if v.vm.Error != nil {
ctx.Dice.Logger.Infof("SN指令模板错误: %v", v.vm.Error.Error())
return "", v.vm.Error
@@ -294,7 +298,8 @@ func RegisterBuiltinExtLog(self *Dice) {
group.UpdatedAtTime = time.Now().Unix()
time.Sleep(time.Duration(0.3 * float64(time.Second)))
- getAndUpload(group.GroupID, group.LogCurName)
+ // Note: 2024-10-15 经过简单测试,似乎能缓解#1034的问题,但无法根本解决。
+ go getAndUpload(group.GroupID, group.LogCurName)
group.LogCurName = ""
group.UpdatedAtTime = time.Now().Unix()
return CmdExecuteResult{Matched: true, Solved: true}
@@ -880,11 +885,11 @@ func LogAppend(ctx *MsgContext, groupID string, logName string, logItem *model.L
if ok {
if size, okCount := model.LogLinesCountGet(ctx.Dice.DBLogs, groupID, logName); okCount {
// 默认每记录500条发出提示
- if ctx.Dice.LogSizeNoticeEnable {
- if ctx.Dice.LogSizeNoticeCount == 0 {
- ctx.Dice.LogSizeNoticeCount = 500
+ if ctx.Dice.Config.LogSizeNoticeEnable {
+ if ctx.Dice.Config.LogSizeNoticeCount == 0 {
+ ctx.Dice.Config.LogSizeNoticeCount = DefaultConfig.LogSizeNoticeCount
}
- if size > 0 && int(size)%ctx.Dice.LogSizeNoticeCount == 0 {
+ if size > 0 && int(size)%ctx.Dice.Config.LogSizeNoticeCount == 0 {
VarSetValueInt64(ctx, "$t条数", size)
text := DiceFormatTmpl(ctx, "日志:记录_条数提醒")
// text := fmt.Sprintf("提示: 当前故事的文本已经记录了 %d 条", size)
diff --git a/dice/ext_reply.go b/dice/ext_reply.go
index 816057fb..0fb2ed0e 100644
--- a/dice/ext_reply.go
+++ b/dice/ext_reply.go
@@ -143,7 +143,7 @@ func RegisterBuiltinExtReply(dice *Dice) {
OnNotCommandReceived: func(ctx *MsgContext, msg *Message) {
// 当前,只有非指令才会匹配
rcs := ctx.Dice.CustomReplyConfig
- if !ctx.Dice.CustomReplyConfigEnable {
+ if !ctx.Dice.Config.CustomReplyConfigEnable {
return
}
executed := false
@@ -153,7 +153,7 @@ func RegisterBuiltinExtReply(dice *Dice) {
cleanText = strings.TrimSpace(cleanText)
VarSetValueInt64(ctx, "$t文本长度", int64(len(cleanText)))
- if dice.ReplyDebugMode {
+ if dice.Config.ReplyDebugMode {
log.Infof("[回复调试]当前文本:“%s” hex: %x 字节形式: %v", cleanText, cleanText, []byte(cleanText))
}
diff --git a/dice/ext_reply_logic.go b/dice/ext_reply_logic.go
index 3ba4903a..e1e039b3 100644
--- a/dice/ext_reply_logic.go
+++ b/dice/ext_reply_logic.go
@@ -10,6 +10,8 @@ import (
"strings"
"time"
+ log "sealdice-core/utils/kratos"
+
"github.com/antlabs/strsim"
"gopkg.in/yaml.v3"
)
@@ -148,7 +150,7 @@ func (m *ReplyConditionExprTrue) Check(ctx *MsgContext, _ *Message, _ *CmdArgs,
// r := ctx.Eval(m.Value, ds.RollConfig{})
flags := RollExtraFlags{
V2Only: true,
- V1Only: ctx.Dice.VMVersionForReply == "v1",
+ V1Only: ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1",
}
r, _, err := DiceExprEvalBase(ctx, m.Value, flags)
@@ -182,7 +184,7 @@ func formatExprForReply(ctx *MsgContext, expr string) string {
var text string
var err error
- if ctx.Dice.VMVersionForReply == "v1" {
+ if ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1" {
text, err = DiceFormatV1(ctx, expr)
if err != nil {
// text = fmt.Sprintf("执行出错V1: %s", err.Error())
@@ -260,7 +262,7 @@ func (m *ReplyResultRunText) Execute(ctx *MsgContext, _ *Message, _ *CmdArgs) {
time.Sleep(time.Duration(m.Delay * float64(time.Second)))
flags := RollExtraFlags{
V2Only: true,
- V1Only: ctx.Dice.VMVersionForReply == "v1",
+ V1Only: ctx.Dice.getTargetVmEngineVersion(VMVersionReply) == "v1",
}
_, _, _ = DiceExprTextBase(ctx, m.Message, flags)
}
@@ -303,7 +305,7 @@ func (c *ReplyConfig) Save(dice *Dice) {
attrConfigFn := dice.GetExtConfigFilePath("reply", c.Filename)
buf, err := yaml.Marshal(c)
if err != nil {
- fmt.Println(err)
+ log.Error("ReplyConfig.Save", err)
} else {
_ = os.WriteFile(attrConfigFn, buf, 0644)
}
diff --git a/dice/ext_story.go b/dice/ext_story.go
index 3cd4cafc..2f2b0974 100644
--- a/dice/ext_story.go
+++ b/dice/ext_story.go
@@ -79,7 +79,7 @@ func cmdRandomName(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs, cmdsList [][
}
// 开始抽取
- for i := int64(0); i < num; i++ {
+ for range num {
rule := rules[rand.Int()%len(rules)]
names = append(names, ctx.Dice.Parent.NamesGenerator.NameGenerate(rule))
}
diff --git a/dice/im_helpers.go b/dice/im_helpers.go
index 0c5bf9c2..8fa6e85f 100644
--- a/dice/im_helpers.go
+++ b/dice/im_helpers.go
@@ -64,9 +64,9 @@ func SetBotOnAtGroup(ctx *MsgContext, groupID string) *GroupInfo {
group.Active = true
} else {
// 设定扩展情况
- sort.Sort(ExtDefaultSettingItemSlice(session.Parent.ExtDefaultSettings))
+ sort.Sort(ExtDefaultSettingItemSlice(session.Parent.Config.ExtDefaultSettings))
var extLst []*ExtInfo
- for _, i := range session.Parent.ExtDefaultSettings {
+ for _, i := range session.Parent.Config.ExtDefaultSettings {
if i.ExtItem != nil {
if i.AutoActive {
extLst = append(extLst, i.ExtItem)
@@ -81,7 +81,7 @@ func SetBotOnAtGroup(ctx *MsgContext, groupID string) *GroupInfo {
GroupID: groupID,
DiceIDActiveMap: new(SyncMap[string, bool]),
DiceIDExistsMap: new(SyncMap[string, bool]),
- CocRuleIndex: int(session.Parent.DefaultCocRuleIndex),
+ CocRuleIndex: int(session.Parent.Config.DefaultCocRuleIndex),
UpdatedAtTime: time.Now().Unix(),
})
// TODO: Pinenutn:总觉得这里不太对,但是又觉得合理,GPT也没说怎么改更好一些,求教
@@ -181,7 +181,7 @@ func ReplyGroupRaw(ctx *MsgContext, msg *Message, text string, flag string) {
ctx.DelegateText = ""
}
- if ctx.Dice.RateLimitEnabled && msg.Platform == "QQ" {
+ if ctx.Dice.Config.RateLimitEnabled && msg.Platform == "QQ" {
if !spamCheckPerson(ctx, msg) {
spamCheckGroup(ctx, msg)
}
@@ -191,7 +191,7 @@ func ReplyGroupRaw(ctx *MsgContext, msg *Message, text string, flag string) {
if d != nil {
d.Logger.Infof("发给(群%s): %s", msg.GroupID, text)
// 敏感词拦截:回复(群)
- if d.EnableCensor && d.CensorMode == OnlyOutputReply {
+ if d.Config.EnableCensor && d.Config.CensorMode == OnlyOutputReply {
// 先拿掉海豹码和CQ码再检查敏感词
checkText := sealCodeRe.ReplaceAllString(text, "")
checkText = cqCodeRe.ReplaceAllString(checkText, "")
@@ -256,7 +256,7 @@ func ReplyPersonRaw(ctx *MsgContext, msg *Message, text string, flag string) {
ctx.DelegateText = ""
}
- if ctx.Dice.RateLimitEnabled && msg.Platform == "QQ" {
+ if ctx.Dice.Config.RateLimitEnabled && msg.Platform == "QQ" {
spamCheckPerson(ctx, msg)
}
@@ -264,7 +264,7 @@ func ReplyPersonRaw(ctx *MsgContext, msg *Message, text string, flag string) {
if d != nil {
d.Logger.Infof("发给(帐号%s): %s", msg.Sender.UserID, text)
// 敏感词拦截:回复(个人)
- if d.EnableCensor && d.CensorMode == OnlyOutputReply {
+ if d.Config.EnableCensor && d.Config.CensorMode == OnlyOutputReply {
// 先拿掉海豹码和CQ码再检查敏感词
checkText := sealCodeRe.ReplaceAllString(text, "")
checkText = cqCodeRe.ReplaceAllString(checkText, "")
@@ -449,16 +449,16 @@ func spamCheckPerson(ctx *MsgContext, msg *Message) bool {
if ctx.Player.RateLimiter == nil {
ctx.Player.RateLimitWarned = false
- if ctx.Dice.PersonalReplenishRateStr == "" {
- ctx.Dice.PersonalReplenishRateStr = "@every 3s"
- ctx.Dice.PersonalReplenishRate = rate.Every(time.Second * 3)
+ if ctx.Dice.Config.PersonalReplenishRateStr == "" {
+ ctx.Dice.Config.PersonalReplenishRateStr = DefaultConfig.PersonalReplenishRateStr
+ ctx.Dice.Config.PersonalReplenishRate = DefaultConfig.PersonalReplenishRate
}
- if ctx.Dice.PersonalBurst == 0 {
- ctx.Dice.PersonalBurst = 3
+ if ctx.Dice.Config.PersonalBurst == 0 {
+ ctx.Dice.Config.PersonalBurst = DefaultConfig.PersonalBurst
}
ctx.Player.RateLimiter = rate.NewLimiter(
- ctx.Dice.PersonalReplenishRate,
- int(ctx.Dice.PersonalBurst),
+ ctx.Dice.Config.PersonalReplenishRate,
+ int(ctx.Dice.Config.PersonalBurst),
)
}
@@ -468,7 +468,7 @@ func spamCheckPerson(ctx *MsgContext, msg *Message) bool {
}
if ctx.Player.RateLimitWarned {
- ctx.Dice.BanList.AddScoreByCommandSpam(ctx.Player.UserID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByCommandSpam(ctx.Player.UserID, msg.GroupID, ctx)
} else {
ctx.Player.RateLimitWarned = true
replyToSenderRawNoCheck(
@@ -500,16 +500,16 @@ func spamCheckGroup(ctx *MsgContext, msg *Message) bool {
if ctx.Group.RateLimiter == nil {
ctx.Group.RateLimitWarned = false
- if ctx.Dice.GroupReplenishRateStr == "" {
- ctx.Dice.GroupReplenishRateStr = "@every 3s"
- ctx.Dice.GroupReplenishRate = rate.Every(time.Second * 3)
+ if ctx.Dice.Config.GroupReplenishRateStr == "" {
+ ctx.Dice.Config.GroupReplenishRateStr = DefaultConfig.GroupReplenishRateStr
+ ctx.Dice.Config.GroupReplenishRate = DefaultConfig.GroupReplenishRate
}
- if ctx.Dice.GroupBurst == 0 {
- ctx.Dice.GroupBurst = 3
+ if ctx.Dice.Config.GroupBurst == 0 {
+ ctx.Dice.Config.GroupBurst = DefaultConfig.GroupBurst
}
ctx.Group.RateLimiter = rate.NewLimiter(
- ctx.Dice.GroupReplenishRate,
- int(ctx.Dice.GroupBurst),
+ ctx.Dice.Config.GroupReplenishRate,
+ int(ctx.Dice.Config.GroupBurst),
)
}
@@ -520,7 +520,7 @@ func spamCheckGroup(ctx *MsgContext, msg *Message) bool {
// If not allow
if ctx.Group.RateLimitWarned {
- ctx.Dice.BanList.AddScoreByCommandSpam(ctx.Group.GroupID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByCommandSpam(ctx.Group.GroupID, msg.GroupID, ctx)
} else {
ctx.Group.RateLimitWarned = true
replyToSenderRawNoCheck(
diff --git a/dice/im_session.go b/dice/im_session.go
index 39dfafe2..a24eefd4 100644
--- a/dice/im_session.go
+++ b/dice/im_session.go
@@ -4,22 +4,25 @@ import (
"encoding/base64"
"encoding/binary"
"fmt"
+ "math/rand"
"regexp"
"runtime/debug"
"sort"
"strings"
- "sync"
"time"
+ "github.com/samber/lo"
+ "gorm.io/gorm"
+
"sealdice-core/dice/model"
"sealdice-core/message"
+ log "sealdice-core/utils/kratos"
"github.com/golang-module/carbon"
ds "github.com/sealdice/dicescript"
rand2 "golang.org/x/exp/rand"
"github.com/dop251/goja"
- "github.com/jmoiron/sqlx"
"golang.org/x/time/rate"
"gopkg.in/yaml.v3"
)
@@ -78,6 +81,7 @@ type GroupPlayerInfo model.GroupPlayerInfoBase
type GroupInfo struct {
Active bool `json:"active" yaml:"active" jsbind:"active"` // 是否在群内开启 - 过渡为象征意义
ActivatedExtList []*ExtInfo `yaml:"activatedExtList,flow" json:"activatedExtList"` // 当前群开启的扩展列表
+ ExtListSnapshot []string `yaml:"-" json:"-"` // 存放当前激活的扩展表,无论其是否存在,用于处理插件重载后优先级混乱的问题
Players *SyncMap[string, *GroupPlayerInfo] `yaml:"-" json:"-"` // 群员角色数据
GroupID string `yaml:"groupId" json:"groupId" jsbind:"groupId"`
@@ -126,6 +130,36 @@ func (group *GroupInfo) ExtActive(ei *ExtInfo) {
group.ExtClear()
}
+// ExtActiveBySnapshotOrder 按照快照顺序开启扩展
+func (group *GroupInfo) ExtActiveBySnapshotOrder(ei *ExtInfo, isFirstTimeLoad bool) {
+ // 这个机制用于解决js插件指令会覆盖原生扩展的指令的问题
+ // 与之相关的问题是插件的自动激活,最好能够检测插件是否为首次加载
+ orderLst := group.ExtListSnapshot
+ m := map[string]*ExtInfo{}
+ for _, i := range group.ActivatedExtList {
+ m[i.Name] = i
+ }
+ m[ei.Name] = ei
+
+ var newLst []*ExtInfo
+ for _, i := range orderLst {
+ if m[i] != nil {
+ newLst = append(newLst, m[i])
+ }
+ }
+
+ // 当首次加载,如果快照列表中没有,将其新增
+ if isFirstTimeLoad {
+ if !lo.Contains(orderLst, ei.Name) {
+ newLst = append(newLst, ei)
+ group.ExtListSnapshot = append(group.ExtListSnapshot, ei.Name)
+ }
+ }
+
+ group.ActivatedExtList = newLst
+ group.ExtClear()
+}
+
// ExtClear 清除多余的扩展项
func (group *GroupInfo) ExtClear() {
m := map[string]bool{}
@@ -191,7 +225,7 @@ func (group *GroupInfo) IsActive(ctx *MsgContext) bool {
return false
}
-func (group *GroupInfo) PlayerGet(db *sqlx.DB, id string) *GroupPlayerInfo {
+func (group *GroupInfo) PlayerGet(db *gorm.DB, id string) *GroupPlayerInfo {
if group.Players == nil {
group.Players = new(SyncMap[string, *GroupPlayerInfo])
}
@@ -257,7 +291,7 @@ type EndPointInfoBase struct {
Enable bool `yaml:"enable" json:"enable" jsbind:"enable"` // 是否启用
ProtocolType string `yaml:"protocolType" json:"protocolType"` // 协议类型,如onebot、koishi等
- IsPublic bool `yaml:"isPublic"`
+ IsPublic bool `yaml:"isPublic" json:"isPublic"`
Session *IMSession `yaml:"-" json:"-"`
}
@@ -521,7 +555,7 @@ func (ctx *MsgContext) fillPrivilege(msg *Message) int {
}
// 加入黑名单相关权限
- if val, exists := ctx.Dice.BanList.GetByID(ctx.Player.UserID); exists {
+ if val, exists := ctx.Dice.Config.BanList.GetByID(ctx.Player.UserID); exists {
switch val.Rank {
case BanRankBanned:
ctx.PrivilegeLevel = -30
@@ -562,7 +596,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) {
// 注意: 此处必须开启,不然下面mctx.player取不到
autoOn := true
if msg.Platform == "QQ-CH" {
- autoOn = d.QQChannelAutoOn
+ autoOn = d.Config.QQChannelAutoOn
}
groupInfo = SetBotOnAtGroup(mctx, msg.GroupID)
groupInfo.Active = autoOn
@@ -710,13 +744,13 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) {
log.Infof("收到群(%s)内<%s>(%s)的指令: %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
} else {
doLog := true
- if d.OnlyLogCommandInGroup {
+ if d.Config.OnlyLogCommandInGroup {
// 检查上级选项
doLog = false
}
if doLog {
// 检查QQ频道的独立选项
- if msg.Platform == "QQ-CH" && (!d.QQChannelLogMessage) {
+ if msg.Platform == "QQ-CH" && (!d.Config.QQChannelLogMessage) {
doLog = false
}
}
@@ -728,7 +762,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) {
}
// 敏感词拦截:全部输入
- if mctx.IsCurGroupBotOn && d.EnableCensor && d.CensorMode == AllInput {
+ if mctx.IsCurGroupBotOn && d.Config.EnableCensor && d.Config.CensorMode == AllInput {
hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "")
if needToTerminate {
return
@@ -760,7 +794,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) {
if msg.MessageType == "private" {
if mctx.CommandID != 0 {
log.Infof("收到<%s>(%s)的私聊指令: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
- } else if !d.OnlyLogCommandInPrivate {
+ } else if !d.Config.OnlyLogCommandInPrivate {
log.Infof("收到<%s>(%s)的私聊消息: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
}
}
@@ -778,7 +812,7 @@ func (s *IMSession) Execute(ep *EndPointInfo, msg *Message, runInSync bool) {
}()
// 敏感词拦截:命令输入
- if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.EnableCensor && d.CensorMode == OnlyInputCommand {
+ if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.Config.EnableCensor && d.Config.CensorMode == OnlyInputCommand {
hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "")
if needToTerminate {
return
@@ -925,7 +959,7 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) {
// 注意: 此处必须开启,不然下面mctx.player取不到
autoOn := true
if msg.Platform == "QQ-CH" {
- autoOn = d.QQChannelAutoOn
+ autoOn = d.Config.QQChannelAutoOn
}
groupInfo = SetBotOnAtGroup(mctx, msg.GroupID)
groupInfo.Active = autoOn
@@ -1047,13 +1081,13 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) {
log.Infof("收到群(%s)内<%s>(%s)的指令: %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
} else {
doLog := true
- if d.OnlyLogCommandInGroup {
+ if d.Config.OnlyLogCommandInGroup {
// 检查上级选项
doLog = false
}
if doLog {
// 检查QQ频道的独立选项
- if msg.Platform == "QQ-CH" && (!d.QQChannelLogMessage) {
+ if msg.Platform == "QQ-CH" && (!d.Config.QQChannelLogMessage) {
doLog = false
}
}
@@ -1069,13 +1103,13 @@ func (s *IMSession) ExecuteNew(ep *EndPointInfo, msg *Message) {
// TODO(Szzrain): 需要优化的写法,不应根据 CommandID 来判断是否是指令,而应该根据 cmdArgs 是否 match 到指令来判断,同上
if mctx.CommandID != 0 {
log.Infof("收到<%s>(%s)的私聊指令: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
- } else if !d.OnlyLogCommandInPrivate {
+ } else if !d.Config.OnlyLogCommandInPrivate {
log.Infof("收到<%s>(%s)的私聊消息: %s", msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
}
}
// 敏感词拦截:全部输入
- if mctx.IsCurGroupBotOn && d.EnableCensor && d.CensorMode == AllInput {
+ if mctx.IsCurGroupBotOn && d.Config.EnableCensor && d.Config.CensorMode == AllInput {
hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "")
if needToTerminate {
return
@@ -1167,7 +1201,7 @@ func (s *IMSession) PreTriggerCommand(mctx *MsgContext, msg *Message, cmdArgs *C
}()
// 敏感词拦截:命令输入
- if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.EnableCensor && d.CensorMode == OnlyInputCommand {
+ if (msg.MessageType == "private" || mctx.IsCurGroupBotOn) && d.Config.EnableCensor && d.Config.CensorMode == OnlyInputCommand {
hit, words, needToTerminate, _ := d.CensorMsg(mctx, msg, msg.Message, "")
if needToTerminate {
return
@@ -1350,10 +1384,7 @@ func (s *IMSession) OnGroupMemberJoined(ctx *MsgContext, msg *Message) {
stdID := msg.Sender.UserID
VarSetValueStr(ctx, "$t帐号ID", stdID)
VarSetValueStr(ctx, "$t账号ID", stdID)
- text, err := DiceFormatV2(ctx, groupInfo.GroupWelcomeMessage)
- if err != nil {
- text = fmt.Sprintf("执行出错V2: %s", err.Error())
- }
+ text := DiceFormat(ctx, groupInfo.GroupWelcomeMessage)
for _, i := range ctx.SplitText(text) {
doSleepQQ(ctx)
ReplyGroup(ctx, msg, strings.TrimSpace(i))
@@ -1363,110 +1394,104 @@ func (s *IMSession) OnGroupMemberJoined(ctx *MsgContext, msg *Message) {
}
}
-// 借助类似操作系统信号量的思路来做一个互斥锁
-var muxAutoQuit sync.Mutex
-var groupLeaveNum int
var platformRE = regexp.MustCompile(`^(.*)-Group:`)
-// LongTimeQuitInactiveGroup 另一种退群方案,其中minute代表间隔多久执行一次,num代表一次退几个群(每次退群之间有10秒的等待时间)
-func (s *IMSession) LongTimeQuitInactiveGroup(threshold, hint time.Time, roundIntervalMinute int, groupsPerRound int) {
- // 该方案目前是issue方案的简化版,是我制作的鲨群机的略高级解决方式。
- // 该方案下,将会创建一个线程,从该时间开始计算将要退出的群聊并以minute为间隔的时间退出num个群。
- if !muxAutoQuit.TryLock() {
- // 如果没能获得“临界资源”信号量
- // 直接输出有任务在运行中
- hint := fmt.Sprintf("有任务在运行中,已经退群 %d 个", groupLeaveNum)
- s.Parent.Logger.Info(hint)
- return
- }
-
- s.Parent.Logger.Infof("开始清理不活跃群聊. 判定线 %s", threshold.Format(time.RFC3339))
- go func() {
- type GroupEndpointPair struct {
- Group *GroupInfo
- Endpoint *EndPointInfo
+// LongTimeQuitInactiveGroupReborn
+// 完全抛弃当初不懂Go的时候的方案,改成如下方案:
+// 每次尝试找到n个符合要求的群,然后启一个线程,将群统一干掉
+// 这样子牺牲了可显示的总群数,但大大增强了稳定性,而且总群数的参考并无意义,因为已经在的群很可能突然活了而不符合判定
+// 当前版本的问题:如果用户设置了很短的时间,那可能之前的群还没退完,就又退那部分的群,造成一些奇怪的问题,但应该概率不大 + 豹错会被捕获
+func (s *IMSession) LongTimeQuitInactiveGroupReborn(threshold time.Time, groupsPerRound int) {
+ s.Parent.Logger.Infof("开始清理不活跃群聊. 判定线 %s, 本次退群数: %d", threshold.Format(time.RFC3339), groupsPerRound)
+ type GroupEndpointPair struct {
+ Group *GroupInfo
+ Endpoint *EndPointInfo
+ Last time.Time
+ }
+ var selectedGroupEndpoints = make([]*GroupEndpointPair, 0)
+ var groupCount int
+ s.ServiceAtNew.Range(func(key string, grp *GroupInfo) bool {
+ // 如果是PG开头的,忽略掉
+ if strings.HasPrefix(grp.GroupID, "PG-") {
+ return true
}
-
- defer muxAutoQuit.Unlock()
-
- groupLeaveNum = 0
- selectedGroupEndpoints := []*GroupEndpointPair{} // 创建一个存放 grp 和 ep 组合的切片
-
- // Pinenutn: Range模板 ServiceAtNew重构代码
- s.ServiceAtNew.Range(func(key string, grp *GroupInfo) bool {
- // Pinenutn: ServiceAtNew重构
- if strings.HasPrefix(grp.GroupID, "PG-") {
- return true
+ // 如果在BanList(这应该是白名单?)内,忽略掉
+ if s.Parent.Config.BanList != nil {
+ info, ok := s.Parent.Config.BanList.GetByID(grp.GroupID)
+ if ok && info.Rank > BanRankNormal {
+ return true // 信任等级高于普通的不清理
}
- if s.Parent.BanList != nil {
- info, ok := s.Parent.BanList.GetByID(grp.GroupID)
- if ok && info.Rank > BanRankNormal {
- return true // 信任等级高于普通的不清理
+ }
+ // 看看是不是QQ群,如果是QQ群,才进一步判断
+ match := platformRE.FindStringSubmatch(grp.GroupID)
+ if len(match) != 2 {
+ return true
+ }
+ platform := match[1]
+ if platform != "QQ" {
+ return true
+ }
+ // 获取上次骰子活动时间
+ last := time.Unix(grp.RecentDiceSendTime, 0)
+ // 如果enter是进入时间,它比活动时间更晚(说明骰子刚进去,但是骰子还没有说话),那么上次骰子活动时间=进入时间
+ if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) {
+ last = enter
+ }
+ // 如果在上述所有操作后,发现时间仍然是0,那么必须忽略该值,因为可能是还没初始化的群,不能人家刚进来就走
+ // 注意不能用last.Equal(time.Time{}),因为这里是时间戳的1970-01-01,而Go初始时间是0000-01-01.
+ // 预防性代码:如果last是0000-01-01,那也不应该被退群。
+ if last.Unix() <= 0 {
+ return true
+ }
+ // 如果时间比要退群的时间早
+ if last.Before(threshold) {
+ for _, ep := range s.EndPoints {
+ // 找到对应的endpoints,并准备退掉它的群
+ if ep.Platform != platform || !grp.DiceIDExistsMap.Exists(ep.UserID) {
+ continue
}
- }
-
- last := time.Unix(grp.RecentDiceSendTime, 0)
- if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) {
- last = enter
- }
- match := platformRE.FindStringSubmatch(grp.GroupID)
- if len(match) != 2 {
- return true
- }
- platform := match[1]
- if platform != "QQ" {
- return true
- }
- if last.Before(threshold) {
- for _, ep := range s.EndPoints {
- if ep.Platform != platform || !grp.DiceIDExistsMap.Exists(ep.UserID) {
- continue
- }
- selectedGroupEndpoints = append(selectedGroupEndpoints, &GroupEndpointPair{Group: grp, Endpoint: ep})
+ selectedGroupEndpoints = append(selectedGroupEndpoints, &GroupEndpointPair{Group: grp, Endpoint: ep, Last: last})
+ // 如果群数量超过本次要退的群数量,就不再继续了,退出出去
+ groupCount++
+ // 如果已经超过了一次退群的数量,则退出循环
+ if groupCount > groupsPerRound {
+ return false
}
- } else if last.Before(hint) {
- s.Parent.Logger.Warnf("检测到群 %s 上次活动时间为 %s,将在未来自动退出", grp.GroupID, last.Format(time.RFC3339))
}
- return true
- })
- // 采用类似分页的手法进行退群
- groupCount := len(selectedGroupEndpoints)
- rounds := (groupCount + groupsPerRound - 1) / groupsPerRound
- for round := 0; round < rounds; round++ {
- startIndex := round * groupsPerRound
- endIndex := (round + 1) * groupsPerRound
- if endIndex > groupCount {
- endIndex = groupCount
- }
- for _, pair := range selectedGroupEndpoints[startIndex:endIndex] {
- grp := pair.Group
- ep := pair.Endpoint
- last := time.Unix(grp.RecentDiceSendTime, 0)
- if enter := time.Unix(grp.EnteredTime, 0); enter.After(last) {
- last = enter
- }
- hint := fmt.Sprintf("检测到群 %s 上次活动时间为 %s,尝试退出", grp.GroupID, last.Format(time.RFC3339))
- s.Parent.Logger.Info(hint)
- msgCtx := CreateTempCtx(ep, &Message{
- MessageType: "group",
- Sender: SenderBase{UserID: ep.UserID},
- GroupID: grp.GroupID,
- })
- msgText := DiceFormatTmpl(msgCtx, "核心:骰子自动退群告别语")
- ep.Adapter.SendToGroup(msgCtx, grp.GroupID, msgText, "")
- // 和我自制的鲨群机时间同步
- time.Sleep(10 * time.Second)
- grp.DiceIDExistsMap.Delete(ep.UserID)
- grp.UpdatedAtTime = time.Now().Unix()
- ep.Adapter.QuitGroup(&MsgContext{Dice: s.Parent}, grp.GroupID)
- // 保证在多次点击时可以收到日志
- groupLeaveNum++
- (&MsgContext{Dice: s.Parent, EndPoint: ep, Session: s}).Notice(hint)
- }
- // 等三十分钟
- hint := fmt.Sprintf("第 %d 轮退群已经完成,共计 %d 轮,休息 %d 分钟中", round, rounds, roundIntervalMinute)
+ }
+ return true
+ })
+ // 循环完毕,要不然是因为够了要退的数量,要不就是遍历完毕了,但是不够,总之要进行退群活动了
+ go func() {
+ if r := recover(); r != nil {
+ log.Errorf("自动退群异常: %v 堆栈: %v", r, string(debug.Stack()))
+ }
+ for i, pair := range selectedGroupEndpoints {
+ grp := pair.Group
+ ep := pair.Endpoint
+ last := pair.Last
+ hint := fmt.Sprintf("检测到群 %s 上次活动时间为 %s,尝试退出,当前为本轮第 %d 个", grp.GroupID, last.Format(time.RFC3339), i+1)
s.Parent.Logger.Info(hint)
- time.Sleep(time.Duration(roundIntervalMinute) * time.Minute)
+ // 创建对应退群信息
+ msgCtx := CreateTempCtx(ep, &Message{
+ MessageType: "group",
+ Sender: SenderBase{UserID: ep.UserID},
+ GroupID: grp.GroupID,
+ })
+ // 发送退群消息
+ msgText := DiceFormatTmpl(msgCtx, "核心:骰子自动退群告别语")
+ ep.Adapter.SendToGroup(msgCtx, grp.GroupID, msgText, "")
+ // 删除群聊绑定信息,更新群处理时间
+ grp.DiceIDExistsMap.Delete(ep.UserID)
+ grp.UpdatedAtTime = time.Now().Unix()
+ // 执行真正的退群活动,理论上这个msgCtx就能直接用
+ ep.Adapter.QuitGroup(msgCtx, grp.GroupID)
+ // 发出提示
+ msgCtx.Notice(hint)
+ // 生成一个随机值(8~11秒随机)
+ randomSleep := time.Duration(rand.Intn(3000)+8000) * time.Millisecond
+ log.Infof("退群等待,等待 %f 秒后继续", randomSleep.Seconds())
+ time.Sleep(randomSleep)
}
}()
}
@@ -1475,6 +1500,11 @@ func (s *IMSession) LongTimeQuitInactiveGroup(threshold, hint time.Time, roundIn
func FormatBlacklistReasons(v *BanListInfoItem) string {
var sb strings.Builder
sb.WriteString("黑名单原因:")
+ if v == nil {
+ sb.WriteString("\n")
+ sb.WriteString("原因未知,请联系开发者获取进一步信息")
+ return sb.String()
+ }
for i, reason := range v.Reasons {
sb.WriteString("\n")
sb.WriteString(carbon.CreateFromTimestamp(v.Times[i]).ToDateTimeString())
@@ -1494,7 +1524,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
var isBanGroup, isWhiteGroup bool
// log.Info("check ban ", msg.MessageType, " ", msg.GroupID, " ", ctx.PrivilegeLevel)
if msg.MessageType == "group" {
- value, exists := d.BanList.GetByID(msg.GroupID)
+ value, exists := d.Config.BanList.GetByID(msg.GroupID)
if exists {
if value.Rank == BanRankBanned {
isBanGroup = true
@@ -1506,7 +1536,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
}
banQuitGroup := func() {
- banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID)
+ banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.Sender.UserID)
reasontext := FormatBlacklistReasons(banListInfoItem)
groupID := msg.GroupID
noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),自动退群\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext)
@@ -1523,9 +1553,9 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
if ctx.PrivilegeLevel == -30 {
groupLevel := ctx.GroupRoleLevel
- if d.BanList.BanBehaviorQuitIfAdmin && msg.MessageType == "group" {
+ if (d.Config.BanList.BanBehaviorQuitIfAdmin || d.Config.BanList.BanBehaviorQuitIfAdminSilentIfNotAdmin) && msg.MessageType == "group" {
// 黑名单用户 - 立即退出所在群
- banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID)
+ banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.Sender.UserID)
reasontext := FormatBlacklistReasons(banListInfoItem)
groupID := msg.GroupID
notReply = true
@@ -1546,16 +1576,21 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
log.Infof("收到群(%s)内普通群员黑名单用户<%s>(%s)的消息,但在信任群所以不做其他操作", groupID, msg.Sender.Nickname, msg.Sender.UserID)
} else {
notReply = true
- noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,进行群内通告\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext)
- log.Info(noticeMsg)
+ if d.Config.BanList.BanBehaviorQuitIfAdmin {
+ noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,进行群内通告\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext)
+ log.Info(noticeMsg)
- text := fmt.Sprintf("警告: <%s>(%s)是黑名单用户,将对骰主进行通知。", msg.Sender.Nickname, msg.Sender.UserID)
- ReplyGroupRaw(ctx, &Message{GroupID: groupID}, text, "")
+ text := fmt.Sprintf("警告: <%s>(%s)是黑名单用户,将对骰主进行通知。", msg.Sender.Nickname, msg.Sender.UserID)
+ ReplyGroupRaw(ctx, &Message{GroupID: groupID}, text, "")
- ctx.Notice(noticeMsg)
+ ctx.Notice(noticeMsg)
+ } else {
+ noticeMsg := fmt.Sprintf("检测到群(%s)内黑名单用户<%s>(%s),因是普通群员,忽略黑名单用户信息,不做其他操作\n%s", groupID, msg.Sender.Nickname, msg.Sender.UserID, reasontext)
+ log.Info(noticeMsg)
+ }
}
}
- } else if d.BanList.BanBehaviorQuitPlaceImmediately && msg.MessageType == "group" {
+ } else if d.Config.BanList.BanBehaviorQuitPlaceImmediately && msg.MessageType == "group" {
notReply = true
// 黑名单用户 - 立即退出所在群
groupID := msg.GroupID
@@ -1564,16 +1599,17 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
} else {
banQuitGroup()
}
- } else if d.BanList.BanBehaviorRefuseReply {
+ } else if d.Config.BanList.BanBehaviorRefuseReply {
notReply = true
// 黑名单用户 - 拒绝回复
log.Infof("忽略黑名单用户信息: 来自群(%s)内<%s>(%s): %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
}
} else if isBanGroup {
- if d.BanList.BanBehaviorQuitPlaceImmediately && !isWhiteGroup {
+ if d.Config.BanList.BanBehaviorQuitPlaceImmediately && !isWhiteGroup {
notReply = true
// 黑名单群 - 立即退出
- banListInfoItem, _ := ctx.Dice.BanList.Map.Load(msg.Sender.UserID)
+ // 退群使用GroupID进行判断
+ banListInfoItem, _ := ctx.Dice.Config.BanList.GetByID(msg.GroupID)
reasontext := FormatBlacklistReasons(banListInfoItem)
groupID := msg.GroupID
if isWhiteGroup {
@@ -1589,7 +1625,7 @@ func checkBan(ctx *MsgContext, msg *Message) (notReply bool) {
time.Sleep(1 * time.Second)
ctx.EndPoint.Adapter.QuitGroup(ctx, groupID)
}
- } else if d.BanList.BanBehaviorRefuseReply {
+ } else if d.Config.BanList.BanBehaviorRefuseReply {
notReply = true
// 黑名单群 - 拒绝回复
log.Infof("忽略黑名单群消息: 来自群(%s)内<%s>(%s): %s", msg.GroupID, msg.Sender.Nickname, msg.Sender.UserID, msg.Message)
@@ -1668,7 +1704,7 @@ func (s *IMSession) commandSolve(ctx *MsgContext, msg *Message, cmdArgs *CmdArgs
}
if cur != -1 {
- if ctx.Dice.PlayerNameWrapEnable {
+ if ctx.Dice.Config.PlayerNameWrapEnable {
ctx.DelegateText = fmt.Sprintf("由<%s>代骰:\n", ctx.Player.Name)
} else {
ctx.DelegateText = fmt.Sprintf("由%s代骰:\n", ctx.Player.Name)
@@ -1958,13 +1994,13 @@ func (d *Dice) NoticeForEveryEndpoint(txt string, allowCrossPlatform bool) {
}
}()
- if d.MailEnable {
+ if d.Config.MailEnable {
_ = d.SendMail(txt, MailTypeNotice)
return
}
for _, ep := range d.ImSession.EndPoints {
- for _, i := range d.NoticeIDs {
+ for _, i := range d.Config.NoticeIDs {
n := strings.Split(i, ":")
// 如果文本中没有-,则会取到整个字符串
// 但好像不严谨,比如QQ-CH-Group
@@ -2001,14 +2037,14 @@ func (ctx *MsgContext) NoticeCrossPlatform(txt string) {
}
}()
- if ctx.Dice.MailEnable {
+ if ctx.Dice.Config.MailEnable {
_ = ctx.Dice.SendMail(txt, MailTypeNotice)
return
}
sent := false
- for _, i := range ctx.Dice.NoticeIDs {
+ for _, i := range ctx.Dice.Config.NoticeIDs {
n := strings.Split(i, ":")
if len(n) < 2 {
continue
@@ -2059,14 +2095,14 @@ func (ctx *MsgContext) Notice(txt string) {
}
}()
- if ctx.Dice.MailEnable {
+ if ctx.Dice.Config.MailEnable {
_ = ctx.Dice.SendMail(txt, MailTypeNotice)
return
}
sent := false
if ctx.EndPoint.Enable {
- for _, i := range ctx.Dice.NoticeIDs {
+ for _, i := range ctx.Dice.Config.NoticeIDs {
n := strings.Split(i, ":")
if len(n) >= 2 {
if strings.HasSuffix(n[0], "-Group") {
diff --git a/dice/im_vars.go b/dice/im_vars.go
index 23cc6087..6403b9a9 100644
--- a/dice/im_vars.go
+++ b/dice/im_vars.go
@@ -222,7 +222,7 @@ func SetTempVars(ctx *MsgContext, qqNickname string) {
pcName = strings.ReplaceAll(pcName, `\f`, "")
VarSetValueStr(ctx, "$t玩家", fmt.Sprintf("<%s>", pcName))
- if ctx.Dice != nil && !ctx.Dice.PlayerNameWrapEnable {
+ if ctx.Dice != nil && !ctx.Dice.Config.PlayerNameWrapEnable {
VarSetValueStr(ctx, "$t玩家", pcName)
}
VarSetValueStr(ctx, "$t玩家_RAW", pcName)
diff --git a/dice/js_api.go b/dice/js_api.go
index a9cf539a..d7f00a3a 100644
--- a/dice/js_api.go
+++ b/dice/js_api.go
@@ -3,15 +3,15 @@ package dice
import (
"crypto/md5"
"encoding/base64"
+ "encoding/hex"
"errors"
- "fmt"
"os"
"path/filepath"
- "go.uber.org/zap"
+ log "sealdice-core/utils/kratos"
)
-func Base64ToImageFunc(logger *zap.SugaredLogger) func(string) (string, error) {
+func Base64ToImageFunc(logger *log.Helper) func(string) (string, error) {
return func(b64 string) (string, error) {
// 解码 Base64 值
data, e := base64.StdEncoding.DecodeString(b64)
@@ -21,7 +21,7 @@ func Base64ToImageFunc(logger *zap.SugaredLogger) func(string) (string, error) {
}
// 计算 MD5 哈希值作为文件名
hash := md5.Sum(data) //nolint:gosec
- filename := fmt.Sprintf("%x", hash)
+ filename := hex.EncodeToString(hash[:])
tempDir := os.TempDir()
// 构建文件路径
imageurlPath := filepath.Join(tempDir, filename)
diff --git a/dice/logger/logger.go b/dice/logger/logger.go
index 7a0359cf..a6b0ae93 100644
--- a/dice/logger/logger.go
+++ b/dice/logger/logger.go
@@ -1,113 +1,20 @@
package logger
import (
- "encoding/json"
- "os"
-
- "github.com/natefinch/lumberjack"
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
+ log "sealdice-core/utils/kratos"
)
-var enabledLevel = zap.InfoLevel
-
-func SetEnableLevel(level zapcore.Level) {
- switch level {
- case zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel,
- zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
- {
- enabledLevel = level
- }
- default: // no-op
- }
-}
-
-type LogItem struct {
- Level string `json:"level"`
- TS float64 `json:"ts"`
- Caller string `json:"caller"`
- Msg string `json:"msg"`
-}
-
-type WriterX struct {
- LogLimit int64
- Items []*LogItem
-}
-
type LogInfo struct {
- LoggerRaw *zap.Logger
- Logger *zap.SugaredLogger
- WX *WriterX
+ Logger *log.Helper
+ WX *log.WriterX
}
-var logLimitDefault int64 = 100
-
-func (w *WriterX) Write(p []byte) (n int, err error) {
- var a LogItem
- err2 := json.Unmarshal(p, &a)
- if err2 == nil {
- w.Items = append(w.Items, &a)
- limit := w.LogLimit
- if limit == 0 {
- w.LogLimit = logLimitDefault
- }
- if len(w.Items) > int(limit) {
- w.Items = w.Items[1:]
- }
- }
- return len(p), nil
-}
-
-func Init(path string, name string, enableConsoleLog bool) *LogInfo {
- lumlog := &lumberjack.Logger{
- Filename: path,
- MaxSize: 10, // megabytes
- MaxBackups: 3, // number of log files
- MaxAge: 7, // days
- }
-
- encoder := getEncoder()
-
- pe := zap.NewProductionEncoderConfig()
- wx := &WriterX{}
-
- cores := []zapcore.Core{
- zapcore.NewCore(encoder, zapcore.AddSync(lumlog), enabledLevel),
-
- // This outputs to WebUI, DO NOT apply enabledLevel
- zapcore.NewCore(zapcore.NewJSONEncoder(pe), zapcore.AddSync(wx), zapcore.InfoLevel),
- }
-
- if enableConsoleLog {
- pe2 := zap.NewProductionEncoderConfig()
- pe2.EncodeTime = zapcore.ISO8601TimeEncoder
-
- consoleEncoder := zapcore.NewConsoleEncoder(pe2)
- consoleEncoder.AddString("dice", name)
- cores = append(cores, zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), enabledLevel))
- }
-
- core := zapcore.NewTee(cores...)
-
- loggerRaw := zap.New(core, zap.AddCaller())
- defer func(loggerRaw *zap.Logger) {
- _ = loggerRaw.Sync()
- }(loggerRaw) // flushes buffer, if any
-
- logger := loggerRaw.Sugar()
- logger.Infow("Dice日志开始记录")
-
+func Init() *LogInfo {
+ // KV输出
+ loghelper := log.NewCustomHelper(log.LOG_DICE, false, nil)
+ loghelper.Info("Dice日志开始记录")
return &LogInfo{
- LoggerRaw: loggerRaw,
- Logger: logger,
- WX: wx,
+ Logger: loghelper,
+ WX: log.GetWriterX(),
}
}
-
-func getEncoder() zapcore.Encoder {
- encoderConfig := zap.NewProductionEncoderConfig()
- encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
- encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
-
- return zapcore.NewConsoleEncoder(encoderConfig)
-}
diff --git a/dice/model/attr.go b/dice/model/attr.go
deleted file mode 100644
index ddcc9d77..00000000
--- a/dice/model/attr.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package model
-
-import (
- "fmt"
-
- "github.com/jmoiron/sqlx"
-)
-
-func attrGetAllBase(db *sqlx.DB, bucket string, key string) []byte {
- var buf []byte
-
- query := `SELECT updated_at, data FROM ` + bucket + ` WHERE id=:id`
- rows, err := db.NamedQuery(query, map[string]interface{}{"id": key})
- if err != nil {
- fmt.Println("Failed to execute query:", err)
- return buf
- }
-
- defer rows.Close()
-
- for rows.Next() {
- var updatedAt int64
- var data []byte
-
- err := rows.Scan(&updatedAt, &data)
- if err != nil {
- fmt.Println("Failed to scan row:", err)
- break
- }
-
- buf = data
- }
-
- return buf
-}
-
-func AttrUserGetAll(db *sqlx.DB, userID string) []byte {
- return attrGetAllBase(db, "attrs_user", userID)
-}
diff --git a/dice/model/attrs_new.go b/dice/model/attrs_new.go
index 6f3d0242..eae1ce87 100644
--- a/dice/model/attrs_new.go
+++ b/dice/model/attrs_new.go
@@ -1,13 +1,13 @@
package model
import (
- "database/sql"
"errors"
+ "fmt"
"time"
- "sealdice-core/utils"
+ "gorm.io/gorm"
- "github.com/jmoiron/sqlx"
+ "sealdice-core/utils"
ds "github.com/sealdice/dicescript"
)
@@ -22,188 +22,279 @@ const (
// 注: 角色表有用sheet也有用sheets的,这里数据结构中使用sheet
// AttributesItemModel 新版人物卡。说明一下,这里带s的原因是attrs指的是一个map
+// 补全GORM缺少部分
type AttributesItemModel struct {
- Id string `json:"id" db:"id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid
- Data []byte `json:"data" db:"data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些?
- AttrsType string `json:"attrsType" db:"attrs_type"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user)
+ Id string `json:"id" gorm:"column:id"` // 如果是群内,那么是类似 QQ-Group:12345-QQ:678910,群外是nanoid
+ Data []byte `json:"data" gorm:"column:data"` // 序列化后的卡数据,理论上[]byte不会进入字符串缓存,要更好些?
+ AttrsType string `json:"attrsType" gorm:"column:attrs_type;index:idx_attrs_attrs_type_id;default:NULL"` // 分为: 角色卡(character)、组内用户(group_user)、群组(group)、用户(user)
// 这些是群组内置卡专用的,其实就是替代了绑卡关系表,作为群组内置卡时,这个字段用于存放绑卡关系
- BindingSheetId string `json:"bindingSheetId" db:"binding_sheet_id"` // 绑定的卡片ID
+ BindingSheetId string `json:"bindingSheetId" gorm:"column:binding_sheet_id;default:'';index:idx_attrs_binding_sheet_id"` // 绑定的卡片ID
// 这些是角色卡专用的
- Name string `json:"name" db:"name"` // 卡片名称
- OwnerId string `json:"ownerId" db:"owner_id"` // 若有明确归属,就是对应的UniformID
- SheetType string `json:"sheetType" db:"sheet_type"` // 卡片类型,如dnd5e coc7
- IsHidden bool `json:"isHidden" db:"is_hidden"` // 隐藏的卡片不出现在 pc list 中
+ Name string `json:"name" gorm:"column:name"` // 卡片名称
+ OwnerId string `json:"ownerId" gorm:"column:owner_id;index:idx_attrs_owner_id_id"` // 若有明确归属,就是对应的UniformID
+ SheetType string `json:"sheetType" gorm:"column:sheet_type"` // 卡片类型,如dnd5e coc7
+ // 手动定义bool类的豹存方式
+ IsHidden bool `json:"isHidden" gorm:"column:is_hidden;type:bool"` // 隐藏的卡片不出现在 pc list 中
// 通用属性
- CreatedAt int64 `json:"createdAt" db:"created_at"`
- UpdatedAt int64 `json:"updatedAt" db:"updated_at"`
+ CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
+ UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
// 下面的属性并非数据库字段,而是用于内存中的缓存
- BindingGroupsNum int64 `json:"bindingGroupNum"` // 当前绑定中群数
+ BindingGroupsNum int64 `json:"bindingGroupNum" gorm:"-"` // 当前绑定中群数
+}
+
+// 兼容旧版本数据库
+func (*AttributesItemModel) TableName() string {
+ return "attrs"
}
func (m *AttributesItemModel) IsDataExists() bool {
- return m.Data != nil && len(m.Data) > 0
+ return len(m.Data) > 0
}
// TOOD: 下面这个表记得添加 unique 索引
// PlatformMappingModel 虚拟ID - 平台用户ID 映射表
type PlatformMappingModel struct {
- Id string `json:"id" db:"id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal
- IMUserID string `json:"IMUserID" db:"im_user_id"` // IM平台的用户ID
+ Id string `json:"id" gorm:"column:id"` // 虚拟ID,格式为 U:nanoid 意为 User / Uniform / Universal
+ IMUserID string `json:"IMUserID" gorm:"column:im_user_id"` // IM平台的用户ID
}
-func AttrsGetById(db *sqlx.DB, id string) (*AttributesItemModel, error) {
+func AttrsGetById(db *gorm.DB, id string) (*AttributesItemModel, error) {
+ // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错
var item AttributesItemModel
- err := db.Get(&item, `select id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id,
- sheet_type, is_hidden, created_at, updated_at from attrs where id = $1`, id)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ err := db.Model(&AttributesItemModel{}).
+ Select("id, data, COALESCE(attrs_type, '') as attrs_type, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at").
+ Where("id = ?", id).
+ Limit(1).
+ // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象本身就是预期正常的行为
+ Find(&item).Error
+ if err != nil {
return nil, err
}
return &item, nil
}
// AttrsGetBindingSheetIdByGroupId 获取当前正在绑定的ID
-func AttrsGetBindingSheetIdByGroupId(db *sqlx.DB, id string) (string, error) {
+func AttrsGetBindingSheetIdByGroupId(db *gorm.DB, id string) (string, error) {
+ // 这里必须使用AttributesItemModel结构体,如果你定义一个只有ID属性的结构体去接收,居然能接收到值,这样就会豹错
var item AttributesItemModel
- err := db.Get(&item, "select binding_sheet_id from attrs where id = $1", id)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ err := db.Model(&AttributesItemModel{}).
+ Select("binding_sheet_id").
+ Where("id = ?", id).
+ Limit(1).
+ // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回id=""就是预期正常的行为
+ Find(&item).Error
+ if err != nil {
return "", err
}
return item.BindingSheetId, nil
}
-func AttrsGetIdByUidAndName(db *sqlx.DB, userId string, name string) (string, error) {
+func AttrsGetIdByUidAndName(db *gorm.DB, userId string, name string) (string, error) {
+ // 这里必须使用AttributesItemModel结构体
+ // 如果你定义一个只有ID属性的结构体去接收,居然有概率能接收到值,这样就会和之前的行为不一致了
var item AttributesItemModel
- err := db.Get(&item, "select id from attrs where owner_id = $1 and name = $2", userId, name)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ err := db.Model(&AttributesItemModel{}).
+ Select("id").
+ Where("owner_id = ? AND name = ?", userId, name).
+ Limit(1).
+ // 使用Find,如果找不到不会豹错,而是提示RowsAffected = 0,此处返回空对象的id=""就是预期正常的行为
+ Find(&item).Error
+ if err != nil {
return "", err
}
return item.Id, nil
}
-func AttrsPutById(db *sqlx.DB, tx *sql.Tx, id string, data []byte, name, sheetType string) error {
- // TODO: 好像还不够,需要nickname 需要sheetType,还有别的吗
- var err error
- now := time.Now().Unix()
- query := `insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at, name, sheet_type)
- values ($1, $2, true, '', $3, $3, $4, $5)
- on conflict (id) do update set data = $2, updated_at = $3, name = $4, sheet_type = $5`
- args := []any{id, data, now, name, sheetType}
-
- if tx != nil {
- _, err = tx.Exec(query, args...)
- } else {
- _, err = db.Exec(query, args...)
+func AttrsPutById(db *gorm.DB, id string, data []byte, name, sheetType string) error {
+ now := time.Now().Unix() // 获取当前时间
+ // 这里的原本逻辑是:第一次全量创建,第二次修改部分属性
+ // 所以使用了Attrs和Assign配合使用
+ if err := db.Where("id = ?", id).
+ Attrs(map[string]any{
+ // 第一次全量建表
+ "id": id,
+ // 使用BYTE规避无法插入的问题
+ "data": BYTE(data),
+ "is_hidden": true,
+ "binding_sheet_id": "",
+ "name": name,
+ "sheet_type": sheetType,
+ "created_at": now,
+ "updated_at": now,
+ }).
+ // 如果是更新的情况,更新下面这部分,则需要被更新的为:
+ Assign(map[string]any{
+ "data": BYTE(data),
+ "updated_at": now,
+ "name": name,
+ "sheet_type": sheetType,
+ }).FirstOrCreate(&AttributesItemModel{}).Error; err != nil {
+ return err // 返回错误
}
- return err
+ return nil // 操作成功,返回 nil
}
-func AttrsDeleteById(db *sqlx.DB, id string) error {
- var err error
- query := `delete from attrs where id = ?`
- args := []any{id}
-
- _, err = db.Exec(query, args...)
- return err
+func AttrsDeleteById(db *gorm.DB, id string) error {
+ // 使用 GORM 的 Delete 方法删除指定 id 的记录
+ if err := db.Where("id = ?", id).Delete(&AttributesItemModel{}).Error; err != nil {
+ return err // 返回错误
+ }
+ return nil // 操作成功,返回 nil
}
-func AttrsCharGetBindingList(db *sqlx.DB, id string) ([]string, error) {
- rows, err := db.Query(`select id from attrs where binding_sheet_id = $1`, id)
- if err != nil {
- return nil, err
- }
+func AttrsCharGetBindingList(db *gorm.DB, id string) ([]string, error) {
+ // 定义一个切片用于存储结果
+ var lst []string
- lst := []string{}
- for rows.Next() {
- item := ""
- err = rows.Scan(&item)
- if err != nil {
- return nil, err
- }
- lst = append(lst, item)
+ // 使用 GORM 查询绑定的 id 列表
+ if err := db.Model(&AttributesItemModel{}).
+ Select("id").
+ Where("binding_sheet_id = ?", id).
+ Find(&lst).Error; err != nil {
+ return nil, err // 返回错误
}
- return lst, err
+ return lst, nil // 返回结果切片
}
-func AttrsCharUnbindAll(db *sqlx.DB, id string) (int64, error) {
- rows, err := db.Exec(`update attrs set binding_sheet_id = '' where binding_sheet_id = $1`, id)
- if err != nil {
- return 0, err
- }
- affected, err := rows.RowsAffected()
- if err != nil {
- return 0, err
+func AttrsCharUnbindAll(db *gorm.DB, id string) (int64, error) {
+ // 使用 GORM 更新绑定的记录,将 binding_sheet_id 设为空字符串
+ result := db.Model(&AttributesItemModel{}).
+ Where("binding_sheet_id = ?", id).
+ Update("binding_sheet_id", "")
+
+ if result.Error != nil {
+ return 0, result.Error // 返回错误
}
- return affected, err
+ return result.RowsAffected, nil // 返回受影响的行数
}
// AttrsNewItem 新建一个角色卡/属性容器
-func AttrsNewItem(db *sqlx.DB, item *AttributesItemModel) (*AttributesItemModel, error) {
- id := utils.NewID()
- now := time.Now().Unix()
- item.CreatedAt, item.UpdatedAt = now, now
+func AttrsNewItem(db *gorm.DB, item *AttributesItemModel) (*AttributesItemModel, error) {
+ id := utils.NewID() // 生成新的 ID
+ now := time.Now().Unix() // 获取当前时间
+ item.CreatedAt, item.UpdatedAt = now, now // 设置创建和更新时间
+
if item.Id == "" {
- item.Id = id
+ item.Id = id // 如果 ID 为空,则赋值新生成的 ID
}
- var err error
- _, err = db.Exec(`
- insert into attrs (id, data, binding_sheet_id, name, owner_id, sheet_type, is_hidden, created_at, updated_at, attrs_type)
- values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
- item.Id, item.Data, item.BindingSheetId, item.Name, item.OwnerId, item.SheetType, item.IsHidden,
- item.CreatedAt, item.UpdatedAt, item.AttrsType)
- return item, err
+ // 使用 GORM 的 Create 方法插入新记录
+ // 这个木落没有忽略错误,所以说这个可以安心使用Create而不用担心出现问题……
+ // 这里使用Create可以正确插入byte数组,注意map[string]any里面不可以用byte数组,否则无法入库
+ if err := db.Create(item).Error; err != nil {
+ return nil, err // 返回错误
+ }
+ return item, nil // 返回新创建的项
}
-func AttrsBindCharacter(db *sqlx.DB, charId string, id string) error {
+func AttrsBindCharacter(db *gorm.DB, charId string, id string) error {
+ // 开始事务
+ tx := db.Begin()
+ if tx.Error != nil {
+ return tx.Error // 返回错误
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback() // 发生恐慌时回滚
+ }
+ }()
+
+ // 将新字典值转换为 JSON
+ now := time.Now().Unix()
json, err := ds.NewDictVal(nil).V().ToJSON()
if err != nil {
+ tx.Rollback() // 返回错误时回滚
return err
}
- _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at)
- values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json)
- ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id)
- if err == nil {
- var affected int64
- affected, err = ret.RowsAffected()
- if err != nil {
- return err
- }
- if affected == 0 {
- return errors.New("群信息不存在: " + id)
- }
+ // 原本代码为:
+ // _, _ = db.Exec(`insert into attrs (id, data, is_hidden, binding_sheet_id, created_at, updated_at)
+ // values ($1, $3, true, '', $2, $2)`, id, time.Now().Unix(), json)
+ //
+ // ret, err := db.Exec(`update attrs set binding_sheet_id = $1 where id = $2`, charId, id)
+
+ result := tx.Where("id = ?", id).
+ // 按照木落的原版代码,应该是这么个逻辑:查不到的时候能正确执行,查到了就不执行了,所以用Attrs而不是Assign
+ Attrs(map[string]any{
+ "id": id,
+ // 如果想在[]bytes里输入值,注意传参的时候不能给any传[]bytes,否则会无法读取,同时还没有豹错,浪费大量时间。
+ // 这里为了兼容,不使用gob的序列化方法处理结构体(同时,也不知道序列化方法是否可用)
+ "data": BYTE(json),
+ "is_hidden": true,
+ // 如果插入成功,原版代码接下来更新这个值,那么现在就是等价的
+ "binding_sheet_id": charId,
+ "created_at": now,
+ "updated_at": now,
+ }).
+ // 按照原版代码,无论是不是能插入成功,都要更新这个值,所以这么写就是等价的了
+ Assign(map[string]any{
+ "binding_sheet_id": charId,
+ }).
+ FirstOrCreate(&AttributesItemModel{})
+ if result.Error != nil {
+ tx.Rollback() // 返回错误时回滚
+ return result.Error
+ }
+ // 四种情况:没有数据->初始化成功->返回1条
+ // 没有数据->更新失败->返回0条
+ // 有数据->更新成功->返回1条
+ // 有数据->更新失败->返回0条,但理论上所有返回0条的情况应该都会被丢出去
+ // 对于FirstOrCreate来说应该不会遇到下面的情况,但是保底一下
+ if result.RowsAffected == 0 {
+ tx.Rollback()
+ return errors.New("群信息不存在或发生更新异常: " + id)
}
- return err
+
+ // 提交事务
+ return tx.Commit().Error
}
-func AttrsGetCharacterListByUserId(db *sqlx.DB, userId string) (lst []*AttributesItemModel, err error) {
- rows, err := db.Queryx(`
- select id, name, sheet_type,
- (select count(id) from attrs where binding_sheet_id = t1.id)
- from attrs as t1 where owner_id = $1 and is_hidden is false
- `, userId)
+func AttrsGetCharacterListByUserId(db *gorm.DB, userId string) ([]*AttributesItemModel, error) {
+ // Pinenutn: 在Gorm中,如果gorm:"-",优先级似乎很高,经过我自己测试:
+ // 结构体内若使用gorm="-" ,Scan将无法映射到结果中(GPT胡说八道说可以映射上,我试了半天,被骗。)
+ // 如果不带任何标签: GORM对结构体名称进行转换,如BindingGroupNum对应映射:binding_group_num,结果里有binding_group_num自动映射
+ // 如果带上标签"column:xxxxx",则会使用指定的名称映射,如column:xxxxx对应映射xxxxx
+ // GPT 说带上JSON标签,可以映射到结果中,但实际上是错误的,无法映射。
+ // 所以最终”BindingGroupNum“需要创建这个结构体用来临时存放结果,然后将结果映射到AttributesItemModel结构体上。
+ // 在gorm="-"这里的配置还有更多可以使用无写入权限,有读权限的标签,但要求必须BindingGroupNum的结构体名称和数据库查询结果一致
+ // 且不能指定columns,否则会建表,没找到更好方案。
+ type AttrResult struct {
+ ID string `gorm:"column:id"`
+ Name string `gorm:"column:name"`
+ SheetType string `gorm:"column:sheet_type"`
+ BindingGroupNum int64 `gorm:"column:binding_group_num"` // 映射 COUNT(a.id)
+ }
+ var tempResultList []AttrResult
+ // 由于是复杂查询,无法直接使用Models,又为了防止以后attrs表名称修改,故不使用Table而是用TableName替换
+ model := AttributesItemModel{}
+ tableName := model.TableName()
+ // 此处使用了JOIN来避免子查询,数据库一般对JOIN有使用索引的优化,所以有性能提升,但是我没有实际测试过性能差距。
+ err := db.Table(fmt.Sprintf("%s AS t1", tableName)).
+ Select("t1.id, t1.name, t1.sheet_type, COUNT(a.id) AS binding_group_num").
+ Joins(fmt.Sprintf("LEFT JOIN %s AS a ON a.binding_sheet_id = t1.id", tableName)).
+ Where("t1.owner_id = ? AND t1.is_hidden IS FALSE", userId).
+ Group("t1.id, t1.name, t1.sheet_type").
+ // Pinenutn:此处我根据创建时间对创建的卡进行排序,不知道是否有意义?
+ Order("t1.created_at ASC").
+ Scan(&tempResultList).Error
if err != nil {
return nil, err
}
- var items []*AttributesItemModel
- for rows.Next() {
- item := &AttributesItemModel{}
- err := rows.Scan(
- &item.Id,
- &item.Name,
- &item.SheetType,
- &item.BindingGroupsNum,
- )
- if err != nil {
- return nil, err
+ items := make([]*AttributesItemModel, len(tempResultList))
+ for i, tempResult := range tempResultList {
+ items[i] = &AttributesItemModel{
+ Id: tempResult.ID,
+ Name: tempResult.Name,
+ SheetType: tempResult.SheetType,
+ BindingGroupsNum: tempResult.BindingGroupNum,
}
- items = append(items, item)
}
- return items, nil
+ return items, nil // 返回角色列表
}
diff --git a/dice/model/backup.go b/dice/model/backup.go
index a564a077..131da51a 100644
--- a/dice/model/backup.go
+++ b/dice/model/backup.go
@@ -1,19 +1,35 @@
package model
import (
- "github.com/jmoiron/sqlx"
+ "strings"
+
+ "gorm.io/gorm"
)
-func Vacuum(db *sqlx.DB, path string) error {
- _, err := db.Exec("vacuum into $1", path)
- return err
+// Vacuum 执行数据库的 vacuum 操作
+func Vacuum(db *gorm.DB, path string) error {
+ // 检查数据库驱动是否为 SQLite
+ if !strings.Contains(db.Dialector.Name(), "sqlite") {
+ return nil
+ }
+
+ // 使用 GORM 执行 vacuum 操作,并将数据库保存到指定路径
+ err := db.Exec("VACUUM INTO ?", path).Error
+ return err // 返回错误
}
-func FlushWAL(db *sqlx.DB) error {
- _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);")
- if err != nil {
- return err
+// FlushWAL 执行 WAL 日志的检查点和内存收缩
+func FlushWAL(db *gorm.DB) error {
+ // 检查数据库驱动是否为 SQLite
+ if !strings.Contains(db.Dialector.Name(), "sqlite") {
+ return nil
+ }
+
+ // 执行 WAL 检查点操作
+ if err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
+ return err // 返回错误
}
- _, err = db.Exec("PRAGMA shrink_memory")
- return err
+ // 执行内存收缩操作
+ err := db.Exec("PRAGMA shrink_memory;").Error
+ return err // 返回错误
}
diff --git a/dice/model/ban.go b/dice/model/ban.go
index 6f4d3328..755ff942 100644
--- a/dice/model/ban.go
+++ b/dice/model/ban.go
@@ -1,36 +1,60 @@
package model
import (
- "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
)
-func BanItemDel(db *sqlx.DB, id string) error {
- _, err := db.Exec("delete from ban_info where id=$1", id)
- return err
+// BanInfo 模型
+// GORM STRUCT
+type BanInfo struct {
+ ID string `gorm:"primaryKey;column:id"` // 主键列
+ BanUpdatedAt int `gorm:"index:idx_ban_info_ban_updated_at;column:ban_updated_at"` // BanUpdatedAt 列
+ UpdatedAt int `gorm:"index:idx_ban_info_updated_at;column:updated_at"` // UpdatedAt 列
+ Data []byte `gorm:"column:data"` // BLOB 类型
}
-func BanItemSave(db *sqlx.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error {
- _, err := db.NamedExec("replace into ban_info (id, updated_at, ban_updated_at, data) values (:id, :updated_at, :ban_updated_at, :data)",
- map[string]interface{}{
- "id": id,
- "updated_at": updatedAt,
- "ban_updated_at": banUpdatedAt,
- "data": data,
- })
- return err
+func (*BanInfo) TableName() string {
+ return "ban_info"
}
-func BanItemList(db *sqlx.DB, callback func(id string, banUpdatedAt int64, data []byte)) error {
- var items []struct {
- ID string `db:"id"`
- BanUpdatedAt int64 `db:"ban_updated_at"`
- Data []byte `db:"data"`
+// BanItemDel 删除指定 ID 的禁用项
+func BanItemDel(db *gorm.DB, id string) error {
+ // 使用 GORM 的 Delete 方法删除指定 ID 的记录
+ result := db.Where("id = ?", id).Delete(&BanInfo{})
+ return result.Error // 返回错误
+}
+
+// BanItemSave 保存或替换禁用项 这里的[]byte也是json反序列化产物
+func BanItemSave(db *gorm.DB, id string, updatedAt int64, banUpdatedAt int64, data []byte) error {
+ // 使用 FirstOrCreate ,这里显然,第一次初始化的时候替换ID,而剩余的时候只换ID以外的数据
+ if err := db.Where("id = ?", id).Attrs(map[string]any{
+ "id": id,
+ "updated_at": int(updatedAt),
+ "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段
+ "data": BYTE(data), // 禁用项数据
+ }).
+ Assign(map[string]any{
+ "updated_at": int(updatedAt),
+ "ban_updated_at": int(banUpdatedAt), // 只在创建时设置的字段
+ "data": BYTE(data), // 禁用项数据
+ }).FirstOrCreate(&BanInfo{}).Error; err != nil {
+ return err // 返回错误
}
- if err := db.Select(&items, "SELECT id, ban_updated_at, data FROM ban_info ORDER BY ban_updated_at DESC"); err != nil {
- return err
+ return nil // 操作成功,返回 nil
+}
+
+// BanItemList 列出所有禁用项并调用回调函数处理
+func BanItemList(db *gorm.DB, callback func(id string, banUpdatedAt int64, data []byte)) error {
+ var items []BanInfo
+
+ // 使用 GORM 查询所有禁用项
+ if err := db.Order("ban_updated_at DESC").Find(&items).Error; err != nil {
+ return err // 返回错误
}
+
+ // 遍历每个禁用项并调用回调函数
for _, item := range items {
- callback(item.ID, item.BanUpdatedAt, item.Data)
+ callback(item.ID, int64(item.BanUpdatedAt), item.Data) // 确保类型一致
}
- return nil
+ return nil // 操作成功,返回 nil
}
diff --git a/dice/model/censor_log.go b/dice/model/censor_log.go
index 745d2b3c..fb497e40 100644
--- a/dice/model/censor_log.go
+++ b/dice/model/censor_log.go
@@ -4,113 +4,129 @@ import (
"encoding/json"
"time"
- "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
"sealdice-core/dice/censor"
+ log "sealdice-core/utils/kratos"
)
type CensorLog struct {
- ID uint64 `json:"id"`
- MsgType string `json:"msgType"`
- UserID string `json:"userId"`
- GroupID string `json:"groupId"`
- Content string `json:"content"`
- HighestLevel int `json:"highestLevel"`
- CreatedAt int `json:"createdAt"`
+ ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
+ MsgType string `json:"msgType" gorm:"column:msg_type"`
+ UserID string `json:"userId" gorm:"index:idx_censor_log_user_id;column:user_id"`
+ GroupID string `json:"groupId" gorm:"column:group_id"`
+ Content string `json:"content" gorm:"column:content"`
+ HighestLevel int `json:"highestLevel" gorm:"index:idx_censor_log_level;column:highest_level"`
+ CreatedAt int `json:"createdAt" gorm:"column:created_at"`
+ // 补充gorm有的部分:
+ SensitiveWords string `json:"-" gorm:"column:sensitive_words"`
+ ClearMark bool `json:"-" gorm:"column:clear_mark;type:bool"`
}
-func CensorAppend(db *sqlx.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool {
- now := time.Now()
- nowTimestamp := now.Unix()
+func (CensorLog) TableName() string {
+ return "censor_log"
+}
+
+// 添加一个敏感词记录
+func CensorAppend(db *gorm.DB, msgType string, userID string, groupID string, content string, sensitiveWords interface{}, highestLevel int) bool {
+ // 获取当前时间的 Unix 时间戳
+ nowTimestamp := time.Now().Unix()
+ // 将敏感词转换为 JSON 字符串
words, err := json.Marshal(sensitiveWords)
if err != nil {
return false
}
- _, err = db.Exec(`
-INSERT INTO censor_log(
- msg_type,
- user_id,
- group_id,
- content,
- sensitive_words,
- highest_level,
- created_at,
- clear_mark
-) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
- msgType, userID, groupID, content, words, highestLevel, nowTimestamp, false)
-
- if err != nil {
+ // 创建 CensorLog 实例,手动设置 CreatedAt
+ censorLog := CensorLog{
+ MsgType: msgType,
+ UserID: userID,
+ GroupID: groupID,
+ Content: content,
+ SensitiveWords: string(words),
+ HighestLevel: highestLevel,
+ CreatedAt: int(nowTimestamp), // Unix 时间戳
+ ClearMark: false,
+ }
+ // 使用 GORM 的 Create 方法插入记录
+ if err := db.Create(&censorLog).Error; err != nil {
return false
}
- return err == nil
+ return true
}
-func CensorCount(db *sqlx.DB, userID string) map[censor.Level]int {
+func CensorCount(db *gorm.DB, userID string) map[censor.Level]int {
+ // 定义要查询的不同敏感级别
levels := [5]censor.Level{censor.Ignore, censor.Notice, censor.Caution, censor.Warning, censor.Danger}
- var temp int
+ var temp int64
res := make(map[censor.Level]int)
+
+ // 遍历每个敏感级别并执行查询
for _, level := range levels {
- _ = db.Get(&temp, `SELECT COUNT(*) FROM censor_log WHERE user_id = ? AND highest_level = ? AND clear_mark = ?`, userID, level, false)
- res[level] = temp
+ // 使用 GORM 的链式查询
+ err := db.Model(&CensorLog{}).Where("user_id = ? AND highest_level = ? AND clear_mark = ?", userID, level, false).
+ Count(&temp).Error
+
+ // 如果查询出现错误,忽略并赋值为 0
+ if err != nil {
+ res[level] = 0
+ } else {
+ res[level] = int(temp)
+ }
}
+
return res
}
-func CensorClearLevelCount(db *sqlx.DB, userID string, level censor.Level) {
- _, _ = db.Exec(`UPDATE censor_log SET clear_mark = ? WHERE user_id = ? AND highest_level = ?`, true, userID, level)
+func CensorClearLevelCount(db *gorm.DB, userID string, level censor.Level) {
+ // 使用 GORM 的链式查询执行批量更新
+ err := db.Model(&CensorLog{}).
+ Where("user_id = ? AND highest_level = ?", userID, level).
+ Update("clear_mark", true).Error
+ if err != nil {
+ log.Error(err)
+ }
}
+// QueryCensorLog 是分页查询的参数
type QueryCensorLog struct {
- PageNum int `query:"pageNum"`
- PageSize int `query:"pageSize"`
- UserID string `query:"userId"`
- Level int `query:"level"`
+ PageNum int `query:"pageNum"` // 当前页码
+ PageSize int `query:"pageSize"` // 每页条数
+ UserID string `query:"userId"` // 用户ID
+ Level int `query:"level"` // 敏感级别
}
-func CensorGetLogPage(db *sqlx.DB, params QueryCensorLog) (int, []CensorLog, error) {
- var total int
- res := make([]CensorLog, 0, params.PageSize)
+// CensorGetLogPage 使用 GORM 进行分页查询
+func CensorGetLogPage(db *gorm.DB, params QueryCensorLog) (int64, []CensorLog, error) {
+ var total int64
+ var logs []CensorLog
- err := db.QueryRow("SELECT COUNT(*) FROM censor_log").Scan(&total)
- if err != nil {
- return 0, nil, err
+ // 首先统计总记录数
+ query := db.Model(&CensorLog{})
+
+ // 如果传入了 UserID 和 Level,则添加查询条件
+ if params.UserID != "" {
+ query = query.Where("user_id = ?", params.UserID)
}
- rows, err := db.Queryx(`
-SELECT id,
- msg_type,
- user_id,
- group_id,
- content,
- highest_level,
- created_at
-FROM censor_log
-ORDER BY created_at DESC
-LIMIT ? OFFSET ?`, params.PageSize, (params.PageNum-1)*params.PageSize)
- if err != nil {
+ if params.Level != 0 {
+ query = query.Where("highest_level = ?", params.Level)
+ }
+
+ // 统计符合条件的总记录数
+ if err := query.Count(&total).Error; err != nil {
return 0, nil, err
}
- defer func(rows *sqlx.Rows) {
- _ = rows.Close()
- }(rows)
-
- for rows.Next() {
- log := CensorLog{}
- err := rows.Scan(
- &log.ID,
- &log.MsgType,
- &log.UserID,
- &log.GroupID,
- &log.Content,
- &log.HighestLevel,
- &log.CreatedAt,
- )
- if err != nil {
- return 0, nil, err
- }
- res = append(res, log)
+
+ // 查询分页数据
+ if err := query.
+ Order("created_at DESC"). // 按照创建时间倒序排列
+ Limit(params.PageSize). // 限制返回条数
+ Offset((params.PageNum - 1) * params.PageSize). // 偏移
+ Find(&logs). // 查询数据
+ Error; err != nil {
+ return 0, nil, err
}
- return total, res, nil
+ return total, logs, nil
}
diff --git a/dice/model/const.go b/dice/model/const.go
new file mode 100644
index 00000000..cd2cb7ff
--- /dev/null
+++ b/dice/model/const.go
@@ -0,0 +1,7 @@
+package model
+
+const (
+ SQLITE = "sqlite"
+ MYSQL = "mysql"
+ POSTGRESQL = "postgres"
+)
diff --git a/dice/model/database/cache/gormcache.go b/dice/model/database/cache/gormcache.go
new file mode 100644
index 00000000..4e964717
--- /dev/null
+++ b/dice/model/database/cache/gormcache.go
@@ -0,0 +1,132 @@
+package cache
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "time"
+
+ "github.com/go-gorm/caches/v4"
+ "github.com/spaolacci/murmur3"
+ "github.com/tidwall/buntdb"
+ "gorm.io/gorm"
+)
+
+type buntDBCacher struct {
+ db *buntdb.DB
+}
+
+func generateHashKey(key string) string {
+ hash := murmur3.Sum64([]byte(key))
+ return strconv.FormatUint(hash, 16) // 返回十六进制字符串
+}
+
+// Get 从缓存中获取与给定键关联的数据。
+// 该方法接受一个上下文、一个键和一个查询对象作为参数。
+// 它首先将键转换为哈希键,然后从数据库中获取相应的值。
+// 如果键不存在于数据库中,则返回nil, nil。
+// 如果存在错误,将返回错误信息。
+// 如果成功获取数据,将返回填充了数据的查询对象。
+func (c *buntDBCacher) Get(_ context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
+ // 生成哈希键以确定缓存的位置。
+ hashedKey := generateHashKey(key)
+
+ // 尝试查找对应的关联值
+ var res string
+ err := c.db.View(func(tx *buntdb.Tx) error {
+ var err error
+ // 从事务中获取与哈希键关联的值。
+ res, err = tx.Get(hashedKey)
+ return err
+ })
+
+ // 如果键在数据库中不存在,记录信息并返回nil, nil。
+ if errors.Is(err, buntdb.ErrNotFound) {
+ // 此处不得不忽略,因为这个cache的实现机理就是如此,除非修改gorm cache的源码。
+ return nil, nil //nolint:nilnil
+ }
+
+ // 如果发生其他错误,返回错误信息。
+ if err != nil {
+ return nil, err
+ }
+ // 将获取到的值解码为查询对象。
+ if err = q.Unmarshal([]byte(res)); err != nil {
+ return nil, err
+ }
+
+ return q, nil
+}
+
+// Store 方法用于将查询结果存储到缓存中。
+// 该方法接收一个上下文、一个键和一个查询对象作为参数。
+// 它首先对键进行哈希处理,然后将查询对象序列化为字节切片。
+// 序列化成功后,它将数据存储到缓存数据库中,并设置数据过期时间为5秒。
+// 参数:
+//
+// _ context.Context: 上下文,本例中未使用。
+// key string: 需要存储的数据的键。
+// val *caches.Query[any]: 需要存储的查询对象。
+//
+// 返回值:
+//
+// error: 在序列化或存储过程中遇到的错误,如果没有错误则返回nil。
+func (c *buntDBCacher) Store(_ context.Context, key string, val *caches.Query[any]) error {
+ // 生成哈希键以确保键的均匀分布和避免潜在的键冲突。
+ hashedKey := generateHashKey(key)
+ // 将查询对象序列化为字节切片,以便存储到缓存中。
+ res, err := val.Marshal()
+ if err != nil {
+ return err
+ }
+ // 使用数据库的Update方法来原子地设置数据。
+ err = c.db.Update(func(tx *buntdb.Tx) error {
+ // 设置键值对,并指定数据过期时间为5秒。
+ _, _, err = tx.Set(hashedKey, string(res), &buntdb.SetOptions{Expires: true, TTL: time.Second * 5})
+ return err
+ })
+
+ return err
+}
+
+// Invalidate 使缓存器中的所有缓存项失效。
+// 该方法通过删除数据库中所有以 caches.IdentifierPrefix 开头的键来实现。
+// 参数:
+//
+// _context.Context: 未使用。
+//
+// 返回值:
+//
+// error: 如果在使缓存项失效的过程中发生错误,则返回该错误。
+func (c *buntDBCacher) Invalidate(_ context.Context) error {
+ // 清理所有缓存
+ err := c.db.Update(func(tx *buntdb.Tx) error {
+ err := tx.DeleteAll()
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ return err
+}
+
+func GetBuntCacheDB(db *gorm.DB) (*gorm.DB, error) {
+ open, err := buntdb.Open(":memory:")
+ if err != nil {
+ return nil, err
+ }
+ // Easer参数:使用ServantGo任务执行与合并库
+ // ServantGo提供了一种简单且惯用的方法来合并同时运行的相同类型的任务。
+ // 可以先尝试一下easer=true是否可以加速
+ cachesPlugin := &caches.Caches{Conf: &caches.Config{
+ Easer: true,
+ Cacher: &buntDBCacher{
+ db: open,
+ },
+ }}
+ err = db.Use(cachesPlugin)
+ if err != nil {
+ return nil, err
+ }
+ return db, nil
+}
diff --git a/dice/model/database/mysql.go b/dice/model/database/mysql.go
new file mode 100644
index 00000000..f4035690
--- /dev/null
+++ b/dice/model/database/mysql.go
@@ -0,0 +1,27 @@
+package database
+
+import (
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/dice/model/database/cache"
+)
+
+func MySQLDBInit(dsn string) (*gorm.DB, error) {
+ // 构建 MySQL DSN (Data Source Name)
+ // 使用 GORM 连接 MySQL
+ db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info)})
+ if err != nil {
+ return nil, err
+ }
+ // 存疑,MYSQL是否需要使用缓存
+ cacheDB, err := cache.GetBuntCacheDB(db)
+ if err != nil {
+ return nil, err
+ }
+ // 返回数据库连接
+ return cacheDB, nil
+}
diff --git a/dice/model/database/pgsql.go b/dice/model/database/pgsql.go
new file mode 100644
index 00000000..aa24e54a
--- /dev/null
+++ b/dice/model/database/pgsql.go
@@ -0,0 +1,31 @@
+package database
+
+import (
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/dice/model/database/cache"
+)
+
+func PostgresDBInit(dsn string) (*gorm.DB, error) {
+ // 构建 PostgreSQL DSN (Data Source Name)
+
+ // 使用 GORM 连接 PostgreSQL
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // GetBuntCacheDB 逻辑保持不变
+ cacheDB, err := cache.GetBuntCacheDB(db)
+ if err != nil {
+ return nil, err
+ }
+
+ // 返回数据库连接
+ return cacheDB, nil
+}
diff --git a/dice/model/database/sqlite.go b/dice/model/database/sqlite.go
new file mode 100644
index 00000000..3e87ac9f
--- /dev/null
+++ b/dice/model/database/sqlite.go
@@ -0,0 +1,38 @@
+//go:build !cgo
+// +build !cgo
+
+package database
+
+import (
+ "github.com/glebarez/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/dice/model/database/cache"
+)
+
+func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) {
+ db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ // https://github.com/glebarez/sqlite/issues/52 尚未遇见问题,可以先考虑不使用
+ // sqlDB, _ := db.DB()
+ // sqlDB.SetMaxOpenConns(1)
+ if err != nil {
+ return nil, err
+ }
+ // Enable Cache Mode
+ db, err = cache.GetBuntCacheDB(db)
+ if err != nil {
+ return nil, err
+ }
+ // enable WAL mode
+ if useWAL {
+ err = db.Exec("PRAGMA journal_mode=WAL").Error
+ if err != nil {
+ return nil, err
+ }
+ }
+ return db, err
+}
diff --git a/dice/model/database/sqlite_cgo.go b/dice/model/database/sqlite_cgo.go
new file mode 100644
index 00000000..40ec5679
--- /dev/null
+++ b/dice/model/database/sqlite_cgo.go
@@ -0,0 +1,37 @@
+//go:build cgo
+// +build cgo
+
+package database
+
+import (
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/dice/model/database/cache"
+)
+
+func SQLiteDBInit(path string, useWAL bool) (*gorm.DB, error) {
+ open, err := gorm.Open(sqlite.Open(path), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ if err != nil {
+ return nil, err
+ }
+ // Enable Cache Mode
+ // DELETE
+ open, err = cache.GetBuntCacheDB(open)
+ if err != nil {
+ return nil, err
+ }
+ // enable WAL mode
+ if useWAL {
+ err = open.Exec("PRAGMA journal_mode=WAL").Error
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ return open, err
+}
diff --git a/dice/model/db.go b/dice/model/db.go
index f1b0bd5a..902abeca 100644
--- a/dice/model/db.go
+++ b/dice/model/db.go
@@ -1,258 +1,119 @@
package model
import (
- "fmt"
- "path/filepath"
+ "os"
+ "sync"
- "github.com/jmoiron/sqlx"
-)
+ "gorm.io/gorm"
-func DBCheck(dataDir string) {
- checkDB := func(db *sqlx.DB) bool {
- rows, err := db.Query("PRAGMA integrity_check") //nolint:execinquery
- if err != nil {
- return false
- }
- var ok bool
- for rows.Next() {
- var s string
- if errR := rows.Scan(&s); errR != nil {
- ok = false
- break
- }
- fmt.Println(s)
- if s == "ok" {
- ok = true
- }
- }
-
- if errR := rows.Err(); errR != nil {
- ok = false
- }
- return ok
- }
-
- var ok1, ok2, ok3 bool
- var dataDB *sqlx.DB
- var logsDB *sqlx.DB
- var censorDB *sqlx.DB
- var err error
+ log "sealdice-core/utils/kratos"
+)
- dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db"))
- dataDB, err = _SQLiteDBInit(dbDataPath, false)
- if err != nil {
- fmt.Println("数据库 data.db 无法打开")
- } else {
- ok1 = checkDB(dataDB)
- dataDB.Close()
- }
+var (
+ engine DatabaseOperator
+ once sync.Once
+ errEngineInstance error
+)
- dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db"))
- logsDB, err = _SQLiteDBInit(dbDataLogsPath, false)
- if err != nil {
- fmt.Println("数据库 data-logs.db 无法打开")
- } else {
- ok2 = checkDB(logsDB)
- logsDB.Close()
+// initEngine 初始化数据库引擎,仅执行一次
+func initEngine() {
+ dbType := os.Getenv("DB_TYPE")
+ switch dbType {
+ case SQLITE:
+ log.Info("当前选择使用: SQLITE数据库")
+ engine = &SQLiteEngine{}
+ case MYSQL:
+ log.Info("当前选择使用: MYSQL数据库")
+ engine = &MYSQLEngine{}
+ case POSTGRESQL:
+ log.Info("当前选择使用: POSTGRESQL数据库")
+ engine = &PGSQLEngine{}
+ default:
+ log.Warn("未配置数据库类型,默认使用: SQLITE数据库")
+ engine = &SQLiteEngine{}
}
- dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db"))
- censorDB, err = _SQLiteDBInit(dbDataCensorPath, false)
- if err != nil {
- fmt.Println("数据库 data-censor.db 无法打开")
- } else {
- ok3 = checkDB(censorDB)
- censorDB.Close()
+ errEngineInstance = engine.Init()
+ if errEngineInstance != nil {
+ log.Error("数据库引擎初始化失败:", errEngineInstance)
}
+}
- fmt.Println("数据库检查结果:")
- fmt.Println("data.db:", ok1)
- fmt.Println("data-logs.db:", ok2)
- fmt.Println("data-censor.db:", ok3)
+// getEngine 获取数据库引擎,确保只初始化一次
+func getEngine() (DatabaseOperator, error) {
+ once.Do(initEngine)
+ return engine, errEngineInstance
}
-func SQLiteDBInit(dataDir string) (dataDB *sqlx.DB, logsDB *sqlx.DB, err error) {
- dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db"))
- dataDB, err = _SQLiteDBInit(dbDataPath, true)
+// DatabaseInit 初始化数据和日志数据库
+func DatabaseInit() (dataDB *gorm.DB, logsDB *gorm.DB, err error) {
+ engine, err = getEngine()
if err != nil {
- return
+ return nil, nil, err
}
- dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db"))
- logsDB, err = _SQLiteDBInit(dbDataLogsPath, true)
+ dataDB, err = engine.DataDBInit()
if err != nil {
- return
+ return nil, nil, err
}
- // data建表
- texts := []string{
- `
-create table if not exists group_player_info
-(
- id INTEGER
- primary key autoincrement,
- group_id TEXT,
- user_id TEXT,
- name TEXT,
- created_at INTEGER,
- updated_at INTEGER,
- last_command_time INTEGER,
- auto_set_name_template TEXT,
- dice_side_num TEXT
-);`,
- `create index if not exists idx_group_player_info_group_id on group_player_info (group_id);`,
- `create index if not exists idx_group_player_info_user_id on group_player_info (user_id);`,
- `create unique index if not exists idx_group_player_info_group_user on group_player_info (group_id, user_id);`,
- `
-create table if not exists group_info
-(
- id TEXT primary key,
- created_at INTEGER,
- updated_at INTEGER,
- data BLOB
-);`,
-
- `
-create table if not exists ban_info
-(
- id TEXT primary key,
- ban_updated_at INTEGER,
- updated_at INTEGER,
- data BLOB
-);`,
- `create index if not exists idx_ban_info_updated_at on ban_info (updated_at);`,
- `create index if not exists idx_ban_info_ban_updated_at on ban_info (ban_updated_at);`,
-
- `CREATE TABLE IF NOT EXISTS endpoint_info (
-user_id TEXT PRIMARY KEY,
-cmd_num INTEGER,
-cmd_last_time INTEGER,
-online_time INTEGER,
-updated_at INTEGER
-);`,
-
- `
-CREATE TABLE IF NOT EXISTS attrs (
- id TEXT PRIMARY KEY,
- data BYTEA,
- attrs_type TEXT,
-
- -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化
- binding_sheet_id TEXT default '',
-
- name TEXT default '',
- owner_id TEXT default '',
- sheet_type TEXT default '',
- is_hidden BOOLEAN default FALSE,
-
- created_at INTEGER default 0,
- updated_at INTEGER default 0
-);
-`,
- `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`,
- `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`,
- `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`,
- }
- for _, i := range texts {
- _, _ = dataDB.Exec(i)
+ logsDB, err = engine.LogDBInit()
+ if err != nil {
+ return nil, nil, err
}
-
- // logs建表
- texts = []string{
- `
-create table if not exists logs
-(
- id INTEGER primary key autoincrement,
- name TEXT,
- group_id TEXT,
- extra TEXT,
- created_at INTEGER,
- updated_at INTEGER,
- upload_url TEXT,
- upload_time INTEGER
-);`,
- `
-create index if not exists idx_logs_group
- on logs (group_id);`,
- `
-create index if not exists idx_logs_update_at
- on logs (updated_at);`,
- `
-create unique index if not exists idx_log_group_id_name
- on logs (group_id, name);`,
- // 如果log_items有更改,需同步检查migrate/convert_logs.go
- `
-create table if not exists log_items
-(
- id INTEGER primary key autoincrement,
- log_id INTEGER,
- group_id TEXT,
- nickname TEXT,
- im_userid TEXT,
- time INTEGER,
- message TEXT,
- is_dice INTEGER,
- command_id INTEGER,
- command_info TEXT,
- raw_msg_id TEXT,
- user_uniform_id TEXT,
- removed INTEGER,
- parent_id INTEGER
-);`,
- `
-create index if not exists idx_log_items_group_id
- on log_items (log_id);`,
- `
-create index if not exists idx_log_items_log_id
- on log_items (log_id);`,
-
- `alter table logs add upload_url text;`, // 测试版特供
- `alter table logs add upload_time integer;`,
+ // TODO: 将这段逻辑挪移到Migrator上
+ var ids []uint64
+ var logItemSums []struct {
+ LogID uint64
+ Count int64
}
+ logsDB.Model(&LogInfo{}).Where("size IS NULL").Pluck("id", &ids)
+ if len(ids) > 0 {
+ // 根据 LogInfo 表中的 IDs 查找对应的 LogOneItem 记录
+ err = logsDB.Model(&LogOneItem{}).
+ Where("log_id IN ?", ids).
+ Group("log_id").
+ Select("log_id, COUNT(*) AS count"). // 如果需要求和其他字段,可以使用 Sum
+ Scan(&logItemSums).Error
+ if err != nil {
+ // 错误处理
+ log.Infof("Error querying LogOneItem: %v", err)
+ return nil, nil, err
+ }
- for _, i := range texts {
- _, _ = logsDB.Exec(i)
+ // 2. 更新 LogInfo 表的 Size 字段
+ for _, sum := range logItemSums {
+ // 将求和结果更新到对应的 LogInfo 的 Size 字段
+ err = logsDB.Model(&LogInfo{}).
+ Where("id = ?", sum.LogID).
+ UpdateColumn("size", sum.Count).Error // 或者是 sum.Time 等,如果要是其他字段的求和
+ if err != nil {
+ // 错误处理
+ log.Errorf("Error updating LogInfo: %v", err)
+ return nil, nil, err
+ }
+ }
}
-
- return
+ return dataDB, logsDB, nil
}
-func SQLiteCensorDBInit(dataDir string) (censorDB *sqlx.DB, err error) {
- path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db"))
- if err != nil {
- return
- }
- censorDB, err = _SQLiteDBInit(path, true)
+// DBCheck 检查数据库状态
+func DBCheck() {
+ dbEngine, err := getEngine()
if err != nil {
+ log.Error("数据库引擎获取失败:", err)
return
}
- texts := []string{`
-CREATE TABLE IF NOT EXISTS censor_log
-(
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- msg_type TEXT,
- user_id TEXT,
- group_id TEXT,
- content TEXT,
- sensitive_words TEXT,
- highest_level INTEGER,
- created_at INTEGER,
- clear_mark BOOLEAN
-);
-`,
- `
-CREATE INDEX IF NOT EXISTS idx_censor_log_user_id
- ON censor_log (user_id);
-`,
- `
-CREATE INDEX IF NOT EXISTS idx_censor_log_level
- ON censor_log (highest_level);
-`,
- }
+ dbEngine.DBCheck()
+}
- for _, i := range texts {
- _, _ = censorDB.Exec(i)
+// CensorDBInit 初始化敏感词数据库
+func CensorDBInit() (censorDB *gorm.DB, err error) {
+ censorEngine, err := getEngine()
+ if err != nil {
+ return nil, err
}
- return
+
+ return censorEngine.CensorDBInit()
}
diff --git a/dice/model/db_init.go b/dice/model/db_init.go
deleted file mode 100644
index 99c78ccf..00000000
--- a/dice/model/db_init.go
+++ /dev/null
@@ -1,31 +0,0 @@
-//go:build !cgo
-// +build !cgo
-
-package model
-
-import (
- _ "github.com/glebarez/go-sqlite"
- "github.com/jmoiron/sqlx"
-)
-
-func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) {
- db, err := sqlx.Open("sqlite", path)
- if err != nil {
- panic(err)
- }
-
- // _, err = db.Exec("vacuum")
- // if err != nil {
- // panic(err)
- // }
-
- // enable WAL mode
- if useWAL {
- _, err = db.Exec("PRAGMA journal_mode=WAL")
- if err != nil {
- panic(err)
- }
- }
-
- return db, err
-}
diff --git a/dice/model/db_init_cgo.go b/dice/model/db_init_cgo.go
deleted file mode 100644
index dcf25921..00000000
--- a/dice/model/db_init_cgo.go
+++ /dev/null
@@ -1,26 +0,0 @@
-//go:build cgo
-// +build cgo
-
-package model
-
-import (
- "github.com/jmoiron/sqlx"
- _ "github.com/mattn/go-sqlite3" // sqlite3 driver
-)
-
-func _SQLiteDBInit(path string, useWAL bool) (*sqlx.DB, error) {
- db, err := sqlx.Open("sqlite3", path)
- if err != nil {
- panic(err)
- }
-
- // enable WAL mode
- if useWAL {
- _, err = db.Exec("PRAGMA journal_mode=WAL")
- if err != nil {
- panic(err)
- }
- }
-
- return db, err
-}
diff --git a/dice/model/db_utils.go b/dice/model/db_utils.go
index f9ae79fd..e24eea7d 100644
--- a/dice/model/db_utils.go
+++ b/dice/model/db_utils.go
@@ -1,58 +1,52 @@
package model
import (
+ "database/sql/driver"
+ "errors"
"fmt"
- "os"
- "path/filepath"
- "runtime"
+ "strings"
"sync"
+ "sealdice-core/dice/model/database"
+ log "sealdice-core/utils/kratos"
"sealdice-core/utils/spinner"
)
-func DBCacheDelete() bool {
- // d.BaseConfig.DataDir
- dataDir := "./data/default"
+// BYTES类
+// 如果我们使用FirstOrCreate,不可避免的会遇到这样的问题:
+// 传入的是BYTE数组,由于使用了any会被转换为[]int8,而gorm又不会处理这种数据,进而导致转换失败
+// 通过强制设置一个封装,可以确认any的类型,进而避免转换失败
- tryDelete := func(fn string) bool {
- fnPath, _ := filepath.Abs(filepath.Join(dataDir, fn))
- if _, err := os.Stat(fnPath); err != nil {
- // 文件不在了,就当作删除成功
- return true
- }
- return os.Remove(fnPath) == nil
- }
+// 定义一个新的类型 JSON,封装 []byte
+type BYTE []byte
- // 非 windows 不删缓存
- if runtime.GOOS != "windows" {
- return true
+// Scan 实现 sql.Scanner 接口,用于扫描数据库中的 JSON 数据
+func (j *BYTE) Scan(value interface{}) error {
+ // 将数据库中的值转换为 []byte
+ bytes, ok := value.([]byte)
+ if !ok {
+ return errors.New(fmt.Sprint("Failed to unmarshal JSON value:", value))
}
- ok := true
- if ok {
- ok = tryDelete("data.db-shm")
- }
- if ok {
- tryDelete("data.db-wal")
- }
- if ok {
- tryDelete("data-logs.db-shm")
- }
- if ok {
- tryDelete("data-logs.db-wal")
- }
- if ok {
- tryDelete("data-censor.db-shm")
- }
- if ok {
- tryDelete("data-censor.db-wal")
+ // 将 []byte 赋值给 JSON 类型的指针
+ *j = bytes
+ return nil
+}
+
+// Value 实现 driver.Valuer 接口,用于将 JSON 类型存储到数据库中
+func (j BYTE) Value() (driver.Value, error) {
+ // 如果 BYTE 数据为空,则返回 nil
+ if len(j) == 0 {
+ return nil, nil //nolint:nilnil
}
- return ok
+ // 返回原始的 []byte
+ return []byte(j), nil
}
+// DBVacuum 整理数据库
func DBVacuum() {
done := make(chan interface{}, 1)
- fmt.Println("开始进行数据库整理")
+ log.Info("开始进行数据库整理")
go spinner.WithLines(done, 3, 10)
defer func() {
@@ -64,15 +58,29 @@ func DBVacuum() {
vacuum := func(path string, wg *sync.WaitGroup) {
defer wg.Done()
- db, err := _SQLiteDBInit(path, true)
- defer func() { _ = db.Close() }()
+ // 使用 GORM 初始化数据库
+ vacuumDB, err := database.SQLiteDBInit(path, true)
+ // 数据库类型不是 SQLite 直接返回
+ if !strings.Contains(vacuumDB.Dialector.Name(), "sqlite") {
+ return
+ }
+ defer func() {
+ rawdb, err2 := vacuumDB.DB()
+ if err2 != nil {
+ return
+ }
+ err = rawdb.Close()
+ if err != nil {
+ return
+ }
+ }()
if err != nil {
- fmt.Printf("清理 %q 时出现错误:%v", path, err)
+ log.Errorf("清理 %q 时出现错误:%v", path, err)
return
}
- _, err = db.Exec("VACUUM;")
+ err = vacuumDB.Exec("VACUUM;").Error
if err != nil {
- fmt.Printf("清理 %q 时出现错误:%v", path, err)
+ log.Errorf("清理 %q 时出现错误:%v", path, err)
}
}
@@ -82,5 +90,5 @@ func DBVacuum() {
wg.Wait()
- fmt.Println("\n数据库整理完成")
+ log.Info("数据库整理完成")
}
diff --git a/dice/model/endpoint_info.go b/dice/model/endpoint_info.go
index 947f72df..df582488 100644
--- a/dice/model/endpoint_info.go
+++ b/dice/model/endpoint_info.go
@@ -1,54 +1,59 @@
package model
import (
- "database/sql"
"errors"
- "time"
- "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
)
+var ErrEndpointInfoUIDEmpty = errors.New("user id is empty")
+
+// 仅修改为gorm格式
type EndpointInfo struct {
- UserID string `db:"user_id"`
- CmdNum int64 `db:"cmd_num"`
- CmdLastTime int64 `db:"cmd_last_time"`
- OnlineTime int64 `db:"online_time"`
- UpdatedAt int64 `db:"updated_at"`
+ UserID string `gorm:"column:user_id;primaryKey"`
+ CmdNum int64 `gorm:"column:cmd_num;"`
+ CmdLastTime int64 `gorm:"column:cmd_last_time;"`
+ OnlineTime int64 `gorm:"column:online_time;"`
+ UpdatedAt int64 `gorm:"column:updated_at;"`
}
-var ErrEndpointInfoUIDEmpty = errors.New("user id is empty")
+func (EndpointInfo) TableName() string {
+ return "endpoint_info"
+}
-func (e *EndpointInfo) Query(db *sqlx.DB) error {
+func (e *EndpointInfo) Query(db *gorm.DB) error {
if len(e.UserID) == 0 {
return ErrEndpointInfoUIDEmpty
}
if db == nil {
return errors.New("db is nil")
}
- row := db.QueryRowx(
- `SELECT cmd_num, cmd_last_time, online_time, updated_at FROM endpoint_info WHERE user_id = $1`,
- e.UserID,
- )
- err := row.Scan(&e.CmdNum, &e.CmdLastTime, &e.OnlineTime, &e.UpdatedAt)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
+
+ err := db.Model(&EndpointInfo{}).
+ Where("user_id = ?", e.UserID).
+ Select("cmd_num", "cmd_last_time", "online_time", "updated_at").
+ Scan(&e).Error
+
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
+
return nil
}
-func (e *EndpointInfo) Save(db *sqlx.DB) error {
+func (e *EndpointInfo) Save(db *gorm.DB) error {
+ // 检查 user_id 是否为空
if len(e.UserID) == 0 {
return ErrEndpointInfoUIDEmpty
}
- if db == nil {
- return errors.New("db is nil")
- }
- now := time.Now().Unix()
- e.UpdatedAt = now
-
- _, err := db.Exec(
- `REPLACE INTO endpoint_info (user_id, cmd_num, cmd_last_time, online_time, updated_at) VALUES (?, ?, ?, ?, ?)`,
- e.UserID, e.CmdNum, e.CmdLastTime, e.OnlineTime, e.UpdatedAt,
- )
- return err
+ // 检查user_id冲突时更新,否则进行创建
+ result := db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "user_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{
+ "cmd_num", "cmd_last_time", "online_time", "updated_at",
+ }),
+ }).Create(e)
+
+ return result.Error
}
diff --git a/dice/model/engine_interface.go b/dice/model/engine_interface.go
new file mode 100644
index 00000000..28281f9b
--- /dev/null
+++ b/dice/model/engine_interface.go
@@ -0,0 +1,13 @@
+package model
+
+import "gorm.io/gorm"
+
+// DatabaseOperator 本来是单独放了个文件夹的,但是由于现在所有的model都和处理逻辑在一起,如果放在单独文件夹必然会循环依赖
+// 只能放在外面
+type DatabaseOperator interface {
+ Init() error
+ DBCheck()
+ DataDBInit() (*gorm.DB, error)
+ LogDBInit() (*gorm.DB, error)
+ CensorDBInit() (*gorm.DB, error)
+}
diff --git a/dice/model/engine_mysql.go b/dice/model/engine_mysql.go
new file mode 100644
index 00000000..06fbf46d
--- /dev/null
+++ b/dice/model/engine_mysql.go
@@ -0,0 +1,163 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "gorm.io/gorm"
+
+ "sealdice-core/dice/model/database"
+ log "sealdice-core/utils/kratos"
+)
+
+type MYSQLEngine struct {
+ DSN string
+ DB *gorm.DB
+}
+
+type LogInfoHookMySQL struct {
+ ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
+ Name string `json:"name" gorm:"column:name"`
+ GroupID string `json:"groupId" gorm:"column:group_id"`
+ CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
+ UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
+ Size *int `json:"size" gorm:"<-:false"`
+ Extra *string `json:"-" gorm:"column:extra"`
+ UploadURL string `json:"-" gorm:"column:upload_url"`
+ UploadTime int `json:"-" gorm:"column:upload_time"`
+}
+
+func (*LogInfoHookMySQL) TableName() string {
+ return "logs"
+}
+
+type LogOneItemHookMySQL struct {
+ ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
+ LogID uint64 `json:"-" gorm:"column:log_id"`
+ GroupID string `gorm:"column:group_id"`
+ Nickname string `json:"nickname" gorm:"column:nickname"`
+ IMUserID string `json:"IMUserId" gorm:"column:im_userid"`
+ Time int64 `json:"time" gorm:"column:time"`
+ Message string `json:"message" gorm:"column:message"`
+ IsDice bool `json:"isDice" gorm:"column:is_dice"`
+ CommandID int64 `json:"commandId" gorm:"column:command_id"`
+ CommandInfo interface{} `json:"commandInfo" gorm:"-"`
+ CommandInfoStr string `json:"-" gorm:"column:command_info"`
+ RawMsgID interface{} `json:"rawMsgId" gorm:"-"`
+ RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id"`
+ UniformID string `json:"uniformId" gorm:"column:user_uniform_id"`
+ Channel string `json:"channel" gorm:"-"`
+ Removed *int `gorm:"column:removed" json:"-"`
+ ParentID *int `gorm:"column:parent_id" json:"-"`
+}
+
+func (*LogOneItemHookMySQL) TableName() string {
+ return "log_items"
+}
+
+// 利用前缀索引,规避索引BUG
+// 创建不出来也没关系,反正MYSQL数据库
+func createIndexForLogInfo(db *gorm.DB) (err error) {
+ // 创建前缀索引
+ // 检查并创建索引
+ if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_log_name") {
+ err = db.Exec("CREATE INDEX idx_log_name ON logs (name(20));").Error
+ if err != nil {
+ log.Errorf("创建idx_log_name索引失败,原因为 %v", err)
+ }
+ }
+
+ if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_group") {
+ err = db.Exec("CREATE INDEX idx_logs_group ON logs (group_id(20));").Error
+ if err != nil {
+ log.Errorf("创建idx_logs_group索引失败,原因为 %v", err)
+ }
+ }
+
+ if !db.Migrator().HasIndex(&LogInfoHookMySQL{}, "idx_logs_updated_at") {
+ err = db.Exec("CREATE INDEX idx_logs_updated_at ON logs (updated_at);").Error
+ if err != nil {
+ log.Errorf("创建idx_logs_updated_at索引失败,原因为 %v", err)
+ }
+ }
+ return nil
+}
+
+func createIndexForLogOneItem(db *gorm.DB) (err error) {
+ // 创建前缀索引
+ // 检查并创建索引
+ if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_log_items_group_id") {
+ err = db.Exec("CREATE INDEX idx_log_items_group_id ON log_items(group_id(20))").Error
+ if err != nil {
+ log.Errorf("创建idx_logs_group索引失败,原因为 %v", err)
+ }
+ }
+ if !db.Migrator().HasIndex(&LogOneItemHookMySQL{}, "idx_raw_msg_id") {
+ err = db.Exec("CREATE INDEX idx_raw_msg_id ON log_items(raw_msg_id(20))").Error
+ if err != nil {
+ log.Errorf("创建idx_log_group_id_name索引失败,原因为 %v", err)
+ }
+ }
+ // MYSQL似乎不能创建前缀联合索引,放弃所有的前缀联合索引
+ return nil
+}
+
+func (s *MYSQLEngine) Init() error {
+ s.DSN = os.Getenv("DB_DSN")
+ if s.DSN == "" {
+ return errors.New("DB_DSN is missing")
+ }
+ var err error
+ s.DB, err = database.MySQLDBInit(s.DSN)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// DBCheck DB检查
+func (s *MYSQLEngine) DBCheck() {
+ fmt.Fprintln(os.Stdout, "MYSQL 海豹不提供检查,请自行检查数据库!")
+}
+
+// DataDBInit 初始化
+func (s *MYSQLEngine) DataDBInit() (*gorm.DB, error) {
+ err := s.DB.AutoMigrate(
+ // TODO: 这个的索引有没有必要进行修改
+ &GroupPlayerInfoBase{},
+ &GroupInfo{},
+ &BanInfo{},
+ &EndpointInfo{},
+ &AttributesItemModel{},
+ )
+ if err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
+
+func (s *MYSQLEngine) LogDBInit() (*gorm.DB, error) {
+ // logs特殊建表
+ if err := s.DB.AutoMigrate(&LogInfoHookMySQL{}, &LogOneItemHookMySQL{}); err != nil {
+ return nil, err
+ }
+ // logs建立索引
+ err := createIndexForLogInfo(s.DB)
+ if err != nil {
+ return nil, err
+ }
+ err = createIndexForLogOneItem(s.DB)
+ if err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
+
+func (s *MYSQLEngine) CensorDBInit() (*gorm.DB, error) {
+ // 创建基本的表结构,并通过标签定义索引
+ if err := s.DB.AutoMigrate(&CensorLog{}); err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
diff --git a/dice/model/engine_pgsql.go b/dice/model/engine_pgsql.go
new file mode 100644
index 00000000..a734840e
--- /dev/null
+++ b/dice/model/engine_pgsql.go
@@ -0,0 +1,66 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "gorm.io/gorm"
+
+ "sealdice-core/dice/model/database"
+)
+
+type PGSQLEngine struct {
+ DSN string
+ DB *gorm.DB
+}
+
+func (s *PGSQLEngine) Init() error {
+ s.DSN = os.Getenv("DB_DSN")
+ if s.DSN == "" {
+ return errors.New("DB_DSN is missing")
+ }
+ var err error
+ s.DB, err = database.PostgresDBInit(s.DSN)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// DBCheck DB检查
+func (s *PGSQLEngine) DBCheck() {
+ fmt.Fprintln(os.Stdout, "PostGRESQL 海豹不提供检查,请自行检查数据库!")
+}
+
+// DataDBInit 初始化
+func (s *PGSQLEngine) DataDBInit() (*gorm.DB, error) {
+ // data建表
+ err := s.DB.AutoMigrate(
+ &GroupPlayerInfoBase{},
+ &GroupInfo{},
+ &BanInfo{},
+ &EndpointInfo{},
+ &AttributesItemModel{},
+ )
+ if err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
+
+func (s *PGSQLEngine) LogDBInit() (*gorm.DB, error) {
+ // logs建表
+ if err := s.DB.AutoMigrate(&LogInfo{}, &LogOneItem{}); err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
+
+func (s *PGSQLEngine) CensorDBInit() (*gorm.DB, error) {
+ // 创建基本的表结构,并通过标签定义索引
+ if err := s.DB.AutoMigrate(&CensorLog{}); err != nil {
+ return nil, err
+ }
+ return s.DB, nil
+}
diff --git a/dice/model/engine_sqlite.go b/dice/model/engine_sqlite.go
new file mode 100644
index 00000000..4d42aee4
--- /dev/null
+++ b/dice/model/engine_sqlite.go
@@ -0,0 +1,291 @@
+package model
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "gorm.io/gorm"
+
+ "sealdice-core/dice/model/database"
+ log "sealdice-core/utils/kratos"
+)
+
+type SQLiteEngine struct {
+ DataDir string
+}
+
+const defaultDataDir = "./data/default"
+
+const createSql = `
+CREATE TABLE attrs__temp (
+ id TEXT PRIMARY KEY,
+ data BYTEA,
+ attrs_type TEXT,
+ binding_sheet_id TEXT default '',
+ name TEXT default '',
+ owner_id TEXT default '',
+ sheet_type TEXT default '',
+ is_hidden BOOLEAN default FALSE,
+ created_at INTEGER default 0,
+ updated_at INTEGER default 0
+);
+`
+
+func (s *SQLiteEngine) Init() error {
+ s.DataDir = os.Getenv("DATADIR")
+ if s.DataDir == "" {
+ log.Debug("未能发现SQLITE定义位置,使用默认data地址")
+ s.DataDir = defaultDataDir
+ }
+ return nil
+}
+
+// DB检查 BUG FIXME
+func (s *SQLiteEngine) DBCheck() {
+ dataDir := s.DataDir
+ checkDB := func(db *gorm.DB) bool {
+ rows, err := db.Raw("PRAGMA integrity_check").Rows()
+ if err != nil {
+ return false
+ }
+ defer rows.Close()
+ var ok bool
+ for rows.Next() {
+ var s string
+ if errR := rows.Scan(&s); errR != nil {
+ ok = false
+ break
+ }
+ fmt.Fprintln(os.Stdout, s)
+ if s == "ok" {
+ ok = true
+ }
+ }
+
+ if errR := rows.Err(); errR != nil {
+ ok = false
+ }
+ return ok
+ }
+
+ var ok1, ok2, ok3 bool
+ var dataDB *gorm.DB
+ var logsDB *gorm.DB
+ var censorDB *gorm.DB
+ var err error
+
+ dbDataPath, _ := filepath.Abs(filepath.Join(dataDir, "data.db"))
+ dataDB, err = database.SQLiteDBInit(dbDataPath, false)
+ if err != nil {
+ fmt.Fprintln(os.Stdout, "数据库 data.db 无法打开")
+ } else {
+ ok1 = checkDB(dataDB)
+ db, _ := dataDB.DB()
+ // 关闭
+ db.Close()
+ }
+
+ dbDataLogsPath, _ := filepath.Abs(filepath.Join(dataDir, "data-logs.db"))
+ logsDB, err = database.SQLiteDBInit(dbDataLogsPath, false)
+ if err != nil {
+ fmt.Fprintln(os.Stdout, "数据库 data-logs.db 无法打开")
+ } else {
+ ok2 = checkDB(logsDB)
+ db, _ := logsDB.DB()
+ // 关闭db
+ db.Close()
+ }
+
+ dbDataCensorPath, _ := filepath.Abs(filepath.Join(dataDir, "data-censor.db"))
+ censorDB, err = database.SQLiteDBInit(dbDataCensorPath, false)
+ if err != nil {
+ fmt.Fprintln(os.Stdout, "数据库 data-censor.db 无法打开")
+ } else {
+ ok3 = checkDB(censorDB)
+ db, _ := censorDB.DB()
+ // 关闭db
+ db.Close()
+ }
+
+ fmt.Fprintln(os.Stdout, "数据库检查结果:")
+ fmt.Fprintln(os.Stdout, "data.db:", ok1)
+ fmt.Fprintln(os.Stdout, "data-logs.db:", ok2)
+ fmt.Fprintln(os.Stdout, "data-censor.db:", ok3)
+}
+
+// 初始化
+func (s *SQLiteEngine) DataDBInit() (*gorm.DB, error) {
+ dbDataPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data.db"))
+ dataDB, err := database.SQLiteDBInit(dbDataPath, true)
+ if err != nil {
+ return nil, err
+ }
+ // 特殊情况建表语句处置
+ tx := dataDB.Begin()
+ // 检查是否有这个影响的注释
+ var count int64
+ err = dataDB.Raw("SELECT count(*) FROM `sqlite_master` WHERE tbl_name = 'attrs' AND `sql` LIKE '%这个方法太严格了%'").Count(&count).Error
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ if count > 0 {
+ log.Warn("数据库 attrs 表结构为前置测试版本150,重建中")
+ // 创建临时表
+ err = tx.Exec(createSql).Error
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ // 迁移数据
+ err = tx.Exec("INSERT INTO `attrs__temp` SELECT * FROM `attrs`").Error
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ // 删除旧的表
+ err = tx.Exec("DROP TABLE `attrs`").Error
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ // 改名
+ err = tx.Exec("ALTER TABLE `attrs__temp` RENAME TO `attrs`").Error
+ if err != nil {
+ tx.Rollback()
+ return nil, err
+ }
+ tx.Commit()
+ }
+
+ // data建表
+ err = dataDB.AutoMigrate(
+ &GroupPlayerInfoBase{},
+ &GroupInfo{},
+ &BanInfo{},
+ &EndpointInfo{},
+ &AttributesItemModel{},
+ )
+ if err != nil {
+ return nil, err
+ }
+ return dataDB, nil
+}
+
+func (s *SQLiteEngine) LogDBInit() (*gorm.DB, error) {
+ dbDataLogsPath, _ := filepath.Abs(filepath.Join(s.DataDir, "data-logs.db"))
+ logsDB, err := database.SQLiteDBInit(dbDataLogsPath, true)
+ if err != nil {
+ return nil, err
+ }
+ // logs建表
+ if err = logsDB.AutoMigrate(&LogInfo{}); err != nil {
+ return nil, err
+ }
+
+ itemsAutoMigrate := false
+ // 用于确认是否需要重建LogOneItem数据库
+ if logsDB.Migrator().HasTable(&LogOneItem{}) {
+ if err = logItemsSQLiteMigrate(logsDB); err != nil {
+ return nil, err
+ }
+ } else {
+ itemsAutoMigrate = true
+ }
+ if itemsAutoMigrate {
+ if err = logsDB.AutoMigrate(&LogOneItem{}); err != nil {
+ return nil, err
+ }
+ }
+ return logsDB, nil
+}
+
+func (s *SQLiteEngine) CensorDBInit() (*gorm.DB, error) {
+ dataDir := os.Getenv("DATA_DIR")
+ if dataDir == "" {
+ dataDir = defaultDataDir
+ }
+ path, err := filepath.Abs(filepath.Join(dataDir, "data-censor.db"))
+ if err != nil {
+ return nil, err
+ }
+ censorDB, err := database.SQLiteDBInit(path, true)
+ if err != nil {
+ return nil, err
+ }
+ // 创建基本的表结构,并通过标签定义索引
+ if err = censorDB.AutoMigrate(&CensorLog{}); err != nil {
+ return nil, err
+ }
+ return censorDB, nil
+}
+
+func logItemsSQLiteMigrate(db *gorm.DB) error {
+ type DBColumn struct {
+ Name string
+ Type string
+ }
+
+ // 获取当前列信息
+ var currentColumns []DBColumn
+ err := db.Raw("PRAGMA table_info(log_items)").Scan(¤tColumns).Error
+ if err != nil {
+ return err
+ }
+
+ // 获取模型定义的列信息
+ var modelColumns []DBColumn
+ stmt := &gorm.Statement{DB: db}
+ err = stmt.Parse(&LogOneItem{})
+ if err != nil {
+ return err
+ }
+ for _, field := range stmt.Schema.Fields {
+ if field.DBName != "" {
+ x := db.Migrator().FullDataTypeOf(field)
+ col := strings.SplitN(x.SQL, " ", 2)[0]
+ modelColumns = append(modelColumns, DBColumn{field.DBName, strings.ToLower(col)})
+ }
+ }
+
+ // 比较列是否有变化
+ needMigrate := false
+ if len(currentColumns) != len(modelColumns) {
+ needMigrate = true
+ } else {
+ columnMap := make(map[string]string)
+ for _, col := range currentColumns {
+ columnMap[col.Name] = strings.ToLower(col.Type)
+ }
+
+ for _, col := range modelColumns {
+ newType := col.Type
+ currentType := columnMap[col.Name]
+
+ // 特殊处理 is_dice 列,允许 bool 或 numeric 类型
+ if col.Name == "is_dice" {
+ if currentType != "bool" && currentType != "numeric" {
+ needMigrate = true
+ break
+ }
+ continue
+ }
+
+ if currentType != newType {
+ needMigrate = true
+ break
+ }
+ }
+ }
+
+ // 如果需要迁移则执行
+ if needMigrate {
+ log.Info("现在进行log_items表的迁移,如果数据库较大,会花费较长时间,请耐心等待")
+ log.Info("若是迁移后观察到数据库体积显著膨胀,可以关闭骰子使用 sealdice-core --vacuum 进行数据库整理,这同样会花费较长时间")
+ return db.AutoMigrate()
+ }
+
+ return nil
+}
diff --git a/dice/model/group_info.go b/dice/model/group_info.go
index b13230ba..3f22619a 100644
--- a/dice/model/group_info.go
+++ b/dice/model/group_info.go
@@ -1,142 +1,189 @@
package model
import (
- "fmt"
-
- "github.com/jmoiron/sqlx"
"golang.org/x/time/rate"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
ds "github.com/sealdice/dicescript"
+
+ log "sealdice-core/utils/kratos"
)
-func GroupInfoListGet(db *sqlx.DB, callback func(id string, updatedAt int64, data []byte)) error {
- rows, err := db.Queryx("SELECT id, updated_at, data FROM group_info")
+// GroupInfo 模型
+type GroupInfo struct {
+ ID string `gorm:"column:id;primaryKey"` // 主键,字符串类型
+ CreatedAt int `gorm:"column:created_at"` // 创建时间
+ UpdatedAt *int64 `gorm:"column:updated_at"` // 更新时间,int64类型
+ Data []byte `gorm:"column:data"` // BLOB 类型字段,使用 []byte 表示
+}
+
+func (*GroupInfo) TableName() string {
+ return "group_info"
+}
+
+// GroupInfoListGet 使用 GORM 实现,遍历 group_info 表中的数据并调用回调函数
+func GroupInfoListGet(db *gorm.DB, callback func(id string, updatedAt int64, data []byte)) error {
+ // 创建一个保存查询结果的结构体
+ var results []struct {
+ ID string `gorm:"column:id"` // 字段 id
+ UpdatedAt *int64 `gorm:"column:updated_at"` // 由于可能存在 NULL,定义为指针类型
+ Data []byte `gorm:"column:data"` // 字段 data
+ }
+
+ // 使用 GORM 查询 group_info 表中的 id, updated_at, data 列
+ err := db.Model(&GroupInfo{}).Select("id, updated_at, data").Find(&results).Error
if err != nil {
+ // 如果查询发生错误,返回错误信息
return err
}
- defer rows.Close()
- for rows.Next() {
- var id string
+ // 遍历查询结果
+ for _, result := range results {
var updatedAt int64
- var data []byte
-
- var pUpdatedAt *int64
- err = rows.Scan(&id, &pUpdatedAt, &data)
- if err != nil {
- fmt.Println("!!!", err.Error())
- return err
+ // 如果 updatedAt 是 NULL,需要跳过该字段
+ if result.UpdatedAt != nil {
+ updatedAt = *result.UpdatedAt
}
- if pUpdatedAt != nil {
- updatedAt = *pUpdatedAt
- }
- callback(id, updatedAt, data)
+ // 调用回调函数,传递 id, updatedAt, data
+ callback(result.ID, updatedAt, result.Data)
}
- return rows.Err()
+ // 返回 nil 表示操作成功
+ return nil
}
// GroupInfoSave 保存群组信息
-func GroupInfoSave(db *sqlx.DB, groupID string, updatedAt int64, data []byte) error {
- // INSERT OR REPLACE 语句可以根据是否已存在对应记录自动插入或更新记录
- _, err := db.Exec("INSERT OR REPLACE INTO group_info (id, updated_at, data) VALUES (?, ?, ?)", groupID, updatedAt, data)
- return err
-}
-
-// GroupPlayerNumGet 查询指定群组中玩家数量
-func GroupPlayerNumGet(db *sqlx.DB, groupID string) (int64, error) {
- var count int64
-
- // 使用Named方法绑定命名参数
- // sqlitex.ExecuteTransient(conn, `select count(id) from group_player_info where group_id=$group_id`, &sqlitex.ExecOptions{
- query, args, err := sqlx.Named("SELECT COUNT(id) FROM group_player_info WHERE group_id = :group_id", map[string]interface{}{"group_id": groupID})
- if err != nil {
- return 0, err
+func GroupInfoSave(db *gorm.DB, groupID string, updatedAt int64, data []byte) error {
+ // 使用 gorm 的 Upsert 功能实现插入或更新
+ groupInfo := GroupInfo{
+ ID: groupID,
+ UpdatedAt: &updatedAt,
+ Data: data,
}
-
- // 执行查询并将结果存储到 count 变量中
- if err := db.QueryRowx(query, args...).Scan(&count); err != nil {
- return 0, err
- }
-
- return count, nil
+ result := db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"updated_at", "data"}),
+ }).Create(&groupInfo)
+ return result.Error
}
// GroupPlayerInfoBase 群内玩家信息
type GroupPlayerInfoBase struct {
- Name string `yaml:"name" jsbind:"name"` // 玩家昵称
- UserID string `yaml:"userId" jsbind:"userId"`
- InGroup bool `yaml:"inGroup"` // 是否在群内,有时一个人走了,信息还暂时残留
- LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime"` // 上次发送指令时间
- RateLimiter *rate.Limiter `yaml:"-"` // 限速器
- RateLimitWarned bool `yaml:"-"` // 是否已经警告过限速
- AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate"` // 名片模板
+ // 补充这个字段,从而保证包含主键ID
+ ID uint `yaml:"-" jsbind:"-" gorm:"column:id;primaryKey;autoIncrement"` // 主键ID字段,自增
+ Name string `yaml:"name" jsbind:"name" gorm:"column:name"` // 玩家昵称
+ UserID string `yaml:"userId" jsbind:"userId" gorm:"column:user_id;index:idx_group_player_info_user_id; uniqueIndex:idx_group_player_info_group_user"`
+ // 非数据库信息:是否在群内
+ InGroup bool `yaml:"inGroup" gorm:"-"` // 是否在群内,有时一个人走了,信息还暂时残留
+ LastCommandTime int64 `yaml:"lastCommandTime" jsbind:"lastCommandTime" gorm:"column:last_command_time"` // 上次发送指令时间
+ // 非数据库信息
+ RateLimiter *rate.Limiter `yaml:"-" gorm:"-"` // 限速器
+ // 非数据库信息
+ RateLimitWarned bool `yaml:"-" gorm:"-"` // 是否已经警告过限速
+ AutoSetNameTemplate string `yaml:"autoSetNameTemplate" jsbind:"autoSetNameTemplate" gorm:"column:auto_set_name_template"` // 名片模板
// level int 权限
- DiceSideNum int `yaml:"diceSideNum"` // 面数,为0时等同于d100
- ValueMapTemp *ds.ValueMap `yaml:"-"` // 玩家的群内临时变量
+ DiceSideNum int `yaml:"diceSideNum" gorm:"column:dice_side_num"` // 面数,为0时等同于d100
+ // 非数据库信息
+ ValueMapTemp *ds.ValueMap `yaml:"-" gorm:"-"` // 玩家的群内临时变量
// ValueMapTemp map[string]*VMValue `yaml:"-"` // 玩家的群内临时变量
- TempValueAlias *map[string][]string `yaml:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里?
-
- UpdatedAtTime int64 `yaml:"-" json:"-"`
- RecentUsedTime int64 `yaml:"-" json:"-"`
+ // 非数据库信息
+ TempValueAlias *map[string][]string `yaml:"-" gorm:"-"` // 群内临时变量别名 - 其实这个有点怪的,为什么在这里?
+
+ // 非数据库信息
+ UpdatedAtTime int64 `yaml:"-" json:"-" gorm:"-"`
+ // 非数据库信息
+ RecentUsedTime int64 `yaml:"-" json:"-" gorm:"-"`
+ // 缺少信息 -> 这边原来就是int吗?
+ CreatedAt int `yaml:"-" json:"-" gorm:"column:created_at"` // 创建时间
+ UpdatedAt int `yaml:"-" json:"-" gorm:"column:updated_at"` // 更新时间
+ GroupID string `yaml:"-" json:"-" gorm:"column:group_id;index:idx_group_player_info_group_id; uniqueIndex:idx_group_player_info_group_user"`
}
-func GroupPlayerInfoGet(db *sqlx.DB, groupID string, playerID string) *GroupPlayerInfoBase {
- var ret GroupPlayerInfoBase
+// 兼容设置
+func (GroupPlayerInfoBase) TableName() string {
+ return "group_player_info"
+}
- rows, err := db.NamedQuery("SELECT name, last_command_time, auto_set_name_template, dice_side_num FROM group_player_info WHERE group_id=:group_id AND user_id=:user_id", map[string]interface{}{
- "group_id": groupID,
- "user_id": playerID,
- })
+// GroupPlayerNumGet 获取指定群组的玩家数量
+func GroupPlayerNumGet(db *gorm.DB, groupID string) (int64, error) {
+ var count int64
+ // 使用 GORM 的 Table 方法指定表名进行查询
+ // db.Table("表名").Where("条件").Count(&count) 是通用的 GORM 用法
+ // 将 group_id 作为查询条件
+ err := db.Model(&GroupPlayerInfoBase{}).Where("group_id = ?", groupID).Count(&count).Error
if err != nil {
- fmt.Printf("error getting group player info: %s", err.Error())
- return nil
+ // 如果查询出现错误,返回错误信息
+ return 0, err
}
- defer rows.Close()
-
- // Name: stmt.ColumnText(0),
- // UserId: playerId,
- // LastCommandTime: stmt.ColumnInt64(2),
- // AutoSetNameTemplate: stmt.ColumnText(3),
- // DiceSideNum: int(stmt.ColumnInt64(4)),
-
- exists := false
- for rows.Next() {
- exists = true
- // 使用Scan方法将查询结果映射到结构体中
- if err := rows.Scan(
- &ret.Name,
- &ret.LastCommandTime,
- &ret.AutoSetNameTemplate,
- &ret.DiceSideNum,
- ); err != nil {
- fmt.Printf("error getting group player info: %s", err.Error())
- return nil
- }
+ // 返回统计的数量
+ return count, nil
+}
+
+// GroupPlayerInfoGet 获取指定群组中的玩家信息
+func GroupPlayerInfoGet(db *gorm.DB, groupID string, playerID string) *GroupPlayerInfoBase {
+ var ret GroupPlayerInfoBase
+
+ // 使用 GORM 查询数据并绑定到结构体中
+ // db.Table("表名").Where("条件").First(&ret) 查询一条数据并映射到结构体
+ result := db.Model(&GroupPlayerInfoBase{}).
+ Where("group_id = ? AND user_id = ?", groupID, playerID).
+ Select("name, last_command_time, auto_set_name_template, dice_side_num").
+ Limit(1).
+ Find(&ret)
+ err := result.Error
+ // 如果查询发生错误,打印错误并返回 nil
+ if err != nil {
+ log.Errorf("error getting group player info: %s", err.Error())
+ return nil
}
- if !exists {
+ if result.RowsAffected == 0 {
return nil
}
+
+ // 将 playerID 赋值给结构体中的 UserID 字段
ret.UserID = playerID
+
+ // 返回查询结果
return &ret
}
-func GroupPlayerInfoSave(db *sqlx.DB, groupID string, playerID string, info *GroupPlayerInfoBase) error {
- _, err := db.NamedExec("REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id) VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id)", map[string]interface{}{
+// GroupPlayerInfoSave 保存玩家信息,不再使用 REPLACE INTO 语句
+func GroupPlayerInfoSave(db *gorm.DB, info *GroupPlayerInfoBase) error {
+ // 考虑到info是指针,为了防止可能info还会被用到其他地方,这里的给info指针赋值也是有意义的
+ // 但强烈建议将这段去除掉,数据库层面理论上不应该混杂业务层逻辑?
+ // 判断条件:联合主键相同
+ // TODO: 那自增的ID是干嘛的……
+ conditions := map[string]any{
+ "user_id": info.UserID,
+ "group_id": info.GroupID,
+ }
+ data := map[string]any{
"name": info.Name,
- "updated_at": info.UpdatedAtTime,
+ "user_id": info.UserID,
"last_command_time": info.LastCommandTime,
"auto_set_name_template": info.AutoSetNameTemplate,
"dice_side_num": info.DiceSideNum,
- "group_id": groupID,
- "user_id": playerID,
- })
- return err
+ "group_id": info.GroupID,
+ "updated_at": info.UpdatedAt,
+ }
+ // 原代码逻辑:
+ // REPLACE INTO group_player_info (name, updated_at, last_command_time, auto_set_name_template, dice_side_num, group_id, user_id)
+ // VALUES (:name, :updated_at, :last_command_time, :auto_set_name_template, :dice_side_num, :group_id, :user_id)
+ // 所以它是全局替换,使用Assign方法,无论如何都给我替换
+ if err := db.
+ Where(conditions).
+ Assign(data).FirstOrCreate(&GroupPlayerInfoBase{}).Error; err != nil {
+ return err
+ }
+
+ // 返回 nil 表示操作成功
+ return nil
}
diff --git a/dice/model/log.go b/dice/model/log.go
index 1732fde5..68628fad 100644
--- a/dice/model/log.go
+++ b/dice/model/log.go
@@ -5,10 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
- "strings"
"time"
- "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
+
+ log "sealdice-core/utils/kratos"
)
type LogOne struct {
@@ -18,70 +19,139 @@ type LogOne struct {
}
type LogOneItem struct {
- ID uint64 `json:"id" db:"id"`
- Nickname string `json:"nickname" db:"nickname"`
- IMUserID string `json:"IMUserId" db:"im_userid"`
- Time int64 `json:"time" db:"time"`
- Message string `json:"message" db:"message"`
- IsDice bool `json:"isDice" db:"is_dice"`
- CommandID int64 `json:"commandId" db:"command_id"`
- CommandInfo interface{} `json:"commandInfo" db:"command_info"`
- RawMsgID interface{} `json:"rawMsgId" db:"raw_msg_id"`
-
- UniformID string `json:"uniformId" db:"user_uniform_id"`
- Channel string `json:"channel"`
+ ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
+ LogID uint64 `json:"-" gorm:"column:log_id;index:idx_log_items_log_id"`
+ GroupID string `gorm:"index:idx_log_items_group_id;column:group_id;index:idx_log_delete_by_id"`
+ Nickname string `json:"nickname" gorm:"column:nickname"`
+ IMUserID string `json:"IMUserId" gorm:"column:im_userid"`
+ Time int64 `json:"time" gorm:"column:time"`
+ Message string `json:"message" gorm:"column:message"`
+ IsDice bool `json:"isDice" gorm:"column:is_dice"`
+ CommandID int64 `json:"commandId" gorm:"column:command_id"`
+ CommandInfo interface{} `json:"commandInfo" gorm:"-"`
+ CommandInfoStr string `json:"-" gorm:"column:command_info"`
+ // 这里的RawMsgID 真的什么都有可能
+ RawMsgID interface{} `json:"rawMsgId" gorm:"-"`
+ RawMsgIDStr string `json:"-" gorm:"column:raw_msg_id;index:idx_raw_msg_id;index:idx_log_delete_by_id"`
+ UniformID string `json:"uniformId" gorm:"column:user_uniform_id"`
+ // 数据库里没有的
+ Channel string `json:"channel" gorm:"-"`
+ // 数据库里有,JSON里没有的
+ // 允许default=NULL
+ Removed *int `gorm:"column:removed" json:"-"`
+ ParentID *int `gorm:"column:parent_id" json:"-"`
+}
+
+// 兼容旧版本的数据库设计
+func (*LogOneItem) TableName() string {
+ return "log_items"
+}
+
+// BeforeSave 钩子函数: 查询前,interface{}转换为json
+func (item *LogOneItem) BeforeSave(_ *gorm.DB) (err error) {
+ // 将 CommandInfo 转换为 JSON 字符串保存到 CommandInfoStr
+ if item.CommandInfo != nil {
+ if data, err := json.Marshal(item.CommandInfo); err == nil {
+ item.CommandInfoStr = string(data)
+ } else {
+ return err
+ }
+ }
+
+ // 将 RawMsgID 转换为 string 字符串,保存到 RawMsgIDStr
+ if item.RawMsgID != nil {
+ item.RawMsgIDStr = fmt.Sprintf("%v", item.RawMsgID)
+ }
+
+ return nil
+}
+
+// AfterFind 钩子函数: 查询后,interface{}转换为json
+func (item *LogOneItem) AfterFind(_ *gorm.DB) (err error) {
+ // 将 CommandInfoStr 从 JSON 字符串反序列化为 CommandInfo
+ if item.CommandInfoStr != "" {
+ if err := json.Unmarshal([]byte(item.CommandInfoStr), &item.CommandInfo); err != nil {
+ return err
+ }
+ }
+
+ // 将 RawMsgIDStr string 直接赋值给 RawMsgID
+ if item.RawMsgIDStr != "" {
+ item.RawMsgID = item.RawMsgIDStr
+ }
+
+ return nil
}
type LogInfo struct {
- ID uint64 `json:"id" db:"id"`
- Name string `json:"name" db:"name"`
- GroupID string `json:"groupId" db:"groupId"`
- CreatedAt int64 `json:"createdAt" db:"created_at"`
- UpdatedAt int64 `json:"updatedAt" db:"updated_at"`
- Size int `json:"size" db:"size"`
+ ID uint64 `json:"id" gorm:"primaryKey;autoIncrement;column:id"`
+ Name string `json:"name" gorm:"index:idx_log_group_id_name,unique"`
+ GroupID string `json:"groupId" gorm:"index:idx_logs_group;index:idx_log_group_id_name,unique"`
+ CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
+ UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at;index:idx_logs_update_at"`
+ // 允许数据库NULL值
+ // 原版代码中,此处标记了db:size,但实际上,该列并不存在。
+ // 考虑到该处数据将会为未来log查询提供优化手段,保留该结构体定义,但不使用。
+ // 使用GORM:<-:false 无写入权限,这样它就不会建库,但请注意,下面LogGetLogPage处,如果你查询出的名称不是size
+ // 不能在这里绑定column,因为column会给你建立那一列。
+ // TODO: 将这个字段使用上会不会比后台查询就JOIN更合适?
+ Size *int `json:"size" gorm:"column:size"`
+ // 数据库里有,json不展示的
+ // 允许数据库NULL值(该字段当前不使用)
+ Extra *string `json:"-" gorm:"column:extra"`
+ // 原本标记为:测试版特供,由于原代码每次都会执行,故直接启用此处column记录。
+ UploadURL string `json:"-" gorm:"column:upload_url"` // 测试版特供
+ UploadTime int `json:"-" gorm:"column:upload_time"` // 测试版特供
+}
+
+func (*LogInfo) TableName() string {
+ return "logs"
}
-func LogGetInfo(db *sqlx.DB) ([]int, error) {
+// LogGetInfo 查询日志简略信息,使用通用函数替代SQLITE专属函数
+func LogGetInfo(db *gorm.DB) ([]int, error) {
lst := []int{0, 0, 0, 0}
- err := db.Get(&lst[0], "SELECT seq FROM sqlite_sequence WHERE name == 'logs'")
+
+ var maxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值
+ var itemsMaxID sql.NullInt64 // 使用sql.NullInt64来处理NULL值
+ // 获取 logs 表的记录数和最大 ID
+ err := db.Model(&LogInfo{}).Select("COUNT(*)").Scan(&lst[2]).Error
if err != nil {
return nil, err
}
- err = db.Get(&lst[1], "SELECT seq FROM sqlite_sequence WHERE name == 'log_items'")
+
+ err = db.Model(&LogInfo{}).Select("MAX(id)").Scan(&maxID).Error
if err != nil {
return nil, err
}
- err = db.Get(&lst[2], "SELECT COUNT(*) FROM logs")
+ lst[0] = int(maxID.Int64)
+
+ // 获取 log_items 表的记录数和最大 ID
+ err = db.Model(&LogOneItem{}).Select("COUNT(*)").Scan(&lst[3]).Error
if err != nil {
return nil, err
}
- err = db.Get(&lst[3], "SELECT COUNT(*) FROM log_items")
+
+ err = db.Model(&LogOneItem{}).Select("MAX(id)").Scan(&itemsMaxID).Error
if err != nil {
return nil, err
}
+ lst[1] = int(itemsMaxID.Int64)
+
return lst, nil
}
// Deprecated: replaced by page
-func LogGetLogs(db *sqlx.DB) ([]*LogInfo, error) {
+func LogGetLogs(db *gorm.DB) ([]*LogInfo, error) {
var lst []*LogInfo
- rows, err := db.Queryx("SELECT id,name,group_id,created_at, updated_at FROM logs")
- if err != nil {
+
+ // 使用 GORM 查询 logs 表
+ if err := db.Model(&LogInfo{}).
+ Select("id, name, group_id, created_at, updated_at").
+ Find(&lst).Error; err != nil {
return nil, err
}
- for rows.Next() {
- log := &LogInfo{}
- if err := rows.Scan(
- &log.ID,
- &log.Name,
- &log.GroupID,
- &log.CreatedAt,
- &log.UpdatedAt,
- ); err != nil {
- return nil, err
- }
- lst = append(lst, log)
- }
+
return lst, nil
}
@@ -95,181 +165,138 @@ type QueryLogPage struct {
}
// LogGetLogPage 获取分页
-func LogGetLogPage(db *sqlx.DB, param *QueryLogPage) (int, []*LogInfo, error) {
- countQuery := `SELECT count(*) FROM logs`
- query := `
-SELECT logs.id as id,
- logs.name as name,
- logs.group_id as group_id,
- logs.created_at as created_at,
- logs.updated_at as updated_at,
- count(logs.id) as size
-FROM logs
- LEFT JOIN log_items items ON logs.id = items.log_id
-`
- var conditions []string
+func LogGetLogPage(db *gorm.DB, param *QueryLogPage) (int, []*LogInfo, error) {
+ var lst []*LogInfo
+
+ // 构建基础查询
+ query := db.Model(&LogInfo{}).Select("logs.id, logs.name, logs.group_id, logs.created_at, logs.updated_at,COALESCE(logs.size, 0) as size").Order("logs.updated_at desc")
+ // 添加条件
if param.Name != "" {
- conditions = append(conditions, "logs.name like '%' || :name || '%'")
+ query = query.Where("logs.name LIKE ?", "%"+param.Name+"%")
}
if param.GroupID != "" {
- conditions = append(conditions, "logs.group_id like '%' || :group_id || '%'")
+ query = query.Where("logs.group_id LIKE ?", "%"+param.GroupID+"%")
}
if param.CreatedTimeBegin != "" {
- conditions = append(conditions, "logs.created_at >= :created_time_begin")
+ query = query.Where("logs.created_at >= ?", param.CreatedTimeBegin)
}
if param.CreatedTimeEnd != "" {
- conditions = append(conditions, "logs.created_at <= :created_time_end")
- }
- if len(conditions) > 0 {
- where := " WHERE " + strings.Join(conditions, " AND ")
- query += where
- countQuery += where
+ query = query.Where("logs.created_at <= ?", param.CreatedTimeEnd)
}
- query += fmt.Sprintf(" GROUP BY logs.id LIMIT %d, %d", (param.PageNum-1)*param.PageSize, param.PageSize)
-
- var total int
- count, err := db.NamedQuery(countQuery, param)
- if err != nil {
- return 0, nil, err
- }
- count.Next()
- err = count.Scan(&total)
- if err != nil {
+ // 获取总数
+ var count int64
+ if err := db.Model(&LogInfo{}).Count(&count).Error; err != nil {
return 0, nil, err
}
- lst := make([]*LogInfo, 0, param.PageSize)
- rows, err := db.NamedQuery(query, param)
- if err != nil {
+ // 分页查询
+ query = query.Group("logs.id").Limit(param.PageSize).Offset((param.PageNum - 1) * param.PageSize)
+
+ // 执行查询
+ if err := query.Scan(&lst).Error; err != nil {
return 0, nil, err
}
- for rows.Next() {
- log := &LogInfo{}
- if err := rows.Scan(
- &log.ID,
- &log.Name,
- &log.GroupID,
- &log.CreatedAt,
- &log.UpdatedAt,
- &log.Size,
- ); err != nil {
- return 0, nil, err
- }
- lst = append(lst, log)
- }
- return total, lst, nil
+
+ return int(count), lst, nil
}
// LogGetList 获取列表
-func LogGetList(db *sqlx.DB, groupID string) ([]string, error) {
+func LogGetList(db *gorm.DB, groupID string) ([]string, error) {
var lst []string
- err := db.Select(&lst, "SELECT name FROM logs WHERE group_id = $1 ORDER BY updated_at DESC", groupID)
- if err != nil {
+
+ // 执行查询
+ if err := db.Model(&LogInfo{}).
+ Select("name").
+ Where("group_id = ?", groupID).
+ Order("updated_at DESC").
+ Pluck("name", &lst).Error; err != nil {
return nil, err
}
+
return lst, nil
}
// LogGetIDByGroupIDAndName 获取ID
-func LogGetIDByGroupIDAndName(db *sqlx.DB, groupID string, logName string) (logID int64, err error) {
- err = db.Get(&logID, "SELECT id FROM logs WHERE group_id = $1 AND name = $2", groupID, logName)
+func LogGetIDByGroupIDAndName(db *gorm.DB, groupID string, logName string) (logID uint64, err error) {
+ err = db.Model(&LogInfo{}).
+ Select("id").
+ Where("group_id = ? AND name = ?", groupID, logName).
+ Scan(&logID).Error
+
if err != nil {
// 如果出现错误,判断是否没有找到对应的记录
- if errors.Is(err, sql.ErrNoRows) {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, err
}
+
return logID, nil
}
-func LogGetUploadInfo(db *sqlx.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) {
- res, err := db.Queryx(
- `SELECT updated_at, upload_url, upload_time FROM logs WHERE group_id = $1 AND name = $2`,
- groupID, logName,
- )
- if err != nil {
- return "", 0, 0, err
+// LogGetUploadInfo 获取上传信息
+func LogGetUploadInfo(db *gorm.DB, groupID string, logName string) (url string, uploadTime, updateTime int64, err error) {
+ var logInfo struct {
+ UpdatedAt int64 `gorm:"column:updated_at"`
+ UploadURL string `gorm:"column:upload_url"`
+ UploadTime int64 `gorm:"column:upload_time"`
}
- defer func() { _ = res.Close() }()
- for res.Next() {
- err = res.Scan(&updateTime, &url, &uploadTime)
- if err != nil {
- return "", 0, 0, err
- }
+ err = db.Model(&LogInfo{}).
+ Select("updated_at, upload_url, upload_time").
+ Where("group_id = ? AND name = ?", groupID, logName).
+ Scan(&logInfo).Error
+
+ if err != nil {
+ return "", 0, 0, err
}
+ // 提取结果
+ updateTime = logInfo.UpdatedAt
+ url = logInfo.UploadURL
+ uploadTime = logInfo.UploadTime
return
}
-func LogSetUploadInfo(db *sqlx.DB, groupID string, logName string, url string) error {
+// LogSetUploadInfo 设置上传信息
+func LogSetUploadInfo(db *gorm.DB, groupID string, logName string, url string) error {
if len(url) == 0 {
return nil
}
now := time.Now().Unix()
- _, err := db.Exec(
- `UPDATE logs SET upload_url = $1, upload_time = $2 WHERE group_id = $3 AND name = $4`,
- url, now, groupID, logName,
- )
+ // 使用 GORM 更新上传信息
+ err := db.Model(&LogInfo{}).Where("group_id = ? AND name = ?", groupID, logName).
+ Update("upload_url", url).
+ Update("upload_time", now).
+ Error
+
return err
}
// LogGetAllLines 获取log的所有行数据
-func LogGetAllLines(db *sqlx.DB, groupID string, logName string) ([]*LogOneItem, error) {
+func LogGetAllLines(db *gorm.DB, groupID string, logName string) ([]*LogOneItem, error) {
// 获取log的ID
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil {
return nil, err
}
- // 查询行数据
- rows, err := db.Queryx(`SELECT id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id
- FROM log_items WHERE log_id=$1 ORDER BY time ASC`, logID)
- if err != nil {
- return nil, err
- }
- defer func(rows *sqlx.Rows) {
- _ = rows.Close()
- }(rows)
-
- var ret []*LogOneItem
- for rows.Next() {
- item := &LogOneItem{}
- var commandInfoStr []byte
-
- // 使用Scan方法将查询结果映射到结构体中
- if err := rows.Scan(
- &item.ID,
- &item.Nickname,
- &item.IMUserID,
- &item.Time,
- &item.Message,
- &item.IsDice,
- &item.CommandID,
- &commandInfoStr,
- &item.RawMsgID,
- &item.UniformID,
- ); err != nil {
- return nil, err
- }
-
- // 反序列化commandInfo
- if commandInfoStr != nil {
- _ = json.Unmarshal(commandInfoStr, &item.CommandInfo)
- }
+ var items []*LogOneItem
- ret = append(ret, item)
- }
+ // 查询行数据
+ err = db.Model(&LogOneItem{}).
+ Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id").
+ Where("log_id = ?", logID).
+ Order("time ASC").
+ Find(&items).Error
- if err := rows.Err(); err != nil {
+ if err != nil {
return nil, err
}
-
- return ret, nil
+ return items, nil
}
type QueryLogLinePage struct {
@@ -280,75 +307,33 @@ type QueryLogLinePage struct {
}
// LogGetLinePage 获取log的行分页
-func LogGetLinePage(db *sqlx.DB, param *QueryLogLinePage) ([]*LogOneItem, error) {
+func LogGetLinePage(db *gorm.DB, param *QueryLogLinePage) ([]*LogOneItem, error) {
// 获取log的ID
logID, err := LogGetIDByGroupIDAndName(db, param.GroupID, param.LogName)
if err != nil {
return nil, err
}
+ var items []*LogOneItem
+
// 查询行数据
- rows, err := db.Queryx(`
-SELECT id,
- nickname,
- im_userid,
- time,
- message,
- is_dice,
- command_id,
- command_info,
- raw_msg_id,
- user_uniform_id
-FROM log_items
-WHERE log_id =$1
-ORDER BY time ASC
-LIMIT $2, $3;`, logID, (param.PageNum-1)*param.PageSize, param.PageSize)
+ err = db.Model(&LogOneItem{}).
+ Select("id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id").
+ Where("log_id = ?", logID).
+ Order("time ASC").
+ Limit(param.PageSize).
+ Offset((param.PageNum - 1) * param.PageSize).
+ Scan(&items).Error
if err != nil {
return nil, err
}
- defer func(rows *sqlx.Rows) {
- _ = rows.Close()
- }(rows)
-
- var ret []*LogOneItem
- for rows.Next() {
- item := &LogOneItem{}
- var commandInfoStr []byte
-
- // 使用Scan方法将查询结果映射到结构体中
- if err := rows.Scan(
- &item.ID,
- &item.Nickname,
- &item.IMUserID,
- &item.Time,
- &item.Message,
- &item.IsDice,
- &item.CommandID,
- &commandInfoStr,
- &item.RawMsgID,
- &item.UniformID,
- ); err != nil {
- return nil, err
- }
-
- // 反序列化commandInfo
- if commandInfoStr != nil {
- _ = json.Unmarshal(commandInfoStr, &item.CommandInfo)
- }
-
- ret = append(ret, item)
- }
-
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return ret, nil
+ return items, nil
}
// LogLinesCountGet 获取日志行数
-func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool) {
+func LogLinesCountGet(db *gorm.DB, groupID string, logName string) (int64, bool) {
// 获取日志 ID
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil || logID == 0 {
@@ -357,9 +342,10 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool)
// 获取日志行数
var count int64
- err = db.Get(&count, `
- SELECT COUNT(id) FROM log_items WHERE log_id=$1 AND removed IS NULL
- `, logID)
+ err = db.Model(&LogOneItem{}).
+ Where("log_id = ? and removed IS NULL", logID).
+ Count(&count).Error
+
if err != nil {
return 0, false
}
@@ -368,17 +354,16 @@ func LogLinesCountGet(db *sqlx.DB, groupID string, logName string) (int64, bool)
}
// LogDelete 删除log
-func LogDelete(db *sqlx.DB, groupID string, logName string) bool {
- // 获取 log id
+func LogDelete(db *gorm.DB, groupID string, logName string) bool {
+ // 获取 log ID
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil || logID == 0 {
return false
}
- // 获取文本
- // 通过BeginTxx方法开启事务
- tx, err := db.Beginx()
- if err != nil {
+ // 开启事务
+ tx := db.Begin()
+ if err = tx.Error; err != nil {
return false
}
defer func() {
@@ -387,41 +372,38 @@ func LogDelete(db *sqlx.DB, groupID string, logName string) bool {
}
}()
- // 删除log_id相关的log_items记录
- _, err = tx.Exec("DELETE FROM log_items WHERE log_id = $1", logID)
- if err != nil {
+ // 删除 log_id 相关的 log_items 记录
+ if err = tx.Where("log_id = ?", logID).Delete(&LogOneItem{}).Error; err != nil {
return false
}
- // 删除log_id相关的logs记录
- _, err = tx.Exec("DELETE FROM logs WHERE id = $1", logID)
- if err != nil {
+ // 删除 log_id 相关的 logs 记录
+ if err = tx.Where("id = ?", logID).Delete(&LogInfo{}).Error; err != nil {
return false
}
// 提交事务
- err = tx.Commit()
+ err = tx.Commit().Error
return err == nil
}
// LogAppend 向指定的log中添加一条信息
-func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem) bool {
- // 获取 log id
+func LogAppend(db *gorm.DB, groupID string, logName string, logItem *LogOneItem) bool {
+ // 获取 log ID
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil {
return false
}
- // 如果不存在,创建
+ // 获取当前时间戳
now := time.Now()
nowTimestamp := now.Unix()
// 开始事务
- tx, err := db.Beginx()
- if err != nil {
+ tx := db.Begin()
+ if err = tx.Error; err != nil {
return false
}
- // 执行事务时发生错误时回滚
defer func() {
if err != nil {
_ = tx.Rollback()
@@ -429,65 +411,86 @@ func LogAppend(db *sqlx.DB, groupID string, logName string, logItem *LogOneItem)
}()
if logID == 0 {
- // 创建一个新的log
- query := "INSERT INTO logs (name, group_id, created_at, updated_at) VALUES (?, ?, ?, ?)"
- rst, errNew := tx.Exec(query, logName, groupID, nowTimestamp, nowTimestamp)
- if errNew != nil {
- return false
- }
- // 获取新创建log的ID
- logID, errNew = rst.LastInsertId()
- if errNew != nil {
+ // 创建一个新的 log
+ newLog := LogInfo{Name: logName, GroupID: groupID, CreatedAt: nowTimestamp, UpdatedAt: nowTimestamp}
+ if err = tx.Create(&newLog).Error; err != nil {
return false
}
+ logID = newLog.ID
}
- // 向log_items表中添加一条信息
- data, err := json.Marshal(logItem.CommandInfo)
- query := "INSERT INTO log_items (log_id, group_id, nickname, im_userid, time, message, is_dice, command_id, command_info, raw_msg_id, user_uniform_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ // 向 log_items 表中添加一条信息
+ // Pinenutn: 由此可以推知,CommandInfo必然是一个 map[string]interface{}
- rid := ""
- if logItem.RawMsgID != nil {
- rid = fmt.Sprintf("%v", logItem.RawMsgID)
+ if err != nil {
+ return false
}
- // fmt.Println("log append", logId, rid, "|", groupId, logName)
- _, err = tx.Exec(query, logID, groupID, logItem.Nickname, logItem.IMUserID, nowTimestamp, logItem.Message, logItem.IsDice, logItem.CommandID, data, rid, logItem.UniformID)
- _, err = tx.Exec("UPDATE logs SET updated_at = ? WHERE id = ?", nowTimestamp, logID)
- if err != nil {
+ newLogItem := LogOneItem{
+ LogID: logID,
+ GroupID: groupID,
+ Nickname: logItem.Nickname,
+ IMUserID: logItem.IMUserID,
+ Time: nowTimestamp,
+ Message: logItem.Message,
+ IsDice: logItem.IsDice,
+ CommandID: logItem.CommandID,
+ CommandInfo: logItem.CommandInfo,
+ RawMsgID: logItem.RawMsgID,
+ UniformID: logItem.UniformID,
+ }
+
+ if err = tx.Create(&newLogItem).Error; err != nil {
+ return false
+ }
+
+ // 更新 logs 表中的 updated_at 字段 和 size 字段
+ if err = tx.Model(&LogInfo{}).
+ Where("id = ?", logID).
+ Updates(map[string]interface{}{
+ "updated_at": nowTimestamp,
+ "size": gorm.Expr("COALESCE(size, 0) + ?", 1),
+ }).Error; err != nil {
return false
}
// 提交事务
- err = tx.Commit()
+ err = tx.Commit().Error
return err == nil
}
// LogMarkDeleteByMsgID 撤回删除
-func LogMarkDeleteByMsgID(db *sqlx.DB, groupID string, logName string, rawID interface{}) error {
+func LogMarkDeleteByMsgID(db *gorm.DB, groupID string, logName string, rawID interface{}) error {
// 获取 log id
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil {
return err
}
-
- // 删除记录
- rid := ""
- if rawID != nil {
- rid = fmt.Sprintf("%v", rawID)
+ rid := fmt.Sprintf("%v", rawID)
+ tx := db.Begin()
+ defer func() {
+ if err != nil {
+ _ = tx.Rollback()
+ }
+ }()
+ if err = tx.Where("log_id = ? AND raw_msg_id = ?", logID, rid).Delete(&LogOneItem{}).Error; err != nil {
+ log.Errorf("log delete error %s", err.Error())
+ return err
}
-
- // fmt.Printf("log delete %v %d\n", rawId, logId)
- _, err = db.Exec("DELETE FROM log_items WHERE log_id=? AND raw_msg_id=?", logID, rid)
- if err != nil {
- fmt.Println("log delete error", err.Error())
+ // 更新 logs 表中的 updated_at 字段 和 size 字段
+ // 真的有默认为NULL还能触发删除的情况吗?!
+ if err = tx.Model(&LogInfo{}).Where("id = ?", logID).Updates(map[string]interface{}{
+ "updated_at": time.Now().Unix(),
+ "size": gorm.Expr("COALESCE(size, 0) - ?", 1),
+ }).Error; err != nil {
return err
}
-
- return nil
+ err = tx.Commit().Error
+ return err
}
-func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID interface{}) error {
+// LogEditByMsgID 编辑日志
+func LogEditByMsgID(db *gorm.DB, groupID, logName, newContent string, rawID interface{}) error {
logID, err := LogGetIDByGroupIDAndName(db, groupID, logName)
if err != nil {
return err
@@ -498,11 +501,11 @@ func LogEditByMsgID(db *sqlx.DB, groupID, logName, newContent string, rawID inte
rid = fmt.Sprintf("%v", rawID)
}
- _, err = db.Exec(`UPDATE log_items
-SET message = ?
-WHERE log_id = ? AND raw_msg_id = ?`, newContent, logID, rid)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
+ // 更新 log_items 表中的内容
+ if err := db.Model(&LogOneItem{}).
+ Where("log_id = ? AND raw_msg_id = ?", logID, rid).
+ Update("message", newContent).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("log edit: %w", err)
diff --git a/dice/platform_adapter_gocq.go b/dice/platform_adapter_gocq.go
index 223afc58..fad8f55b 100644
--- a/dice/platform_adapter_gocq.go
+++ b/dice/platform_adapter_gocq.go
@@ -2,6 +2,7 @@ package dice
import (
"encoding/json"
+ "errors"
"fmt"
"math/rand"
"os"
@@ -14,15 +15,18 @@ import (
"syscall"
"time"
- "github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
- "github.com/sacOO7/gowebsocket"
- "github.com/samber/lo"
- "go.uber.org/zap"
+ "github.com/tidwall/gjson"
+ "github.com/tidwall/sjson"
"gopkg.in/yaml.v3"
"sealdice-core/message"
+ log "sealdice-core/utils/kratos"
"sealdice-core/utils/procs"
+
+ "github.com/gorilla/websocket"
+ "github.com/sacOO7/gowebsocket"
+ "github.com/samber/lo"
)
// 0 默认 1登录中 2登录中-二维码 3登录中-滑条 4登录中-手机验证码 10登录成功 11登录失败
@@ -96,6 +100,8 @@ type PlatformAdapterGocq struct {
riskAlertShieldCount int // 风控警告屏蔽次数,一个临时变量
useArrayMessage bool `yaml:"-"` // 使用分段消息
lagrangeRebootTimes int
+ SignServerVer string `yaml:"signServerVer" json:"signServerVer"` // 用于前端显示
+ SignServerName string `yaml:"signServerName" json:"signServerName"` // 用于前端显示
}
type Sender struct {
@@ -273,38 +279,61 @@ func FormatDiceIDQQChGroup(guildID, channelID string) string {
return fmt.Sprintf("QQ-CH-Group:%s-%s", guildID, channelID)
}
-func tryParseOneBot11ArrayMessage(log *zap.SugaredLogger, message string, writeTo *MessageQQ) error {
- msgQQType2 := new(MessageQQArray)
- err := json.Unmarshal([]byte(message), msgQQType2)
+func hasURLScheme(text string) bool {
+ // 正则表达式匹配三种情况:file URI、http(s) URL 和 base64 URI
+ regex := `^[a-z]+://`
+ match, _ := regexp.MatchString(regex, text)
+ return match
+}
- if err != nil {
+func tryParseOneBot11ArrayMessage(log *log.Helper, message string, writeTo *MessageQQ) error {
+ // 不合法的信息体
+ if !gjson.Valid(message) {
log.Warn("无法解析 onebot11 字段:", message)
- return err
+ return errors.New("解析失败")
}
-
+ // 原版本转换为gjson对象
+ parseContent := gjson.Parse(message)
+ arrayContent := parseContent.Get("message").Array()
cqMessage := strings.Builder{}
- for _, i := range msgQQType2.Message {
- switch i.Type {
+ for _, i := range arrayContent {
+ // 使用String()方法,如果为空,会自动产生空字符串
+ typeStr := i.Get("type").String()
+ dataObj := i.Get("data")
+ switch typeStr {
case "text":
- cqMessage.WriteString(i.Data["text"].(string))
+ cqMessage.WriteString(dataObj.Get("text").String())
case "image":
- cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", i.Data["file"]))
+ // 兼容NC情况, 此时file字段只有文件名, 完整URL在url字段
+ if !hasURLScheme(dataObj.Get("file").String()) && hasURLScheme(dataObj.Get("url").String()) {
+ cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", dataObj.Get("url").String()))
+ } else {
+ cqMessage.WriteString(fmt.Sprintf("[CQ:image,file=%v]", dataObj.Get("file").String()))
+ }
case "face":
// 兼容四叶草,移除 .(string)。自动获取的信息表示此类型为 float64,这是go解析的问题
- cqMessage.WriteString(fmt.Sprintf("[CQ:face,id=%v]", i.Data["id"]))
+ cqMessage.WriteString(fmt.Sprintf("[CQ:face,id=%v]", dataObj.Get("id").String()))
case "record":
- cqMessage.WriteString(fmt.Sprintf("[CQ:record,file=%v]", i.Data["file"]))
+ cqMessage.WriteString(fmt.Sprintf("[CQ:record,file=%v]", dataObj.Get("file").String()))
case "at":
- cqMessage.WriteString(fmt.Sprintf("[CQ:at,qq=%v]", i.Data["qq"]))
+ cqMessage.WriteString(fmt.Sprintf("[CQ:at,qq=%v]", dataObj.Get("qq").String()))
case "poke":
cqMessage.WriteString("[CQ:poke]")
case "reply":
- cqMessage.WriteString(fmt.Sprintf("[CQ:reply,id=%v]", i.Data["id"]))
+ cqMessage.WriteString(fmt.Sprintf("[CQ:reply,id=%v]", dataObj.Get("id").String()))
}
}
- writeTo.MessageQQBase = msgQQType2.MessageQQBase
- writeTo.Message = cqMessage.String()
+ // 赋值对应的Message
+ tempStr, err := sjson.Set(parseContent.String(), "message", cqMessage.String())
+ if err != nil {
+ return err
+ }
+ // 返回被转换成结构体的结果
+ err = json.Unmarshal([]byte(tempStr), &writeTo)
+ if err != nil {
+ return err
+ }
return nil
}
@@ -329,8 +358,14 @@ func OneBot11CqMessageToArrayMessage(longText string) []interface{} {
// 将 CQ 拼入数组
switch cq.Type {
case "image":
- i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["file"]}}
- arr = append(arr, i)
+ // 兼容NC情况, 此时file字段只有文件名, 完整URL在url字段
+ if !hasURLScheme(cq.Args["file"]) && hasURLScheme(cq.Args["url"]) {
+ i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["url"]}}
+ arr = append(arr, i)
+ } else {
+ i := OneBotV11ArrMsgItem[OneBotV11MsgItemImageType]{Type: "image", Data: OneBotV11MsgItemImageType{File: cq.Args["file"]}}
+ arr = append(arr, i)
+ }
case "record":
i := OneBotV11ArrMsgItem[OneBotV11MsgItemRecordType]{Type: "record", Data: OneBotV11MsgItemRecordType{File: cq.Args["file"]}}
arr = append(arr, i)
@@ -366,7 +401,7 @@ func (pa *PlatformAdapterGocq) SendSegmentToPerson(ctx *MsgContext, userID strin
}
func (pa *PlatformAdapterGocq) Serve() int {
- if pa.BuiltinMode == "lagrange" {
+ if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" {
pa.Implementation = "lagrange"
} else {
pa.Implementation = "gocq"
@@ -429,9 +464,9 @@ func (pa *PlatformAdapterGocq) Serve() int {
// log.Info("...", message)
// }
if strings.Contains(message, `"guild_id"`) {
- // log.Info("!!!", message, s.Parent.WorkInQQChannel)
+ // log.Info("!!!", message, s.Parent.Config.WorkInQQChannel)
// 暂时忽略频道消息
- if s.Parent.WorkInQQChannel {
+ if s.Parent.Config.WorkInQQChannel {
pa.QQChannelTrySolve(message)
}
return
@@ -529,9 +564,9 @@ func (pa *PlatformAdapterGocq) Serve() int {
// 处理被强制拉群的情况
uid := groupInfo.InviteUserID
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
// 如果是被ban之后拉群,判定为强制拉群
if groupInfo.EnteredTime > 0 && groupInfo.EnteredTime > banInfo.BanTime {
text := fmt.Sprintf("本次入群为遭遇强制邀请,即将主动退群,因为邀请人%s正处于黑名单上。打扰各位还请见谅。感谢使用海豹核心。", groupInfo.InviteUserID)
@@ -544,7 +579,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
}
// 强制拉群情况2 - 群在黑名单
- banInfo, ok = ctx.Dice.BanList.GetByID(groupID)
+ banInfo, ok = ctx.Dice.Config.BanList.GetByID(groupID)
if ok {
if banInfo.Rank == BanRankBanned {
// 如果是被ban之后拉群,判定为强制拉群
@@ -560,7 +595,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
} else {
// TODO: 这玩意的创建是个专业活,等下来弄
// session.ServiceAtNew[groupId] = GroupInfo{}
- fmt.Println("TODO create group")
+ log.Debug("TODO create group")
}
// 这句话太吵了
// log.Debug("群信息刷新: ", msgQQ.Data.GroupName)
@@ -593,9 +628,9 @@ func (pa *PlatformAdapterGocq) Serve() int {
tempInviteMap2[msg.GroupID] = uid
// 邀请人在黑名单上
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "黑名单")
return
}
@@ -603,13 +638,13 @@ func (pa *PlatformAdapterGocq) Serve() int {
// 信任模式,如果不是信任,又不是master则拒绝拉群邀请
isMaster := ctx.Dice.IsMaster(uid)
- if ctx.Dice.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
+ if ctx.Dice.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "只允许骰主设置信任的人拉群")
return
}
// 群在黑名单上
- banInfo, ok = ctx.Dice.BanList.GetByID(msg.GroupID)
+ banInfo, ok = ctx.Dice.Config.BanList.GetByID(msg.GroupID)
if ok {
if banInfo.Rank == BanRankBanned {
pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "群黑名单")
@@ -617,7 +652,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
}
}
- if ctx.Dice.RefuseGroupInvite {
+ if ctx.Dice.Config.RefuseGroupInvite {
pa.SetGroupAddRequest(msgQQ.Flag, msgQQ.SubType, false, "设置拒绝加群")
return
}
@@ -645,7 +680,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
comment = strings.ReplaceAll(comment, "\u00a0", "")
}
- toMatch := strings.TrimSpace(session.Parent.FriendAddComment)
+ toMatch := strings.TrimSpace(session.Parent.Config.FriendAddComment)
willAccept := comment == DiceFormat(ctx, toMatch)
if toMatch == "" {
willAccept = true
@@ -666,7 +701,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
if len(m2) == len(items) {
ok := true
- for i := 0; i < len(m2); i++ {
+ for i := range m2 {
if m2[i] != items[i] {
ok = false
break
@@ -685,9 +720,9 @@ func (pa *PlatformAdapterGocq) Serve() int {
// 检查黑名单
extra := ""
uid := FormatDiceIDQQ(string(msgQQ.UserID))
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
if willAccept {
extra = "。回答正确,但为被禁止用户,准备自动拒绝"
} else {
@@ -924,7 +959,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
skip := false
skipReason := ""
- banInfo, ok := ctx.Dice.BanList.GetByID(opUID)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(opUID)
if ok {
if banInfo.Rank == 30 {
skip = true
@@ -940,7 +975,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
if skip {
extra = fmt.Sprintf("\n取消处罚,原因为%s", skipReason)
} else {
- ctx.Dice.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx)
}
txt := fmt.Sprintf("被踢出群: 在QQ群组<%s>(%s)中被踢出,操作者:<%s>(%s)%s", groupName, msgQQ.GroupID, userName, msgQQ.OperatorID, extra)
@@ -958,6 +993,18 @@ func (pa *PlatformAdapterGocq) Serve() int {
// {"group_id":564808710,"notice_type":"group_decrease","operator_id":2589922907,"post_type":"notice","self_id":2589922907,"sub_type":"leave","time":1651584460,"user_id":2589922907}
groupName := dm.TryGetGroupName(msg.GroupID)
txt := fmt.Sprintf("离开群组或群解散: <%s>(%s)", groupName, msgQQ.GroupID)
+ // 这个就是要删除的部分,离开这个群组=群组退出=删除对应的群聊绑定信息(也就是用户的骰子和这个群聊无关了)
+ // 同时考虑到:QQ群团队发布公告称,由于业务调整,“恢复QQ群”功能将于2023年10月13日起正式下线,届时涉及QQ群相关的恢复功能都将无法使用,可以安心删除群聊对应绑定信息。
+ group, exists := session.ServiceAtNew.Load(msg.GroupID)
+ if !exists {
+ txtErr := fmt.Sprintf("离开群组或群解散,删除对应群聊信息失败: <%s>(%s)", groupName, msgQQ.GroupID)
+ log.Error(txtErr)
+ ctx.Notice(txtErr)
+ return
+ }
+ // TODO:存疑,根据DISMISS的代码复制而来
+ group.DiceIDExistsMap.Delete(ep.UserID)
+ group.UpdatedAtTime = time.Now().Unix()
log.Info(txt)
ctx.Notice(txt)
return
@@ -971,7 +1018,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
groupName := dm.TryGetGroupName(msg.GroupID)
userName := dm.TryGetUserName(opUID)
- ctx.Dice.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx)
txt := fmt.Sprintf("被禁言: 在群组<%s>(%s)中被禁言,时长%d秒,操作者:<%s>(%s)", groupName, msgQQ.GroupID, msgQQ.Duration, userName, msgQQ.OperatorID)
log.Info(txt)
ctx.Notice(txt)
@@ -992,7 +1039,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
if pa.riskAlertShieldCount > 0 {
pa.riskAlertShieldCount--
} else {
- fmt.Println("群消息发送失败: 账号可能被风控")
+ log.Warn("群消息发送失败: 账号可能被风控")
_ = ctx.Dice.SendMail("群消息发送失败: 账号可能被风控", MailTypeCIAMLock)
}
}
@@ -1002,7 +1049,7 @@ func (pa *PlatformAdapterGocq) Serve() int {
// {"post_type":"notice","notice_type":"notify","time":1672489767,"self_id":2589922907,"sub_type":"poke","group_id":131687852,"user_id":303451945,"sender_id":303451945,"target_id":2589922907}
// 检查设置中是否开启
- if !ctx.Dice.QQEnablePoke {
+ if !ctx.Dice.Config.QQEnablePoke {
return
}
@@ -1050,21 +1097,15 @@ func (pa *PlatformAdapterGocq) Serve() int {
}
session.Execute(ep, msg, false)
} else {
- fmt.Println("Received message " + message)
+ log.Debug("Received message " + message)
}
}
- socket.OnBinaryMessage = func(data []byte, socket gowebsocket.Socket) {
- log.Debug("Recieved binary data ", data)
- }
+ socket.OnBinaryMessage = func(_ /* data */ []byte, _ /* socket */ gowebsocket.Socket) {}
- socket.OnPingReceived = func(data string, socket gowebsocket.Socket) {
- log.Debug("Recieved ping " + data)
- }
+ socket.OnPingReceived = func(_ /* data */ string, _ /* socket */ gowebsocket.Socket) {}
- socket.OnPongReceived = func(data string, socket gowebsocket.Socket) {
- log.Debug("Recieved pong " + data)
- }
+ socket.OnPongReceived = func(_ /* data */ string, _ /* socket */ gowebsocket.Socket) {}
var lastDisconnect int64
socket.OnDisconnected = func(err error, socket gowebsocket.Socket) {
@@ -1182,7 +1223,7 @@ func (pa *PlatformAdapterGocq) DoRelogin() bool {
if pa.InPackGoCqhttpDisconnectedCH != nil {
pa.InPackGoCqhttpDisconnectedCH <- -1
}
- if pa.BuiltinMode == "lagrange" {
+ if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" {
myDice.Logger.Infof("重新启动 lagrange 进程,对应账号: <%s>(%s)", ep.Nickname, ep.UserID)
pa.CurLoginIndex++
pa.GoCqhttpState = StateCodeInit
@@ -1229,7 +1270,7 @@ func (pa *PlatformAdapterGocq) SetEnable(enable bool) {
c.Enable = true
if pa.UseInPackClient {
- if pa.BuiltinMode == "lagrange" {
+ if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" {
BuiltinQQServeProcessKill(d, c)
time.Sleep(1 * time.Second)
LagrangeServe(d, c, LagrangeLoginInfo{
diff --git a/dice/platform_adapter_gocq_actions.go b/dice/platform_adapter_gocq_actions.go
index 0963a629..aa1fb291 100644
--- a/dice/platform_adapter_gocq_actions.go
+++ b/dice/platform_adapter_gocq_actions.go
@@ -139,6 +139,7 @@ func socketSendText(socket *gowebsocket.Socket, s string) {
}()
if socket != nil {
+ // 什么也不做,这样就能用来做不发话的测试
socket.SendText(s)
}
}
@@ -159,8 +160,8 @@ func socketSendBinary(socket *gowebsocket.Socket, data []byte) { //nolint
func doSleepQQ(ctx *MsgContext) {
if ctx.Dice != nil {
d := ctx.Dice
- offset := d.MessageDelayRangeEnd - d.MessageDelayRangeStart
- time.Sleep(time.Duration((d.MessageDelayRangeStart + rand.Float64()*offset) * float64(time.Second)))
+ offset := d.Config.MessageDelayRangeEnd - d.Config.MessageDelayRangeStart
+ time.Sleep(time.Duration((d.Config.MessageDelayRangeStart + rand.Float64()*offset) * float64(time.Second)))
} else {
time.Sleep(time.Duration((0.4 + rand.Float64()/2) * float64(time.Second)))
}
@@ -197,6 +198,18 @@ func (pa *PlatformAdapterGocq) SendToPerson(ctx *MsgContext, userID string, text
text = textAssetsConvert(text)
texts := textSplit(text)
+
+ for index, subText := range texts {
+ re := regexp.MustCompile(`\[CQ:poke,qq=(\d+)\]`)
+
+ if re.MatchString(subText) {
+ re = regexp.MustCompile(`\d+`)
+ qq := re.FindStringSubmatch(subText)
+ pa.FriendPoke(qq[0])
+ texts = append(texts[:index], texts[index+1:]...)
+ }
+ }
+
for _, subText := range texts {
a, _ := json.Marshal(oneBotCommand{
Action: "send_msg",
@@ -211,6 +224,41 @@ func (pa *PlatformAdapterGocq) SendToPerson(ctx *MsgContext, userID string, text
}
}
+type PokeStruct struct {
+ UserID int64 `json:"user_id"`
+ GroupID int64 `json:"group_id,omitempty"`
+}
+
+func (pa *PlatformAdapterGocq) FriendPoke(userId string) {
+ userID, _ := strconv.ParseInt(userId, 10, 64)
+
+ text, _ := json.Marshal(oneBotCommand{
+ Action: "friend_poke",
+ Params: PokeStruct{
+ UserID: userID,
+ },
+ })
+ s := string(text)
+
+ socketSendText(pa.Socket, s)
+}
+
+func (pa *PlatformAdapterGocq) GroupPoke(ctx *MsgContext, userId string) {
+ groupId := strings.ReplaceAll(ctx.Group.GroupID, "QQ-Group:", "")
+ groupID, _ := strconv.ParseInt(groupId, 10, 64)
+ userID, _ := strconv.ParseInt(userId, 10, 64)
+
+ text, _ := json.Marshal(oneBotCommand{
+ Action: "group_poke",
+ Params: PokeStruct{
+ UserID: userID,
+ GroupID: groupID,
+ },
+ })
+ s := string(text)
+ socketSendText(pa.Socket, s)
+}
+
func (pa *PlatformAdapterGocq) SendToGroup(ctx *MsgContext, groupID string, text string, flag string) {
if groupID == "" {
return
@@ -254,6 +302,17 @@ func (pa *PlatformAdapterGocq) SendToGroup(ctx *MsgContext, groupID string, text
text = textAssetsConvert(text)
texts := textSplit(text)
+ for index, subText := range texts {
+ re := regexp.MustCompile(`\[CQ:poke,qq=(\d+)\]`)
+
+ if re.MatchString(subText) {
+ re = regexp.MustCompile(`\d+`)
+ qq := re.FindStringSubmatch(subText)
+ pa.GroupPoke(ctx, qq[0])
+ texts = append(texts[:index], texts[index+1:]...)
+ }
+ }
+
for index, subText := range texts {
var a []byte
if pa.useArrayMessage {
@@ -626,7 +685,6 @@ func textSplit(input string) []string {
input = input[0:span[0]] + input[span[1]:]
}
}
-
splits := utils.SplitLongText(input, 2000, utils.DefaultSplitPaginationHint)
splits = append(splits, poke...)
diff --git a/dice/platform_adapter_gocq_channel.go b/dice/platform_adapter_gocq_channel.go
index 2adc9f6c..dcf7a41b 100644
--- a/dice/platform_adapter_gocq_channel.go
+++ b/dice/platform_adapter_gocq_channel.go
@@ -2,7 +2,6 @@ package dice
import (
"encoding/json"
- "fmt"
"strings"
"sealdice-core/dice/model"
@@ -88,8 +87,6 @@ func (pa *PlatformAdapterGocq) QQChannelTrySolve(message string) {
// fmt.Println("Recieved message1 " + message)
session.Execute(ep, msg, false)
- } else {
- fmt.Println("CH Recieved message " + message)
}
}
// pa.SendToChannelGroup(ctx, msg.GroupId, msg.Message+"asdasd", "")
diff --git a/dice/platform_adapter_gocq_helper.go b/dice/platform_adapter_gocq_helper.go
index 0a7f121e..1d5ba3b3 100644
--- a/dice/platform_adapter_gocq_helper.go
+++ b/dice/platform_adapter_gocq_helper.go
@@ -12,15 +12,16 @@ import (
"regexp"
"runtime"
"runtime/debug"
+ "strconv"
"strings"
"time"
"github.com/ShiraazMoollatjie/goluhn"
"github.com/acarl005/stripansi"
"github.com/google/uuid"
- "go.uber.org/zap"
"sealdice-core/utils"
+ log "sealdice-core/utils/kratos"
"sealdice-core/utils/procs"
)
@@ -74,7 +75,7 @@ func RandString(n int) string {
r := rand.New(rand.NewSource(time.Now().Unix()))
bytes := make([]byte, n)
- for i := 0; i < n; i++ {
+ for i := range n {
b := r.Intn(26) + 65
bytes[i] = byte(b)
}
@@ -380,8 +381,8 @@ servers:
`
func GenerateConfig(qq int64, port int, info GoCqhttpLoginInfo) string {
- ret := strings.ReplaceAll(defaultConfig, "{WS端口}", fmt.Sprintf("%d", port))
- ret = strings.Replace(ret, "{QQ帐号}", fmt.Sprintf("%d", qq), 1)
+ ret := strings.ReplaceAll(defaultConfig, "{WS端口}", strconv.Itoa(port))
+ ret = strings.Replace(ret, "{QQ帐号}", strconv.FormatInt(qq, 10), 1)
ret = strings.Replace(ret, "{QQ密码}", info.Password, 1)
if info.UseSignServer && info.SignServerConfig != nil {
@@ -820,7 +821,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
pa.GoCqhttpLoginCaptcha = ""
go func() {
// 检查是否有短信验证码
- for i := 0; i < 100; i++ {
+ for range 100 {
if pa.GoCqhttpState != GoCqhttpStateCodeInLoginBar {
break
}
@@ -844,7 +845,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
pa.GoCqhttpLoginVerifyCode = ""
go func() {
// 检查是否有短信验证码
- for i := 0; i < 100; i++ {
+ for range 100 {
if pa.GoCqhttpState != GoCqhttpStateCodeInLoginVerifyCode {
break
}
@@ -881,7 +882,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
if strings.Contains(line, "请使用手机QQ扫描二维码以继续登录") {
// TODO
- fmt.Println("请使用手机QQ扫描二维码以继续登录")
+ log.Info("请使用手机QQ扫描二维码以继续登录")
}
if (pa.IsLoginSuccessed() && strings.Contains(line, "[ERROR]:") && strings.Contains(line, "Protocol -> sendPacket msg error: 120")) || strings.Contains(line, "账号可能被风控####2测试触发语句") {
@@ -930,11 +931,11 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
if !skip {
dice.Logger.Infof("onebot | %s", stripansi.Strip(line))
} else if strings.HasSuffix(line, "\n") {
- fmt.Printf("onebot | %s", line)
+ dice.Logger.Infof("onebot | %s", line[:len(line)-1])
}
} else {
if strings.HasSuffix(line, "\n") {
- fmt.Printf("onebot | %s", line)
+ dice.Logger.Infof("onebot | %s", line[:len(line)-1])
}
skip := false
@@ -963,7 +964,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
<-chQrCode
if _, err := os.Stat(qrcodeFile); err == nil {
dice.Logger.Info("onebot: 二维码已经就绪")
- fmt.Println("如控制台二维码不好扫描,可以手动打开 ./data/default/extra/go-cqhttp-qqXXXXX 目录下qrcode.png")
+ fmt.Fprintln(os.Stdout, "如控制台二维码不好扫描,可以手动打开 ./data/default/extra/go-cqhttp-qqXXXXX 目录下qrcode.png")
qrdata, err := os.ReadFile(qrcodeFile)
if err == nil {
pa.GoCqhttpState = StateCodeInLoginQrCode
@@ -1036,7 +1037,7 @@ func builtinGoCqhttpServe(dice *Dice, conn *EndPointInfo, loginInfo GoCqhttpLogi
var isGocqDownloading = false
-func downloadGoCqhttp(logger *zap.SugaredLogger) {
+func downloadGoCqhttp(logger *log.Helper) {
fn := "go-cqhttp/go-cqhttp"
if runtime.GOOS == "windows" {
fn += ".exe"
diff --git a/dice/platform_adapter_gocq_helper_others.go b/dice/platform_adapter_gocq_helper_others.go
index 9e04044e..be107d06 100644
--- a/dice/platform_adapter_gocq_helper_others.go
+++ b/dice/platform_adapter_gocq_helper_others.go
@@ -8,7 +8,7 @@ import "os"
type ProcessExitGroup uintptr
func NewProcessExitGroup() (ProcessExitGroup, error) {
- return 0, nil
+ return 0, nil //nolint:nilnil
}
func (g ProcessExitGroup) Dispose() error {
diff --git a/dice/platform_adapter_kook.go b/dice/platform_adapter_kook.go
index 55c669f4..c01d3193 100644
--- a/dice/platform_adapter_kook.go
+++ b/dice/platform_adapter_kook.go
@@ -412,6 +412,9 @@ func (pa *PlatformAdapterKook) SendSegmentToPerson(ctx *MsgContext, userID strin
}
func (pa *PlatformAdapterKook) SendToPerson(ctx *MsgContext, userID string, text string, flag string) {
+ if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 {
+ return
+ }
channel, err := pa.IntentSession.UserChatCreate(ExtractKookUserID(userID))
if err != nil {
pa.Session.Parent.Logger.Errorf("创建Kook用户#%s的私聊频道时出错:%s", userID, err)
@@ -430,6 +433,9 @@ func (pa *PlatformAdapterKook) SendToPerson(ctx *MsgContext, userID string, text
}
func (pa *PlatformAdapterKook) SendToGroup(ctx *MsgContext, groupID string, text string, flag string) {
+ if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 {
+ return
+ }
pa.SendToChannelRaw(ExtractKookChannelID(groupID), text, false)
pa.Session.OnMessageSend(ctx, &Message{
Platform: "KOOK",
@@ -444,6 +450,9 @@ func (pa *PlatformAdapterKook) SendToGroup(ctx *MsgContext, groupID string, text
}
func (pa *PlatformAdapterKook) SendFileToPerson(_ *MsgContext, userID string, path string, _ string) {
+ if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 {
+ return
+ }
channel, err := pa.IntentSession.UserChatCreate(ExtractKookUserID(userID))
if err != nil {
pa.Session.Parent.Logger.Errorf("创建Kook用户#%s的私聊频道时出错:%s", userID, err)
@@ -453,6 +462,9 @@ func (pa *PlatformAdapterKook) SendFileToPerson(_ *MsgContext, userID string, pa
}
func (pa *PlatformAdapterKook) SendFileToGroup(_ *MsgContext, groupID string, path string, _ string) {
+ if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 {
+ return
+ }
pa.SendFileToChannelRaw(ExtractKookChannelID(groupID), path, false)
}
@@ -474,6 +486,9 @@ func (pa *PlatformAdapterKook) EditMessage(ctx *MsgContext, msgID, message strin
func (pa *PlatformAdapterKook) RecallMessage(ctx *MsgContext, msgID string) {
// TODO: not tested
+ if !pa.EndPoint.Enable || pa.IntentSession == nil || pa.EndPoint.State != 1 {
+ return
+ }
_ = pa.IntentSession.MessageDelete(msgID)
}
diff --git a/dice/platform_adapter_lagrange_helper.go b/dice/platform_adapter_lagrange_helper.go
index 1827b9e5..62c98856 100644
--- a/dice/platform_adapter_lagrange_helper.go
+++ b/dice/platform_adapter_lagrange_helper.go
@@ -4,23 +4,29 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io"
+ "net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
+ "strconv"
"strings"
+ "sync"
"time"
"github.com/google/uuid"
+ "gopkg.in/yaml.v3"
+ log "sealdice-core/utils/kratos"
"sealdice-core/utils/procs"
)
type LagrangeLoginInfo struct {
UIN int64
- SignServerUrl string
+ SignServerName string
SignServerVersion string
IsAsyncRun bool
}
@@ -30,19 +36,23 @@ func lagrangeGetWorkDir(dice *Dice, conn *EndPointInfo) string {
return workDir
}
-func NewLagrangeConnectInfoItem(account string) *EndPointInfo {
+func NewLagrangeConnectInfoItem(account string, isGocq bool) *EndPointInfo {
conn := new(EndPointInfo)
conn.ID = uuid.New().String()
conn.Platform = "QQ"
conn.ProtocolType = "onebot"
conn.Enable = false
conn.RelWorkDir = "extra/lagrange-qq" + account
-
conn.Adapter = &PlatformAdapterGocq{
EndPoint: conn,
UseInPackClient: true,
BuiltinMode: "lagrange",
}
+
+ if isGocq {
+ conn.RelWorkDir = "extra/lagrange-gocq-qq" + account
+ conn.Adapter.(*PlatformAdapterGocq).BuiltinMode = "lagrange-gocq"
+ }
return conn
}
@@ -53,11 +63,15 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
loginIndex := pa.CurLoginIndex
pa.GoCqhttpState = StateCodeInLogin
- if pa.UseInPackClient && pa.BuiltinMode == "lagrange" { //nolint:nestif
- log := dice.Logger
+ if pa.UseInPackClient && (pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq") { //nolint:nestif
+ helper := log.NewCustomHelper(log.LOG_LAGR, false, nil)
if dice.ContainerMode {
- log.Warn("onebot: 尝试启动内置客户端,但内置客户端在容器模式下被禁用")
+ if pa.BuiltinMode == "lagrange" {
+ helper.Warn("onebot: 尝试启动内置客户端,但内置客户端在容器模式下被禁用")
+ } else {
+ helper.Warn("onebot: 尝试启动内置gocq,但内置gocq在容器模式下被禁用")
+ }
conn.State = 3
pa.GoCqhttpState = StateCodeLoginFailed
dice.Save(false)
@@ -68,40 +82,66 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
_ = os.MkdirAll(workDir, 0o755)
wd, _ := os.Getwd()
exeFilePath, _ := filepath.Abs(filepath.Join(wd, "lagrange/Lagrange.OneBot"))
+ qrcodeFilePath := filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.UserID[3:]))
+ configFilePath := filepath.Join(workDir, "appsettings.json")
+ appinfoFilePath := filepath.Join(workDir, "appinfo.json")
+
+ if pa.BuiltinMode == "lagrange-gocq" {
+ exeFilePath, _ = filepath.Abs(filepath.Join(wd, "lagrange/go-cqhttp"))
+ qrcodeFilePath = filepath.Join(workDir, "qrcode.png")
+ configFilePath = filepath.Join(workDir, "config.yml")
+ appinfoFilePath = filepath.Join(workDir, "data/versions/7.json")
+ }
+
exeFilePath = filepath.ToSlash(exeFilePath) // windows平台需要这个替换
if runtime.GOOS == "windows" {
exeFilePath += ".exe"
}
- qrcodeFilePath := filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.UserID[3:]))
- configFilePath := filepath.Join(workDir, "appsettings.json")
if _, err := os.Stat(qrcodeFilePath); err == nil {
// 如果已经存在二维码文件,将其删除
_ = os.Remove(qrcodeFilePath)
- } else {
- // 如果找不到二维码文件,有一种可能是用户添加账号时写错了账号,这里做个兼容让错误的账号依旧能获取到二维码
- qrcodeFilePath = filepath.Join(workDir, fmt.Sprintf("qr-%s.png", conn.RelWorkDir[17:]))
- if _, err := os.Stat(qrcodeFilePath); err == nil {
- _ = os.Remove(qrcodeFilePath)
- }
}
- log.Info("onebot: 删除已存在的二维码文件")
+ helper.Info("onebot: 删除已存在的二维码文件")
// 创建配置文件
pa.ConnectURL = ""
if file, err := os.ReadFile(configFilePath); err == nil {
var result map[string]interface{}
- if err := json.Unmarshal(file, &result); err == nil {
- if val, ok := result["Implementations"].([]interface{})[0].(map[string]interface{})["Port"].(float64); ok {
- pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", int(val))
+ if pa.BuiltinMode == "lagrange" {
+ if err := json.Unmarshal(file, &result); err == nil {
+ if val, ok := result["Implementations"].([]interface{})[0].(map[string]interface{})["Port"].(float64); ok {
+ pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", int(val))
+ }
+ }
+ } else {
+ if err := yaml.Unmarshal(file, &result); err == nil {
+ if val, ok := result["servers"].([]interface{})[0].(map[string]interface{})["ws"].(map[string]interface{})["address"].(string); ok {
+ pa.ConnectURL = fmt.Sprintf("ws://%s", val)
+ }
}
}
}
if pa.ConnectURL == "" {
p, _ := GetRandomFreePort()
pa.ConnectURL = fmt.Sprintf("ws://127.0.0.1:%d", p)
- c := GenerateLagrangeConfig(p, loginInfo.SignServerUrl, loginInfo.SignServerVersion, conn)
- _ = os.WriteFile(configFilePath, []byte(c), 0o644)
+ // 这里是为了防止用户手动删除配置,但数据库里还存有账号
+ if loginInfo.SignServerName == "" {
+ loginInfo.SignServerName = pa.SignServerName
+ }
+ if loginInfo.SignServerVersion == "" {
+ loginInfo.SignServerVersion = pa.SignServerVer
+ }
+ // 生成appinfo和signserverurl写入文件
+ a, c := GenerateLagrangeConfig(p, loginInfo.SignServerName, loginInfo.SignServerVersion, dice, conn)
+ if a != nil {
+ dir := filepath.Dir(appinfoFilePath)
+ if _, err := os.Stat(dir); err != nil {
+ _ = os.MkdirAll(dir, 0o755)
+ }
+ _ = os.WriteFile(appinfoFilePath, a, 0o644)
+ }
+ _ = os.WriteFile(configFilePath, c, 0o644)
}
if pa.GoCqhttpProcess != nil {
@@ -116,7 +156,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
if runtime.GOOS == "android" {
for i, s := range os.Environ() {
if strings.HasPrefix(s, "RUNNER_PATH=") {
- log.Infof("RUNNER_PATH: %s", os.Environ()[i][12:])
+ helper.Infof("RUNNER_PATH: %s", os.Environ()[i][12:])
command = os.Environ()[i][12:]
break
}
@@ -127,7 +167,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
} else {
command = fmt.Sprintf(`"%s"`, exeFilePath)
}
- log.Info("onebot: 正在启动 onebot 客户端…… ", command)
+ helper.Info("onebot: 正在启动 onebot 客户端…… ", command)
conn.State = 2
conn.Enable = true
p := procs.NewProcess(command)
@@ -143,7 +183,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
if loginIndex != pa.CurLoginIndex {
// 当前连接已经无用,进程自杀
if !isSelfKilling {
- log.Infof("检测到新的连接序号 %d,当前连接 %d 将自动退出", pa.CurLoginIndex, loginIndex)
+ helper.Infof("检测到新的连接序号 %d,当前连接 %d 将自动退出", pa.CurLoginIndex, loginIndex)
// 注: 这里不要调用kill
isSelfKilling = true
_ = p.Stop()
@@ -153,16 +193,24 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
// 登录中
if pa.IsInLogin() {
+ qrcodeSignal := "QrCode Fetched"
+ onlineSignal := "Bot Online: "
+ qrcodeExpiredSignal := "QrCode Expired, Please Fetch QrCode Again"
+ if pa.BuiltinMode == "lagrange-gocq" {
+ qrcodeSignal = "请使用手机QQ扫描二维码"
+ onlineSignal = "登录成功"
+ qrcodeExpiredSignal = "二维码过期"
+ }
// 读取二维码
- if strings.Contains(line, "QrCode Fetched") {
+ if strings.Contains(line, qrcodeSignal) {
chQrCode <- 1
}
// 登录成功
- if strings.Contains(line, "Success") || strings.Contains(line, "Bot Online: ") {
+ if strings.Contains(line, "Success") || strings.Contains(line, onlineSignal) || strings.Contains(line, "Bot Uin:") {
pa.GoCqhttpState = StateCodeLoginSuccessed
pa.GoCqhttpLoginSucceeded = true
- log.Infof("onebot: 登录成功,账号:<%s>(%s)", conn.Nickname, conn.UserID)
+ helper.Infof("onebot: 登录成功,账号:<%s>(%s)", conn.Nickname, conn.UserID)
dice.LastUpdatedTime = time.Now().Unix()
dice.Save(false)
isPrintLog = false
@@ -172,23 +220,23 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
go ServeQQ(dice, conn)
}
- if strings.Contains(line, "QrCode Expired, Please Fetch QrCode Again") {
+ if strings.Contains(line, qrcodeExpiredSignal) {
// 二维码过期,登录失败,杀掉进程
pa.GoCqhttpState = StateCodeLoginFailed
- log.Infof("onebot: 二维码过期,登录失败,账号:%s", conn.UserID)
+ helper.Infof("onebot: 二维码过期,登录失败,账号:%s", conn.UserID)
BuiltinQQServeProcessKill(dice, conn)
}
}
if _type == "stderr" {
- log.Error("onebot | ", line)
+ helper.Error("onebot | ", line)
} else {
isPrint := isPrintLog || pa.ForcePrintLog || strings.HasPrefix(line, "warn:")
if isPrint {
- log.Warn("onebot | ", line)
+ helper.Warn("onebot | ", line)
}
if regFatal.MatchString(line) {
- log.Error("onebot | ", line)
+ helper.Error("onebot | ", line)
}
}
@@ -199,12 +247,12 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
<-chQrCode
time.Sleep(3 * time.Second)
if _, err := os.Stat(qrcodeFilePath); err == nil {
- log.Info("onebot: 二维码已就绪")
+ helper.Info("onebot: 二维码已就绪")
qrdata, err := os.ReadFile(qrcodeFilePath)
if err == nil {
pa.GoCqhttpState = StateCodeInLoginQrCode
pa.GoCqhttpQrcodeData = qrdata
- log.Info("onebot: 读取二维码成功")
+ helper.Info("onebot: 读取二维码成功")
dice.LastUpdatedTime = time.Now().Unix()
dice.Save(false)
} else {
@@ -213,7 +261,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
pa.GocqhttpLoginFailedReason = "读取二维码失败"
dice.LastUpdatedTime = time.Now().Unix()
dice.Save(false)
- log.Infof("onebot: 读取二维码失败:%s", err)
+ helper.Infof("onebot: 读取二维码失败:%s", err)
}
}
}()
@@ -221,7 +269,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
run := func() {
defer func() {
if r := recover(); r != nil {
- log.Errorf("onebot: 异常: %v 堆栈: %v", r, string(debug.Stack()))
+ helper.Errorf("onebot: 异常: %v 堆栈: %v", r, string(debug.Stack()))
}
}()
@@ -249,7 +297,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
}
if err != nil {
- log.Info("lagrange 进程异常退出: ", err)
+ helper.Info("lagrange 进程异常退出: ", err)
pa.GoCqhttpState = StateCodeLoginFailed
var exitErr *exec.ExitError
@@ -259,21 +307,21 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
case 137:
// Failed to create CoreCLR, HRESULT: 0x8007054F
// +++ exited with 137 +++
- log.Info("你的设备尚未被支持,请等待后续更新。")
+ helper.Info("你的设备尚未被支持,请等待后续更新。")
case 134:
// Resource temporarily unavailable
// System.Net.Dns.GetHostEntryOrAddressesCore(String hostName, Boolean justAddresses, AddressFamily addressFamily, Int64 startingTimestamp)
- log.Info("当前网络无法进行域名解析,请更换网络。")
+ helper.Info("当前网络无法进行域名解析,请更换网络。")
default:
if time.Now().Unix()-processStartTime < 10 {
- log.Info("进程在启动后10秒内即退出,请检查配置是否正确")
+ helper.Info("进程在启动后10秒内即退出,请检查配置是否正确")
} else {
if pa.lagrangeRebootTimes > 5 {
- log.Info("自动重启次数达到上限,放弃")
+ helper.Info("自动重启次数达到上限,放弃")
} else {
pa.lagrangeRebootTimes++
if conn.Enable {
- log.Info("5秒后,尝试对其进行重启")
+ helper.Info("5秒后,尝试对其进行重启")
time.Sleep(5 * time.Second)
}
if conn.Enable {
@@ -284,7 +332,7 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
}
}
} else {
- log.Info("lagrange 进程退出")
+ helper.Info("lagrange 进程退出")
}
}
@@ -301,74 +349,42 @@ func LagrangeServe(dice *Dice, conn *EndPointInfo, loginInfo LagrangeLoginInfo)
}
}
-var defaultLagrangeConfig = `
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- },
- "SignServerUrl": "{NTSignServer地址}",
- "Account": {
- "Uin": {账号UIN},
- "Password": "",
- "Protocol": "Linux",
- "AutoReconnect": true,
- "GetOptimumServer": true
- },
- "Message": {
- "IgnoreSelf": true,
- "StringPost": false
- },
- "QrCode": {
- "ConsoleCompatibilityMode": false
- },
- "Implementations": [
- {
- "Type": "ForwardWebSocket",
- "Host": "127.0.0.1",
- "Port": {WS端口},
- "HeartBeatInterval": 5000,
- "AccessToken": ""
- }
- ]
-}
-`
-
// 在构建时注入
-var defaultNTSignServer = `https://lwxmagic.sealdice.com/api/sign`
-var lagrangeNTSignServer = "https://sign.lagrangecore.org/api/sign"
-
-func GenerateLagrangeConfig(port int, signServerUrl string, signServerVersion string, info *EndPointInfo) string {
- switch signServerUrl {
- case "":
- signServerUrl = defaultNTSignServer
- if signServerVersion != "" && signServerVersion != "13107" {
- signServerUrl += "/" + signServerVersion
- }
- case "sealdice":
- signServerUrl = defaultNTSignServer
- if signServerVersion != "" && signServerVersion != "13107" {
- signServerUrl += "/" + signServerVersion
- }
- case "lagrange":
- signServerUrl = lagrangeNTSignServer
- if signServerVersion != "" && signServerVersion != "13107" {
- signServerUrl += "/" + signServerVersion
+// var defaultNTSignServer = `https://lwxmagic.sealdice.com/api/sign`
+// var lagrangeNTSignServer = "https://sign.lagrangecore.org/api/sign"
+
+func GenerateLagrangeConfig(port int, signServerName string, signServerVersion string, dice *Dice, info *EndPointInfo) ([]byte, []byte) {
+ var appinfo []byte
+ var signServerUrl string
+ pa := info.Adapter.(*PlatformAdapterGocq)
+ if signServerVersion == "自定义" {
+ appinfo, _ = lagrangeGetAppinfoFromSignServer(signServerName)
+ signServerUrl = signServerName
+ } else {
+ if len(signInfoGlobal) == 0 {
+ _, _ = LagrangeGetSignInfo(dice)
}
+ appinfo, signServerUrl = lagrangeGetSignSeverFromInfo(signServerVersion, signServerName)
+ }
+ conf := strings.ReplaceAll(defaultLagrangeConfig, "{WS端口}", strconv.Itoa(port))
+ if pa.BuiltinMode == "lagrange-gocq" {
+ conf = strings.ReplaceAll(defaultLagrangeGocqConfig, "{WS端口}", strconv.Itoa(port))
}
- conf := strings.ReplaceAll(defaultLagrangeConfig, "{WS端口}", fmt.Sprintf("%d", port))
conf = strings.ReplaceAll(conf, "{NTSignServer地址}", signServerUrl)
conf = strings.ReplaceAll(conf, "{账号UIN}", info.UserID[3:])
- return conf
+ return appinfo, []byte(conf)
}
+// 该函数后续考虑优化掉
func LagrangeServeRemoveSession(dice *Dice, conn *EndPointInfo) {
workDir := gocqGetWorkDir(dice, conn)
- if _, err := os.Stat(filepath.Join(workDir, "keystore.json")); err == nil {
- _ = os.Remove(filepath.Join(workDir, "keystore.json"))
+ file := filepath.Join(workDir, "keystore.json")
+ pa := conn.Adapter.(*PlatformAdapterGocq)
+ if pa.BuiltinMode == "lagrange-gocq" {
+ file = filepath.Join(workDir, "session.token")
+ }
+ if _, err := os.Stat(file); err == nil {
+ _ = os.Remove(file)
}
}
@@ -383,53 +399,332 @@ func LagrangeServeRemoveConfig(dice *Dice, conn *EndPointInfo) {
}
}
-func RWLagrangeSignServerUrl(dice *Dice, conn *EndPointInfo, signServerUrl string, w bool, signServerVersion string) (string, string) {
- switch signServerUrl {
- case "sealdice":
- signServerUrl = defaultNTSignServer
- if signServerVersion != "" && signServerVersion != "13107" {
- signServerUrl += "/" + signServerVersion
- }
- case "lagrange":
- signServerUrl = "https://sign.lagrangecore.org/api/sign"
- if signServerVersion != "" && signServerVersion != "13107" {
- signServerUrl += "/" + signServerVersion
+// 云端SignInfo.Servers结构
+type SignServerInfo struct {
+ Name string `json:"name"`
+ Url string `json:"url"`
+ Latency int `json:"latency"`
+ Selected bool `json:"selected"`
+ Ignored bool `json:"ignored"`
+ Note string `json:"note"`
+}
+
+// 云端SignInfo结构
+type SignInfo struct {
+ Version string `json:"version"`
+ Appinfo map[string]interface{} `json:"appinfo"`
+ Servers []*SignServerInfo `json:"servers"`
+ Selected bool `json:"selected"`
+ Ignored bool `json:"ignored"`
+ Note string `json:"note"`
+}
+
+// 小概率出现并发读写,需上锁
+var mu sync.Mutex
+var signInfoGlobal []SignInfo
+
+func LagrangeGetSignInfo(dice *Dice) ([]SignInfo, error) {
+ mu.Lock()
+ defer mu.Unlock()
+ cachePath := filepath.Join(dice.BaseConfig.DataDir, "extra/SignInfo.cache")
+ signInfo, err := lagrangeGetSignInfoFromCloud(cachePath)
+ if err == nil && len(signInfo) > 0 {
+ signInfoGlobal = append([]SignInfo(nil), signInfo...)
+ return signInfo, nil
+ }
+ dice.Logger.Infof("无法从云端获取SignInfo,即将读取本地缓存数据, 原因: %s", err.Error())
+
+ signInfo, err = lagrangeGetSignInfoFromCache(cachePath)
+ if err == nil && len(signInfo) > 0 {
+ signInfoGlobal = append([]SignInfo(nil), signInfo...)
+ return signInfo, nil
+ }
+ dice.Logger.Infof("无法从本地缓存获取SignInfo,即将读取内置数据, 原因: %s", err.Error())
+
+ if err = json.Unmarshal([]byte(signInfoJson), &signInfo); err == nil {
+ lagrangeGetSignServerLatency(signInfo)
+ signInfoGlobal = append([]SignInfo(nil), signInfo...)
+ return signInfo, nil
+ }
+ dice.Logger.Infof("无法从内置数据获取SignInfo,请联系开发者上报问题, 原因: %s", err.Error())
+ return nil, errors.New("内置SignInfo信息有误")
+}
+
+func lagrangeGetSignInfoFromCloud(cachePath string) ([]SignInfo, error) {
+ now := time.Now()
+ unixTimestamp := now.Unix()
+ url := fmt.Sprintf("https://d1.sealdice.com/sealsign/signinfo.json?v=%v", unixTimestamp)
+ c := http.Client{
+ Timeout: 3 * time.Second,
+ }
+ resp, err := c.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ var signInfo []SignInfo
+ err = json.Unmarshal(body, &signInfo)
+ if err != nil {
+ return nil, err
+ }
+ _ = os.WriteFile(cachePath, body, 0o644)
+ lagrangeGetSignServerLatency(signInfo)
+ return signInfo, nil
+}
+
+func lagrangeGetSignInfoFromCache(cachePath string) ([]SignInfo, error) {
+ var err error
+ if _, err = os.Stat(cachePath); err == nil {
+ var file []byte
+ if file, err = os.ReadFile(cachePath); err == nil {
+ var signInfo []SignInfo
+ if err = json.Unmarshal(file, &signInfo); err == nil {
+ lagrangeGetSignServerLatency(signInfo)
+ return signInfo, nil
+ }
}
}
- workDir := lagrangeGetWorkDir(dice, conn)
- configFilePath := filepath.Join(workDir, "appsettings.json")
- file, err := os.ReadFile(configFilePath)
- if err == nil {
- var result map[string]interface{}
- err = json.Unmarshal(file, &result)
- if err == nil {
- if val, ok := result["SignServerUrl"].(string); ok {
- if w {
- result["SignServerUrl"] = signServerUrl
- result["SignServerVersion"] = signServerVersion
- var c []byte
- if c, err = json.MarshalIndent(result, "", " "); err == nil {
- _ = os.WriteFile(configFilePath, c, 0o644)
- } else {
- dice.Logger.Infof("SignServerUrl字段无法正常覆写,账号:%s, 原因: %s", conn.UserID, err.Error())
- }
- }
+ return nil, err
+}
- var version string
- if strings.HasPrefix(val, defaultNTSignServer) {
- version, _ = strings.CutPrefix(val, defaultNTSignServer)
- version, _ = strings.CutPrefix(version, "/")
- val = "sealdice"
- } else if strings.HasPrefix(val, lagrangeNTSignServer) {
- version, _ = strings.CutPrefix(val, lagrangeNTSignServer)
- version, _ = strings.CutPrefix(version, "/")
- val = "lagrange"
+func lagrangeGetSignSeverFromInfo(serverVer string, serverName string) ([]byte, string) {
+ mu.Lock()
+ defer mu.Unlock()
+ for _, info := range signInfoGlobal {
+ if info.Version == serverVer {
+ for _, server := range info.Servers {
+ if server.Name == serverName {
+ if appinfo, err := json.Marshal(info.Appinfo); err == nil {
+ return appinfo, server.Url
+ }
}
- return val, version
}
- err = errors.New("SignServerUrl字段无法正常读取")
}
}
- dice.Logger.Infof("读取内置客户端配置失败,账号:%s, 原因: %s", conn.UserID, err.Error())
- return "", ""
+ return nil, ""
+}
+
+func lagrangeGetSignServerLatency(signInfo []SignInfo) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ c := &http.Client{
+ Timeout: 3 * time.Second,
+ }
+ for _, si := range signInfo {
+ for _, server := range si.Servers {
+ wg.Add(1)
+ go func(server *SignServerInfo) {
+ defer wg.Done()
+ latency := testLatency(c, server.Url)
+ mu.Lock()
+ server.Latency = latency
+ mu.Unlock()
+ }(server)
+ }
+ }
+ wg.Wait()
}
+
+func testLatency(c *http.Client, url string) int {
+ start := time.Now()
+ resp, err := c.Get(url)
+ if err != nil {
+ return 999
+ }
+ defer resp.Body.Close()
+ duration := time.Since(start)
+ return int(duration.Milliseconds())
+}
+
+// 当自定义签名地址时,从/appinfo路径获取appinfo信息
+func lagrangeGetAppinfoFromSignServer(serverName string) ([]byte, error) {
+ c := http.Client{
+ Timeout: 3 * time.Second,
+ }
+ resp, err := c.Get(serverName + "/appinfo")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ var test map[string]interface{}
+ err = json.Unmarshal(body, &test)
+ if err != nil {
+ return nil, err
+ }
+ return body, nil
+}
+
+var signInfoJson string = `
+[
+ {
+ "version": "25765",
+ "appinfo": {
+ "AppClientVersion": 25765,
+ "AppId": 1600001615,
+ "AppIdQrCode": 13697054,
+ "CurrentVersion": "3.2.10-25765",
+ "Kernel": "Linux",
+ "MainSigMap": 169742560,
+ "MiscBitmap": 32764,
+ "NTLoginType": 1,
+ "Os": "Linux",
+ "PackageName": "com.tencent.qq",
+ "PtVersion": "2.0.0",
+ "SsoVersion": 19,
+ "SubAppId": 537234773,
+ "SubSigMap": 0,
+ "VendorOs": "linux",
+ "WtLoginSdk": "nt.wtlogin.0.0.1"
+ },
+ "servers": [
+ {
+ "name": "海豹",
+ "url": "https://lwxmagic.sealdice.com/api/sign/25765"
+ },
+ {
+ "name": "Lagrange",
+ "url": "https://sign.lagrangecore.org/api/sign/25765"
+ }
+ ]
+ },
+ {
+ "version": "30366",
+ "appinfo": {
+ "AppClientVersion": 30366,
+ "AppId": 1600001615,
+ "AppIdQrCode": 13697054,
+ "CurrentVersion": "3.2.15-30366",
+ "Kernel": "Linux",
+ "MainSigMap": 169742560,
+ "MiscBitmap": 32764,
+ "NTLoginType": 1,
+ "Os": "Linux",
+ "PackageName": "com.tencent.qq",
+ "PtVersion": "2.0.0",
+ "SsoVersion": 19,
+ "SubAppId": 537258424,
+ "SubSigMap": 0,
+ "VendorOs": "linux",
+ "WtLoginSdk": "nt.wtlogin.0.0.1"
+ },
+ "servers": [
+ {
+ "name": "海豹",
+ "url": "https://lwxmagic.sealdice.com/api/sign/30366",
+ "selected": true,
+ "note": "部分地区用户可能无法连接"
+ },
+ {
+ "name": "Lagrange",
+ "url": "https://sign.lagrangecore.org/api/sign/30366"
+ }
+ ],
+ "selected": true
+ }
+]
+ `
+
+var defaultLagrangeConfig = `
+ {
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "SignServerUrl": "{NTSignServer地址}",
+ "Account": {
+ "Uin": {账号UIN},
+ "Password": "",
+ "Protocol": "Linux",
+ "AutoReconnect": true,
+ "GetOptimumServer": true
+ },
+ "Message": {
+ "IgnoreSelf": true,
+ "StringPost": false
+ },
+ "QrCode": {
+ "ConsoleCompatibilityMode": false
+ },
+ "Implementations": [
+ {
+ "Type": "ForwardWebSocket",
+ "Host": "127.0.0.1",
+ "Port": {WS端口},
+ "HeartBeatInterval": 5000,
+ "AccessToken": ""
+ }
+ ]
+ }
+ `
+var defaultLagrangeGocqConfig = `
+account:
+ uin: {账号UIN}
+ password: ''
+ encrypt: false
+ status: 0
+ relogin:
+ delay: 3
+ interval: 3
+ max-times: 0
+ use-sso-address: true
+ allow-temp-session: false
+ sign-servers:
+ - url: '{NTSignServer地址}'
+ max-check-count: 0
+ sign-server-timeout: 60
+
+heartbeat:
+ interval: 5
+
+message:
+ post-format: array
+ ignore-invalid-cqcode: false
+ force-fragment: false
+ fix-url: false
+ proxy-rewrite: ''
+ report-self-message: false
+ remove-reply-at: false
+ extra-reply-data: false
+ skip-mime-scan: false
+ convert-webp-image: false
+ http-timeout: 15
+
+output:
+ log-level: warn
+ log-aging: 15
+ log-force-new: true
+ log-colorful: true
+ debug: false
+
+default-middlewares: &default
+ access-token: ''
+ filter: ''
+ rate-limit:
+ enabled: false
+ frequency: 1
+ bucket: 1
+
+database:
+ leveldb:
+ enable: true
+ sqlite3:
+ enable: false
+ cachettl: 3600000000000
+
+servers:
+ - ws:
+ address: 127.0.0.1:{WS端口}
+ middlewares:
+ <<: *default
+`
diff --git a/dice/platform_adapter_official_qq.go b/dice/platform_adapter_official_qq.go
index 2e4f6994..11296b98 100644
--- a/dice/platform_adapter_official_qq.go
+++ b/dice/platform_adapter_official_qq.go
@@ -2,6 +2,7 @@ package dice
import (
"context"
+ "errors"
"fmt"
"path/filepath"
"strconv"
@@ -42,7 +43,7 @@ func (pa *PlatformAdapterOfficialQQ) Serve() int {
d := pa.Session.Parent
log.Debug("official qq server")
- qqbot.SetLogger(NewDummyLogger(log.Desugar()))
+ qqbot.SetLogger(NewDummyLogger())
token := qqtoken.BotToken(pa.AppID, pa.Token)
pa.Api = qqbot.NewOpenAPI(token).WithTimeout(3 * time.Second)
pa.Ctx, pa.CancelFunc = context.WithCancel(context.Background())
@@ -241,9 +242,9 @@ func (pa *PlatformAdapterOfficialQQ) SendToPerson(ctx *MsgContext, uid string, t
pa.sendQQGuildDirectMsgRaw(ctx, rowID, guildID, channelID, text)
}
-func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel(ctx *MsgContext, guildID, userID string) (string, string, error) {
+func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel( /* ctx */ _ *MsgContext, guildID, userID string) (string, string, error) {
if guildID == "" || userID == "" {
- err := fmt.Errorf("创建私信频道的参数不全")
+ err := errors.New("创建私信频道的参数不全")
pa.Session.Parent.Logger.Error("official qq 创建私信频道失败:" + err.Error())
return "", "", err
}
@@ -260,7 +261,7 @@ func (pa *PlatformAdapterOfficialQQ) createQQGuildDirectChannel(ctx *MsgContext,
return info.GuildID, info.ChannelID, nil
}
-func (pa *PlatformAdapterOfficialQQ) sendQQGuildDirectMsgRaw(ctx *MsgContext, rowMsgID string, guildID, channelID string, text string) {
+func (pa *PlatformAdapterOfficialQQ) sendQQGuildDirectMsgRaw( /* ctx */ _ *MsgContext, rowMsgID string, guildID, channelID string, text string) {
qctx := context.Background()
elems := message.ConvertStringMessage(text)
var (
@@ -308,7 +309,7 @@ func (pa *PlatformAdapterOfficialQQ) SendToGroup(ctx *MsgContext, uid string, te
}
}
-func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw(ctx *MsgContext, rowMsgID, groupID string, text string) {
+func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw( /* ctx */ _ *MsgContext, rowMsgID, groupID string, text string) {
qctx := context.Background()
elems := message.ConvertStringMessage(text)
var (
@@ -383,7 +384,7 @@ func (pa *PlatformAdapterOfficialQQ) sendQQGroupMsgRaw(ctx *MsgContext, rowMsgID
}
}
-func (pa *PlatformAdapterOfficialQQ) sendQQChannelMsgRaw(ctx *MsgContext, rowMsgID, channelID string, text string) {
+func (pa *PlatformAdapterOfficialQQ) sendQQChannelMsgRaw( /* ctx */ _ *MsgContext, rowMsgID, channelID string, text string) {
qctx := context.Background()
elems := message.ConvertStringMessage(text)
var (
diff --git a/dice/platform_adapter_official_qq_helper.go b/dice/platform_adapter_official_qq_helper.go
index b0b0220f..15ce105c 100644
--- a/dice/platform_adapter_official_qq_helper.go
+++ b/dice/platform_adapter_official_qq_helper.go
@@ -9,7 +9,8 @@ import (
"time"
"github.com/google/uuid"
- "go.uber.org/zap"
+
+ log "sealdice-core/utils/kratos"
)
func NewOfficialQQConnItem(appID uint64, token string, appSecret string, onlyQQGuild bool) *EndPointInfo {
@@ -45,12 +46,12 @@ func ServerOfficialQQ(d *Dice, ep *EndPointInfo) {
}
type DummyLogger struct {
- logger *zap.Logger
+ logger *log.Helper
}
-func NewDummyLogger(logger *zap.Logger) DummyLogger {
+func NewDummyLogger() DummyLogger {
return DummyLogger{
- logger: logger,
+ logger: log.NewHelper(log.With(log.GetLogger(), "caller", "officialQQ")),
}
}
diff --git a/dice/platform_adapter_red.go b/dice/platform_adapter_red.go
index 742ed74e..b1f6cd47 100644
--- a/dice/platform_adapter_red.go
+++ b/dice/platform_adapter_red.go
@@ -43,7 +43,7 @@ type PlatformAdapterRed struct {
memberMap *SyncMap[string, *SyncMap[string, *GroupMember]]
}
-type RedPack[T interface{}] struct {
+type RedPack[T any] struct {
Type string `json:"type"`
Payload *T `json:"payload"`
}
@@ -883,7 +883,7 @@ func (pa *PlatformAdapterRed) httpDo(method, action string, headers map[string]s
}
// encodeMessage 将带 cq code 的内容转换为 red 所需的格式
-func (pa *PlatformAdapterRed) encodeMessage(ctx *MsgContext, content string) []*RedElement {
+func (pa *PlatformAdapterRed) encodeMessage( /* ctx */ _ *MsgContext, content string) []*RedElement {
elems := message.ConvertStringMessage(content)
var redElems []*RedElement
for _, elem := range elems {
diff --git a/dice/platform_adapter_satori.go b/dice/platform_adapter_satori.go
index 858666d4..770d8e52 100644
--- a/dice/platform_adapter_satori.go
+++ b/dice/platform_adapter_satori.go
@@ -440,7 +440,7 @@ func (pa *PlatformAdapterSatori) SendToGroup(ctx *MsgContext, groupID string, te
pa.sendMsgRaw(ctx, UserIDExtract(groupID), text, flag, "group")
}
-func (pa *PlatformAdapterSatori) sendMsgRaw(ctx *MsgContext, channelID string, text string, flag string, msgType string) {
+func (pa *PlatformAdapterSatori) sendMsgRaw( /* ctx */ _ *MsgContext, channelID string, text string /* flag */, _ string, msgType string) {
log := pa.Session.Parent.Logger
req, err := json.Marshal(map[string]interface{}{
"channel_id": channelID,
@@ -554,6 +554,7 @@ func (pa *PlatformAdapterSatori) post(resource string, body io.Reader) ([]byte,
request.Header.Add("Authorization", "Bearer "+pa.Token)
}
request.Header.Add("X-Platform", pa.Platform)
+ //nolint:canonicalheader
request.Header.Add("X-Self-ID", UserIDExtract(pa.EndPoint.UserID))
resp, err := client.Do(request)
if err != nil {
@@ -809,21 +810,21 @@ func (pa *PlatformAdapterSatori) guildRequestHandle(e *SatoriEvent) {
eid := e.ID.String()
// 邀请人在黑名单上
- banInfo, ok := d.BanList.GetByID(uid)
+ banInfo, ok := d.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && d.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && d.Config.BanList.BanBehaviorRefuseInvite {
pa.sendGuildRequestResult(eid, false, "黑名单")
return
}
}
// 信任模式,如果不是信任,又不是 master 则拒绝拉群邀请
isMaster := d.IsMaster(uid)
- if d.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
+ if d.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
pa.sendGuildRequestResult(eid, false, "只允许骰主设置信任的人拉群")
return
}
// 群在黑名单上
- banInfo, ok = d.BanList.GetByID(guildID)
+ banInfo, ok = d.Config.BanList.GetByID(guildID)
if ok {
if banInfo.Rank == BanRankBanned {
pa.sendGuildRequestResult(eid, false, "群黑名单")
@@ -831,7 +832,7 @@ func (pa *PlatformAdapterSatori) guildRequestHandle(e *SatoriEvent) {
}
}
// 拒绝加群
- if d.RefuseGroupInvite {
+ if d.Config.RefuseGroupInvite {
pa.sendGuildRequestResult(eid, false, "设置拒绝加群")
return
}
@@ -870,15 +871,15 @@ func (pa *PlatformAdapterSatori) friendRequestHandle(e *SatoriEvent) {
eid := e.ID.String()
// 申请人在黑名单上
- banInfo, ok := d.BanList.GetByID(uid)
+ banInfo, ok := d.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && d.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && d.Config.BanList.BanBehaviorRefuseInvite {
pa.sendGuildRequestResult(eid, false, "为被禁止用户,准备自动拒绝")
return
}
}
- if strings.TrimSpace(d.FriendAddComment) == "" {
+ if strings.TrimSpace(d.Config.FriendAddComment) == "" {
pa.sendFriendRequestResult(eid, true, "")
} else {
pa.sendFriendRequestResult(eid, false, "存在好友问题校验,准备自动拒绝,请联系骰主")
diff --git a/dice/platform_adapter_sealchat.go b/dice/platform_adapter_sealchat.go
index e068e1b8..bc5d2c1c 100644
--- a/dice/platform_adapter_sealchat.go
+++ b/dice/platform_adapter_sealchat.go
@@ -24,18 +24,20 @@ type PlatformAdapterSealChat struct {
EchoMap SyncMap[string, chan any] `yaml:"-" json:"-"`
UserID string `yaml:"-" json:"-"`
- Reconnecting bool `yaml:"-" json:"-"`
- RetryTimes int `yaml:"-" json:"-"`
+ Reconnecting bool `yaml:"-" json:"-"`
+ RetryTimes int `yaml:"-" json:"-"`
+ RetryTimesLimit int `yaml:"-" json:"-"`
}
func (pa *PlatformAdapterSealChat) Serve() int {
- if !strings.HasPrefix(pa.ConnectURL, "ws://") {
+ if !strings.HasPrefix(pa.ConnectURL, "ws://") && !strings.HasPrefix(pa.ConnectURL, "wss://") {
pa.ConnectURL = "ws://" + pa.ConnectURL
}
socket := gowebsocket.New(pa.ConnectURL)
pa.Socket = &socket
pa.EndPoint.Nickname = "SealChat Bot"
pa.EndPoint.UserID = "SEALCHAT:BOT"
+ pa.RetryTimesLimit = 1
d := pa.Session.Parent
d.LastUpdatedTime = time.Now().Unix()
d.Save(false)
@@ -62,7 +64,6 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
pa.Reconnecting = true
ep.State = 2
ep.Enable = true
- pa.RetryTimes = 0
d := pa.Session.Parent
d.LastUpdatedTime = time.Now().Unix()
@@ -75,7 +76,7 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
},
})
- log.Info("SealChat 已连接,正在发送身份验证信息")
+ log.Info("SealChat 建立连接,正在发送身份验证信息")
pa.Reconnecting = false
}
socket.OnTextMessage = func(message string, socket gowebsocket.Socket) {
@@ -109,6 +110,10 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
ep.Nickname = data.Body.User.Nick
ep.State = 1
log.Infof("SealChat 连接成功: %s", ep.Nickname)
+
+ // 握手成功,通过验证
+ pa.RetryTimes = 0
+ pa.RetryTimesLimit = 15
}
go func() {
@@ -143,6 +148,7 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
log.Errorf("SealChat websocket出现错误: %s", err)
if !socket.IsConnected && !pa.Reconnecting {
// socket.Close()
+ time.Sleep(time.Duration(10) * time.Second)
if !pa.tryReconnect(*pa.Socket) {
log.Errorf("短时间内连接失败次数过多,不再进行重连")
ep.State = 3
@@ -150,11 +156,11 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
}
}
socket.OnDisconnected = func(err error, socket gowebsocket.Socket) {
- log.Errorf("与SealChat服务器断开连接")
+ log.Info("与SealChat服务器断开连接,尝试进行重连")
time.Sleep(time.Duration(2) * time.Second)
if !pa.tryReconnect(*pa.Socket) {
- log.Errorf("尝试进行重连")
ep.State = 3
+ log.Errorf("到达连接次数上限,不再进行重连")
}
}
pa.Socket = socket
@@ -162,25 +168,26 @@ func (pa *PlatformAdapterSealChat) socketSetup() {
func (pa *PlatformAdapterSealChat) tryReconnect(socket gowebsocket.Socket) bool {
log := pa.Session.Parent.Logger
- if pa.Reconnecting {
- return false
+ if socket.IsConnected {
+ return true
}
pa.Reconnecting = true
- pa.RetryTimes = 0
- allTimes := 500
- for pa.RetryTimes <= allTimes && !socket.IsConnected {
- if !pa.EndPoint.Enable {
- return false
- }
- pa.RetryTimes++
- log.Infof("尝试重新连接SealChat中[%d/%d]", pa.RetryTimes, allTimes)
- socket = gowebsocket.New(pa.ConnectURL)
- pa.Socket = &socket
- pa.socketSetup()
- socket.Connect()
- time.Sleep(time.Duration(10) * time.Second)
+ if !pa.EndPoint.Enable {
+ return true
+ }
+
+ if pa.RetryTimes >= pa.RetryTimesLimit {
+ return false
}
+
+ pa.RetryTimes++
+ log.Infof("尝试重新连接SealChat中[%d/%d]", pa.RetryTimes, pa.RetryTimesLimit)
+ socket = gowebsocket.New(pa.ConnectURL)
+ pa.Socket = &socket
+ pa.socketSetup()
+ socket.Connect()
+
pa.Reconnecting = false
return true
}
@@ -378,7 +385,7 @@ func (pa *PlatformAdapterSealChat) dispatchMessage(msg string) {
ev := satori.Event{}
err := json.Unmarshal([]byte(msg), &ev)
if err != nil {
- fmt.Println(err)
+ pa.Session.Parent.Logger.Error("PlatformAdapterSealChat.dispatchMessage", err)
return
}
diff --git a/dice/platform_adapter_telegram.go b/dice/platform_adapter_telegram.go
index df09c5e8..b8729fcf 100644
--- a/dice/platform_adapter_telegram.go
+++ b/dice/platform_adapter_telegram.go
@@ -13,8 +13,21 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"sealdice-core/message"
+ log "sealdice-core/utils/kratos"
)
+type BotLoggerWrapper struct {
+ Logger *log.Helper
+}
+
+func (b *BotLoggerWrapper) Println(v ...interface{}) {
+ b.Logger.Error(v...)
+}
+
+func (b *BotLoggerWrapper) Printf(format string, v ...interface{}) {
+ b.Logger.Errorf(format, v...)
+}
+
type PlatformAdapterTelegram struct {
Session *IMSession `yaml:"-" json:"-"`
Token string `yaml:"token" json:"token"`
@@ -56,6 +69,7 @@ func (pa *PlatformAdapterTelegram) Serve() int {
var bot *tgbotapi.BotAPI
var err error
+ _ = tgbotapi.SetLogger(&BotLoggerWrapper{Logger: logger})
if len(pa.ProxyURL) > 0 {
var u *url.URL
u, err = url.Parse(pa.ProxyURL)
diff --git a/dice/platform_adapter_walleq.go b/dice/platform_adapter_walleq.go
index c1c041d0..a1c36fbb 100644
--- a/dice/platform_adapter_walleq.go
+++ b/dice/platform_adapter_walleq.go
@@ -205,7 +205,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
// tempFriendInviteSent := map[string]int64{} // gocq会重新发送已经发过的邀请
socket.OnTextMessage = func(message string, socket gowebsocket.Socket) {
- fmt.Println(message)
+ log.Debug(message)
event := new(EventWalleQBase)
err := json.Unmarshal([]byte(message), event)
if err != nil {
@@ -404,7 +404,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
if event.UserID == event.Self.UserID {
skip := false
skipReason := ""
- banInfo, ok := ctx.Dice.BanList.GetByID(opUID)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(opUID)
if ok {
if banInfo.Rank == 30 {
skip = true
@@ -420,7 +420,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
if skip {
extra = fmt.Sprintf("\n取消处罚,原因为%s", skipReason)
} else {
- ctx.Dice.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByGroupKicked(opUID, msg.GroupID, ctx)
}
txt := fmt.Sprintf("被踢出群: 在QQ群组<%s>(%s)中被踢出,操作者:<%s>(%s)%s", groupName, event.GroupID, userName, n.OperatorID, extra)
@@ -429,7 +429,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
}
case "group_member_ban": // 被禁言
if event.UserID == event.Self.UserID {
- ctx.Dice.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx)
+ ctx.Dice.Config.BanList.AddScoreByGroupMuted(opUID, msg.GroupID, ctx)
txt := fmt.Sprintf("被禁言: 在群组<%s>(%s)中被禁言,时长%d秒,操作者:<%s>(%s)", groupName, msg.GroupID, n.Duration, userName, n.OperatorID)
log.Info(txt)
ctx.Notice(txt)
@@ -470,7 +470,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
comment = strings.ReplaceAll(comment, "\u00a0", "")
}
- toMatch := strings.TrimSpace(s.Parent.FriendAddComment)
+ toMatch := strings.TrimSpace(s.Parent.Config.FriendAddComment)
willAccept := comment == DiceFormat(ctx, toMatch)
if toMatch == "" {
willAccept = true
@@ -491,7 +491,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
if len(m2) == len(items) {
ok := true
- for i := 0; i < len(m2); i++ {
+ for i := range m2 {
if m2[i] != items[i] {
ok = false
break
@@ -510,9 +510,9 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
// 检查黑名单
extra := ""
uid := msg.Sender.UserID
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
if willAccept {
extra = "。回答正确,但为被禁止用户,准备自动拒绝"
} else {
@@ -564,21 +564,21 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
// tempInviteMap2[msg.GroupId] = uid
// 邀请人在黑名单上
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
pa.SetGroupAddRequest(req.RequestID, event.GroupID, false)
return
}
}
// 信任模式,如果不是信任,又不是master则拒绝拉群邀请
isMaster := ctx.Dice.IsMaster(uid)
- if ctx.Dice.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
+ if ctx.Dice.Config.TrustOnlyMode && ((banInfo != nil && banInfo.Rank != BanRankTrusted) && !isMaster) {
pa.SetGroupAddRequest(req.RequestID, event.GroupID, false)
return
}
// 群在黑名单上
- banInfo, ok = ctx.Dice.BanList.GetByID(gid)
+ banInfo, ok = ctx.Dice.Config.BanList.GetByID(gid)
if ok {
if banInfo.Rank == BanRankBanned {
pa.SetGroupAddRequest(req.RequestID, event.GroupID, false)
@@ -586,7 +586,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
}
}
// 拒绝加入新群
- if ctx.Dice.RefuseGroupInvite {
+ if ctx.Dice.Config.RefuseGroupInvite {
pa.SetGroupAddRequest(req.RequestID, event.GroupID, false)
return
}
@@ -598,7 +598,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
// 事件都有 ID,没有就是响应 but 有几个元事件 ID 是 "" ;把响应处理放到最后吧
//nolint:nestif
if event.ID == "" {
- fmt.Println(message)
+ log.Debug(message)
echo := new(EchoWalleQ)
err = json.Unmarshal([]byte(message), echo)
if err != nil {
@@ -643,9 +643,9 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
// 处理被强制拉群的情况
uid := groupInfo.InviteUserID
- banInfo, ok := ctx.Dice.BanList.GetByID(uid)
+ banInfo, ok := ctx.Dice.Config.BanList.GetByID(uid)
if ok {
- if banInfo.Rank == BanRankBanned && ctx.Dice.BanList.BanBehaviorRefuseInvite {
+ if banInfo.Rank == BanRankBanned && ctx.Dice.Config.BanList.BanBehaviorRefuseInvite {
// 如果是被ban之后拉群,判定为强制拉群
if groupInfo.EnteredTime > 0 && groupInfo.EnteredTime > banInfo.BanTime {
text := fmt.Sprintf("本次入群为遭遇强制邀请,即将主动退群,因为邀请人%s正处于黑名单上。打扰各位还请见谅。感谢使用海豹核心。", groupInfo.InviteUserID)
@@ -658,7 +658,7 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
}
// 强制拉群情况2 - 群在黑名单
- banInfo, ok = ctx.Dice.BanList.GetByID(groupID)
+ banInfo, ok = ctx.Dice.Config.BanList.GetByID(groupID)
if ok {
if banInfo.Rank == BanRankBanned {
// 如果是被ban之后拉群,判定为强制拉群
@@ -701,11 +701,11 @@ func (pa *PlatformAdapterWalleQ) Serve() int {
socket.Connect()
defer func() {
- fmt.Println("socket close")
+ log.Info("socket close")
go func() {
defer func() {
if r := recover(); r != nil {
- fmt.Println("关闭连接时遭遇异常")
+ log.Error("关闭连接时遭遇异常", r)
}
}()
socket.Close()
diff --git a/dice/platform_adapter_walleq_helper.go b/dice/platform_adapter_walleq_helper.go
index 91f9ec2a..15c28848 100644
--- a/dice/platform_adapter_walleq_helper.go
+++ b/dice/platform_adapter_walleq_helper.go
@@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/pelletier/go-toml/v2"
+ log2 "sealdice-core/utils/kratos"
"sealdice-core/utils/procs"
)
@@ -133,7 +134,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int,
pa.CurLoginIndex++
loginIndex := pa.CurLoginIndex
pa.WalleQState = WqStateCodeInLogin
- fmt.Println("WalleQServe begin")
+ log2.Debug("WalleQServe begin")
workDir := filepath.Join(dice.BaseConfig.DataDir, conn.RelWorkDir)
_ = os.MkdirAll(workDir, 0o755)
log := dice.Logger
@@ -173,7 +174,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int,
wqc := new(WalleQConfig)
_, err = toml.DecodeFile(configFilePath,wqc)
if err != nil {
- dice.Logger.Error("读取 Walle-q 配置文件失败,请检查!")
+ dice.Zlogger.Error("读取 Walle-q 配置文件失败,请检查!")
return
}
id, _ := pa.mustExtractId(conn.UserId)
@@ -197,7 +198,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int,
isSeldKilling := false
p.OutputHandler = func(line string, _type string) string {
- fmt.Println(line)
+ log.Debug(line)
if loginIndex != pa.CurLoginIndex {
// 当前连接已经无用,进程自杀
if !isSeldKilling {
@@ -271,9 +272,7 @@ func WalleQServe(dice *Dice, conn *EndPointInfo, password string, protocol int,
dice.Logger.Warn("添加到进程组失败,若主进程崩溃,walle-q 进程可能需要手动结束")
}
}
- fmt.Println("wait!")
err = p.Wait()
- fmt.Println(err)
}
if err != nil {
diff --git a/dice/rollvm.go b/dice/rollvm.go
index 095b972d..4e06ac74 100644
--- a/dice/rollvm.go
+++ b/dice/rollvm.go
@@ -559,7 +559,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
num := int(code.Value)
outStr := ""
- for index := 0; index < num; index++ {
+ for index := range num {
var val VMStack
if top-num+index < 0 {
return nil, "", errors.New("E3:无效的表达式")
@@ -733,7 +733,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
return nil, "", getE5()
}
- for i := int64(0); i < t.Value.(int64); i++ {
+ for range t.Value.(int64) {
n := DiceRoll64x(ctx._v1Rand, 10)
if n == 10 {
@@ -1269,7 +1269,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
checkDice(&code)
text := ""
sum := int64(0)
- for i := 0; i < 4; i++ {
+ for range 4 {
n := rand.Int63()%3 - 1
sum += n
switch n {
@@ -1313,7 +1313,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
}
var nums []int64
- for i := int64(0); i < aInt; i++ {
+ for range aInt {
if e.flags.BigFailDiceOn {
nums = append(nums, bInt)
} else {
@@ -1328,7 +1328,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
}
num := int64(0)
- for i := int64(0); i < diceKQ; i++ {
+ for i := range diceKQ {
// 当取数大于上限 跳过
if i >= int64(len(nums)) {
continue
@@ -1337,8 +1337,8 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
}
text := "{"
- for i := int64(0); i < int64(len(nums)); i++ {
- if i == diceKQ {
+ for i := range nums {
+ if int64(i) == diceKQ {
text += "| "
}
text += fmt.Sprintf("%d ", nums[i])
@@ -1355,7 +1355,7 @@ func (e *RollExpression) Evaluate(_ *Dice, ctx *MsgContext) (*VMStack, string, e
// XXX Dice YYY, 如 3d100
var num int64
text := ""
- for i := int64(0); i < aInt; i++ {
+ for range aInt {
var curNum int64
if e.flags.BigFailDiceOn {
curNum = bInt
@@ -1439,7 +1439,7 @@ func DiceDCRoll(randSrc *rand2.PCGSource, addLine int64, pool int64, points int6
var detailsOne []string
maxDice := int64(0)
- for i := int64(0); i < pool; i++ {
+ for range pool {
one := DiceRoll64x(randSrc, points)
if one > maxDice {
maxDice = one
@@ -1496,7 +1496,7 @@ func DiceWodRoll(randSrc *rand2.PCGSource, addLine int64, pool int64, points int
addCount := int64(0)
var detailsOne []string
- for i := int64(0); i < pool; i++ {
+ for range pool {
var reachSuccess bool
var reachAddRound bool
one := DiceRoll64x(randSrc, points)
diff --git a/dice/rollvm_migrate.go b/dice/rollvm_migrate.go
index b4762804..4a7adb1f 100644
--- a/dice/rollvm_migrate.go
+++ b/dice/rollvm_migrate.go
@@ -12,8 +12,209 @@ import (
"github.com/samber/lo"
ds "github.com/sealdice/dicescript"
+
+ log "sealdice-core/utils/kratos"
)
+func (ctx *MsgContext) GenDefaultRollVmConfig() *ds.RollConfig {
+ config := ds.RollConfig{}
+
+ // 根据当前规则开语法 - 暂时是都开
+ config.EnableDiceWoD = true
+ config.EnableDiceCoC = true
+ config.EnableDiceFate = true
+ config.EnableDiceDoubleCross = true
+ config.OpCountLimit = 30000
+
+ am := ctx.Dice.AttrsManager
+ config.HookFuncValueStore = func(vm *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) {
+ // 临时变量
+ if strings.HasPrefix(name, "$t") {
+ if ctx.Player.ValueMapTemp == nil {
+ ctx.Player.ValueMapTemp = &ds.ValueMap{}
+ }
+ ctx.Player.ValueMapTemp.Store(name, v)
+ // 继续存入local 因此solved为false
+ return nil, false
+ }
+
+ // 个人变量
+ if strings.HasPrefix(name, "$m") {
+ if ctx.Session != nil && ctx.Player != nil {
+ playerAttrs := lo.Must(am.LoadById(ctx.Player.UserID))
+ playerAttrs.Store(name, v)
+ }
+ return nil, true
+ }
+
+ // 群变量
+ if ctx.Group != nil && strings.HasPrefix(name, "$g") {
+ groupAttrs := lo.Must(am.LoadById(ctx.Group.GroupID))
+ groupAttrs.Store(name, v)
+ return nil, true
+ }
+ return nil, false
+ }
+
+ reSimpleBP := regexp.MustCompile(`^[bpBP]\d*$`)
+ mctx := ctx
+ config.CustomMakeDetailFunc = func(ctx *ds.Context, details []ds.BufferSpan, dataBuffer []byte, parsedOffset int) string {
+ detailResult := dataBuffer[:parsedOffset]
+
+ var curPoint ds.IntType
+ lastEnd := ds.IntType(-1) //nolint:ineffassign
+
+ type Group struct {
+ begin ds.IntType
+ end ds.IntType
+ tag string
+ spans []ds.BufferSpan
+ val *ds.VMValue
+ }
+
+ // 特殊机制: 从模板读取detail进行覆盖
+ for index, i := range details {
+ if i.Tag == "load" && mctx.SystemTemplate != nil && ctx.UpCtx == nil {
+ expr := string(detailResult[i.Begin:i.End])
+ detailExpr := mctx.SystemTemplate.DetailOverwrite[expr]
+ if detailExpr == "" {
+ // 如果没有,尝试使用通配
+ detailExpr = mctx.SystemTemplate.DetailOverwrite["*"]
+ if detailExpr != "" {
+ // key 应该是等于expr的
+ ctx.StoreNameLocal("name", ds.NewStrVal(expr))
+ }
+ }
+ if detailExpr != "" {
+ v, err := ctx.RunExpr(detailExpr, true)
+ if v != nil {
+ details[index].Text = v.ToString()
+ }
+ if err != nil {
+ details[index].Text = err.Error()
+ }
+ }
+ }
+ }
+
+ var m []Group
+ for _, i := range details {
+ // fmt.Println("?", i, lastEnd)
+ if i.Begin > lastEnd {
+ curPoint = i.Begin
+ m = append(m, Group{begin: curPoint, end: i.End, tag: i.Tag, spans: []ds.BufferSpan{i}, val: i.Ret})
+ } else {
+ m[len(m)-1].spans = append(m[len(m)-1].spans, i)
+ if i.End > m[len(m)-1].end {
+ m[len(m)-1].end = i.End
+ }
+ }
+
+ if i.End > lastEnd {
+ lastEnd = i.End
+ }
+ }
+
+ var detailArr []*ds.VMValue
+ for i := len(m) - 1; i >= 0; i-- {
+ buf := bytes.Buffer{}
+ writeBuf := func(p []byte) {
+ buf.Write(p)
+ }
+ writeBufStr := func(s string) {
+ buf.WriteString(s)
+ }
+
+ item := m[i]
+ size := len(item.spans)
+ sort.Sort(spanByEnd(item.spans))
+ last := item.spans[size-1]
+
+ subDetailsText := ""
+ if size > 1 {
+ // 次级结果,如 (10d3)d5 中,此处为10d3的结果
+ // 例如 (10d3)d5=63[(10d3)d5=...,10d3=19]
+ for j := range len(item.spans) - 1 {
+ span := item.spans[j]
+ subDetailsText += "," + string(detailResult[span.Begin:span.End]) + "=" + span.Ret.ToString()
+ }
+ }
+
+ exprText := last.Expr
+ baseExprText := string(detailResult[item.begin:item.end])
+ if last.Expr == "" {
+ exprText = baseExprText
+ }
+
+ writeBuf(detailResult[:item.begin])
+
+ // 主体结果部分,如 (10d3)d5=63[(10d3)d5=2+2+2+5+2+5+5+4+1+3+4+1+4+5+4+3+4+5+2,10d3=19]
+ partRet := last.Ret.ToString()
+
+ detail := "[" + exprText
+ if last.Text != "" && partRet != last.Text { // 规则1.1
+ detail += "=" + last.Text
+ }
+
+ switch item.tag {
+ case "dnd-rc":
+ detail = "[" + last.Text
+ case "load":
+ detail = "[" + exprText
+ if last.Text != "" {
+ detail += "," + last.Text
+ }
+ case "dice-coc-bonus", "dice-coc-penalty":
+ // 对简单式子进行结果简化,未来或许可以做成通配规则(给左式加个规则进行消除)
+ if reSimpleBP.MatchString(exprText) {
+ detail = "[" + last.Text[1:len(last.Text)-1]
+ }
+ case "load.computed":
+ detail += "=" + partRet
+ }
+
+ detail += subDetailsText + "]"
+ if len(m) == 1 && detail == "["+baseExprText+"]" {
+ detail = "" // 规则1.3
+ }
+ if len(detail) > 400 {
+ detail = "[略]"
+ }
+ writeBufStr(partRet + detail)
+ writeBuf(detailResult[item.end:])
+ detailResult = buf.Bytes()
+
+ d := ds.NewDictValWithArrayMust(
+ ds.NewStrVal("tag"), ds.NewStrVal(item.tag),
+ ds.NewStrVal("expr"), ds.NewStrVal(exprText),
+ ds.NewStrVal("val"), item.val,
+ )
+ detailArr = append(detailArr, d.V())
+ }
+
+ // TODO: 此时加了TrimSpace表现正常,但深层原因是ds在处理"d3 x"这个表达式时多吃了一个空格,修复后取消trim
+ detailStr := strings.TrimSpace(string(detailResult))
+ if detailStr == ctx.Ret.ToString() {
+ detailStr = "" // 如果detail和结果值完全一致,那么将其置空
+ }
+ ctx.StoreNameLocal("details", ds.NewArrayValRaw(lo.Reverse(detailArr)))
+ return detailStr
+ }
+
+ // 设置默认骰子面数
+ if ctx.Group != nil {
+ // 情况不明,在sealchat的第一次测试中出现Group为nil
+ config.DefaultDiceSideExpr = strconv.FormatInt(ctx.Group.DiceSideNum, 10)
+ if config.DefaultDiceSideExpr == "0" {
+ config.DefaultDiceSideExpr = "100"
+ }
+ } else {
+ config.DefaultDiceSideExpr = "100"
+ }
+
+ return &config
+}
+
func (v *VMValue) ConvertToV2() *ds.VMValue {
switch v.TypeID {
case VMTypeInt64:
@@ -78,13 +279,19 @@ func DiceFormatV1(ctx *MsgContext, s string) (string, error) { //nolint:revive
}
func DiceFormat(ctx *MsgContext, s string) string {
- ret, err := DiceFormatV2(ctx, s)
- if err != nil {
- // 遇到异常,尝试一下V1
- ret, _ = DiceFormatV1(ctx, s)
+ engineVersion := ctx.Dice.getTargetVmEngineVersion(VmVersionMsg)
+ if engineVersion == "v2" {
+ ret, err := DiceFormatV2(ctx, s)
+ if err != nil {
+ // 遇到异常,尝试一下V1
+ ret, _ = DiceFormatV1(ctx, s)
+ return ret
+ }
+ return ret
+ } else {
+ ret, _ := DiceFormatV1(ctx, s)
return ret
}
- return ret
}
func DiceFormatTmpl(ctx *MsgContext, s string) string {
@@ -96,7 +303,7 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string {
text = ctx.Dice.TextMap[s].PickSource(randSourceDrawAndTmplSelect).(string)
// 找出其兼容情况,以决定使用什么版本的引擎
- engineVersion := "v2"
+ engineVersion := ctx.Dice.getTargetVmEngineVersion(VMVersionCustomText)
if items, exists := ctx.Dice.TextMapCompatible.Load(s); exists {
if info, exists := items.Load(text); exists {
if info.Version == "v1" {
@@ -108,7 +315,7 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string {
if engineVersion == "v2" {
ret, _ := DiceFormatV2(ctx, text)
return ret
- } else if engineVersion == "v1" {
+ } else {
ret, _ := DiceFormatV1(ctx, text)
return ret
}
@@ -120,10 +327,12 @@ func DiceFormatTmpl(ctx *MsgContext, s string) string {
func (ctx *MsgContext) Eval(expr string, flags *ds.RollConfig) *VMResultV2 {
ctx.CreateVmIfNotExists()
vm := ctx.vm
+ prevConfig := vm.Config
if flags != nil {
vm.Config = *flags
}
err := vm.Run(expr)
+ vm.Config = prevConfig
if err != nil {
return &VMResultV2{vm: vm}
@@ -138,7 +347,7 @@ func (ctx *MsgContext) EvalFString(expr string, flags *ds.RollConfig) *VMResultV
// 隐藏的内置字符串符号 \x1e
r := ctx.Eval("\x1e"+expr+"\x1e", flags)
if r.vm.Error != nil {
- fmt.Println("脚本执行出错: ", expr, "->", r.vm.Error)
+ log.Error("脚本执行出错: ", expr, "->", r.vm.Error)
}
return r
}
@@ -235,7 +444,7 @@ func DiceExprEvalBase(ctx *MsgContext, s string, flags RollExtraFlags) (*VMResul
if flags.V2Only {
return nil, "", err
}
- fmt.Println("脚本执行出错V2: ", strings.ReplaceAll(s, "\x1e", "`"), "->", err)
+ log.Error("脚本执行出错V2: ", strings.ReplaceAll(s, "\x1e", "`"), "->", err)
errV2 := err // 某种情况下没有这个值,很奇怪
// 尝试一下V1
@@ -426,43 +635,9 @@ func (ctx *MsgContext) CreateVmIfNotExists() {
// 初始化骰子
ctx.vm = ds.NewVM()
- // 根据当前规则开语法 - 暂时是都开
- ctx.vm.Config.EnableDiceWoD = true
- ctx.vm.Config.EnableDiceCoC = true
- ctx.vm.Config.EnableDiceFate = true
- ctx.vm.Config.EnableDiceDoubleCross = true
- ctx.vm.Config.OpCountLimit = 30000
+ ctx.vm.Config = *ctx.GenDefaultRollVmConfig()
am := ctx.Dice.AttrsManager
- ctx.vm.Config.HookFuncValueStore = func(vm *ds.Context, name string, v *ds.VMValue) (overwrite *ds.VMValue, solved bool) {
- // 临时变量
- if strings.HasPrefix(name, "$t") {
- if ctx.Player.ValueMapTemp == nil {
- ctx.Player.ValueMapTemp = &ds.ValueMap{}
- }
- ctx.Player.ValueMapTemp.Store(name, v)
- // 继续存入local 因此solved为false
- return nil, false
- }
-
- // 个人变量
- if strings.HasPrefix(name, "$m") {
- if ctx.Session != nil && ctx.Player != nil {
- playerAttrs := lo.Must(am.LoadById(ctx.Player.UserID))
- playerAttrs.Store(name, v)
- }
- return nil, true
- }
-
- // 群变量
- if ctx.Group != nil && strings.HasPrefix(name, "$g") {
- groupAttrs := lo.Must(am.LoadById(ctx.Group.GroupID))
- groupAttrs.Store(name, v)
- return nil, true
- }
- return nil, false
- }
-
ctx.vm.GlobalValueLoadOverwriteFunc = func(name string, curVal *ds.VMValue) *ds.VMValue {
// 临时变量
if strings.HasPrefix(name, "$t") {
@@ -538,162 +713,6 @@ func (ctx *MsgContext) CreateVmIfNotExists() {
return curVal
}
-
- reSimpleBP := regexp.MustCompile(`^[bpBP]\d*$`)
-
- mctx := ctx
- ctx.vm.Config.CustomMakeDetailFunc = func(ctx *ds.Context, details []ds.BufferSpan, dataBuffer []byte, parsedOffset int) string {
- detailResult := dataBuffer[:parsedOffset]
-
- var curPoint ds.IntType
- lastEnd := ds.IntType(-1) //nolint:ineffassign
-
- type Group struct {
- begin ds.IntType
- end ds.IntType
- tag string
- spans []ds.BufferSpan
- val *ds.VMValue
- }
-
- // 特殊机制: 从模板读取detail进行覆盖
- for index, i := range details {
- if i.Tag == "load" && mctx.SystemTemplate != nil && ctx.UpCtx == nil {
- expr := string(detailResult[i.Begin:i.End])
- detailExpr := mctx.SystemTemplate.DetailOverwrite[expr]
- if detailExpr == "" {
- // 如果没有,尝试使用通配
- detailExpr = mctx.SystemTemplate.DetailOverwrite["*"]
- if detailExpr != "" {
- // key 应该是等于expr的
- ctx.StoreNameLocal("name", ds.NewStrVal(expr))
- }
- }
- if detailExpr != "" {
- v, err := ctx.RunExpr(detailExpr, true)
- if v != nil {
- details[index].Text = v.ToString()
- }
- if err != nil {
- details[index].Text = err.Error()
- }
- }
- }
- }
-
- var m []Group
- for _, i := range details {
- // fmt.Println("?", i, lastEnd)
- if i.Begin > lastEnd {
- curPoint = i.Begin
- m = append(m, Group{begin: curPoint, end: i.End, tag: i.Tag, spans: []ds.BufferSpan{i}, val: i.Ret})
- } else {
- m[len(m)-1].spans = append(m[len(m)-1].spans, i)
- if i.End > m[len(m)-1].end {
- m[len(m)-1].end = i.End
- }
- }
-
- if i.End > lastEnd {
- lastEnd = i.End
- }
- }
-
- var detailArr []*ds.VMValue
- for i := len(m) - 1; i >= 0; i-- {
- buf := bytes.Buffer{}
- writeBuf := func(p []byte) {
- buf.Write(p)
- }
- writeBufStr := func(s string) {
- buf.WriteString(s)
- }
-
- item := m[i]
- size := len(item.spans)
- sort.Sort(spanByEnd(item.spans))
- last := item.spans[size-1]
-
- subDetailsText := ""
- if size > 1 {
- // 次级结果,如 (10d3)d5 中,此处为10d3的结果
- // 例如 (10d3)d5=63[(10d3)d5=...,10d3=19]
- for j := 0; j < len(item.spans)-1; j++ {
- span := item.spans[j]
- subDetailsText += "," + string(detailResult[span.Begin:span.End]) + "=" + span.Ret.ToString()
- }
- }
-
- exprText := last.Expr
- baseExprText := string(detailResult[item.begin:item.end])
- if last.Expr == "" {
- exprText = baseExprText
- }
-
- writeBuf(detailResult[:item.begin])
-
- // 主体结果部分,如 (10d3)d5=63[(10d3)d5=2+2+2+5+2+5+5+4+1+3+4+1+4+5+4+3+4+5+2,10d3=19]
- partRet := last.Ret.ToString()
-
- detail := "[" + exprText
- if last.Text != "" && partRet != last.Text { // 规则1.1
- detail += "=" + last.Text
- }
-
- switch item.tag {
- case "dnd-rc":
- detail = "[" + last.Text
- case "load":
- detail = "[" + exprText
- if last.Text != "" {
- detail += "," + last.Text
- }
- case "dice-coc-bonus", "dice-coc-penalty":
- // 对简单式子进行结果简化,未来或许可以做成通配规则(给左式加个规则进行消除)
- if reSimpleBP.MatchString(exprText) {
- detail = "[" + last.Text[1:len(last.Text)-1]
- }
- case "load.computed":
- detail += "=" + partRet
- }
-
- detail += subDetailsText + "]"
- if len(m) == 1 && detail == "["+baseExprText+"]" {
- detail = "" // 规则1.3
- }
- if len(detail) > 400 {
- detail = "[略]"
- }
- writeBufStr(partRet + detail)
- writeBuf(detailResult[item.end:])
- detailResult = buf.Bytes()
-
- d := ds.NewDictValWithArrayMust(
- ds.NewStrVal("tag"), ds.NewStrVal(item.tag),
- ds.NewStrVal("expr"), ds.NewStrVal(exprText),
- ds.NewStrVal("val"), item.val,
- )
- detailArr = append(detailArr, d.V())
- }
-
- detailStr := string(detailResult)
- if detailStr == ctx.Ret.ToString() {
- detailStr = "" // 如果detail和结果值完全一致,那么将其置空
- }
- ctx.StoreNameLocal("details", ds.NewArrayValRaw(lo.Reverse(detailArr)))
- return detailStr
- }
-
- // 设置默认骰子面数
- if ctx.Group != nil {
- // 情况不明,在sealchat的第一次测试中出现Group为nil
- ctx.vm.Config.DefaultDiceSideExpr = fmt.Sprintf("%d", ctx.Group.DiceSideNum)
- if ctx.vm.Config.DefaultDiceSideExpr == "0" {
- ctx.vm.Config.DefaultDiceSideExpr = "100"
- }
- } else {
- ctx.vm.Config.DefaultDiceSideExpr = "100"
- }
}
func DiceFormatV2(ctx *MsgContext, s string) (string, error) { //nolint:revive
diff --git a/dice/storylog/storylog.go b/dice/storylog/storylog.go
index 156c96ae..28dd9bef 100644
--- a/dice/storylog/storylog.go
+++ b/dice/storylog/storylog.go
@@ -9,16 +9,16 @@ import (
"net/http"
"strconv"
- "github.com/jmoiron/sqlx"
- "go.uber.org/zap"
+ "gorm.io/gorm"
"sealdice-core/dice/model"
+ log "sealdice-core/utils/kratos"
)
type UploadEnv struct {
Dir string
- Db *sqlx.DB
- Log *zap.SugaredLogger
+ Db *gorm.DB
+ Log *log.Helper
Backends []string
Version StoryVersion
diff --git a/dice/utils.go b/dice/utils.go
index 30ca375a..a161267c 100644
--- a/dice/utils.go
+++ b/dice/utils.go
@@ -573,3 +573,25 @@ func UnpackGroupUserId(id string) (groupIdPart, userIdPart string, ok bool) {
return "", "", false
}
+
+const (
+ VMVersionReply = "reply"
+ VMVersionDeck = "deck"
+ VMVersionCustomText = "custom-text"
+ VmVersionMsg = "msg"
+)
+
+func (d *Dice) getTargetVmEngineVersion(targetType string) string {
+ switch targetType {
+ case VMVersionReply:
+ return d.Config.VMVersionForReply
+ case VMVersionDeck:
+ return d.Config.VMVersionForDeck
+ case VMVersionCustomText:
+ return d.Config.VMVersionForCustomText
+ case VmVersionMsg:
+ return d.Config.VMVersionForMsg
+ default:
+ return "v2"
+ }
+}
diff --git a/dice/utils_email.go b/dice/utils_email.go
index f23b0835..782e4f92 100644
--- a/dice/utils_email.go
+++ b/dice/utils_email.go
@@ -1,6 +1,7 @@
package dice
import (
+ "errors"
"fmt"
"strings"
@@ -23,7 +24,7 @@ const (
)
func (d *Dice) CanSendMail() bool {
- if d.MailFrom == "" || d.MailPassword == "" || d.MailSMTP == "" {
+ if d.Config.MailFrom == "" || d.Config.MailPassword == "" || d.Config.MailSMTP == "" {
return false
}
return true
@@ -31,7 +32,7 @@ func (d *Dice) CanSendMail() bool {
func (d *Dice) SendMail(body string, m MailCode) error {
if !d.CanSendMail() {
- return fmt.Errorf("邮件配置不完整")
+ return errors.New("邮件配置不完整")
}
sub := "Seal News: "
switch m {
@@ -47,7 +48,7 @@ func (d *Dice) SendMail(body string, m MailCode) error {
sub += "Test 测试邮件"
}
var to []string
- for _, id := range d.NoticeIDs {
+ for _, id := range d.Config.NoticeIDs {
if strings.HasPrefix(id, "QQ:") {
to = append(to, id[3:]+"@qq.com")
}
@@ -70,7 +71,7 @@ func (d *Dice) SendMailRow(subject string, to []string, content string, attachme
}
}
m.SetHeader("Subject", fmt.Sprintf("[%s] %s", diceName, subject))
- m.SetHeader("From", d.MailFrom)
+ m.SetHeader("From", d.Config.MailFrom)
m.SetHeader("To", to...)
if content == "" {
m.SetBody("text/plain", "***自动邮件,无需回复***")
@@ -83,7 +84,7 @@ func (d *Dice) SendMailRow(subject string, to []string, content string, attachme
}
}
- dialer := gomail.NewDialer(d.MailSMTP, 25, d.MailFrom, d.MailPassword)
+ dialer := gomail.NewDialer(d.Config.MailSMTP, 25, d.Config.MailFrom, d.Config.MailPassword)
if err := dialer.DialAndSend(m); err != nil {
d.Logger.Error(err)
} else {
diff --git a/dice/verify.go b/dice/verify.go
index 2521556c..7ce89937 100644
--- a/dice/verify.go
+++ b/dice/verify.go
@@ -1,6 +1,7 @@
package dice
import (
+ "crypto/sha256"
"encoding/base64"
"fmt"
"os"
@@ -10,6 +11,7 @@ import (
"github.com/vmihailenco/msgpack"
"sealdice-core/utils/crypto"
+ log "sealdice-core/utils/kratos"
)
var (
@@ -22,8 +24,8 @@ func initVerify() {
key := os.Getenv("SEAL_TRUSTED_PRIVATE_KEY")
if len(key) > 0 {
SealTrustedClientPrivateKey = key
- } else {
- fmt.Println("SEAL_TRUSTED_PRIVATE_KEY not found, maybe in development mode")
+ } else if len(SealTrustedClientPrivateKey) == 0 {
+ log.Warn("SEAL_TRUSTED_PRIVATE_KEY not found, maybe in development mode")
}
}
@@ -70,3 +72,37 @@ func GenerateVerificationCode(platform string, userID string, username string, u
return fmt.Sprintf("SEAL%%%s", base2048.DefaultEncoding.EncodeToString(dp))
}
}
+
+type payloadPublicDice struct {
+ Version string `msgpack:"version,omitempty"`
+ Sign []byte `msgpack:"sign,omitempty"`
+}
+
+func GenerateVerificationKeyForPublicDice(data any) string {
+ doEcdsaSign := len(SealTrustedClientPrivateKey) > 0
+ pp, _ := msgpack.Marshal(data)
+
+ var sign []byte
+ if doEcdsaSign {
+ var err error
+ sign, err = crypto.EcdsaSignRow(pp, SealTrustedClientPrivateKey)
+ if err != nil {
+ return ""
+ }
+ } else {
+ h := sha256.New()
+ h.Write(pp)
+ sign = h.Sum(nil)
+ }
+
+ d := payloadPublicDice{
+ Version: VERSION.String(),
+ Sign: sign,
+ }
+
+ dp, _ := msgpack.Marshal(d)
+ if doEcdsaSign {
+ return fmt.Sprintf("SEAL#%s", base64.StdEncoding.EncodeToString(dp))
+ }
+ return fmt.Sprintf("SEAL~%s", base64.StdEncoding.EncodeToString(dp))
+}
diff --git a/dice/version.go b/dice/version.go
index 178d469f..d32a46a3 100644
--- a/dice/version.go
+++ b/dice/version.go
@@ -21,7 +21,7 @@ var (
// APP_CHANNEL 更新频道,stable/dev,在 action 构建时自动注入
APP_CHANNEL = "dev" //nolint:revive
- VERSION_CODE = int64(1004006) //nolint:revive
+ VERSION_CODE = int64(1004101) //nolint:revive
VERSION_JSAPI_COMPATIBLE = []*semver.Version{
VERSION,
diff --git a/go.mod b/go.mod
index c7bbda3f..f684dfcb 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
module sealdice-core
-go 1.20
+go 1.22
require (
- github.com/Masterminds/semver/v3 v3.2.1
+ github.com/Masterminds/semver/v3 v3.3.0
github.com/Milly/go-base2048 v0.1.0
github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a
github.com/Szzrain/DingTalk-go v0.0.8-alpha
@@ -12,10 +12,10 @@ require (
github.com/alexmullins/zip v0.0.0-20180717182244-4affb64b04d0
github.com/antlabs/strsim v0.0.3
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
- github.com/blevesearch/bleve/v2 v2.3.10
+ github.com/blevesearch/bleve/v2 v2.4.3
github.com/bwmarrin/discordgo v0.28.1
- github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
- github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab
+ github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd
+ github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/evanw/esbuild v0.23.1
github.com/fy0/go-autostart v0.0.0-20220515100644-a25d81ed766b
github.com/fy0/gojax v0.0.0-20221225152702-4140cf8509bd
@@ -23,10 +23,10 @@ require (
github.com/fyrchik/go-shlex v0.0.0-20210215145004-cd7f49bfd959
github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc
github.com/glebarez/go-sqlite v1.22.0
+ github.com/glebarez/sqlite v1.11.0
github.com/go-creed/sat v1.0.3
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/golang-module/carbon v1.7.3
- github.com/gonutz/w32/v2 v2.11.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/grokify/html-strip-tags-go v0.0.1
@@ -39,7 +39,7 @@ require (
github.com/lonelyevil/kook/log_adapter/plog v0.0.31
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
github.com/matoous/go-nanoid/v2 v2.1.0
- github.com/mattn/go-sqlite3 v1.14.23
+ github.com/mattn/go-sqlite3 v1.14.24
github.com/mitchellh/mapstructure v1.5.0
github.com/monaco-io/request v1.0.16
github.com/mozillazg/go-pinyin v0.20.0
@@ -54,50 +54,69 @@ require (
github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105
github.com/sahilm/fuzzy v0.1.1
github.com/samber/lo v1.44.0
- github.com/schollz/progressbar/v3 v3.14.6
+ github.com/schollz/progressbar/v3 v3.17.0
github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083
github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c
- github.com/slack-go/slack v0.13.0
+ github.com/slack-go/slack v0.15.0
github.com/sunshineplan/imgconv v1.1.4
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tdewolff/minify/v2 v2.20.37
github.com/tidwall/buntdb v1.3.1
github.com/vmihailenco/msgpack v4.0.4+incompatible
- github.com/xuri/excelize/v2 v2.8.1
+ github.com/xuri/excelize/v2 v2.9.0
github.com/yuin/goldmark v1.7.4
- go.etcd.io/bbolt v1.3.9
+ go.etcd.io/bbolt v1.3.11
go.uber.org/zap v1.27.0
- golang.org/x/crypto v0.27.0
+ golang.org/x/crypto v0.29.0
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8
- golang.org/x/sys v0.25.0
- golang.org/x/text v0.18.0
+ golang.org/x/sys v0.27.0
+ golang.org/x/text v0.20.0
golang.org/x/time v0.5.0
gopkg.in/elazarl/goproxy.v1 v1.0.0-20180725130230-947c36da3153
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
+ gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276
+ gorm.io/gorm v1.25.12
+)
+
+require (
+ github.com/blevesearch/bleve_index_api v1.1.13
+ github.com/go-gorm/caches/v4 v4.0.5
+ github.com/gofrs/flock v0.12.1
+ github.com/gonutz/w32/v2 v2.11.1
+ github.com/joho/godotenv v1.5.1
+ github.com/spaolacci/murmur3 v1.1.0
+ github.com/tidwall/gjson v1.17.0
+ github.com/tidwall/sjson v1.2.5
+ gorm.io/driver/mysql v1.5.7
+ gorm.io/driver/postgres v1.5.11
+ moul.io/zapfilter v1.7.0
)
require (
- github.com/RoaringBitmap/roaring v1.2.3 // indirect
- github.com/bits-and-blooms/bitset v1.2.2 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/RoaringBitmap/roaring v1.9.3 // indirect
+ github.com/bits-and-blooms/bitset v1.12.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.2.0 // indirect
- github.com/blevesearch/bleve_index_api v1.0.6 // indirect
- github.com/blevesearch/geo v0.1.18 // indirect
+ github.com/blevesearch/geo v0.1.20 // indirect
+ github.com/blevesearch/go-faiss v1.0.23 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
- github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect
+ github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
- github.com/blevesearch/vellum v1.0.10 // indirect
+ github.com/blevesearch/vellum v1.0.11 // indirect
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
- github.com/blevesearch/zapx/v15 v15.3.13 // indirect
+ github.com/blevesearch/zapx/v15 v15.3.16 // indirect
+ github.com/blevesearch/zapx/v16 v16.1.8 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
- github.com/dlclark/regexp2 v1.10.0 // indirect
+ github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a // indirect
github.com/elazarl/goproxy/ext v0.0.0-20230808193330-2592e75ae04a // indirect
@@ -107,9 +126,10 @@ require (
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
- github.com/go-ole/go-ole v1.2.4 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-resty/resty/v2 v2.11.0 // indirect
- github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
+ github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobuffalo/envy v1.7.0 // indirect
@@ -118,18 +138,23 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/geo v0.0.0-20230404232722-c4acd7a044dc // indirect
- github.com/golang/protobuf v1.5.3 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
- github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7 // indirect
- github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae // indirect
- github.com/joho/godotenv v1.3.0 // indirect
+ github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
+ github.com/hhrutter/lzw v1.0.0 // indirect
+ github.com/hhrutter/tiff v1.0.1 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.5.5 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-runewidth v0.0.14 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -137,19 +162,19 @@ require (
github.com/mschoch/smat v0.2.0 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
- github.com/pdfcpu/pdfcpu v0.4.0 // indirect
+ github.com/pdfcpu/pdfcpu v0.8.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
- github.com/richardlehane/msoleps v1.0.3 // indirect
+ github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d // indirect
- github.com/sunshineplan/pdf v1.0.3 // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ github.com/sunshineplan/pdf v1.0.7 // indirect
github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tdewolff/parse/v2 v2.7.15 // indirect
github.com/tidwall/btree v1.7.0 // indirect
- github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
@@ -157,14 +182,14 @@ require (
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
- github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
- github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
+ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
+ github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
go.uber.org/multierr v1.10.0 // indirect
- golang.org/x/image v0.18.0 // indirect
+ golang.org/x/image v0.19.0 // indirect
golang.org/x/mod v0.18.0 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sync v0.8.0 // indirect
- golang.org/x/term v0.24.0 // indirect
+ golang.org/x/net v0.31.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/term v0.26.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
@@ -178,6 +203,10 @@ require (
replace (
github.com/Szzrain/dodo-open-go v0.2.7 => github.com/sealdice/dodo-open-go v0.2.8
+ // Try to fix arm64 bug with better snappy.
+ github.com/blevesearch/zapx/v16 v16.1.8 => github.com/PaienNate/zapx/v16 v16.1.9
+ // Try to fix sqlite in cgofree
+ github.com/glebarez/sqlite v1.11.0 => github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685
github.com/lonelyevil/kook v0.0.31 => github.com/sealdice/kook v0.0.3
github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 => github.com/fy0/GoWebsocket v0.0.0-20231128163937-aa5c110b25c6
)
diff --git a/go.sum b/go.sum
index 27aa379c..8f9459d1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,18 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Milly/go-base2048 v0.1.0 h1:7ZgpCR3cjcAAVqIo+B8Q3P1+VFHRS8zilzAq062rUUk=
github.com/Milly/go-base2048 v0.1.0/go.mod h1:kl6eYBwGnoIjv8k9UmgS+bekm6870ojptcVnT11e3jE=
-github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
-github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
+github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685 h1:O6OPpCufcEJD+eWENAwuwVZHONKmP77X2wlzMTQZ5gg=
+github.com/PaienNate/sqlite v0.0.0-20241102151933-067d82f14685/go.mod h1:GajiCpqLxU0a1gP13oAEiJAx9r87kVSdfEQy4O69ZTo=
+github.com/PaienNate/zapx/v16 v16.1.9 h1:GA4jIOx9OPFqTGym7ucqKNNSw01BaIyOIjDPxR3w47A=
+github.com/PaienNate/zapx/v16 v16.1.9/go.mod h1:zuxVgVaLZ0g4lZvrv06xDc24N6nLCOzXYHVkXI7LMHM=
+github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
+github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a h1:NPnGVqpua4c1iEFVdxnBJA9viP5bo2Zp2jfflbcjdto=
github.com/ShiraazMoollatjie/goluhn v0.0.0-20211017190329-0d86158c056a/go.mod h1:5LI6VqIHoGmWsR0EJLbct5bBrtM/0pTonaAyGKmFk9U=
github.com/Szzrain/DingTalk-go v0.0.8-alpha h1:mSR/ORDDjtndoR12WrEdd3hdxxXXm9VMQ/r75NJkkkE=
@@ -23,33 +28,37 @@ github.com/antlabs/strsim v0.0.3/go.mod h1:bIcymn+2jtt01korFun0bs8PsYZeQa82aHoYM
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.2.0 h1:N+g3GTQ0TVbghahYyzwkQbMZR+IwIwFFC8dpIChtN0U=
github.com/bits-and-blooms/bloom/v3 v3.2.0/go.mod h1:MC8muvBzzPOFsrcdND/A7kU7kMhkqb9KI70JlZCP+C8=
-github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
-github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
-github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8=
-github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
-github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
-github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
+github.com/blevesearch/bleve/v2 v2.4.3 h1:XDYj+1prgX84L2Cf+V3ojrOPqXxy0qxyd2uLMmeuD+4=
+github.com/blevesearch/bleve/v2 v2.4.3/go.mod h1:hEPDPrbYw3vyrm5VOa36GyS4bHWuIf4Fflp7460QQXY=
+github.com/blevesearch/bleve_index_api v1.1.13 h1:+nrA6oRJr85aCPyqaeZtsruObwKojutfonHJin/BP48=
+github.com/blevesearch/bleve_index_api v1.1.13/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
+github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
+github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
+github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc=
+github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
-github.com/blevesearch/scorch_segment_api/v2 v2.1.6 h1:CdekX/Ob6YCYmeHzD72cKpwzBjvkOGegHOqhAkXp6yA=
-github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY=
+github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
-github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
-github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
+github.com/blevesearch/vellum v1.0.11 h1:SJI97toEFTtA9WsDZxkyGTaBWFdWl1n2LEDCXLCq/AU=
+github.com/blevesearch/vellum v1.0.11/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
@@ -58,16 +67,15 @@ github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIq
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
-github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
-github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
+github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE=
+github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
-github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
-github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
+github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -79,17 +87,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
-github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
-github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
-github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
-github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
-github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
-github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
-github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab h1:LrVf0AFnp5WiGKJ0a6cFf4RwNIN327uNUeVGJtmAFEE=
-github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
+github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
+github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE=
+github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
+github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
+github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
@@ -129,14 +132,18 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-creed/sat v1.0.3 h1:V1IkiYYFDPKXaRhdg95oAh5IHZ3Qhs5AEVlhteM+6XA=
github.com/go-creed/sat v1.0.3/go.mod h1:ZxAhQ0ikMzjqeMbFeoMdCr6es8p10Y87F2nHkqNjSbY=
-github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
+github.com/go-gorm/caches/v4 v4.0.5 h1:Sdj9vxbEM0sCmv5+s5o6GzoVMuraWF0bjJJvUU+7c1U=
+github.com/go-gorm/caches/v4 v4.0.5/go.mod h1:Ms8LnWVoW4GkTofpDzFH8OfDGNTjLxQDyxBmRN67Ujw=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
-github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
-github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
+github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
@@ -158,6 +165,8 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U=
@@ -173,8 +182,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gonutz/w32/v2 v2.11.1 h1:plG738ZY7VIkPGf3adZ6lFeAf2evCKrULKyZT5GrPoc=
@@ -186,10 +195,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
-github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
-github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
+github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -200,24 +209,35 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7 h1:oYOKPR69u1kReWwnVhZlkduTrEtXRYJTDj5rUCMyPLY=
-github.com/hhrutter/lzw v0.0.0-20230302233922-b0c9d7de54a7/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
-github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae h1:cpxrFNY+FIz7W4nuaG5McM/OyOBQt44Thl0Q/hFBhGo=
-github.com/hhrutter/tiff v0.0.0-20230302235510-5b20711894ae/go.mod h1:zluYmeCkNexc8HFzfc2MTVwA8gcPuFQp/ngjvIQ0CFo=
+github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
+github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
+github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0=
+github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
-github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juliangruber/go-intersect v1.1.0 h1:sc+y5dCjMMx0pAdYk/N6KBm00tD/f3tq+Iox7dYDUrY=
github.com/juliangruber/go-intersect v1.1.0/go.mod h1:WMau+1kAmnlQnKiikekNJbtGtfmILU/mMU6H7AgKbWQ=
-github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
@@ -226,8 +246,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -254,11 +274,11 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
-github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
-github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -297,16 +317,19 @@ github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
+github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
-github.com/pdfcpu/pdfcpu v0.4.0 h1:381iGNvMeLP+GFqIAqgd0LSj36AsK3JH4UTaF6D5jRc=
-github.com/pdfcpu/pdfcpu v0.4.0/go.mod h1:9NDeS6hrCheauxw6YUlzgL/q6At2+PMzUKyFcfUzLLY=
+github.com/pdfcpu/pdfcpu v0.8.1 h1:AiWUb8uXlrXqJ73OmiYXBjDF0Qxt4OuM281eAfkAOMA=
+github.com/pdfcpu/pdfcpu v0.8.1/go.mod h1:M5SFotxdaw0fedxthpjbA/PADytAo6wJnGH0SSBWJ7s=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/phuslu/log v1.0.80/go.mod h1:kzJN3LRifrepxThMjufQwS7S35yFAB+jAV1qgA7eBW4=
github.com/phuslu/log v1.0.88 h1:kivXMpYQ2hd9BxiJNhRM5xnaEZaGunQYlnRQdk/aBw8=
github.com/phuslu/log v1.0.88/go.mod h1:F8osGJADo5qLK/0F88djWwdyoZZ9xDJQL1HYRHFEkS0=
+github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -316,8 +339,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
-github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
-github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -326,7 +349,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -336,18 +358,19 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA=
github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
-github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
-github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
+github.com/schollz/progressbar/v3 v3.17.0 h1:Fv+vG6O6jnJwdjCelvfyYO7sF2jaUGQVmdH4CxcZdsQ=
+github.com/schollz/progressbar/v3 v3.17.0/go.mod h1:5H4fLgifX+KeQCsEJnZTOepgZLe1jFF1lpPXb68IJTA=
github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083 h1:s/jzCGYlM/0+TYTXwva5574EFnIv/ggPCoXHFpdbSUw=
github.com/sealdice/botgo v0.0.0-20240102160217-e61d5bdfe083/go.mod h1:MGtR0REQDslBwQE+Rln4P9iDjH/ZInlu5qzOLdvBWJU=
github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c h1:Z+H+yMma3IcZfX2nLF7nOP50XWmOytLVlaIkT7QgbsA=
github.com/sealdice/dicescript v0.0.0-20240927083134-65269b7d051c/go.mod h1:uof752qJvEQ4Kze+NVag+RKGgj5C4K3kMHoK3e2vOLg=
github.com/sealdice/kook v0.0.3 h1:STMtiKRMFjhSFmUxi0BU5ktNkCQ8qi7Y5EEfrmYKvWY=
github.com/sealdice/kook v0.0.3/go.mod h1:WjHC7AmbmNjInT/U/etBVOmAw7T6EqdCwApceRGs1sk=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
-github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ=
-github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
+github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=
+github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -363,22 +386,25 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/sunshineplan/imgconv v1.1.4 h1:lViOZUbDIgW8o74naySXJqZOFgXSW1AdU/cdzZRnVTo=
github.com/sunshineplan/imgconv v1.1.4/go.mod h1:Bc4qh4Z+nslcq+Csck01QZgzWvirKUdltRI7vnEAKd8=
-github.com/sunshineplan/pdf v1.0.3 h1:Ng+/f35i0jlB87STk6sXaINqhF0JsIyXLZntWWOcGhg=
-github.com/sunshineplan/pdf v1.0.3/go.mod h1:4JqkeywDS6kIsqODkNKZ847P2K8eRpSSzf12FTRmUVg=
+github.com/sunshineplan/pdf v1.0.7 h1:62xlc079jh4tGLDjiihyyhwVFkn0IsxLyDpHplbG9Ew=
+github.com/sunshineplan/pdf v1.0.7/go.mod h1:QsEmZCWBE3uFK8PCrM0pua1WDWLNU77YusiDEcY56OQ=
github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906 h1:+yYRCj+PGQNnnen4+/Q7eKD2J87RJs+O39bjtHhPauk=
github.com/sunshineplan/tiff v0.0.0-20220128141034-29b9d69bd906/go.mod h1:O+Ar7ouRbdfxLgoZLFz447/dvdM1NVKk1VpOQaijvAU=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
+github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw=
@@ -387,18 +413,22 @@ github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGp
github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
+github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
+github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o=
github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
+github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -406,6 +436,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@@ -416,21 +448,29 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0=
-github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
-github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
-github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
-github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
-github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
+github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
+github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
+github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
+github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
-go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
+go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
+go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -441,14 +481,17 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
-golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
+golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
+golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
@@ -467,15 +510,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -483,6 +527,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -493,9 +538,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -505,18 +550,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -525,15 +568,18 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -570,12 +616,23 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276 h1:IHpexPpZZkm4NqbKneioNEYxTpOGZnDm8HPjabyX+Uw=
+gorm.io/driver/sqlite v1.5.7-0.20240930031831-02b8e0623276/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@@ -584,3 +641,5 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+moul.io/zapfilter v1.7.0 h1:7aFrG4N72bDH9a2BtYUuUaDS981Dxu3qybWfeqaeBDU=
+moul.io/zapfilter v1.7.0/go.mod h1:M+N2s+qZiA+bzRoyKMVRxyuERijS2ovi2pnMyiOGMvc=
diff --git a/install.go b/install.go
index ef667756..4c1e675f 100644
--- a/install.go
+++ b/install.go
@@ -45,31 +45,30 @@ func serviceInstall(isInstall bool, serviceName string, user string) {
}
prg := &program{}
- fmt.Println("正在试图访问系统服务 ...")
+ fmt.Fprintln(os.Stdout, "正在试图访问系统服务 ...")
s, err := service.New(prg, svcConfig)
if isInstall {
- fmt.Println("正在安装系统服务,安装完成后,SealDice将自动随系统启动")
+ fmt.Fprintln(os.Stdout, "正在安装系统服务,安装完成后,SealDice将自动随系统启动")
if err != nil {
- fmt.Printf("安装失败: %s\n", err.Error())
+ fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error())
}
_, err = s.Logger(nil)
if err != nil {
- fmt.Printf("安装失败: %s\n", err.Error())
- fmt.Println(err)
+ fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error())
}
err = s.Install()
if err != nil {
- fmt.Printf("安装失败: %s\n", err.Error())
+ fmt.Fprintf(os.Stdout, "安装失败: %s\n", err.Error())
return
}
- fmt.Println("安装完成,正在启动……")
+ fmt.Fprintln(os.Stdout, "安装完成,正在启动……")
_ = s.Start()
} else {
- fmt.Println("正在卸载系统服务……")
+ fmt.Fprintln(os.Stdout, "正在卸载系统服务……")
_ = s.Stop()
_ = s.Uninstall()
- fmt.Println("系统服务已删除")
+ fmt.Fprintln(os.Stdout, "系统服务已删除")
}
}
diff --git a/logger.go b/logger.go
deleted file mode 100644
index e882563c..00000000
--- a/logger.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package main
-
-import (
- "os"
-
- "github.com/natefinch/lumberjack"
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
-)
-
-func MainLoggerInit(path string, enableConsoleLog bool) {
- lumlog := &lumberjack.Logger{
- Filename: path,
- MaxSize: 10, // megabytes
- MaxBackups: 3, // number of log files
- MaxAge: 7, // days
- }
-
- encoder := getEncoder()
- cores := []zapcore.Core{
- zapcore.NewCore(encoder, zapcore.AddSync(lumlog), zapcore.DebugLevel),
- }
-
- if enableConsoleLog {
- pe2 := zap.NewProductionEncoderConfig()
- pe2.EncodeTime = zapcore.ISO8601TimeEncoder
-
- consoleEncoder := zapcore.NewConsoleEncoder(pe2)
- cores = append(cores, zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zapcore.InfoLevel))
- }
-
- core := zapcore.NewTee(cores...)
-
- loggerRaw := zap.New(core, zap.AddCaller())
- defer func(loggerRaw *zap.Logger) {
- _ = loggerRaw.Sync()
- }(loggerRaw) // flushes buffer, if any
-
- logger = loggerRaw.Sugar()
- logger.Infow("核心日志开始记录")
-}
-
-var logger *zap.SugaredLogger
-
-func getEncoder() zapcore.Encoder {
- encoderConfig := zap.NewProductionEncoderConfig()
- encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
- encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
-
- return zapcore.NewConsoleEncoder(encoderConfig)
-}
diff --git a/main.go b/main.go
index 932317a6..12dd4bd4 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,8 @@
package main
+// _ "net/http/pprof"
import (
+ "errors"
"fmt"
"io/fs"
"mime"
@@ -15,44 +17,51 @@ import (
"syscall"
"time"
- // _ "net/http/pprof"
-
+ "github.com/gofrs/flock"
"github.com/jessevdk/go-flags"
+ "github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
- "go.uber.org/zap"
"go.uber.org/zap/zapcore"
"sealdice-core/api"
"sealdice-core/dice"
- diceLogger "sealdice-core/dice/logger"
"sealdice-core/dice/model"
"sealdice-core/migrate"
"sealdice-core/static"
"sealdice-core/utils/crypto"
+ log "sealdice-core/utils/kratos"
+ "sealdice-core/utils/oschecker"
+ "sealdice-core/utils/paniclog"
)
-/**
+/*
+*
二进制目录结构:
data/configs
data/extensions
data/logs
-
extensions/
*/
+var sealLock = flock.New("sealdice-lock.lock")
+
func cleanupCreate(diceManager *dice.DiceManager) func() {
return func() {
- logger.Info("程序即将退出,进行清理……")
+ log.Info("程序即将退出,进行清理……")
err := recover()
if err != nil {
showWindow()
- logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
+ log.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
// 顺便修正一下上面这个,应该是木落忘了。
if runtime.GOOS == "windows" {
exec.Command("pause") // windows专属
}
}
+ err = sealLock.Unlock()
+ if err != nil {
+ log.Errorf("文件锁归还出现异常 %v", err)
+ }
if !diceManager.CleanupFlag.CompareAndSwap(0, 1) {
// 尝试更新cleanup标记,如果已经为1则退出
@@ -61,15 +70,16 @@ func cleanupCreate(diceManager *dice.DiceManager) func() {
for _, i := range diceManager.Dice {
if i.IsAlreadyLoadConfig {
- i.BanList.SaveChanged(i)
+ i.Config.BanList.SaveChanged(i)
i.Save(true)
+ i.AttrsManager.Stop()
for _, j := range i.ExtList {
if j.Storage != nil {
// 关闭
err := j.StorageClose()
if err != nil {
showWindow()
- logger.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
+ log.Errorf("异常: %v\n堆栈: %v", err, string(debug.Stack()))
// 木落没有加该检查 补充上
if runtime.GOOS == "windows" {
exec.Command("pause") // windows专属
@@ -90,7 +100,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() {
dbData := d.DBData
if dbData != nil {
d.DBData = nil
- _ = dbData.Close()
+ db, err := dbData.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
@@ -101,7 +115,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() {
dbLogs := d.DBLogs
if dbLogs != nil {
d.DBLogs = nil
- _ = dbLogs.Close()
+ db, err := dbLogs.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
@@ -113,7 +131,11 @@ func cleanupCreate(diceManager *dice.DiceManager) func() {
if cm != nil && cm.DB != nil {
dbCensor := cm.DB
cm.DB = nil
- _ = dbCensor.Close()
+ db, err := dbCensor.DB()
+ if err != nil {
+ return
+ }
+ _ = db.Close()
}
})()
}
@@ -192,18 +214,46 @@ func main() {
LogLevel int8 `long:"log-level" description:"设置日志等级" default:"0" choice:"-1" choice:"0" choice:"1" choice:"2" choice:"3" choice:"4" choice:"5"`
ContainerMode bool `long:"container-mode" description:"容器模式,该模式下禁用内置客户端"`
}
-
+ // 读取命令行传参
_, err := flags.ParseArgs(&opts, os.Args)
if err != nil {
return
}
-
+ // 提前到最开始初始化所有日志
+ // 1. 初始化全局Kartos日志
+ log.InitZapWithKartosLog(zapcore.Level(opts.LogLevel))
+ // 2. 初始化全局panic捕获日志
+ paniclog.InitPanicLog()
+ // 3. 提示日志打印
+ log.Info("运行日志开始记录,海豹出现故障时可查看 data/main.log 与 data/panic.log 获取更多信息")
+ // 加载env相关
+ err = godotenv.Load()
+ if err != nil {
+ log.Errorf("未读取到.env参数,若您未使用docker或第三方数据库,可安全忽略。")
+ }
+ // 初始化文件加锁系统
+ locked, err := sealLock.TryLock()
+ // 如果有错误,或者未能取到锁
+ if err != nil || !locked {
+ // 打日志的时候防止打出nil
+ if err == nil {
+ err = errors.New("海豹正在运行中")
+ }
+ log.Errorf("获取锁文件失败,原因为: %v", err)
+ showMsgBox("获取锁文件失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份海豹程序!")
+ return
+ }
+ judge, osr := oschecker.OldVersionCheck()
+ // 预留收集信息的接口,如果有需要可以考虑从这里拿数据。不从这里做提示的原因是Windows和Linux的展示方式不同。
+ if judge {
+ log.Info(osr)
+ }
if opts.Version {
- fmt.Println(dice.VERSION.String())
+ fmt.Fprintln(os.Stdout, dice.VERSION.String())
return
}
if opts.DBCheck {
- model.DBCheck("data/default")
+ model.DBCheck()
return
}
if opts.VacuumDB {
@@ -212,14 +262,14 @@ func main() {
}
if opts.ShowEnv {
for i, e := range os.Environ() {
- println(i, e)
+ fmt.Fprintln(os.Stdout, i, e)
}
return
}
deleteOldWrongFile()
if opts.Delay != 0 {
- fmt.Println("延迟启动", opts.Delay, "秒")
+ log.Infof("延迟启动 %d 秒", opts.Delay)
time.Sleep(time.Duration(opts.Delay) * time.Second)
}
@@ -228,15 +278,12 @@ func main() {
}
_ = os.MkdirAll("./data", 0o755)
- MainLoggerInit("./data/main.log", true)
-
- diceLogger.SetEnableLevel(zapcore.Level(opts.LogLevel))
// 提早初始化是为了读取ServiceName
diceManager := &dice.DiceManager{}
if opts.ContainerMode {
- logger.Info("当前为容器模式,内置适配器与更新功能已被禁用")
+ log.Info("当前为容器模式,内置适配器与更新功能已被禁用")
diceManager.ContainerMode = true
}
@@ -244,7 +291,7 @@ func main() {
diceManager.IsReady = true
if opts.Address != "" {
- fmt.Println("由参数输入了服务地址:", opts.Address)
+ log.Infof("由参数输入了服务地址: %s", opts.Address)
diceManager.ServeAddress = opts.Address
}
@@ -281,17 +328,17 @@ func main() {
// 只有不同文件才进行校验
// windows平台旧版本到1.4.0流程
_ = os.WriteFile("./升级失败指引.txt", []byte("如果升级成功不用理会此文档,直接删除即可。\r\n\r\n如果升级后无法启动,或再次启动后恢复到旧版本,先不要紧张。\r\n你升级前的数据备份在backups目录。\r\n如果无法启动,请删除海豹目录中的\"update\"、\"auto_update.exe\"并手动进行升级。\n如果升级成功但在再次重启后回退版本,同上。\n\n如有其他问题可以加企鹅群询问:524364253 562897832"), 0o644)
- logger.Warn("检测到 auto_update.exe,即将自动退出当前程序并进行升级")
- logger.Warn("程序目录下会出现“升级日志.log”,这代表升级正在进行中,如果失败了请检查此文件。")
+ log.Warn("检测到 auto_update.exe,即将自动退出当前程序并进行升级")
+ log.Warn("程序目录下会出现“升级日志.log”,这代表升级正在进行中,如果失败了请检查此文件。")
- err := CheckUpdater(diceManager)
+ err = CheckUpdater(diceManager)
if err != nil {
- logger.Error("升级程序检查失败: ", err.Error())
+ log.Error("升级程序检查失败: ", err.Error())
} else {
_ = os.Remove("./auto_update.exe")
// ui资源已经内置,删除旧的ui文件,这里有点风险,但是此时已经不考虑升级失败的情况
_ = os.RemoveAll("./frontend")
- UpdateByFile(diceManager, nil, "./update/update.zip", true)
+ UpdateByFile(diceManager, "./update/update.zip", true)
}
return
}
@@ -306,14 +353,14 @@ func main() {
}
if doNext {
- err := CheckUpdater(diceManager)
+ err = CheckUpdater(diceManager)
if err != nil {
- logger.Error("升级程序检查失败: ", err.Error())
+ log.Error("升级程序检查失败: ", err.Error())
} else {
_ = os.Remove("./auto_update")
// ui资源已经内置,删除旧的ui文件,这里有点风险,但是此时已经不考虑升级失败的情况
_ = os.RemoveAll("./frontend")
- UpdateByFile(diceManager, nil, "./update/update.tar.gz", true)
+ UpdateByFile(diceManager, "./update/update.tar.gz", true)
}
return
}
@@ -321,28 +368,27 @@ func main() {
removeUpdateFiles()
if opts.UpdateTest {
- err := CheckUpdater(diceManager)
+ err = CheckUpdater(diceManager)
if err != nil {
- logger.Error("升级程序检查失败: ", err.Error())
+ log.Error("升级程序检查失败: ", err.Error())
} else {
- UpdateByFile(diceManager, nil, "./xx.zip", true)
+ UpdateByFile(diceManager, "./xx.zip", true)
}
}
// 先临时放这里,后面再整理一下升级模块
- diceManager.UpdateSealdiceByFile = func(packName string, log *zap.SugaredLogger) bool {
- err := CheckUpdater(diceManager)
+ diceManager.UpdateSealdiceByFile = func(packName string, log *log.Helper) bool {
+ err = CheckUpdater(diceManager)
if err != nil {
- logger.Error("升级程序检查失败: ", err.Error())
+ log.Error("升级程序检查失败: ", err.Error())
return false
} else {
- return UpdateByFile(diceManager, log, packName, false)
+ return UpdateByFile(diceManager, packName, false)
}
}
cwd, _ := os.Getwd()
- fmt.Printf("%s %s\n", dice.APPNAME, dice.VERSION.String())
- fmt.Println("工作路径: ", cwd)
+ log.Info(dice.APPNAME, dice.VERSION.String(), "当前工作路径: ", cwd)
if strings.HasPrefix(cwd, os.TempDir()) {
// C:\Users\XXX\AppData\Local\Temp
@@ -353,47 +399,51 @@ func main() {
useBuiltinUI := false
checkFrontendExists := func() bool {
- stat, err := os.Stat("./frontend_overwrite")
+ var stat os.FileInfo
+ stat, err = os.Stat("./frontend_overwrite")
return err == nil && stat.IsDir()
}
if !checkFrontendExists() {
- logger.Info("未检测到外置的UI资源文件,将使用内置资源启动UI")
+ log.Info("未检测到外置的UI资源文件,将使用内置资源启动UI")
useBuiltinUI = true
} else {
- logger.Info("检测到外置的UI资源文件,将使用frontend_overwrite文件夹内的资源启动UI")
+ log.Info("检测到外置的UI资源文件,将使用frontend_overwrite文件夹内的资源启动UI")
}
// 删除遗留的shm和wal文件
- if !model.DBCacheDelete() {
- logger.Error("数据库缓存文件删除失败")
- showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!")
- return
- }
+ // if !model.DBCacheDelete() {
+ // log.Error("数据库缓存文件删除失败")
+ // showMsgBox("数据库缓存文件删除失败", "为避免数据损坏,拒绝继续启动。请检查是否启动多份程序,或有其他程序正在使用数据库文件!")
+ // return
+ // }
// 尝试进行升级
migrate.TryMigrateToV12()
// 尝试修正log_items表的message字段类型
if migrateErr := migrate.LogItemFixDatatype(); migrateErr != nil {
- logger.Errorf("修正log_items表时出错,%s", migrateErr.Error())
+ log.Fatalf("修正log_items表时出错,%s", migrateErr.Error())
return
}
// v131迁移历史设置项到自定义文案
if migrateErr := migrate.V131DeprecatedConfig2CustomText(); migrateErr != nil {
- logger.Errorf("迁移历史设置项时出错,%s", migrateErr.Error())
+ log.Fatalf("迁移历史设置项时出错,%s", migrateErr.Error())
return
}
// v141重命名刷屏警告字段
if migrateErr := migrate.V141DeprecatedConfigRename(); migrateErr != nil {
- logger.Errorf("迁移历史设置项时出错,%s", migrateErr.Error())
+ log.Fatalf("迁移历史设置项时出错,%s", migrateErr.Error())
return
}
// v144删除旧的帮助文档
if migrateErr := migrate.V144RemoveOldHelpdoc(); migrateErr != nil {
- logger.Errorf("移除旧帮助文档时出错,%v", migrateErr)
+ log.Fatalf("移除旧帮助文档时出错,%v", migrateErr)
}
// v150升级
- if !migrate.V150Upgrade() {
- return
+ err = migrate.V150Upgrade()
+ if err != nil {
+ // Fatalf将会退出程序...或许应该用Errorf一类的吗?
+ log.Fatalf("您的146数据库可能存在问题,为保护数据,已经停止执行150升级命令。请尝试联系开发者,并提供你的日志。\n"+
+ "数据已回滚,您可暂时使用旧版本等待进一步的修复和更新。您的报错内容为: %v", err)
}
if !opts.ShowConsole || opts.MultiInstanceOnWindows {
@@ -437,7 +487,7 @@ func main() {
})()
if opts.Address != "" {
- fmt.Println("由参数输入了服务地址:", opts.Address)
+ log.Infof("由参数输入了服务地址: %s", opts.Address)
}
for _, d := range diceManager.Dice {
@@ -454,7 +504,7 @@ func main() {
// err = nil
// err = http.ListenAndServe(":9090", nil)
// if err != nil {
- // fmt.Printf("ListenAndServe: %s", err)
+ // fmt.Fprintf(os.Stdout, "ListenAndServe: %s", err)
// }
// darwin 的托盘菜单似乎需要在主线程启动才能工作,调整到这里
@@ -475,7 +525,7 @@ func removeUpdateFiles() {
func diceServe(d *dice.Dice) {
defer dice.CrashLog()
if len(d.ImSession.EndPoints) == 0 {
- d.Logger.Infof("未检测到任何帐号,请先到“帐号设置”进行添加")
+ log.Infof("未检测到任何帐号,请先到“帐号设置”进行添加")
}
d.UIEndpoint = new(dice.EndPointInfo)
@@ -502,7 +552,7 @@ func diceServe(d *dice.Dice) {
}
if conn.EndPointInfoBase.ProtocolType == "onebot" {
pa := conn.Adapter.(*dice.PlatformAdapterGocq)
- if pa.BuiltinMode == "lagrange" {
+ if pa.BuiltinMode == "lagrange" || pa.BuiltinMode == "lagrange-gocq" {
dice.LagrangeServe(d, conn, dice.LagrangeLoginInfo{
IsAsyncRun: true,
})
@@ -558,13 +608,13 @@ func diceServe(d *dice.Dice) {
}
func uiServe(dm *dice.DiceManager, hideUI bool, useBuiltin bool) {
- logger.Info("即将启动webui")
+ log.Info("即将启动webui")
// Echo instance
e := echo.New()
- // Middleware
- // e.Use(middleware.Logger())
- // e.Use(middleware.Recover())
+ // 为UI添加日志,以echo方式输出
+ echoHelper := log.GetWebLogger()
+ e.Use(log.EchoMiddleLogger(echoHelper))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, "token"},
diff --git a/migrate/convert_logs.go b/migrate/convert_logs.go
index 4216f0d3..b099e6bf 100644
--- a/migrate/convert_logs.go
+++ b/migrate/convert_logs.go
@@ -4,7 +4,6 @@ import (
"encoding/binary"
"encoding/json"
"errors"
- "fmt"
"path/filepath"
"strconv"
"strings"
@@ -12,6 +11,8 @@ import (
"github.com/jmoiron/sqlx"
"go.etcd.io/bbolt"
+
+ log "sealdice-core/utils/kratos"
)
type LogOneItem struct {
@@ -212,7 +213,7 @@ func LogAppend(ctx *MsgContext, group *GroupInfo, l *LogOneItem) error {
return ctx.Dice.DB.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("logs"))
if err != nil {
- // ctx.Dice.Logger.Error("日志写入问题", err.Error())
+ // ctx.Dice.Zlogger.Error("日志写入问题", err.Error())
return err
}
@@ -353,7 +354,6 @@ create index if not exists idx_log_items_log_id
dbSQL, err := openDB(dbDataLogsPath)
if err != nil {
- fmt.Println("xxx", err)
return err
}
defer func(dbSql *sqlx.DB) {
@@ -380,7 +380,7 @@ create index if not exists idx_log_items_log_id
})
})
- fmt.Println("群组数量", len(groupIds))
+ log.Info("群组数量", len(groupIds))
times := 0
itemNumber := 0
@@ -409,14 +409,14 @@ create index if not exists idx_log_items_log_id
}
exec, errExec := dbSQL.NamedExec(`insert into logs (name, group_id, created_at, updated_at) VALUES (:name, :group_id, :created_at, :updated_at)`, args)
if errExec != nil {
- fmt.Println("错误:", errExec, i, j)
+ log.Error("错误:", errExec, i, j)
return errExec
}
logID, _ := exec.LastInsertId()
logNum++
if logNum%10 == 0 {
- fmt.Printf("进度: %d\n", logNum)
+ log.Infof("进度: %d\n", logNum)
}
tx := dbSQL.MustBegin()
@@ -450,15 +450,15 @@ create index if not exists idx_log_items_log_id
}
}
- fmt.Println("群组数量", len(groupIds))
- fmt.Println("log完成", times)
- fmt.Println("行数", itemNumber)
+ log.Info("群组数量", len(groupIds))
+ log.Info("log完成", times)
+ log.Info("行数", itemNumber)
err = dbSQL.Get(&num, "select count(id) from log_items")
if err != nil {
return err
}
- fmt.Println("行数确认", num)
+ log.Info("行数确认", num)
_ = dbSQL.Close()
return nil
diff --git a/migrate/convert_serve.go b/migrate/convert_serve.go
index 0e6329cc..7d0e5db8 100644
--- a/migrate/convert_serve.go
+++ b/migrate/convert_serve.go
@@ -69,7 +69,7 @@ type EndPointInfoBase struct {
Enable bool `yaml:"enable" json:"enable"` // 是否启用
ProtocolType string `yaml:"protocolType"` // 协议类型,如onebot、koishi等
- IsPublic bool `yaml:"isPublic"`
+ IsPublic bool `yaml:"isPublic" json:"isPublic"`
}
type EndPointInfo struct {
@@ -232,7 +232,7 @@ create table if not exists ban_info
now := time.Now()
nowTimestamp := now.Unix()
- fmt.Println("处理serve.yaml")
+ fmt.Fprintln(os.Stdout, "处理serve.yaml")
times := 0
dNew := &Dice{}
@@ -240,7 +240,7 @@ create table if not exists ban_info
tx := dbSql.MustBegin()
for k, v := range dNew.ImSession.ServiceAtNew {
- fmt.Println("群组", k)
+ fmt.Fprintln(os.Stdout, "群组", k)
times += len(v.Players)
for _, playerInfo := range v.Players {
args := map[string]interface{}{
@@ -271,13 +271,13 @@ create table if not exists ban_info
errTx := tx.Commit()
if errTx != nil {
- fmt.Println("???", errTx)
+ fmt.Fprintln(os.Stdout, "???", errTx)
_ = tx.Rollback()
}
- fmt.Println("群组信息处理完成")
- fmt.Println("群数量", len(dNew.ImSession.ServiceAtNew))
- fmt.Println("群成员数量", times)
+ fmt.Fprintln(os.Stdout, "群组信息处理完成")
+ fmt.Fprintln(os.Stdout, "群数量", len(dNew.ImSession.ServiceAtNew))
+ fmt.Fprintln(os.Stdout, "群成员数量", times)
}
_ = os.WriteFile("./data/default/serve.yaml.old", data, 0644)
@@ -289,7 +289,7 @@ create table if not exists ban_info
_ = db.Close()
}(db)
- fmt.Println("处理属性部分")
+ fmt.Fprintln(os.Stdout, "处理属性部分")
copyByName := func(table string) {
times = 0
tx2 := dbSql.MustBegin()
@@ -308,7 +308,7 @@ create table if not exists ban_info
})
})
- fmt.Println("条目数量"+table, times)
+ fmt.Fprintln(os.Stdout, "条目数量"+table, times)
if tx2.Commit() != nil {
_ = tx2.Rollback()
@@ -319,7 +319,7 @@ create table if not exists ban_info
copyByName("attrs_group")
copyByName("attrs_user")
copyByName("attrs_group_user")
- fmt.Println("完成")
+ fmt.Fprintln(os.Stdout, "完成")
times = 0
tx2 := dbSql.MustBegin()
@@ -356,8 +356,8 @@ create table if not exists ban_info
_ = tx2.Rollback()
}
- fmt.Println("黑名单条目数量", times)
- fmt.Println("完成")
+ fmt.Fprintln(os.Stdout, "黑名单条目数量", times)
+ fmt.Fprintln(os.Stdout, "完成")
return nil
}
diff --git a/migrate/db_util.go b/migrate/db_util.go
index 465f0f58..9ed44029 100644
--- a/migrate/db_util.go
+++ b/migrate/db_util.go
@@ -5,11 +5,24 @@ package migrate
import (
_ "github.com/glebarez/go-sqlite"
+ "github.com/glebarez/sqlite"
"github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/utils"
)
func openDB(path string) (*sqlx.DB, error) {
- db, err := sqlx.Open("sqlite", path)
+ gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ if err != nil {
+ panic(err)
+ }
+ db, err := utils.GetSQLXDB(gdb)
+ // db, err := sqlx.Open("sqlite", path)
if err != nil {
panic(err)
}
diff --git a/migrate/db_util_cgo.go b/migrate/db_util_cgo.go
index 4c9f17e4..4b8ba915 100644
--- a/migrate/db_util_cgo.go
+++ b/migrate/db_util_cgo.go
@@ -6,10 +6,22 @@ package migrate
import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+
+ "sealdice-core/utils"
)
func openDB(path string) (*sqlx.DB, error) {
- db, err := sqlx.Open("sqlite3", path)
+ gdb, err := gorm.Open(sqlite.Open(path), &gorm.Config{
+ // 注意,这里虽然是Info,但实际上打印就变成了Debug.
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ if err != nil {
+ panic(err)
+ }
+ db, err := utils.GetSQLXDB(gdb)
if err != nil {
panic(err)
}
diff --git a/migrate/log_item_fix_type.go b/migrate/log_item_fix_type.go
index 5e2a37cd..76847c9c 100644
--- a/migrate/log_item_fix_type.go
+++ b/migrate/log_item_fix_type.go
@@ -60,8 +60,8 @@ func LogItemFixDatatype() error {
return nil
}
- fmt.Println("开始修复log_items表message字段类型")
- fmt.Println("【不要关闭海豹程序!】")
+ fmt.Fprintln(os.Stdout, "开始修复log_items表message字段类型")
+ fmt.Fprintln(os.Stdout, "【不要关闭海豹程序!】")
done := make(chan interface{}, 1)
@@ -88,7 +88,7 @@ func LogItemFixDatatype() error {
}
_, _ = db.Exec(`VACUUM;`)
- fmt.Println("\n修复log_items表message字段类型成功")
- fmt.Println("您现在可以正常使用海豹程序了")
+ fmt.Fprintln(os.Stdout, "\n修复log_items表message字段类型成功")
+ fmt.Fprintln(os.Stdout, "您现在可以正常使用海豹程序了")
return nil
}
diff --git a/migrate/v12.go b/migrate/v12.go
index 821c0d13..2e9fa821 100644
--- a/migrate/v12.go
+++ b/migrate/v12.go
@@ -11,9 +11,9 @@ func TryMigrateToV12() {
return
}
- fmt.Println("检测到旧数据库存在,试图进行转换")
+ fmt.Fprintln(os.Stdout, "检测到旧数据库存在,试图进行转换")
_ = ConvertServe()
_ = ConvertLogs()
_ = os.Remove("./data/default/data.bdb")
- fmt.Println("V1.2 版本数据库升级完成")
+ fmt.Fprintln(os.Stdout, "V1.2 版本数据库升级完成")
}
diff --git a/migrate/v131_deprecated_config.go b/migrate/v131_deprecated_config.go
index 5007dbe2..e94a565f 100644
--- a/migrate/v131_deprecated_config.go
+++ b/migrate/v131_deprecated_config.go
@@ -99,7 +99,7 @@ func V131DeprecatedConfig2CustomText() error {
}
if needUpdateCustomText {
- fmt.Println("检测到旧设置项需要迁移到自定义文案,试图进行迁移")
+ fmt.Fprintln(os.Stdout, "检测到旧设置项需要迁移到自定义文案,试图进行迁移")
// 保存修改了的 custom text 设置
newData, err := yaml.Marshal(customTexts)
@@ -137,7 +137,7 @@ func V131DeprecatedConfig2CustomText() error {
return err
}
- fmt.Println("旧设置项迁移到自定义文案成功")
+ fmt.Fprintln(os.Stdout, "旧设置项迁移到自定义文案成功")
}
return nil
diff --git a/migrate/v144_remote_old_helpdoc.go b/migrate/v144_remote_old_helpdoc.go
index ec15ddef..10eace17 100644
--- a/migrate/v144_remote_old_helpdoc.go
+++ b/migrate/v144_remote_old_helpdoc.go
@@ -19,29 +19,29 @@ func V144RemoveOldHelpdoc() error {
return nil
}
if err != nil {
- return fmt.Errorf("Get file info for %s failed: %w", oldName, err)
+ return fmt.Errorf("get file info for %s failed: %w", oldName, err)
}
_, err = os.Stat(newName)
if errors.Is(err, os.ErrNotExist) {
- fmt.Printf("New helpdoc %s not found. Skip removing old helpdoc %s\n", newName, oldName)
+ fmt.Fprintf(os.Stdout, "New helpdoc %s not found. Skip removing old helpdoc %s\n", newName, oldName)
return nil
}
if err != nil {
- return fmt.Errorf("Get file info for %s failed: %w", newName, err)
+ return fmt.Errorf("get file info for %s failed: %w", newName, err)
}
if crypto.Sha256Checksum(oldName) != oldSHA256 {
- fmt.Printf("Old helpdoc %s checksum mismatch. You may have edited this file?\n", oldName)
+ fmt.Fprintf(os.Stdout, "Old helpdoc %s checksum mismatch. You may have edited this file?\n", oldName)
return nil
}
if crypto.Sha256Checksum(newName) != newSHA256 {
- fmt.Printf("New helpdoc %s checksum mismatch. Skip removing old helpdoc %s\n", newName, oldName)
+ fmt.Fprintf(os.Stdout, "New helpdoc %s checksum mismatch. Skip removing old helpdoc %s\n", newName, oldName)
return nil
}
- fmt.Printf("Removing old helpdoc %s\n", oldName)
+ fmt.Fprintf(os.Stdout, "Removing old helpdoc %s\n", oldName)
os.Remove(oldName)
return nil
}
diff --git a/migrate/v150_attrs.go b/migrate/v150_attrs.go
index 252fdc74..81e1d342 100644
--- a/migrate/v150_attrs.go
+++ b/migrate/v150_attrs.go
@@ -15,6 +15,8 @@ import (
"sealdice-core/dice"
"sealdice-core/dice/model"
"sealdice-core/utils"
+
+ log "sealdice-core/utils/kratos"
)
func convertToNew(name string, ownerId string, data []byte, updatedAt int64) (*model.AttributesItemModel, error) {
@@ -121,8 +123,8 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) {
_, userIdPart, ok := dice.UnpackGroupUserId(id)
if !ok {
countFailed += 1
- fmt.Println("数据库读取出错,退出转换")
- fmt.Println("ID解析失败: ", id)
+ fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换")
+ fmt.Fprintln(os.Stdout, "ID解析失败: ", id)
continue
}
@@ -131,7 +133,7 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) {
if err != nil {
countFailed += 1
- fmt.Println("解析失败: ", string(data))
+ fmt.Fprintln(os.Stdout, "解析失败: ", string(data))
continue
}
@@ -156,11 +158,10 @@ func attrsGroupUserMigrate(db *sqlx.Tx) (int, int, error) {
rawData, err := ds.NewDictVal(m).V().ToJSON()
if err != nil {
countFailed += 1
- fmt.Printf("群-用户 %s 的数据无法转换\n", id)
+ fmt.Fprintf(os.Stdout, "群-用户 %s 的数据无法转换\n", id)
continue
}
- // fmt.Println("UnpackID:", id, " UserPart:", userIdPart, " Sheet:", sheetIdBindByGroupUserId[id])
item := &model.AttributesItemModel{
Id: id,
Data: rawData,
@@ -212,7 +213,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) {
)
if err != nil {
- fmt.Println("数据库读取出错,退出转换")
+ fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换")
return count, countFailed, err
}
@@ -221,7 +222,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) {
if err != nil {
countFailed += 1
- fmt.Println("解析失败: ", string(data))
+ fmt.Fprintln(os.Stdout, "解析失败: ", string(data))
continue
}
@@ -234,7 +235,7 @@ func attrsGroupMigrate(db *sqlx.Tx) (int, int, error) {
rawData, err := ds.NewDictVal(m).V().ToJSON()
if err != nil {
countFailed += 1
- fmt.Printf("群 %s 的数据无法转换\n", id)
+ fmt.Fprintf(os.Stdout, "群 %s 的数据无法转换\n", id)
continue
}
@@ -283,7 +284,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
)
if err != nil {
- fmt.Println("数据库读取出错,退出转换")
+ fmt.Fprintln(os.Stdout, "数据库读取出错,退出转换")
return count, countSheetsNum, countFailed, err
}
@@ -295,7 +296,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
continue
}
- // fmt.Println("数据转换-用户:", ownerId)
var newSheetsList []*model.AttributesItemModel
var sheetNameBindByGroupId = map[string]string{}
@@ -314,7 +314,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
groupId := k[len("$:group-bind:"):]
name, _ := v.ReadString()
sheetNameBindByGroupId[groupId] = name
- // fmt.Println("绑卡关联:", groupId, name)
continue
}
if strings.HasPrefix(k, "$ch:") {
@@ -324,7 +323,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
toNew, err = convertToNew(name, ownerId, []byte(v.ToString()), updatedAt)
if err != nil {
- fmt.Printf("用户 %s 的角色卡 %s 无法转换", ownerId, name)
+ fmt.Fprintf(os.Stdout, "用户 %s 的角色卡 %s 无法转换", ownerId, name)
continue
}
newSheetsList = append(newSheetsList, toNew)
@@ -337,7 +336,6 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
// 一次性,双循环罢
for groupID, j := range sheetNameBindByGroupId {
if j == i.Name {
- // fmt.Println("GUID:", fmt.Sprintf("%s-%s", groupID, ownerId), " sheetID:", i.Id)
sheetIdBindByGroupUserId[fmt.Sprintf("%s-%s", groupID, ownerId)] = i.Id
}
}
@@ -347,7 +345,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
for _, i := range newSheetsList {
_, err = AttrsNewItem(db, i)
if err != nil {
- fmt.Printf("用户 %s 的角色卡 %s 无法写入数据库: %s\n", ownerId, i.Name, err.Error())
+ fmt.Fprintf(os.Stdout, "用户 %s 的角色卡 %s 无法写入数据库: %s\n", ownerId, i.Name, err.Error())
}
}
@@ -355,7 +353,7 @@ func attrsUserMigrate(db *sqlx.Tx) (int, int, int, error) {
rawData, err := ds.NewDictVal(m).V().ToJSON()
if err != nil {
countFailed += 1
- fmt.Printf("用户 %s 的个人数据无法转换\n", ownerId)
+ fmt.Fprintf(os.Stdout, "用户 %s 的个人数据无法转换\n", ownerId)
continue
}
@@ -395,110 +393,132 @@ func checkTableExists(db *sqlx.DB, tableName string) (bool, error) {
}
}
-func V150Upgrade() bool {
+// Pinenutn: 2024-10-28 我要把这个注释全文背诵,它扰乱了GORM的初始化逻辑
+// -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化
+var v150sqls = []string{
+ `
+CREATE TABLE IF NOT EXISTS attrs (
+ id TEXT PRIMARY KEY,
+ data BYTEA,
+ attrs_type TEXT,
+ binding_sheet_id TEXT default '',
+ name TEXT default '',
+ owner_id TEXT default '',
+ sheet_type TEXT default '',
+ is_hidden BOOLEAN default FALSE,
+ created_at INTEGER default 0,
+ updated_at INTEGER default 0
+);
+`,
+ `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`,
+ `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`,
+ `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`,
+}
+
+func V150Upgrade() error {
dbDataPath, _ := filepath.Abs("./data/default/data.db")
if _, err := os.Stat(dbDataPath); errors.Is(err, os.ErrNotExist) {
- return true
+ log.Error("未找到旧版本数据库,若您启动全新海豹,可安全忽略。")
+ return nil
}
db, err := openDB(dbDataPath)
if err != nil {
- fmt.Println("升级失败,无法打开数据库:", err)
- return false
+ return fmt.Errorf("升级失败,无法打开数据库: %w", err)
+ }
+ defer db.Close()
+
+ tx, err := db.Beginx()
+ if err != nil {
+ return fmt.Errorf("创建事务失败: %w", err)
}
defer func() {
- _ = db.Close()
+ if p := recover(); p != nil {
+ err = tx.Rollback()
+ if err != nil {
+ log.Errorf("回滚事务时出错: %v", err)
+ }
+ panic(p) // 继续传播 panic
+ } else if err != nil {
+ log.Errorf("日志处理时出现异常行为: %v", err)
+ err = tx.Rollback()
+ if err != nil {
+ log.Errorf("回滚事务时出错: %v", err)
+ return
+ }
+ } else {
+ err = tx.Commit()
+ if err != nil {
+ log.Errorf("提交事务时出错: %v", err)
+ }
+ }
}()
exists, err := checkTableExists(db, "attrs")
if err != nil {
- fmt.Println("V150数据转换未知错误:", err.Error())
- return false
+ return fmt.Errorf("检查表是否存在时出错: %w", err)
}
+ // 特判146->150的倒霉蛋
+ exists146, err := checkTableExists(db, "attrs_group")
+
if exists {
- // 表格已经存在,说明转换完成,退出
- return true
+ if exists146 {
+ log.Errorf("1.4.6的数据部分迁移!您可能是150部分版本的受害者,请联系开发者")
+ return errors.New("150和146的数据库共同存在,请联系开发者")
+ }
+ // 表格已经存在,说明转换完成
+ return nil
}
- fmt.Println("1.5 数据迁移")
+ log.Info("1.5 数据迁移")
sheetIdBindByGroupUserId = map[string]string{}
- sqls := []string{
- `
-CREATE TABLE IF NOT EXISTS attrs (
- id TEXT PRIMARY KEY,
- data BYTEA,
- attrs_type TEXT,
-
- -- 坏,Get这个方法太严格了,所有的字段都要有默认值,不然无法反序列化
- binding_sheet_id TEXT default '',
-
- name TEXT default '',
- owner_id TEXT default '',
- sheet_type TEXT default '',
- is_hidden BOOLEAN default FALSE,
-
- created_at INTEGER default 0,
- updated_at INTEGER default 0
-);
-`,
- `create index if not exists idx_attrs_binding_sheet_id on attrs (binding_sheet_id);`,
- `create index if not exists idx_attrs_owner_id_id on attrs (owner_id);`,
- `create index if not exists idx_attrs_attrs_type_id on attrs (attrs_type);`,
- }
- for _, i := range sqls {
- _, _ = db.Exec(i)
- }
-
- tx, err := db.Beginx()
- if err != nil {
- fmt.Println("V150数据转换创建事务失败:", err.Error())
- return false
+ for _, singleSql := range v150sqls {
+ if _, err = tx.Exec(singleSql); err != nil {
+ return fmt.Errorf("执行 SQL 出错: %w", err)
+ }
}
- if exists, _ := checkTableExists(db, "attrs_user"); exists {
- count, countSheetsNum, countFailed, err2 := attrsUserMigrate(tx)
- fmt.Printf("数据卡转换 - 角色卡,成功人数%d 失败人数 %d 卡数 %d\n", count, countFailed, countSheetsNum)
- if err2 != nil {
- fmt.Println("异常", err2.Error())
- return false
+ if exists, _ = checkTableExists(db, "attrs_user"); exists {
+ count, countSheetsNum, countFailed, err0 := attrsUserMigrate(tx)
+ log.Infof("数据卡转换 - 角色卡,成功人数%d 失败人数 %d 卡数 %d\n", count, countFailed, countSheetsNum)
+ if err0 != nil {
+ return fmt.Errorf("角色卡转换出错: %w", err0)
}
}
- if exists, _ := checkTableExists(db, "attrs_group_user"); exists {
- count, countFailed, err2 := attrsGroupUserMigrate(tx)
- fmt.Printf("数据卡转换 - 群组个人数据,成功%d 失败 %d\n", count, countFailed)
- if err2 != nil {
- fmt.Println("异常", err2.Error())
- return false
+ if exists, _ = checkTableExists(db, "attrs_group_user"); exists {
+ count, countFailed, err1 := attrsGroupUserMigrate(tx)
+ log.Infof("数据卡转换 - 群组个人数据,成功%d 失败 %d\n", count, countFailed)
+ if err1 != nil {
+ return fmt.Errorf("群组个人数据转换出错: %w", err1)
}
}
- if exists, _ := checkTableExists(db, "attrs_group"); exists {
+ if exists, _ = checkTableExists(db, "attrs_group"); exists {
count, countFailed, err2 := attrsGroupMigrate(tx)
- fmt.Printf("数据卡转换 - 群数据,成功%d 失败 %d\n", count, countFailed)
+ log.Infof("数据卡转换 - 群数据,成功%d 失败 %d\n", count, countFailed)
if err2 != nil {
- fmt.Println("异常", err2.Error())
- return false
+ return fmt.Errorf("群数据转换出错: %w", err2)
}
}
- // 删档
- fmt.Println("删除旧版本数据")
- _, _ = tx.Exec("drop table attrs_group")
- _, _ = tx.Exec("drop table attrs_group_user")
- _, _ = tx.Exec("drop table attrs_user")
+ // 删除旧版本数据
+ log.Info("删除旧版本数据")
+ deleteSQLs := []string{
+ "drop table attrs_group",
+ "drop table attrs_group_user",
+ "drop table attrs_user",
+ }
+ for _, deleteSQL := range deleteSQLs {
+ if _, err = tx.Exec(deleteSQL); err != nil {
+ return fmt.Errorf("删除旧数据时出错: %w", err)
+ }
+ }
+ // 放在这里保证能执行
_, _ = db.Exec("PRAGMA wal_checkpoint(TRUNCATE);")
- _, _ = tx.Exec("VACUUM;") // 收尾
-
+ _, _ = db.Exec("VACUUM;")
sheetIdBindByGroupUserId = nil
-
- err = tx.Commit()
- if err != nil {
- fmt.Println("V150 数据转换失败:", err.Error())
- return false
- }
-
- fmt.Println("V150 数据转换完成")
- return true
+ log.Info("V150 数据转换完成")
+ return nil
}
diff --git a/readme.md b/readme.md
index bc67977d..26414392 100644
--- a/readme.md
+++ b/readme.md
@@ -31,7 +31,7 @@
### golang 开发环境
-编译的 golang 版本为 1.20。使用更新版本时需注意不要使用新版本引入的新函数。
+编译的 golang 版本为 1.22。在 [构建](https://github.com/sealdice/sealdice-build) 仓库中采用对 go 1.22 进行修补的方式以支持 Windows 7 等低版本系统。
因部分依赖库的需求,可能需要配置国内镜像,个人使用 镜像。
@@ -45,7 +45,7 @@
此工具对于代码开发**不是**必要的。但是,本项目的 CI 流程中配置了 linter 检查,不符合规范的代码不能被合入。
-因此,强烈推荐开发者在本地安装此工具,请参考[这份文档](https://golangci-lint.run/usage/install/#local-installation)。分析器的相关配置位于 `.golangci.yml` 文件中。
+因此,强烈推荐开发者在本地安装此工具,请参考[这份文档](https://golangci-lint.run/welcome/install/#local-installation)。分析器的相关配置位于 `.golangci.yml` 文件中。
你可能需要调整编辑器的相关配置,使用 golangci-lint 为默认的分析工具,并开启自动检查。
@@ -57,11 +57,29 @@
>
> 以上配置没有写入项目的统一设置,以允许开发者不本地使用 golangci-lint
-### 拉取代码并配置数据文件
+### 编译运行
+
+#### 使用 `go-task`
+
+你可以安装 [go-task](https://taskfile.dev/installation) 以执行预置好的任务。安装后可执行:
-使用git拉取项目代码
+```bash
+# 初次编译运行(包括安装依赖和相关工具)
+task install run
+
+# 后续编译运行
+task run
+```
-从已发布的海豹二进制包中,解压 `data`、`gocqhttp` 两个目录到代码目录下。
+#### 手动执行
+
+你也可以按照以下步骤手动进行编译运行:
+
+##### 拉取代码并配置数据文件
+
+使用 git 拉取项目代码
+
+从已发布的海豹二进制包中,解压 `data`、`lagrange` 两个目录到代码目录下。
同时需要在项目的 `static/frontend` 下放置用于打包进 core 的 ui 静态资源文件,可手动提供,也可通过命令自动从 github 拉取:
@@ -82,7 +100,7 @@ static
└─assets
```
-### 编译运行
+##### 运行编译命令
打开项目,或使用终端访问项目目录,运行:
@@ -104,50 +122,50 @@ go run .
### 从哪开始看
-从 main.go 开始,这里海豹分出了几个线程,一个启动核心并提供服务,另一个提供ui的http服务。
+从 `main.go` 开始,这里海豹分出了几个线程,一个启动核心并提供服务,另一个提供 ui 的 http 服务。
-可以顺藤摸瓜了解海豹如何启动,如何提供服务,如何响应指令。指令响应的部分写在im_session.go中
+可以顺藤摸瓜了解海豹如何启动,如何提供服务,如何响应指令。指令响应的部分写在 `im_session.go` 中
-注意有部分代码还在构思中,实际并未使用,例如 CharacterTemplate,请阅读时先Find Usage加以区分
+注意有部分代码还在构思中,实际并未使用,例如 `CharacterTemplate`,请阅读时先 Find Usage 加以区分
### 重要数据结构
-dice.go 中的 Dice 结构体存放着各种核心配置,每个Dice实例是一个骰子,而每个骰子下面可以挂靠多个端点(EndPoint)。端点即交互渠道,例如一个QQ账号是一个端点。
+`dice.go` 中的 `Dice` 结构体存放着各种核心配置,每个 `Dice` 实例是一个骰子,而每个骰子下面可以挂靠多个端点 (EndPoint)。端点即交互渠道,例如一个 QQ 账号是一个端点。
-所有的端点由 IMSession 来统一管理,同样的,这个类也负责接收和分发指令。
+所有的端点由 `IMSession` 来统一管理,同样的,这个类也负责接收和分发指令。
-可能你会注意到有 IMSession 和 IMSessionLegacy,只看前一个就行,IMSessionLegacy对应的是0.99.13的上古版本之前的数据结构,仅用于升级配置文件。
+可能你会注意到有 `IMSession` 和 `IMSessionLegacy`,只看前一个就行,`IMSessionLegacy` 对应的是 0.99.13 的上古版本之前的数据结构,仅用于升级配置文件。
-GroupInfo 是群组信息
+`GroupInfo` 是群组信息
-GroupPlayerInfo 是玩家信息
+`GroupPlayerInfo` 是玩家信息
### 为海豹添加更多平台支持
-海豹使用叫做 PlatformAdapter 的接口来接入平台,只需将接口全部实现,再创建一个 EndPointInfo 塞入当前用户的 IMSession 对象之中即可。
+海豹使用叫做 `PlatformAdapter` 的接口来接入平台,只需将接口全部实现,再创建一个 `EndPointInfo` 塞入当前用户的 `IMSession` 对象之中即可。
-注: 每次在UI上添加QQ账号,其实就是创建了一个EndPointInfo对象,并制定Adapter为PlatformAdapterQQOnebot
+注:每次在 UI 上添加 QQ 账号,其实就是创建了一个 `EndPointInfo` 对象,并制定 Adapter 为 `PlatformAdapterQQOnebot`
-目前实现的两个adapter,一个对应onebot协议,主要用于QQ,另一个对应http,用于UI后台的测试窗口。
+目前实现的两个 adapter,一个对应 onebot 协议,主要用于 QQ,另一个对应 http,用于 UI 后台的测试窗口。
-观察 PlatformAdapterHttp 如何运作起来是一个很好的切入点,因为他非常简单。
+观察 `PlatformAdapterHttp` 如何运作起来是一个很好的切入点,因为他非常简单。
-### 改动扩展模块,如dnd5e,coc7等
+### 改动扩展模块,如 dnd5e,coc7 等
-对应 dice/ext_xxx.go 系列文件
+对应 `dice/ext_xxx.go` 系列文件
-推荐从 ext_template.go 入手,以 ext_dnd5e.go 为参考,因为这个模块书写时间较晚,相对较为完善。
+推荐从 `ext_template.go` 入手,以 `ext_dnd5e.go` 为参考,因为这个模块书写时间较晚,相对较为完善。
### 暂不建议修改的地方
#### 表达式解析器
-dice/roll.peg 是海豹的骰点指令文法文件
+`dice/roll.peg` 是海豹的骰点指令文法文件
-dice/rollvm.go 是骰点指令虚拟机
+`dice/rollvm.go` 是骰点指令虚拟机
-1.5后,已经替换使用 dicescript (RollVM V2) 作为表达式解释器,现有版本不宜轻动。
+1.5 后,已经替换使用 dicescript (RollVM V2) 作为表达式解释器,现有版本不宜轻动。
关于 dicescript 的信息,请移步
-而出于兼容性的考虑,V1版本的解释器将继续保留,直到2.0版本。
+而出于兼容性的考虑,V1 版本的解释器将继续保留,直到 2.0 版本。
diff --git a/sealdice-builtins b/sealdice-builtins
index cbfc3d36..ae995845 160000
--- a/sealdice-builtins
+++ b/sealdice-builtins
@@ -1 +1 @@
-Subproject commit cbfc3d368225ded84cd39ffe1ec54563b5c785ba
+Subproject commit ae99584518d3de211cee671fb84b175e558cda0b
diff --git a/sealdice-ui b/sealdice-ui
index e3a53af5..5a6b3b8b 160000
--- a/sealdice-ui
+++ b/sealdice-ui
@@ -1 +1 @@
-Subproject commit e3a53af5ba8b9702e705040db5a3abc387f31888
+Subproject commit 5a6b3b8bf9798ce09fa00a9765c28a1766c41fe6
diff --git a/static/gen/download-fe.go b/static/gen/download-fe.go
index 24e8b771..9984626c 100644
--- a/static/gen/download-fe.go
+++ b/static/gen/download-fe.go
@@ -2,6 +2,7 @@ package main
import (
"archive/zip"
+ "errors"
"fmt"
"io"
"net/http"
@@ -42,7 +43,7 @@ func downloadFrontendZip() error {
return err
}
if resp.StatusCode != http.StatusOK {
- return fmt.Errorf(resp.Status)
+ return errors.New(resp.Status)
}
defer func() { _ = resp.Body.Close() }()
diff --git a/tray_darwin.go b/tray_darwin.go
index e6901875..7e06ee73 100644
--- a/tray_darwin.go
+++ b/tray_darwin.go
@@ -4,7 +4,6 @@
package main
import (
- "fmt"
"net"
"os"
"os/exec"
@@ -20,6 +19,7 @@ import (
"sealdice-core/dice"
"sealdice-core/icon"
+ log "sealdice-core/utils/kratos"
)
var theDm *dice.DiceManager
@@ -41,11 +41,11 @@ func TestRunning() bool {
}
func tempDirWarn() {
- fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。")
+ log.Info("当前工作路径为临时目录,因此拒绝继续执行。")
}
func showMsgBox(title string, message string) {
- fmt.Println(title, message)
+ log.Info(title, message)
}
func executeWin(name string, arg ...string) *exec.Cmd {
@@ -121,17 +121,16 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) {
ln, err := net.Listen("tcp", ":"+portStr)
if err != nil {
- logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
+ log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
runtime.Goexit()
}
_ = ln.Close()
// exec.Command(`cmd`, `/c`, `start`, fmt.Sprintf(`http://localhost:%s`, portStr)).Start()
- fmt.Println("如果浏览器没有自动打开,请手动访问:")
- fmt.Printf(`http://localhost:%s`, portStr) // 默认:3211
+ log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s", portStr)
err = e.Start(dm.ServeAddress)
if err != nil {
- logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
+ log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
return
}
}
diff --git a/tray_others.go b/tray_others.go
index 75bb354a..e535b080 100644
--- a/tray_others.go
+++ b/tray_others.go
@@ -4,7 +4,6 @@
package main
import (
- "fmt"
"net"
"os"
"os/exec"
@@ -15,6 +14,7 @@ import (
"github.com/labstack/echo/v4"
"sealdice-core/dice"
+ log "sealdice-core/utils/kratos"
)
func trayInit(dm *dice.DiceManager) {
@@ -32,11 +32,11 @@ func TestRunning() bool {
}
func tempDirWarn() {
- fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。")
+ log.Warn("当前工作路径为临时目录,因此拒绝继续执行。")
}
func showMsgBox(title string, message string) {
- fmt.Println(title, message)
+ log.Info(title, message)
}
func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) {
@@ -49,16 +49,15 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) {
ln, err := net.Listen("tcp", ":"+portStr)
if err != nil {
- logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
+ log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
runtime.Goexit()
}
_ = ln.Close()
- fmt.Println("如果浏览器没有自动打开,请手动访问:")
- fmt.Printf(`http://localhost:%s`, portStr) // 默认:3211
+ log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s", portStr)
err = e.Start(dm.ServeAddress)
if err != nil {
- logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
+ log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
return
}
}
diff --git a/tray_windows.go b/tray_windows.go
index 11c482f1..df40c2c6 100644
--- a/tray_windows.go
+++ b/tray_windows.go
@@ -25,6 +25,7 @@ import (
"sealdice-core/dice"
"sealdice-core/icon"
+ log "sealdice-core/utils/kratos"
)
func hideWindow() {
@@ -154,7 +155,7 @@ func onReady() {
s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误")
s2, _ := syscall.UTF16PtrFromString("自启动失败设置失败,原因: " + err.Error())
win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR)
- fmt.Println("自启动设置失败: ", err.Error())
+ log.Errorf("自启动设置失败: %v", err.Error())
}
mAutoBoot.Uncheck()
} else {
@@ -163,7 +164,7 @@ func onReady() {
s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误")
s2, _ := syscall.UTF16PtrFromString("自启动失败设置失败,原因: " + err.Error())
win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR)
- fmt.Println("自启动设置失败: ", err.Error())
+ log.Errorf("自启动设置失败: %v", err.Error())
}
mAutoBoot.Check()
}
@@ -239,12 +240,11 @@ func httpServe(e *echo.Echo, dm *dice.DiceManager, hideUI bool) {
dm.ServeAddress = fmt.Sprintf("0.0.0.0:%d", newPort)
continue
} else {
- logger.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
+ log.Errorf("端口已被占用,即将自动退出: %s", dm.ServeAddress)
os.Exit(0)
}
} else {
- fmt.Println("如果浏览器没有自动打开,请手动访问:")
- fmt.Printf("http://localhost:%s\n", portStr) // 默认:3211
+ log.Infof("如果浏览器没有自动打开,请手动访问:\nhttp://localhost:%s\n", portStr)
go showUI()
break
}
@@ -255,7 +255,7 @@ func tempDirWarn() {
s1, _ := syscall.UTF16PtrFromString("SealDice 临时目录错误")
s2, _ := syscall.UTF16PtrFromString("你正在临时文件目录运行海豹,最可能的情况是没有解压而是直接双击运行!\n请先完整解压后再进行运行操作!\n按确定后将自动退出")
win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR)
- fmt.Println("当前工作路径为临时目录,因此拒绝继续执行。")
+ log.Error("当前工作路径为临时目录,因此拒绝继续执行。")
}
func showMsgBox(title string, message string) {
diff --git a/update.go b/update.go
index 56eea8c6..83ae2379 100644
--- a/update.go
+++ b/update.go
@@ -14,15 +14,14 @@ import (
"syscall"
"time"
- "go.uber.org/zap"
-
"sealdice-core/dice"
"sealdice-core/utils"
+ log "sealdice-core/utils/kratos"
)
var binPrefix = "https://sealdice.coding.net/p/sealdice/d/sealdice-binaries/git/raw/master"
-func downloadUpdate(dm *dice.DiceManager, log *zap.SugaredLogger) (string, error) {
+func downloadUpdate(dm *dice.DiceManager, log *log.Helper) (string, error) {
var packFn string
if dm.AppVersionOnline != nil {
ver := dm.AppVersionOnline
@@ -112,7 +111,7 @@ func doReboot(dm *dice.DiceManager) {
binary, err := exec.LookPath(executablePath)
if err != nil {
- logger.Errorf("Restart Error: %s", err)
+ log.Errorf("Restart Error: %s", err)
return
}
platform := runtime.GOOS
@@ -124,7 +123,7 @@ func doReboot(dm *dice.DiceManager) {
cmd := executeWin(binary, "--delay=15")
err := cmd.Start()
if err != nil {
- logger.Errorf("Restart error: %s %v", binary, err)
+ log.Errorf("Restart error: %s %v", binary, err)
}
} else {
// 手动cleanup
@@ -132,7 +131,7 @@ func doReboot(dm *dice.DiceManager) {
// os.Args[1:]...
execErr := syscall.Exec(binary, []string{os.Args[0], "--delay=25"}, os.Environ())
if execErr != nil {
- logger.Errorf("Restart error: %s %v", binary, execErr)
+ log.Errorf("Restart error: %s %v", binary, execErr)
}
}
os.Exit(0)
diff --git a/update_updater.go b/update_updater.go
index fcaa5148..46096f35 100644
--- a/update_updater.go
+++ b/update_updater.go
@@ -13,10 +13,9 @@ import (
"syscall"
"time"
- "go.uber.org/zap"
-
"sealdice-core/dice"
"sealdice-core/utils"
+ log "sealdice-core/utils/kratos"
)
const updaterVersion = "0.1.1"
@@ -73,7 +72,7 @@ func CheckUpdater(dm *dice.DiceManager) error {
exists := false
fn := getUpdaterFn()
if _, err := os.Stat(fn); err == nil {
- logger.Info("检测到海豹更新程序")
+ log.Info("检测到海豹更新程序")
exists = true
}
@@ -82,15 +81,15 @@ func CheckUpdater(dm *dice.DiceManager) error {
if exists {
err := os.Chmod(fn, 0o755)
if err != nil {
- logger.Error("设置升级程序执行权限失败", err.Error())
+ log.Error("设置升级程序执行权限失败", err.Error())
}
cmd := exec.Command(fn, "--version")
out, err := cmd.Output()
if err != nil {
- logger.Error("获取升级程序版本失败")
+ log.Error("获取升级程序版本失败")
} else {
ver := strings.TrimSpace(string(out))
- logger.Info("升级程序版本:", ver)
+ log.Info("升级程序版本:", ver)
if ver == "seal-updater "+updaterVersion {
isUpdaterOk = true
}
@@ -99,16 +98,16 @@ func CheckUpdater(dm *dice.DiceManager) error {
// 如果升级程序不可用,那么下载一个
if !isUpdaterOk {
- logger.Info("未检测到可用更新程序,开始下载")
+ log.Info("未检测到可用更新程序,开始下载")
err := downloadUpdater(dm)
if err != nil {
- logger.Error("下载更新程序失败")
+ log.Error("下载更新程序失败")
return errors.New("下载更新程序失败,无可用更新程序")
} else {
- logger.Info("下载更新程序成功")
+ log.Info("下载更新程序成功")
err := os.Chmod(fn, 0o755)
if err != nil {
- logger.Error("设置升级程序执行权限失败", err.Error())
+ log.Error("设置升级程序执行权限失败", err.Error())
}
}
}
@@ -145,11 +144,8 @@ func downloadUpdater(dm *dice.DiceManager) error {
return nil
}
-func UpdateByFile(dm *dice.DiceManager, log *zap.SugaredLogger, packName string, syncMode bool) bool {
+func UpdateByFile(dm *dice.DiceManager, packName string, syncMode bool) bool {
// 注意: 当执行完就立即退进程的情况下,需要使用 syncMode 为true
- if log == nil {
- log = logger
- }
fn := getUpdaterFn()
err := os.Chmod(fn, 0o755)
if err != nil {
diff --git a/utils/convertdb.go b/utils/convertdb.go
new file mode 100644
index 00000000..97e5c773
--- /dev/null
+++ b/utils/convertdb.go
@@ -0,0 +1,23 @@
+package utils
+
+import (
+ "github.com/jmoiron/sqlx"
+ "gorm.io/gorm"
+)
+
+// GetSQLXDB 将 GORM 的 *gorm.DB 转换为 *sqlx.DB,并自动获取驱动名称,用于需要sqlx的场景
+func GetSQLXDB(db *gorm.DB) (*sqlx.DB, error) {
+ // 获取底层的 *sql.DB
+ sqlDB, err := db.DB()
+ if err != nil {
+ return nil, err
+ }
+
+ // 获取 GORM 使用的驱动名称
+ driverName := db.Dialector.Name()
+
+ // 使用 sqlx.NewDb 传递现有的 *sql.DB 和驱动名称
+ sqlxDB := sqlx.NewDb(sqlDB, driverName)
+
+ return sqlxDB, nil
+}
diff --git a/utils/crypto/ecdsa.go b/utils/crypto/ecdsa.go
index 551929b9..0a9c234e 100644
--- a/utils/crypto/ecdsa.go
+++ b/utils/crypto/ecdsa.go
@@ -4,7 +4,7 @@ import (
"crypto/ecdsa"
"crypto/rand"
"encoding/base64"
- "fmt"
+ "errors"
)
// EcdsaSign Ecdsa 签名
@@ -39,7 +39,7 @@ func EcdsaVerify(data []byte, base64Sig, publicKey string) error {
if ok := ecdsa.VerifyASN1(key, hashed, sign); ok {
return nil
}
- return fmt.Errorf("verify failed")
+ return errors.New("verify failed")
}
func EcdsaVerifyRow(data []byte, sign []byte, publicKey string) error {
@@ -48,5 +48,5 @@ func EcdsaVerifyRow(data []byte, sign []byte, publicKey string) error {
if ok := ecdsa.VerifyASN1(key, hashed, sign); ok {
return nil
}
- return fmt.Errorf("verify failed")
+ return errors.New("verify failed")
}
diff --git a/utils/download.go b/utils/download.go
index 9f09c976..aeab7ba6 100644
--- a/utils/download.go
+++ b/utils/download.go
@@ -3,10 +3,11 @@ package utils
import (
"compress/gzip"
"errors"
- "fmt"
"io"
"net/http"
"os"
+
+ log "sealdice-core/utils/kratos"
)
func DownloadFile(filepath string, url string) error {
@@ -41,7 +42,7 @@ func DownloadFile(filepath string, url string) error {
var reader io.ReadCloser
reader, err = gzip.NewReader(resp.Body)
if err != nil {
- fmt.Println("GZIP解压出错:", err)
+ log.Errorf("GZIP解压出错: %v", err)
return err
}
defer reader.Close()
diff --git a/utils/kratos/const.go b/utils/kratos/const.go
new file mode 100644
index 00000000..245435fc
--- /dev/null
+++ b/utils/kratos/const.go
@@ -0,0 +1,9 @@
+package log
+
+const (
+ LOG_SEAL = "SEAL"
+ LOG_DICE = "DICE"
+ LOG_WEB = "WEB"
+ LOG_LAGR = "LAGR"
+ LOG_HIDE = "HIDE"
+)
diff --git a/utils/kratos/echologger.go b/utils/kratos/echologger.go
new file mode 100644
index 00000000..aa8a95a3
--- /dev/null
+++ b/utils/kratos/echologger.go
@@ -0,0 +1,52 @@
+// Package middleware provides echo request and response output log
+package log
+
+import (
+ "strconv"
+ "time"
+
+ "github.com/labstack/echo/v4"
+)
+
+// Zlogger returns a middleware that logs HTTP requests.
+func EchoMiddleLogger(elogger *Helper) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ req := c.Request()
+ res := c.Response()
+ start := time.Now()
+
+ var err error
+ if err = next(c); err != nil {
+ c.Error(err)
+ }
+ stop := time.Now()
+
+ id := req.Header.Get(echo.HeaderXRequestID)
+ if id == "" {
+ id = res.Header().Get(echo.HeaderXRequestID)
+ }
+ reqSize := req.Header.Get(echo.HeaderContentLength)
+ if reqSize == "" {
+ reqSize = "0"
+ }
+
+ // 特殊情况下,需要输出DEBUG日志
+ elogger.Debugf("%s %s [%v] %s %-7s %s %3d %s %s %13v %s %s",
+ id,
+ c.RealIP(),
+ stop.Format(time.RFC3339),
+ req.Host,
+ req.Method,
+ req.RequestURI,
+ res.Status,
+ reqSize,
+ strconv.FormatInt(res.Size, 10),
+ stop.Sub(start).String(),
+ req.Referer(),
+ req.UserAgent(),
+ )
+ return err
+ }
+ }
+}
diff --git a/utils/kratos/filter.go b/utils/kratos/filter.go
new file mode 100644
index 00000000..7380d3dd
--- /dev/null
+++ b/utils/kratos/filter.go
@@ -0,0 +1,95 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+// FilterOption is filter option.
+type FilterOption func(*Filter)
+
+const fuzzyStr = "***"
+
+// FilterLevel with filter level.
+func FilterLevel(level Level) FilterOption {
+ return func(opts *Filter) {
+ opts.level = level
+ }
+}
+
+// FilterKey with filter key.
+func FilterKey(key ...string) FilterOption {
+ return func(o *Filter) {
+ for _, v := range key {
+ o.key[v] = struct{}{}
+ }
+ }
+}
+
+// FilterValue with filter value.
+func FilterValue(value ...string) FilterOption {
+ return func(o *Filter) {
+ for _, v := range value {
+ o.value[v] = struct{}{}
+ }
+ }
+}
+
+// FilterFunc with filter func.
+func FilterFunc(f func(level Level, keyvals ...interface{}) bool) FilterOption {
+ return func(o *Filter) {
+ o.filter = f
+ }
+}
+
+// Filter is a logger filter.
+type Filter struct {
+ logger Logger
+ level Level
+ key map[interface{}]struct{}
+ value map[interface{}]struct{}
+ filter func(level Level, keyvals ...interface{}) bool
+}
+
+// NewFilter new a logger filter.
+func NewFilter(logger Logger, opts ...FilterOption) *Filter {
+ options := Filter{
+ logger: logger,
+ key: make(map[interface{}]struct{}),
+ value: make(map[interface{}]struct{}),
+ }
+ for _, o := range opts {
+ o(&options)
+ }
+ return &options
+}
+
+// Log Print log by level and keyvals.
+func (f *Filter) Log(level Level, keyvals ...interface{}) error {
+ if level < f.level {
+ return nil
+ }
+ // prefixkv contains the slice of arguments defined as prefixes during the log initialization
+ var prefixkv []interface{}
+ l, ok := f.logger.(*logger)
+ if ok && len(l.prefix) > 0 {
+ prefixkv = make([]interface{}, 0, len(l.prefix))
+ prefixkv = append(prefixkv, l.prefix...)
+ }
+
+ if f.filter != nil && (f.filter(level, prefixkv...) || f.filter(level, keyvals...)) {
+ return nil
+ }
+
+ if len(f.key) > 0 || len(f.value) > 0 {
+ for i := 0; i < len(keyvals); i += 2 {
+ v := i + 1
+ if v >= len(keyvals) {
+ continue
+ }
+ if _, ok := f.key[keyvals[i]]; ok {
+ keyvals[v] = fuzzyStr
+ }
+ if _, ok := f.value[keyvals[v]]; ok {
+ keyvals[v] = fuzzyStr
+ }
+ }
+ }
+ return f.logger.Log(level, keyvals...)
+}
diff --git a/utils/kratos/global.go b/utils/kratos/global.go
new file mode 100644
index 00000000..cb9446cb
--- /dev/null
+++ b/utils/kratos/global.go
@@ -0,0 +1,134 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "sync"
+)
+
+// globalLogger is designed as a global logger in current process.
+var global = &loggerAppliance{}
+
+// loggerAppliance is the proxy of `Zlogger` to
+// make logger change will affect all sub-logger.
+type loggerAppliance struct {
+ lock sync.Mutex
+ wx *WriterX
+ logger Logger
+}
+
+// 似乎不建议使用init(),我自己手动管理吧
+// func init() {
+// global.SetLogger(DefaultLogger)
+// }
+
+func (a *loggerAppliance) SetLogger(in Logger) {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+ a.logger = in
+}
+
+// SetLogger should be called before any other log call.
+// And it is NOT THREAD SAFE.
+func SetLogger(logger Logger) {
+ global.SetLogger(logger)
+}
+
+// GetLogger returns global logger appliance as logger in current process.
+func GetLogger() Logger {
+ return global.logger
+}
+
+// Log Print log by level and keyvals.
+func Log(level Level, keyvals ...interface{}) {
+ _ = global.logger.Log(level, keyvals...)
+}
+
+// Context with context logger.
+func Context(ctx context.Context) *Helper {
+ return NewHelper(WithContext(ctx, global.logger))
+}
+
+// Debug logs a message at debug level.
+func Debug(a ...interface{}) {
+ _ = global.logger.Log(LevelDebug, DefaultMessageKey, fmt.Sprint(a...))
+}
+
+// Debugf logs a message at debug level.
+func Debugf(format string, a ...interface{}) {
+ _ = global.logger.Log(LevelDebug, DefaultMessageKey, fmt.Sprintf(format, a...))
+}
+
+// Debugw logs a message at debug level.
+func Debugw(keyvals ...interface{}) {
+ _ = global.logger.Log(LevelDebug, keyvals...)
+}
+
+// Info logs a message at info level.
+func Info(a ...interface{}) {
+ _ = global.logger.Log(LevelInfo, DefaultMessageKey, fmt.Sprint(a...))
+}
+
+// Infof logs a message at info level.
+func Infof(format string, a ...interface{}) {
+ _ = global.logger.Log(LevelInfo, DefaultMessageKey, fmt.Sprintf(format, a...))
+}
+
+// Infow logs a message at info level.
+func Infow(keyvals ...interface{}) {
+ _ = global.logger.Log(LevelInfo, keyvals...)
+}
+
+// Warn logs a message at warn level.
+func Warn(a ...interface{}) {
+ _ = global.logger.Log(LevelWarn, DefaultMessageKey, fmt.Sprint(a...))
+}
+
+// Warnf logs a message at warnf level.
+func Warnf(format string, a ...interface{}) {
+ _ = global.logger.Log(LevelWarn, DefaultMessageKey, fmt.Sprintf(format, a...))
+}
+
+// Warnw logs a message at warnf level.
+func Warnw(keyvals ...interface{}) {
+ _ = global.logger.Log(LevelWarn, keyvals...)
+}
+
+// Error logs a message at error level.
+func Error(a ...interface{}) {
+ _ = global.logger.Log(LevelError, DefaultMessageKey, fmt.Sprint(a...))
+}
+
+// Errorf logs a message at error level.
+func Errorf(format string, a ...interface{}) {
+ _ = global.logger.Log(LevelError, DefaultMessageKey, fmt.Sprintf(format, a...))
+}
+
+// Errorw logs a message at error level.
+func Errorw(keyvals ...interface{}) {
+ _ = global.logger.Log(LevelError, keyvals...)
+}
+
+// Fatal logs a message at fatal level.
+func Fatal(a ...interface{}) {
+ _ = global.logger.Log(LevelFatal, DefaultMessageKey, fmt.Sprint(a...))
+ os.Exit(1)
+}
+
+// Fatalf logs a message at fatal level.
+func Fatalf(format string, a ...interface{}) {
+ _ = global.logger.Log(LevelFatal, DefaultMessageKey, fmt.Sprintf(format, a...))
+ os.Exit(1)
+}
+
+// Fatalw logs a message at fatal level.
+func Fatalw(keyvals ...interface{}) {
+ _ = global.logger.Log(LevelFatal, keyvals...)
+ os.Exit(1)
+}
+
+func GetWriterX() *WriterX {
+ return global.wx
+}
diff --git a/utils/kratos/gormlogger.go b/utils/kratos/gormlogger.go
new file mode 100644
index 00000000..ab541dee
--- /dev/null
+++ b/utils/kratos/gormlogger.go
@@ -0,0 +1,113 @@
+package log
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "go.uber.org/zap/zapcore"
+ gormlogger "gorm.io/gorm/logger"
+)
+
+// gorm的格式化字符串抄过来
+var (
+ infoStr = "[info] "
+ warnStr = "[warn] "
+ errStr = "[error] "
+ traceStr = "[%.3fms] [rows:%v] %s"
+ traceWarnStr = "%s\n[%.3fms] [rows:%v] %s"
+ traceErrStr = "%s\n[%.3fms] [rows:%v] %s"
+)
+
+type ContextFn func(ctx context.Context) []zapcore.Field
+
+type GORMLogger struct {
+ // 要被传入的KartosLogger
+ ZapLogger *Helper
+ // 原本的Logger有
+ LogLevel gormlogger.LogLevel
+ // 原本的logger有
+ SlowThreshold time.Duration
+ // 原本的logger有
+ IgnoreRecordNotFoundError bool
+ // logger缺少的
+ ParameterizedQueries bool
+ SkipCallerLookup bool
+ Context ContextFn
+}
+
+func NewGormLogger(zapLogger *Helper) GORMLogger {
+ return GORMLogger{
+ ZapLogger: zapLogger,
+ LogLevel: gormlogger.Warn,
+ SlowThreshold: 100 * time.Millisecond,
+ IgnoreRecordNotFoundError: false,
+ Context: nil,
+ }
+}
+
+func (l GORMLogger) SetAsDefault() {
+ gormlogger.Default = l
+}
+
+func (l GORMLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
+ return GORMLogger{
+ ZapLogger: l.ZapLogger,
+ SlowThreshold: l.SlowThreshold,
+ LogLevel: level,
+ SkipCallerLookup: l.SkipCallerLookup,
+ IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError,
+ Context: l.Context,
+ }
+}
+
+func (l GORMLogger) Info(_ context.Context, msg string, args ...interface{}) {
+ if l.LogLevel >= gormlogger.Info {
+ l.ZapLogger.Infof(infoStr+msg, args...)
+ }
+}
+
+func (l GORMLogger) Warn(_ context.Context, msg string, args ...interface{}) {
+ if l.LogLevel >= gormlogger.Warn {
+ l.ZapLogger.Warnf(warnStr+msg, args...)
+ }
+}
+
+func (l GORMLogger) Error(_ context.Context, msg string, args ...interface{}) {
+ if l.LogLevel >= gormlogger.Error {
+ l.ZapLogger.Errorf(errStr+msg, args...)
+ }
+}
+
+func (l GORMLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) {
+ if l.LogLevel <= gormlogger.Silent {
+ return
+ }
+
+ elapsed := time.Since(begin)
+ switch {
+ case err != nil && l.LogLevel >= gormlogger.Error && (!errors.Is(err, gormlogger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
+ sql, rows := fc()
+ if rows == -1 {
+ l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, "-", sql)
+ } else {
+ l.ZapLogger.Errorf(traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
+ }
+ case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormlogger.Warn:
+ sql, rows := fc()
+ slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
+ if rows == -1 {
+ l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql)
+ } else {
+ l.ZapLogger.Warnf(traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)
+ }
+ case l.LogLevel == gormlogger.Info:
+ sql, rows := fc()
+ if rows == -1 {
+ l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, "-", sql)
+ } else {
+ l.ZapLogger.Debugf(traceStr, float64(elapsed.Nanoseconds())/1e6, rows, sql)
+ }
+ }
+}
diff --git a/utils/kratos/helper.go b/utils/kratos/helper.go
new file mode 100644
index 00000000..e6d6b780
--- /dev/null
+++ b/utils/kratos/helper.go
@@ -0,0 +1,179 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "go.uber.org/zap"
+)
+
+// DefaultMessageKey default message key.
+var DefaultMessageKey = "msg"
+
+// Option is Helper option.
+type Option func(*Helper)
+
+// Helper is a logger helper.
+type Helper struct {
+ logger Logger
+ msgKey string
+ sprint func(...interface{}) string
+ sprintf func(format string, a ...interface{}) string
+}
+
+// WithMessageKey with message key.
+func WithMessageKey(k string) Option {
+ return func(opts *Helper) {
+ opts.msgKey = k
+ }
+}
+
+// WithSprint with sprint
+func WithSprint(sprint func(...interface{}) string) Option {
+ return func(opts *Helper) {
+ opts.sprint = sprint
+ }
+}
+
+// WithSprintf with sprintf
+func WithSprintf(sprintf func(format string, a ...interface{}) string) Option {
+ return func(opts *Helper) {
+ opts.sprintf = sprintf
+ }
+}
+
+// NewHelper new a logger helper.
+func NewHelper(logger Logger, opts ...Option) *Helper {
+ options := &Helper{
+ msgKey: DefaultMessageKey, // default message key
+ logger: logger,
+ sprint: fmt.Sprint,
+ sprintf: fmt.Sprintf,
+ }
+ for _, o := range opts {
+ o(options)
+ }
+ return options
+}
+
+// ADD 获取自定义Helper
+func NewCustomHelper(loggerName string, hideConsole bool, sprintfFunc func(format string, a ...interface{}) string, withOptions ...zap.Option) *Helper {
+ var zapLogger Logger
+ var tempZapLogger *zap.Logger
+ // 添加HIDE的不会被输出到控制台,但会被输出到文件,或者其他位置。
+ // Named会保证克隆一个
+ if hideConsole {
+ tempZapLogger = GetLoggerRaw().Named("HIDE").Named(loggerName)
+ } else {
+ tempZapLogger = GetLoggerRaw().Named(loggerName)
+ }
+ // 添加存在的
+ if len(withOptions) > 0 {
+ tempZapLogger = tempZapLogger.WithOptions(withOptions...)
+ }
+ zapLogger = NewZapLogger(tempZapLogger)
+ // 如果 sprintfFunc 为 nil,则不使用 WithSprintf,直接使用 NewHelper
+ var helper *Helper
+ if sprintfFunc != nil {
+ helper = NewHelper(zapLogger, WithSprintf(sprintfFunc))
+ } else {
+ helper = NewHelper(zapLogger) // 使用默认的 Helper
+ }
+ return helper
+}
+
+// WithContext returns a shallow copy of h with its context changed
+// to ctx. The provided ctx must be non-nil.
+func (h *Helper) WithContext(ctx context.Context) *Helper {
+ return &Helper{
+ msgKey: h.msgKey,
+ logger: WithContext(ctx, h.logger),
+ sprint: h.sprint,
+ sprintf: h.sprintf,
+ }
+}
+
+// Log Print log by level and keyvals.
+func (h *Helper) Log(level Level, keyvals ...interface{}) {
+ _ = h.logger.Log(level, keyvals...)
+}
+
+// Debug logs a message at debug level.
+func (h *Helper) Debug(a ...interface{}) {
+ _ = h.logger.Log(LevelDebug, h.msgKey, h.sprint(a...))
+}
+
+// Debugf logs a message at debug level.
+func (h *Helper) Debugf(format string, a ...interface{}) {
+ _ = h.logger.Log(LevelDebug, h.msgKey, h.sprintf(format, a...))
+}
+
+// Debugw logs a message at debug level.
+func (h *Helper) Debugw(keyvals ...interface{}) {
+ _ = h.logger.Log(LevelDebug, keyvals...)
+}
+
+// Info logs a message at info level.
+func (h *Helper) Info(a ...interface{}) {
+ _ = h.logger.Log(LevelInfo, h.msgKey, h.sprint(a...))
+}
+
+// Infof logs a message at info level.
+func (h *Helper) Infof(format string, a ...interface{}) {
+ _ = h.logger.Log(LevelInfo, h.msgKey, h.sprintf(format, a...))
+}
+
+// Infow logs a message at info level.
+func (h *Helper) Infow(keyvals ...interface{}) {
+ _ = h.logger.Log(LevelInfo, keyvals...)
+}
+
+// Warn logs a message at warn level.
+func (h *Helper) Warn(a ...interface{}) {
+ _ = h.logger.Log(LevelWarn, h.msgKey, h.sprint(a...))
+}
+
+// Warnf logs a message at warnf level.
+func (h *Helper) Warnf(format string, a ...interface{}) {
+ _ = h.logger.Log(LevelWarn, h.msgKey, h.sprintf(format, a...))
+}
+
+// Warnw logs a message at warnf level.
+func (h *Helper) Warnw(keyvals ...interface{}) {
+ _ = h.logger.Log(LevelWarn, keyvals...)
+}
+
+// Error logs a message at error level.
+func (h *Helper) Error(a ...interface{}) {
+ _ = h.logger.Log(LevelError, h.msgKey, h.sprint(a...))
+}
+
+// Errorf logs a message at error level.
+func (h *Helper) Errorf(format string, a ...interface{}) {
+ _ = h.logger.Log(LevelError, h.msgKey, h.sprintf(format, a...))
+}
+
+// Errorw logs a message at error level.
+func (h *Helper) Errorw(keyvals ...interface{}) {
+ _ = h.logger.Log(LevelError, keyvals...)
+}
+
+// Fatal logs a message at fatal level.
+func (h *Helper) Fatal(a ...interface{}) {
+ _ = h.logger.Log(LevelFatal, h.msgKey, h.sprint(a...))
+ os.Exit(1)
+}
+
+// Fatalf logs a message at fatal level.
+func (h *Helper) Fatalf(format string, a ...interface{}) {
+ _ = h.logger.Log(LevelFatal, h.msgKey, h.sprintf(format, a...))
+ os.Exit(1)
+}
+
+// Fatalw logs a message at fatal level.
+func (h *Helper) Fatalw(keyvals ...interface{}) {
+ _ = h.logger.Log(LevelFatal, keyvals...)
+ os.Exit(1)
+}
diff --git a/utils/kratos/level.go b/utils/kratos/level.go
new file mode 100644
index 00000000..d4279247
--- /dev/null
+++ b/utils/kratos/level.go
@@ -0,0 +1,61 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+import "strings"
+
+// Level is a logger level.
+type Level int8
+
+// LevelKey is logger level key.
+const LevelKey = "level"
+
+const (
+ // LevelDebug is logger debug level.
+ LevelDebug Level = iota - 1
+ // LevelInfo is logger info level.
+ LevelInfo
+ // LevelWarn is logger warn level.
+ LevelWarn
+ // LevelError is logger error level.
+ LevelError
+ // LevelFatal is logger fatal level
+ LevelFatal
+)
+
+func (l Level) Key() string {
+ return LevelKey
+}
+
+func (l Level) String() string {
+ switch l {
+ case LevelDebug:
+ return "DEBUG"
+ case LevelInfo:
+ return "INFO"
+ case LevelWarn:
+ return "WARN"
+ case LevelError:
+ return "ERROR"
+ case LevelFatal:
+ return "FATAL"
+ default:
+ return ""
+ }
+}
+
+// ParseLevel parses a level string into a logger Level value.
+func ParseLevel(s string) Level {
+ switch strings.ToUpper(s) {
+ case "DEBUG":
+ return LevelDebug
+ case "INFO":
+ return LevelInfo
+ case "WARN":
+ return LevelWarn
+ case "ERROR":
+ return LevelError
+ case "FATAL":
+ return LevelFatal
+ }
+ return LevelInfo
+}
diff --git a/utils/kratos/log.go b/utils/kratos/log.go
new file mode 100644
index 00000000..2114ee7f
--- /dev/null
+++ b/utils/kratos/log.go
@@ -0,0 +1,62 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+import (
+ "context"
+)
+
+// Zlogger is a logger interface.
+type Logger interface {
+ Log(level Level, keyvals ...interface{}) error
+}
+
+type logger struct {
+ logger Logger
+ prefix []interface{}
+ hasValuer bool
+ ctx context.Context
+}
+
+func (c *logger) Log(level Level, keyvals ...interface{}) error {
+ kvs := make([]interface{}, 0, len(c.prefix)+len(keyvals))
+ kvs = append(kvs, c.prefix...)
+ if c.hasValuer {
+ bindValues(c.ctx, kvs)
+ }
+ kvs = append(kvs, keyvals...)
+ return c.logger.Log(level, kvs...)
+}
+
+// With with logger fields.
+func With(l Logger, kv ...interface{}) Logger {
+ c, ok := l.(*logger)
+ if !ok {
+ return &logger{logger: l, prefix: kv, hasValuer: containsValuer(kv), ctx: context.Background()}
+ }
+ kvs := make([]interface{}, 0, len(c.prefix)+len(kv))
+ kvs = append(kvs, c.prefix...)
+ kvs = append(kvs, kv...)
+ return &logger{
+ logger: c.logger,
+ prefix: kvs,
+ hasValuer: containsValuer(kvs),
+ ctx: c.ctx,
+ }
+}
+
+// WithContext returns a shallow copy of l with its context changed
+// to ctx. The provided ctx must be non-nil.
+func WithContext(ctx context.Context, l Logger) Logger {
+ switch v := l.(type) {
+ default:
+ return &logger{logger: l, ctx: ctx}
+ case *logger:
+ lv := *v
+ lv.ctx = ctx
+ return &lv
+ case *Filter:
+ fv := *v
+ fv.logger = WithContext(ctx, fv.logger)
+ return &fv
+ }
+}
diff --git a/utils/kratos/value.go b/utils/kratos/value.go
new file mode 100644
index 00000000..af88c969
--- /dev/null
+++ b/utils/kratos/value.go
@@ -0,0 +1,66 @@
+// copied from https://github.com/go-kratos/kratos/tree/main/log
+package log
+
+import (
+ "context"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var (
+ // DefaultCaller is a Valuer that returns the file and line.
+ DefaultCaller = Caller(4)
+
+ // DefaultTimestamp is a Valuer that returns the current wallclock time.
+ DefaultTimestamp = Timestamp(time.RFC3339)
+)
+
+// Valuer is returns a log value.
+type Valuer func(ctx context.Context) interface{}
+
+// Value return the function value.
+func Value(ctx context.Context, v interface{}) interface{} {
+ if v, ok := v.(Valuer); ok {
+ return v(ctx)
+ }
+ return v
+}
+
+// Caller returns a Valuer that returns a pkg/file:line description of the caller.
+func Caller(depth int) Valuer {
+ return func(context.Context) interface{} {
+ _, file, line, _ := runtime.Caller(depth)
+ idx := strings.LastIndexByte(file, '/')
+ if idx == -1 {
+ return file[idx+1:] + ":" + strconv.Itoa(line)
+ }
+ idx = strings.LastIndexByte(file[:idx], '/')
+ return file[idx+1:] + ":" + strconv.Itoa(line)
+ }
+}
+
+// Timestamp returns a timestamp Valuer with a custom time format.
+func Timestamp(layout string) Valuer {
+ return func(context.Context) interface{} {
+ return time.Now().Format(layout)
+ }
+}
+
+func bindValues(ctx context.Context, keyvals []interface{}) {
+ for i := 1; i < len(keyvals); i += 2 {
+ if v, ok := keyvals[i].(Valuer); ok {
+ keyvals[i] = v(ctx)
+ }
+ }
+}
+
+func containsValuer(keyvals []interface{}) bool {
+ for i := 1; i < len(keyvals); i += 2 {
+ if _, ok := keyvals[i].(Valuer); ok {
+ return true
+ }
+ }
+ return false
+}
diff --git a/utils/kratos/zap.go b/utils/kratos/zap.go
new file mode 100644
index 00000000..b3883390
--- /dev/null
+++ b/utils/kratos/zap.go
@@ -0,0 +1,148 @@
+package log
+
+import (
+ "encoding/json"
+ "os"
+
+ "github.com/natefinch/lumberjack"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "moul.io/zapfilter"
+)
+
+// TODO:或许有更好的方案,目前只是保证能够使用了
+// 搬运过来WriterX,然后默认初始化,给一个方式获取那个WriterX
+
+var logLimitDefault int64 = 100
+var originZapLogger *zap.Logger
+
+// GetLoggerRaw 特殊情况下,获取原生的LOGGER进行处理
+func GetLoggerRaw() *zap.Logger {
+ return originZapLogger
+}
+
+type LogItem struct {
+ Level string `json:"level"`
+ TS float64 `json:"ts"`
+ Caller string `json:"caller"`
+ Msg string `json:"msg"`
+}
+
+type WriterX struct {
+ LogLimit int64
+ Items []*LogItem
+}
+
+func (w *WriterX) Write(p []byte) (n int, err error) {
+ var a LogItem
+ err2 := json.Unmarshal(p, &a)
+ if err2 == nil {
+ w.Items = append(w.Items, &a)
+ limit := w.LogLimit
+ if limit == 0 {
+ w.LogLimit = logLimitDefault
+ }
+ if len(w.Items) > int(limit) {
+ w.Items = w.Items[1:]
+ }
+ }
+ return len(p), nil
+}
+
+var enabledLevel = zap.InfoLevel
+
+func SetEnableLevel(level zapcore.Level) {
+ switch level {
+ case zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel,
+ zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
+ {
+ enabledLevel = level
+ }
+ default: // no-op
+ }
+}
+
+// InitZapWithKartosLog 将所有的信息都会输出到main.log,以及输出到控制台
+func InitZapWithKartosLog(level zapcore.Level) {
+ SetEnableLevel(level)
+ // 日志文件的路径
+ path := "./data/main.log"
+
+ // 使用lumberjack进行日志文件轮转配置
+ lumlog := &lumberjack.Logger{
+ Filename: path, // 日志文件的名称和路径
+ MaxSize: 10, // 每个日志文件最大10MB
+ MaxBackups: 3, // 最多保留3个旧日志文件
+ MaxAge: 7, // 日志文件保存7天
+ }
+
+ // 获取日志编码器,定义日志的输出格式
+ encoder := getEncoder()
+
+ // 输出到UI的配置部分
+ pe := zap.NewProductionEncoderConfig()
+ global.wx = &WriterX{}
+ // 输出到文件的配置部分
+ mainLogCore := zapcore.NewCore(encoder, zapcore.AddSync(lumlog), zapcore.DebugLevel)
+ // 创建控制台的日志编码器
+ consoleCoreRaw := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), enabledLevel)
+ // 适配隐藏控制台输出的部分,重新设置日志级别,并输出除了HIDE以外的所有情况。这里ByNamespaces注意要先定义”全部选择“,然后定义”HIDE的不要“。
+ consoleCore := zapfilter.NewFilteringCore(consoleCoreRaw, zapfilter.All(zapfilter.MinimumLevel(enabledLevel), zapfilter.ByNamespaces("*,-HIDE.*")))
+
+ // 创建日志核心,将日志写入lumberjack的文件中,并设置日志级别为Debug
+ cores := []zapcore.Core{
+ // 默认输出到main.log的,全量日志文件
+ mainLogCore,
+ // 默认输出到UI的,只输出Info级别
+ // This outputs to WebUI, DO NOT apply enabledLevel
+ zapcore.NewCore(zapcore.NewJSONEncoder(pe), zapcore.AddSync(global.wx), zapcore.InfoLevel),
+ consoleCore,
+ }
+
+ // 将多个日志核心组合到一起,以同时记录到文件和控制台
+ core := zapcore.NewTee(cores...)
+
+ // 创建带有调用者信息的日志记录器,注意跳过两层,这样就能正常提供给log
+ originZapLogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2))
+ // 设置全局日志记录器,默认全局记录器为SEAL命名空间
+ global.SetLogger(NewZapLogger(originZapLogger.Named(LOG_SEAL)))
+ // GORM部分
+ SetDefaultGoRMLogger()
+}
+
+func SetDefaultGoRMLogger() {
+ gormpath := "./data/database.log"
+ gormpathlumlog := &lumberjack.Logger{
+ Filename: gormpath, // 日志文件的名称和路径
+ MaxSize: 10, // 每个日志文件最大10MB
+ MaxBackups: 3, // 最多保留3个旧日志文件
+ MaxAge: 7, // 日志文件保存7天
+ }
+ gormCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(gormpathlumlog), zapcore.DebugLevel)
+ // 层层进行包装
+ gormZapLogger := NewHelper(NewZapLogger(zap.New(gormCore).Named("GORM").WithOptions(zap.WithCaller(true), zap.AddCallerSkip(6))))
+
+ NewGormLogger(gormZapLogger).SetAsDefault()
+}
+
+func GetWebLogger() *Helper {
+ webpath := "./data/web.log"
+ // WEB的可以少一点点~
+ weblumlog := &lumberjack.Logger{
+ Filename: webpath, // 日志文件的名称和路径
+ MaxSize: 5, // 每个日志文件最大5MB
+ MaxBackups: 3, // 最多保留1个旧日志文件
+ MaxAge: 3, // 日志文件保存3天
+ }
+ webCore := zapcore.NewCore(getEncoder(), zapcore.AddSync(weblumlog), zapcore.DebugLevel)
+ webZapLogger := zap.New(webCore, zap.WithCaller(false))
+ return NewHelper(NewZapLogger(webZapLogger.Named("WEB")))
+}
+
+func getEncoder() zapcore.Encoder {
+ encoderConfig := zap.NewProductionEncoderConfig()
+ encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
+ encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
+
+ return zapcore.NewConsoleEncoder(encoderConfig)
+}
diff --git a/utils/kratos/zaplogger.go b/utils/kratos/zaplogger.go
new file mode 100644
index 00000000..9fd05636
--- /dev/null
+++ b/utils/kratos/zaplogger.go
@@ -0,0 +1,75 @@
+// copied from github.com/go-kratos/kratos/contrib/log/zap/v2
+package log
+
+import (
+ "fmt"
+
+ "go.uber.org/zap"
+)
+
+var _ Logger = (*ZapLogger)(nil)
+
+type ZapLogger struct {
+ log *zap.Logger
+ msgKey string
+}
+
+func NewZapLogger(zlog *zap.Logger) *ZapLogger {
+ return &ZapLogger{
+ log: zlog,
+ msgKey: DefaultMessageKey,
+ }
+}
+
+// 保留给Helper使用
+
+type ZapOption func(*ZapLogger)
+
+// WithZapMessageKey with message key.
+func WithZapMessageKey(key string) ZapOption {
+ return func(l *ZapLogger) {
+ l.msgKey = key
+ }
+}
+
+func (l *ZapLogger) Log(level Level, keyvals ...interface{}) error {
+ var (
+ msg = ""
+ keylen = len(keyvals)
+ )
+ if keylen == 0 || keylen%2 != 0 {
+ l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals))
+ return nil
+ }
+
+ data := make([]zap.Field, 0, (keylen/2)+1)
+ for i := 0; i < keylen; i += 2 {
+ if keyvals[i].(string) == l.msgKey {
+ msg, _ = keyvals[i+1].(string)
+ continue
+ }
+ data = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1]))
+ }
+
+ switch level {
+ case LevelDebug:
+ l.log.Debug(msg, data...)
+ case LevelInfo:
+ l.log.Info(msg, data...)
+ case LevelWarn:
+ l.log.Warn(msg, data...)
+ case LevelError:
+ l.log.Error(msg, data...)
+ case LevelFatal:
+ l.log.Fatal(msg, data...)
+ }
+ return nil
+}
+
+func (l *ZapLogger) Sync() error {
+ return l.log.Sync()
+}
+
+func (l *ZapLogger) Close() error {
+ return l.Sync()
+}
diff --git a/utils/oschecker/checker_other.go b/utils/oschecker/checker_other.go
new file mode 100644
index 00000000..9e2d65f0
--- /dev/null
+++ b/utils/oschecker/checker_other.go
@@ -0,0 +1,9 @@
+//go:build !windows
+// +build !windows
+
+package oschecker
+
+// 我们没有对其他的系统进行筛查的打算。
+func OldVersionCheck() (bool, string) {
+ return false, "NOTHING"
+}
diff --git a/utils/oschecker/checker_windows.go b/utils/oschecker/checker_windows.go
new file mode 100644
index 00000000..67b55097
--- /dev/null
+++ b/utils/oschecker/checker_windows.go
@@ -0,0 +1,206 @@
+//go:build windows
+// +build windows
+
+package oschecker
+
+// copied from https://github.com/jfjallid/go-secdump/blob/e307524e114f9abb39e2cd2b13ae421aae02d2de/utils.go with some changes
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "github.com/lxn/win"
+ "golang.org/x/sys/windows/registry"
+
+ log "sealdice-core/utils/kratos"
+)
+
+const (
+ WIN_UNKNOWN = iota
+ WINXP
+ WIN_SERVER_2003
+ WIN_VISTA
+ WIN_SERVER_2008
+ WIN7
+ WIN_SERVER_2008_R2
+ WIN8
+ WIN_SERVER_2012
+ WIN81
+ WIN_SERVER_2012_R2
+ WIN10
+ WIN_SERVER_2016
+ WIN_SERVER_2019
+ WIN_SERVER_2022
+ WIN11
+)
+
+var osNameMap = map[byte]string{
+ WIN_UNKNOWN: "Windows Unknown",
+ WINXP: "Windows XP",
+ WIN_VISTA: "Windows Vista",
+ WIN7: "Windows 7",
+ WIN8: "Windows 8",
+ WIN81: "Windows 8.1",
+ WIN10: "Windows 10",
+ WIN11: "Windows 11",
+ WIN_SERVER_2003: "Windows Server 2003",
+ WIN_SERVER_2008: "Windows Server 2008",
+ WIN_SERVER_2008_R2: "Windows Server 2008 R2",
+ WIN_SERVER_2012: "Windows Server 2012",
+ WIN_SERVER_2012_R2: "Windows Server 2012 R2",
+ WIN_SERVER_2016: "Windows Server 2016",
+ WIN_SERVER_2019: "Windows Server 2019",
+ WIN_SERVER_2022: "Windows Server 2022",
+}
+
+// OldVersionCheck 只获取最低版本
+func OldVersionCheck() (bool, string) {
+ build, f, b, err := getOSVersionBuild()
+ if err != nil {
+ // 不知道的版本,就认为是支持的
+ showNoticeBox("版本确认提示", "海豹无法获取您的操作系统版本,请确认正在使用 Windows 10/Windows Server 2016 或更高版本的 Windows。")
+ return true, osNameMap[WIN_UNKNOWN]
+ }
+ os := GetOSVersion(build, f, b)
+ // 这里用WinXP打底的原因是,WinXP下面是未知系统,我们默认放行未知系统
+ if (WINXP <= os) && (os < WIN10) {
+ // 展示提示弹窗,提示用户升级
+ showMsgBox("版本升级提示", fmt.Sprintf("您的操作系统版本「%s」过旧,海豹未来将不再支持,请尽快升级系统至 Windows 10/Windows Server 2016 或更高版本。", osNameMap[os]))
+ return true, osNameMap[os]
+ } else {
+ return false, osNameMap[os]
+ }
+}
+
+func showMsgBox(title string, message string) {
+ s1, _ := syscall.UTF16PtrFromString(title)
+ s2, _ := syscall.UTF16PtrFromString(message)
+ win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONERROR)
+}
+
+func showNoticeBox(title string, message string) {
+ s1, _ := syscall.UTF16PtrFromString(title)
+ s2, _ := syscall.UTF16PtrFromString(message)
+ win.MessageBox(0, s2, s1, win.MB_OK|win.MB_ICONWARNING)
+}
+
+func GetOSVersion(currentBuild int, currentVersion float64, server bool) (os byte) {
+ currentVersionStr := strconv.FormatFloat(currentVersion, 'f', 1, 64)
+ if server {
+ switch {
+ case currentBuild >= 3790 && currentBuild < 6001:
+ os = WIN_SERVER_2003
+ case currentBuild >= 6001 && currentBuild < 7601:
+ os = WIN_SERVER_2008
+ case currentBuild >= 7601 && currentBuild < 9200:
+ os = WIN_SERVER_2008_R2
+ case currentBuild >= 9200 && currentBuild < 9600:
+ os = WIN_SERVER_2012
+ case currentBuild >= 9200 && currentBuild < 14393:
+ os = WIN_SERVER_2012_R2
+ case currentBuild >= 14393 && currentBuild < 17763:
+ os = WIN_SERVER_2016
+ case currentBuild >= 17763 && currentBuild < 20348:
+ os = WIN_SERVER_2019
+ case currentBuild >= 20348:
+ os = WIN_SERVER_2022
+ default:
+ log.Debugf("Unknown server version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion)
+ os = WIN_UNKNOWN
+ }
+ } else {
+ switch currentVersionStr {
+ case "5.1":
+ os = WINXP
+ case "6.0":
+ // Windows Vista but it shares CurrentVersion and CurrentBuild with Windows Server 2008
+ os = WIN_VISTA
+ case "6.1":
+ // Windows 7 but it shares CurrentVersion and CurrentBuild with Windows Server 2008 R2
+ os = WIN7
+ case "6.2":
+ // Windows 8 but it shares CurrentVersion and CurrentBuild with Windows Server 2012
+ os = WIN8
+ case "6.3":
+ // Windows 8.1 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 R2
+ os = WIN81
+ case "10.0":
+ if currentBuild < 22000 {
+ os = WIN10
+ } else {
+ os = WIN11
+ }
+ default:
+ log.Debugf("Unknown version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion)
+ os = WIN_UNKNOWN
+ }
+ }
+
+ log.Debugf("OS Version: %s\n", osNameMap[os])
+ return
+}
+
+func getOSVersionBuild() (build int, version float64, server bool, err error) {
+ hSubKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
+ if err != nil {
+ log.Errorf("Failed to open registry key CurrentVersion with error: %v\n", err)
+ return
+ }
+ defer func(hSubKey registry.Key) {
+ err = hSubKey.Close()
+ if err != nil {
+ log.Fatalf("Failed to close hSubkey with error: %v\n", err)
+ }
+ }(hSubKey)
+
+ buildStr, _, err := hSubKey.GetStringValue("CurrentBuild")
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ build, err = strconv.Atoi(buildStr)
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ versionStr, _, err := hSubKey.GetStringValue("CurrentVersion")
+ if err != nil {
+ log.Error(err)
+ return
+ }
+
+ version, err = strconv.ParseFloat(versionStr, 32)
+ if err != nil {
+ log.Errorf("Failed to get CurrentVersion with error: %v\n", err)
+ return
+ }
+ // 二次判断:由于有Win8升级成Win10的情况,这个参数不准确。这个参数只有Win10往上有,所以下面
+ majorVersionStr, _, err := hSubKey.GetIntegerValue("CurrentMajorVersionNumber")
+ if err != nil {
+ log.Debug("非Win8以上系统,不包含CurrentMajorVersionNumber参数。")
+ }
+ // TODO: 据说,当前Win11和Win10的大版本号还相同,没有Win11,难以测试
+ if majorVersionStr == 10 {
+ version = 10.0
+ }
+
+ hSubKey, err = registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\ProductOptions`, registry.QUERY_VALUE)
+ if err != nil {
+ log.Errorf("Failed to open registry key ProductOptions with error: %v\n", err)
+ return
+ }
+
+ serverFlag, _, err := hSubKey.GetStringValue("ProductType")
+ if err != nil {
+ log.Error(err)
+ return
+ }
+
+ if strings.Compare(serverFlag, "ServerNT") == 0 {
+ server = true
+ }
+
+ return
+}
diff --git a/utils/paniclog/paniclog.go b/utils/paniclog/paniclog.go
new file mode 100644
index 00000000..9dc835d5
--- /dev/null
+++ b/utils/paniclog/paniclog.go
@@ -0,0 +1,35 @@
+package paniclog
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ log "sealdice-core/utils/kratos"
+)
+
+func InitPanicLog() {
+ // TODO: 全局写死写入在data目录,这东西几乎没有任何值得配置的
+ if err := os.MkdirAll("./data", 0755); err != nil {
+ log.Fatalf("未发现data文件夹,且未能创建data文件夹,请检查写入权限: %v", err)
+ }
+ f, err := os.OpenFile("./data/panic.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+ if err != nil {
+ log.Fatalf("未能创建panic日志文件,请检查写入权限: %v", err)
+ }
+ // Copied from https://github.com/rclone/rclone/tree/master/fs/log
+ // 这里GPT说,因为使用了APPEND,所以保证了不需要使用SEEK。但是rclone既然这么用了,我决定相信rclone的处理。
+ _, err = f.Seek(0, io.SeekEnd)
+ if err != nil {
+ log.Errorf("移动写入位置到末尾失败,请检查写入权限: %v", err)
+ }
+ currentTime := time.Now().Format("2006-01-02 15:04:05")
+ separator := fmt.Sprintf("\n-------- %s --------\n", currentTime)
+ // 将分割线写入文件
+ _, err = f.WriteString(separator)
+ if err != nil {
+ log.Fatalf("写入Panic日志分割线失败,请检查写入权限: %v", err)
+ }
+ redirectStderr(f)
+}
diff --git a/utils/paniclog/redirect_stderr.go b/utils/paniclog/redirect_stderr.go
new file mode 100644
index 00000000..7b64dac4
--- /dev/null
+++ b/utils/paniclog/redirect_stderr.go
@@ -0,0 +1,18 @@
+// Copied from https://github.com/rclone/rclone/tree/master/fs/log
+// Log the panic to the log file - for oses which can't do this
+
+//go:build !windows && !darwin && !dragonfly && !freebsd && !linux && !nacl && !netbsd && !openbsd
+
+package paniclog
+
+import (
+ "os"
+
+ log "sealdice-core/utils/kratos"
+)
+
+// redirectStderr to the file passed in
+func redirectStderr(f *os.File) {
+ // 安卓当前还暂时没有什么头绪,看上去rclone也没头绪。
+ log.Error("Can't redirect stderr to file")
+}
diff --git a/utils/paniclog/redirect_stderr_unix.go b/utils/paniclog/redirect_stderr_unix.go
new file mode 100644
index 00000000..e078b9cd
--- /dev/null
+++ b/utils/paniclog/redirect_stderr_unix.go
@@ -0,0 +1,22 @@
+// Copied from https://github.com/rclone/rclone/tree/master/fs/log
+// Log the panic under unix to the log file
+
+//go:build !windows && !solaris && !plan9 && !js
+
+package paniclog
+
+import (
+ "os"
+
+ "golang.org/x/sys/unix"
+
+ log "sealdice-core/utils/kratos"
+)
+
+// redirectStderr to the file passed in
+func redirectStderr(f *os.File) {
+ err := unix.Dup2(int(f.Fd()), int(os.Stderr.Fd()))
+ if err != nil {
+ log.Fatalf("Failed to redirect stderr to file: %v", err)
+ }
+}
diff --git a/utils/paniclog/redirect_stderr_windows.go b/utils/paniclog/redirect_stderr_windows.go
new file mode 100644
index 00000000..8a6d7a82
--- /dev/null
+++ b/utils/paniclog/redirect_stderr_windows.go
@@ -0,0 +1,45 @@
+// Copied from https://github.com/rclone/rclone/tree/master/fs/log
+// Log the panic under windows to the log file
+//
+// Code from minix, via
+//
+// https://play.golang.org/p/kLtct7lSUg
+
+//go:build windows
+
+package paniclog
+
+import (
+ "os"
+ "syscall"
+
+ log "sealdice-core/utils/kratos"
+)
+
+var (
+ kernel32 = syscall.MustLoadDLL("kernel32.dll")
+ procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
+)
+
+func setStdHandle(stdhandle int32, handle syscall.Handle) error {
+ r0, _, e1 := syscall.SyscallN(procSetStdHandle.Addr(), uintptr(stdhandle), uintptr(handle))
+ if r0 == 0 {
+ if e1 != 0 {
+ return error(e1)
+ }
+ return syscall.EINVAL
+ }
+ return nil
+}
+
+// redirectStderr to the file passed in
+func redirectStderr(f *os.File) {
+ err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
+ if err != nil {
+ log.Fatalf("Failed to redirect stderr to file: %v", err)
+ }
+ // https://stackoverflow.com/questions/34772012/capturing-panic-in-golang rclone can't get some
+ // I did some more experimenting and on window's you must also do os.Stderr = f since SetStdHandle does not affect the prior reference to stderr.
+ // On unix it is not necessary since the Dup2 does affect the prior reference to stderr. ( Tim Lewis Commented)
+ os.Stderr = f
+}
diff --git a/utils/procs/procs.go b/utils/procs/procs.go
index 0baae797..a65b9913 100644
--- a/utils/procs/procs.go
+++ b/utils/procs/procs.go
@@ -3,11 +3,12 @@ package procs
import (
"bufio"
"bytes"
- "fmt"
"io"
"os/exec"
"strings"
+ log "sealdice-core/utils/kratos"
+
"github.com/fyrchik/go-shlex"
)
@@ -87,7 +88,7 @@ func (p *Process) Start() error {
go func() {
defer func() {
if r := recover(); r != nil {
- fmt.Println("Recovered from panic:", r)
+ log.Errorf("Recovered from panic: %v", r)
}
}()
@@ -107,7 +108,7 @@ func (p *Process) Start() error {
go func() {
defer func() {
if r := recover(); r != nil {
- fmt.Println("Recovered from panic:", r)
+ log.Errorf("Recovered from panic: %v", r)
}
}()
diff --git a/utils/public_dice/sdk.go b/utils/public_dice/sdk.go
new file mode 100644
index 00000000..55e9e741
--- /dev/null
+++ b/utils/public_dice/sdk.go
@@ -0,0 +1,165 @@
+package public_dice
+
+import (
+ "github.com/monaco-io/request"
+)
+
+// PublicDiceClient SDK客户端
+type PublicDiceClient struct {
+ baseURL string
+ token string
+}
+
+// NewClient 创建新的SDK客户端
+func NewClient(baseURL string, token string) *PublicDiceClient {
+ return &PublicDiceClient{
+ baseURL: baseURL,
+ token: token,
+ }
+}
+
+// doReq 发送HTTP请求
+func doReq[T any](c *PublicDiceClient, method string, path string, data any, params map[string]string) (*T, int) {
+ req := request.Client{
+ URL: c.baseURL + path,
+ Method: method,
+ JSON: data,
+ Query: params,
+ }
+
+ var result T
+ resp := req.Send()
+ resp.Scan(&result)
+
+ return &result, resp.Code()
+}
+
+// Endpoint 公骰终端信息
+type Endpoint struct {
+ Platform string `json:"platform" msgpack:",omitempty"`
+ UID string `json:"uid" msgpack:",omitempty"`
+ InviteURL string `json:"inviteUrl" msgpack:",omitempty"`
+ IsOnline bool `json:"isOnline" msgpack:",omitempty"`
+
+ ID string `json:"id" msgpack:",omitempty"`
+ CreatedAt string `json:"createdAt" msgpack:",omitempty"`
+ UpdatedAt string `json:"updatedAt" msgpack:",omitempty"`
+ LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"`
+}
+
+// DiceInfo 公骰信息
+type DiceInfo struct {
+ ID string `json:"id" msgpack:",omitempty"`
+ CreatedAt string `json:"createdAt" msgpack:",omitempty"`
+ UpdatedAt string `json:"updatedAt" msgpack:",omitempty"`
+ Name string `json:"name" msgpack:",omitempty"`
+ Brief string `json:"brief" msgpack:",omitempty"`
+ Note string `json:"note" msgpack:",omitempty"`
+ Avatar string `json:"avatar" msgpack:",omitempty"`
+ Version string `json:"version" msgpack:",omitempty"`
+ IsOfficialVersion bool `json:"isOfficialVersion" msgpack:",omitempty"`
+ UpdateTickCount int `json:"updateTickCount" msgpack:",omitempty"`
+ LastTickTime int64 `json:"lastTickTime" msgpack:",omitempty"`
+ Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"`
+}
+
+// ListResponse 公骰列表响应
+type ListResponse struct {
+ Items []*DiceInfo `json:"items"`
+}
+
+// ListGet 获取公骰列表
+func (c *PublicDiceClient) ListGet(keyFunc func(data any) string) (*ListResponse, int) {
+ if keyFunc != nil {
+ data := keyFunc(nil)
+ return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", data, nil)
+ }
+ return doReq[ListResponse](c, "GET", "/dice/api/public-dice/list", nil, nil)
+}
+
+// RegisterRequest 注册公骰请求
+type RegisterRequest struct {
+ ID string `json:"ID,omitempty" msgpack:",omitempty"`
+ Name string `json:"name,omitempty" msgpack:",omitempty"` // 15字
+ Brief string `json:"brief,omitempty" msgpack:",omitempty"`
+ Note string `json:"note,omitempty" msgpack:",omitempty"`
+ Avatar string `json:"avatar,omitempty" msgpack:",omitempty"` // 头像?还是用另一个api进行注册比较好?可以省略
+ Key string `json:"key,omitempty" msgpack:",omitempty"`
+}
+
+// RegisterResponse 注册公骰响应
+type RegisterResponse struct {
+ Item DiceInfo `json:"item"`
+}
+
+// Register 注册公骰
+func (c *PublicDiceClient) Register(req *RegisterRequest, keyFunc func(data any) string) (*RegisterResponse, int) {
+ if keyFunc != nil {
+ req.Key = keyFunc(req)
+ }
+ return doReq[RegisterResponse](c, "POST", "/dice/api/public-dice/register", req, nil)
+}
+
+// DiceUpdateRequest 更新公骰请求
+type DiceUpdateRequest struct {
+ ID string `json:"id" msgpack:",omitempty"`
+ Name string `json:"name" msgpack:",omitempty"`
+ Brief string `json:"brief" msgpack:",omitempty"`
+ Note string `json:"note" msgpack:",omitempty"`
+ Key string `json:"key" msgpack:",omitempty"`
+}
+
+// DiceUpdateResponse 更新公骰响应
+type DiceUpdateResponse struct {
+ Updated int `json:"updated"`
+}
+
+// DiceUpdate 更新公骰
+func (c *PublicDiceClient) DiceUpdate(req *DiceUpdateRequest, keyFunc func(data any) string) (*DiceUpdateResponse, int) {
+ if keyFunc != nil {
+ req.Key = keyFunc(req)
+ }
+ return doReq[DiceUpdateResponse](c, "POST", "/dice/api/public-dice/register?update=1", req, nil)
+}
+
+// EndpointUpdateRequest 更新公骰SNS账号信息请求
+type EndpointUpdateRequest struct {
+ DiceID string `json:"diceId" msgpack:",omitempty"`
+ Key string `json:"key" msgpack:",omitempty"`
+ Endpoints []*Endpoint `json:"endpoints" msgpack:",omitempty"`
+}
+
+// EndpointUpdateResponse 更新公骰SNS账号信息响应
+type EndpointUpdateResponse struct{}
+
+// EndpointUpdate 更新公骰SNS账号信息
+func (c *PublicDiceClient) EndpointUpdate(req *EndpointUpdateRequest, keyFunc func(data any) string) (*EndpointUpdateResponse, int) {
+ if keyFunc != nil {
+ req.Key = keyFunc(req)
+ }
+ return doReq[EndpointUpdateResponse](c, "POST", "/dice/api/public-dice/endpoint-update", req, nil)
+}
+
+// TickUpdateRequest 更新公骰心跳请求
+type TickUpdateRequest struct {
+ ID string `json:"ID" msgpack:",omitempty"`
+ Key string `json:"key" msgpack:",omitempty"`
+ Endpoints []*TickEndpoint `json:"Endpoints" msgpack:",omitempty"`
+}
+
+// TickEndpoint 公骰心跳端点信息
+type TickEndpoint struct {
+ UID string `json:"uid" msgpack:",omitempty"`
+ IsOnline bool `json:"isOnline" msgpack:",omitempty"`
+}
+
+// TickUpdateResponse 更新公骰心跳响应
+type TickUpdateResponse struct{}
+
+// TickUpdate 更新公骰心跳
+func (c *PublicDiceClient) TickUpdate(req *TickUpdateRequest, keyFunc func(data any) string) (*TickUpdateResponse, int) {
+ if keyFunc != nil {
+ req.Key = keyFunc(req)
+ }
+ return doReq[TickUpdateResponse](c, "POST", "/dice/api/public-dice/tick-update", req, nil)
+}