├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── feature_request.yml
└── workflows
│ ├── issue_close.yml
│ ├── issue_geetings.yml
│ ├── issue_similarity.yml
│ └── release.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .release-please-config.json
├── .release-please-manifest.json
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── apps
├── admin.js
├── help.js
├── hijack.js
├── imageTool
│ ├── imageInfo.js
│ └── tool.js
├── info.js
├── list.js
├── meme.js
├── random.js
├── search.js
├── server
│ ├── download.js
│ ├── restart.js
│ ├── start.js
│ ├── status.js
│ └── stop.js
├── stat.js
└── update.js
├── components
├── Config.js
├── Data.js
├── Render.js
├── Version.js
├── YamlReader.js
└── index.js
├── config
└── defSet
│ ├── access.yaml
│ ├── meme.yaml
│ ├── other.yaml
│ ├── protect.yaml
│ ├── server.yaml
│ └── stat.yaml
├── eslint.config.js
├── guoba.support.js
├── index.js
├── models
├── admin
│ └── index.js
├── db
│ ├── base.js
│ ├── index.js
│ ├── meme.js
│ ├── preset.js
│ └── stat.js
├── guoba
│ ├── configInfo.js
│ ├── getMemeList.js
│ ├── index.js
│ ├── pluginInfo.js
│ └── schemas
│ │ ├── access.js
│ │ ├── index.js
│ │ ├── meme.js
│ │ ├── other.js
│ │ ├── protect.js
│ │ ├── server.js
│ │ └── stat.js
├── help
│ ├── config.js
│ ├── index.js
│ ├── list.js
│ └── theme.js
├── imageTool
│ ├── index.js
│ └── tools.js
├── index.js
├── make
│ ├── images.js
│ ├── index.js
│ ├── options.js
│ └── texts.js
├── server
│ ├── index.js
│ ├── manger.js
│ └── utils.js
└── utils
│ ├── common.js
│ ├── index.js
│ ├── preset.js
│ ├── request.js
│ └── tools.js
├── package.json
└── resources
├── admin
├── imgs
│ ├── bg.webp
│ └── cfg.webp
├── index.css
└── index.html
├── code
├── imgs
│ └── bg.webp
├── index.css
└── index.html
├── common
├── common.css
└── layout
│ └── default.html
├── help
├── icon.webp
├── index.css
├── index.html
├── theme
│ ├── bg.webp
│ └── main.webp
├── version-info.css
└── version-info.html
├── list
├── imgs
│ ├── bg.webp
│ └── icons
│ │ ├── image.svg
│ │ ├── option.svg
│ │ └── text.svg
├── index.css
└── index.html
├── server
├── imgs
│ └── bg.webp
├── status.css
└── status.html
└── stat
├── imgs
└── bg.webp
├── index.css
└── index.html
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 错误报告
2 | description: 在使用 meme-plugin 的过程中遇到了错误
3 | title: '[🐛 Bug]: '
4 | labels: [ "bug" ]
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: 感谢您花时间填写此错误报告,请**务必确认您的issue不是重复的**,否则我们可能会直接关闭它。请按照下面的模板填写您的信息。
10 |
11 | - type: checkboxes
12 | attributes:
13 | label: 请确保以下事项
14 | description: 您必须勾选以下所有内容, 否则您的issue可能会被直接关闭。
15 | options:
16 | - label: 我已经阅读了[文档](https://docs.wuliya.cn/candria/meme)。
17 | - label: 我确定没有重复的issue或讨论。
18 | - label: 我确定是`meme-plugin`的问题,而不是其他原因(例如网络,`依赖`或`操作`)。
19 | - label: 我确定这个问题在最新版本中没有被修复。
20 |
21 | - type: input
22 | id: version
23 | attributes:
24 | label: 版本
25 | description: 您使用的是哪个版本/commit id的源码? 请不要使用`latest`或`master`作为答案。
26 | placeholder: v0.xx.xx 或者 commit id
27 | validations:
28 | required: true
29 | - type: textarea
30 | id: bug-description
31 | attributes:
32 | label: 问题描述
33 | validations:
34 | required: true
35 | - type: textarea
36 | id: config
37 | attributes:
38 | label: 配置
39 | description: 请提供您的`meme-plugin`应用的配置文件(隐藏隐私字段)
40 | validations:
41 | required: true
42 | - type: textarea
43 | id: Operation-procedure
44 | attributes:
45 | label: 操作步骤
46 | description: 大意为聊天记录截图
47 | validations:
48 | required: true
49 | - type: textarea
50 | id: logs
51 | attributes:
52 | label: 日志
53 | description: 请复制粘贴错误日志,或者截图。务必完整
54 | validations:
55 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 新功能提议
2 | description: 希望拥有新的功能
3 | title: '[✨ feat]: '
4 | labels: ["enhancement"]
5 |
6 | body:
7 | - type: checkboxes
8 | attributes:
9 | label: 请确保以下事项
10 | description: 您可以选择多个,甚至全部选择。
11 | options:
12 | - label: 我已经阅读了[文档](https://docs.wuliya.cn/candria/meme)。
13 | - label: 我确定没有重复的issue或讨论。
14 | - label: 我确定这个功能没有实现。
15 | - label: 我相信这是一个合理和普遍的要求。
16 | - type: textarea
17 | id: feature-description
18 | attributes:
19 | label: 需求描述
20 | validations:
21 | required: true
22 | - type: textarea
23 | id: suggested-solution
24 | attributes:
25 | label: 实现思路
26 | description: 实现此需求的解决思路。
27 | - type: textarea
28 | id: additional-context
29 | attributes:
30 | label: 附件
31 | description: 相关的任何其他上下文或截图,或者你觉得有帮助的信息
--------------------------------------------------------------------------------
/.github/workflows/issue_close.yml:
--------------------------------------------------------------------------------
1 | name: Close inactive issues
2 | on:
3 | schedule:
4 | - cron: "30 1 * * *"
5 |
6 | permissions:
7 | issues: write
8 | jobs:
9 | close-issues:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | issues: write
13 | pull-requests: write
14 | steps:
15 | - uses: actions/stale@v9
16 | with:
17 | days-before-issue-stale: 60
18 | days-before-issue-close: 30
19 | stale-issue-label: "stale"
20 | stale-issue-message: "📅 你好 @${{ github.event.issue.user.login }},这个问题已经过期了,因为它已经开放了30天,没有任何活动。"
21 | close-issue-message: "🚫 你好 @${{ github.event.issue.user.login }},此问题已关闭,因为它已被标记为过期后14天处于非活动状态。。"
22 | days-before-pr-stale: -1
23 | days-before-pr-close: -1
24 | repo-token: ${{ secrets.GITHUB_TOKEN }}
25 | exempt-pinned: true
--------------------------------------------------------------------------------
/.github/workflows/issue_geetings.yml:
--------------------------------------------------------------------------------
1 | name: Issues Greetings
2 | on:
3 | issues:
4 | types: [labeled]
5 |
6 | permissions:
7 | issues: write
8 |
9 | jobs:
10 | create-comment:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Create comment for enhancement
14 | if: github.event.label.name == 'enhancement'
15 | uses: actions-cool/issues-helper@v3
16 | with:
17 | actions: 'create-comment'
18 | token: ${{ secrets.GITHUB_TOKEN }}
19 | body: |
20 | 你好 @${{ github.event.issue.user.login }},我们已经记录了你的新功能提议。如果你有任何具体的实现想法或设计草图,欢迎随时分享给我们。
21 | emoji: 'eyes'
22 |
23 | - name: Create comment for bug
24 | if: github.event.label.name == 'bug'
25 | uses: actions-cool/issues-helper@v3
26 | with:
27 | actions: 'create-comment'
28 | token: ${{ secrets.GITHUB_TOKEN }}
29 | body: |
30 | 你好 @${{ github.event.issue.user.login }},看来我们的代码不小心打了个盹儿。别担心,我们已经唤醒了开发团队,他们正快马加鞭地赶来修复!🔨🐞
31 | emoji: 'eyes'
--------------------------------------------------------------------------------
/.github/workflows/issue_similarity.yml:
--------------------------------------------------------------------------------
1 | # 问题相似性分析
2 | name: Issues Similarity Analysis
3 |
4 | on:
5 | issues:
6 | types: [opened, edited]
7 |
8 | permissions:
9 | issues: write
10 |
11 | jobs:
12 | similarity-analysis:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: analysis
16 | uses: actions-cool/issues-similarity-analysis@v1
17 | with:
18 | filter-threshold: 0.5
19 | comment-title: '### 似乎有相似的问题'
20 | comment-body: '${index}. ${similarity} #${number}'
21 | show-footer: false
22 | show-mentioned: true
23 | since-days: 730
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: 发布发行版
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 |
9 | permissions:
10 | contents: write
11 | id-token: write
12 | pull-requests: write
13 |
14 | jobs:
15 | release:
16 | runs-on: ubuntu-latest
17 | outputs:
18 | releases_created: ${{ steps.release-please.outputs.releases_created }}
19 | steps:
20 | - name: 获取token
21 | uses: actions/create-github-app-token@v2
22 | id: app-token
23 | with:
24 | app-id: ${{ secrets.APP_ID }}
25 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
26 |
27 | - name: 获取用户ID
28 | id: get-user-id
29 | run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
30 | env:
31 | GH_TOKEN: ${{ steps.app-token.outputs.token }}
32 |
33 | - name: 设置用户信息
34 | run: |
35 | git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
36 | git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
37 |
38 | - name: 运行 release-please-action
39 | id: release-please
40 | uses: googleapis/release-please-action@v4
41 | with:
42 | token: ${{ steps.app-token.outputs.token }}
43 | config-file: .release-please-config.json
44 | manifest-file: .release-please-manifest.json
45 |
46 | create-release:
47 | needs: [release]
48 | if: needs.release.outputs.releases_created == 'true'
49 | runs-on: ubuntu-latest
50 | steps:
51 | - name: 检出代码
52 | uses: actions/checkout@v4
53 | with:
54 | ref: main
55 | fetch-tags: true
56 |
57 | - name: 获取token
58 | uses: actions/create-github-app-token@v2
59 | id: app-token
60 | with:
61 | app-id: ${{ secrets.APP_ID }}
62 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
63 |
64 | - name: 获取用户ID
65 | id: get-user-id
66 | run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
67 | env:
68 | GH_TOKEN: ${{ steps.app-token.outputs.token }}
69 |
70 | - name: 设置用户信息
71 | run: |
72 | git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
73 | git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
74 |
75 | - name: 构建产物
76 | run: |
77 | zip -r build.zip . -x "*.git/*" -x ".github/*"
78 |
79 | - name: 获取最新标签
80 | id: get_tag
81 | run: |
82 | TAG=$(git describe --tags --abbrev=0 origin/main)
83 | echo "最新的tag标签: $TAG"
84 | echo "tag=$TAG" >> $GITHUB_OUTPUT
85 |
86 | - name: 上传产物
87 | uses: softprops/action-gh-release@v2
88 | with:
89 | token: ${{ steps.app-token.outputs.token }}
90 | files: build.zip
91 | tag_name: ${{ steps.get_tag.outputs.tag }}
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | config/config
3 | data
4 | pnpm-lock.yaml
5 | apps/test.js
6 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 |
2 | ci:
3 | autofix_commit_msg: |
4 | [pre-commit.ci] auto fixes from pre-commit.com hooks
5 | autofix_prs: true
6 | autoupdate_branch: main
7 | autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
8 | autoupdate_schedule: weekly
9 | skip: []
10 |
11 | repos:
12 | - repo: https://github.com/pre-commit/mirrors-eslint
13 | rev: v9.25.0
14 | hooks:
15 | - id: eslint
16 | args: [--fix]
17 | files: \.(js)$
18 | types: [file]
19 | language: node
20 | language_version: 22.13.1
21 | additional_dependencies:
22 | - eslint@^9.25.0
23 | - globals@^15.15.0
24 | - neostandard@^0.12.1
25 | - eslint-plugin-simple-import-sort@12.1.1
26 |
27 |
--------------------------------------------------------------------------------
/.release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3 | "release-type": "node",
4 | "include-component-in-tag": false,
5 | "changelog-sections": [
6 | {
7 | "type": "feat",
8 | "section": "✨ 新功能",
9 | "hidden": false
10 | },
11 | {
12 | "type": "fix",
13 | "section": "🐛 错误修复",
14 | "hidden": false
15 | },
16 | {
17 | "type": "perf",
18 | "section": "⚡️ 性能优化",
19 | "hidden": false
20 | },
21 | {
22 | "type": "revert",
23 | "section": "⏪️ 回退提交",
24 | "hidden": false
25 | },
26 | {
27 | "type": "docs",
28 | "section": "📝 文档更新",
29 | "hidden": false
30 | },
31 | {
32 | "type": "style",
33 | "section": "🎨 代码样式",
34 | "hidden": false
35 | },
36 | {
37 | "type": "chore",
38 | "section": "🔧 其他更新",
39 | "hidden": false
40 | },
41 | {
42 | "type": "refactor",
43 | "section": "♻️ 代码重构",
44 | "hidden": false
45 | },
46 | {
47 | "type": "test",
48 | "section": "✅ 测试相关",
49 | "hidden": false
50 | },
51 | {
52 | "type": "deps",
53 | "section": "📦 依赖更新"
54 | },
55 | {
56 | "type": "build",
57 | "section": "📦️ 构建系统",
58 | "hidden": false
59 | },
60 | {
61 | "type": "ci",
62 | "section": "🎡 持续集成",
63 | "hidden": false
64 | }
65 | ],
66 | "packages": {
67 | ".": {
68 | "release-type": "node"
69 | }
70 | },
71 | "commit-search-depth": 50
72 | }
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "2.0.1"
3 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.detectIndentation": false,
3 | "editor.tabSize": 2,
4 | "editor.formatOnSave": true,
5 | "editor.formatOnType": false,
6 | "editor.formatOnPaste": true,
7 | "editor.formatOnSaveMode": "file",
8 | "editor.codeActionsOnSave": {
9 | "source.fixAll.eslint": "always"
10 | }
11 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 变更日志
2 |
3 | ## [2.0.1](https://github.com/CandriaJS/meme-plugin/compare/v2.0.0...v2.0.1) (2025-06-03)
4 |
5 |
6 | ### 🐛 错误修复
7 |
8 | * 载入导入 ([939647f](https://github.com/CandriaJS/meme-plugin/commit/939647f635b526774676adad15f26a2e34ded2d2))
9 | * 锅巴导入 ([3c763d5](https://github.com/CandriaJS/meme-plugin/commit/3c763d5ec635e8086e81027bc2519d9e41a66f2b))
10 |
11 | ## [2.0.0](https://github.com/CandriaJS/meme-plugin/compare/v1.17.1...v2.0.0) (2025-06-03)
12 |
13 |
14 | ### ⚠ BREAKING CHANGES
15 |
16 | * 迁移至Rust 表情包Api ([#5](https://github.com/CandriaJS/meme-plugin/issues/5))
17 |
18 | ### ✨ 新功能
19 |
20 | * **plugins:** 重构表情插件并添加新功能 ([099dfff](https://github.com/CandriaJS/meme-plugin/commit/099dfffffc6833ff40aa58210627c88a9296bacf))
21 | * 迁移至Rust 表情包Api ([#5](https://github.com/CandriaJS/meme-plugin/issues/5)) ([3b9a95c](https://github.com/CandriaJS/meme-plugin/commit/3b9a95c5d9d29cee24253fd68a9b550329bc0665))
22 |
23 |
24 | ### 📝 文档更新
25 |
26 | * 更新 issue 模板并优化文档链接 ([4cf35ab](https://github.com/CandriaJS/meme-plugin/commit/4cf35ab1b4dbdc70a34e06c6e0896940ed5d6db5))
27 | * 更新文档信息 ([9110dd0](https://github.com/CandriaJS/meme-plugin/commit/9110dd0495ce5f844985b8f410e4c3dbf11f1e2c))
28 |
29 | ## [1.17.1](https://github.com/CandriaJS/meme-plugin/compare/v1.17.0...v1.17.1) (2025-06-03)
30 |
31 |
32 | ### 🔧 其他更新
33 |
34 | * 误删仓库,恢复 ([94f4b06](https://github.com/CandriaJS/meme-plugin/commit/94f4b06c1067a8a5e20d263dde24214ad2c7a54e))
35 |
36 | ## [1.17.0](https://github.com/CandriaJS/meme-plugin/compare/v1.16.1...v1.17.0) (2025-04-24)
37 |
38 |
39 | ### ✨ 新功能
40 |
41 | * **config:** 添加表情保护设置功能 ([eda6546](https://github.com/CandriaJS/meme-plugin/commit/eda654662825c5a649ddf862451efb412b2662c4))
42 | * **models:** 添加表情保护功能 ([25fa86e](https://github.com/CandriaJS/meme-plugin/commit/25fa86ed64c02b8fad6747364a8d4de462ec4600))
43 |
44 |
45 | ### 🐛 错误修复
46 |
47 | * **apps:** 修复优化统计模块数据处理逻辑 ([b9fe4a6](https://github.com/CandriaJS/meme-plugin/commit/b9fe4a60816c9d81644b34527776ad6b8a56d95e))
48 | * **config:** 修复指令无法添加黑名单表情列表 ([12d5c95](https://github.com/CandriaJS/meme-plugin/commit/12d5c95ab5f8e339d29903494c1d986ae2cdec5e))
49 | * **models:** 修复生成昵称和性别时的用户 ID 引用错误 ([804d6d1](https://github.com/CandriaJS/meme-plugin/commit/804d6d11d97f53f540efb4c9216522b7d5d03be5))
50 |
51 |
52 | ### ⚡️ 性能优化
53 |
54 | * 重构静态站并使用新的静态站资源 ([411371b](https://github.com/CandriaJS/meme-plugin/commit/411371b76ad8d00cab761c1ffe85ae2ef85ecc94))
55 |
56 |
57 | ### 🎨 代码样式
58 |
59 | * **common:** 更新 YS 字体资源链接 ([6337e2f](https://github.com/CandriaJS/meme-plugin/commit/6337e2f2f18c269e8dcf129f92681079b0e389c7))
60 |
61 |
62 | ### ♻️ 代码重构
63 |
64 | * **apps:** 优化 stat.js 文件 ([b8c23b5](https://github.com/CandriaJS/meme-plugin/commit/b8c23b56d1262e007b0c3c2b052f0295c5ed681e))
65 | * **apps:** 重构表情列表和统计页面 ([73ac96f](https://github.com/CandriaJS/meme-plugin/commit/73ac96f17ef10cd1b2f3f66225370901e7c4faa0))
66 | * **db:** 优化数据库操作逻辑 ([ee27e15](https://github.com/CandriaJS/meme-plugin/commit/ee27e15f37b039b4278e845825eca34bd4d16634))
67 | * **models:** 优化 add 函数并移除冗余代码 ([bec299c](https://github.com/CandriaJS/meme-plugin/commit/bec299c2000255e9aef608153df8bee46e6a0f67))
68 | * **models:** 优化 Meme 模型中处理图片的逻辑 ([e58e85b](https://github.com/CandriaJS/meme-plugin/commit/e58e85b7b04319544e9f236303cf2963d29c7712))
69 | * **models:** 移除 GIF 相关功能 ([c96ae46](https://github.com/CandriaJS/meme-plugin/commit/c96ae468969df148bd3d5514c759e0f126ed0504))
70 | * **models:** 移除表情包快捷方式相关代码 ([20f8f20](https://github.com/CandriaJS/meme-plugin/commit/20f8f20f332e31270c7f2c13757833e61cd997ea))
71 | * **update:** 移除更新检查相关代码 ([db62463](https://github.com/CandriaJS/meme-plugin/commit/db62463e7f7b74ebfbd59d7814e5689550d0adef))
72 | * **update:** 移除更新检查相关功能 ([d299e19](https://github.com/CandriaJS/meme-plugin/commit/d299e1927a6874e1a66d82dde79e551a7f722d2c))
73 |
74 |
75 | ### 🎡 持续集成
76 |
77 | * **release:** 更新获取最新标签的命令 ([71afafa](https://github.com/CandriaJS/meme-plugin/commit/71afafaec53a402a2f471fa2653e5eab2e3e18e9))
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
柠糖表情
2 |
3 |
14 |
15 | ## 介绍 📝
16 | `柠糖表情` 是一个 `Yunzai-Bot` | `Karin` 的扩展插件,提供表情包合成等功能。
17 |
18 | 如有问题请提交 `issue` 或加入 Q 群: `272040396` 📬
19 |
20 | >[!TIP]
21 | >自`V2`版本起,已内置服务端的下载,在`server.yaml`中配置`mode`为`1`重启等待插件下载完成即可使用
22 | >但需手动下载表情资源, 在data/server目录下执行以下命令. windows用户需要加.exe后缀
23 | >```bash
24 | >meme download
25 | >```
26 |
27 |
28 | ## 安装与更新 🔧
29 |
30 | ### Yunzai-Bot
🚀
31 |
32 |
33 | 使用 Github
🐙
34 |
35 | ```bash
36 | git clone --depth=1 https://github.com/CandriaJS/meme-plugin ./plugins/meme-plugin/
37 | ```
38 |
39 |
40 | 使用 Github
镜像 🌐
41 |
42 | ```bash
43 | git clone --depth=1 https://gh.wuliya.xin/https://github.com/CandriaJS/meme-plugin ./plugins/meme-plugin/
44 | ```
45 |
46 |
47 | 使用 Release
🔨
48 |
49 | 在 [Release](https://github.com/CandriaJS/meme-plugin/releases/latest) 页面下载最新版本,解压后修改文件夹名称为 `meme-plugin` 然后放入 `plugins` 文件夹中即可使用。
50 |
51 | **虽然此方式能够使用,不利于后续升级,故不推荐使用 🔔**
52 |
53 |
54 | ### Karin
🤖
55 | 请前往 [Karin仓库](https://github.com/CandriaJS/karin-plugin-meme)
56 |
57 | ### 安装依赖 📦
58 | ```bash
59 | pnpm install --filter=meme-plugin
60 | ```
61 |
62 | ## 使用帮助 ℹ️
63 | 其他内容请查看 [官方文档](https://docs.wuliya.cn/candria/meme)
64 |
65 | ## 更新计划 🛠
66 |
67 | 功能已完成,后续进入功能维护期
68 |
69 | ### 表情后端搭建教程 🌟
70 | 请查看文档
71 |
72 | ## 贡献者 👨💻👩💻
73 |
74 |
75 |
76 |
77 |
78 | 
79 |
80 | # 资源 📚
81 |
82 | - [Miao-Yunzai](https://github.com/yoimiya-kokomi/Miao-Yunzai) : 喵版 Yunzai [Gitee](https://gitee.com/yoimiya-kokomi/Miao-Yunzai) / [Github](https://github.com/yoimiya-kokomi/Miao-Yunzai)
83 | - [Yunzai-V3](https://github.com/yoimiya-kokomi/Yunzai-Bot) :Yunzai V3 - 喵喵维护版(使用 icqq)
84 | - [Yunzai-V3](https://gitee.com/Le-niao/Yunzai-Bot) :Yunzai V3 - 乐神原版(使用 oicq)
85 | - [meme-generator-rs](https://github.com/MeetWq/meme-generator-rs): 表情包生成器,用于制作各种沙雕表情包 ***本插件的来源***
86 |
--------------------------------------------------------------------------------
/apps/admin.js:
--------------------------------------------------------------------------------
1 | import { Config, Render, Version } from '#components'
2 | import { admin } from '#models'
3 |
4 | function checkNumberValue (value, limit) {
5 | const [ min, max ] = limit.split('-').map(Number)
6 | return Math.min(Math.max(value, min), max)
7 | }
8 | const sysCfgReg = () => {
9 | const cfgSchema = admin.AdminConfig
10 | const groupNames = Object.values(cfgSchema)
11 | .map(group => group.title)
12 | .join('|')
13 |
14 | const itemNames = Object.values(cfgSchema)
15 | .flatMap((group) => Object.values(group.cfg).map((item) => item.title))
16 | const sortedKeys = [ ...itemNames ].sort((a, b) => b.length - a.length)
17 | return new RegExp(`^#柠糖表情设置\\s*(${groupNames})?\\s*(${sortedKeys.join('|')})?\\s*(.*)`)
18 | }
19 |
20 | async function renderConfig (e) {
21 | const cfg = Config.getCfg()
22 | const schema = admin.AdminConfig
23 | const img = await Render.render(
24 | 'admin/index',
25 | {
26 | title: Version.Plugin_AliasName,
27 | schema,
28 | cfg
29 | }
30 | )
31 | return await e.reply(img)
32 | }
33 |
34 | export class setting extends plugin {
35 | constructor () {
36 | super({
37 | name: '柠糖表情:设置',
38 | event: 'message',
39 | priority: -Infinity,
40 | rule: [
41 | {
42 | reg: sysCfgReg(),
43 | fnc: 'setting'
44 | }
45 | ]
46 | })
47 | }
48 |
49 | async setting (e) {
50 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
51 | const regRet = sysCfgReg().exec(e.msg)
52 |
53 | if (!regRet) return false
54 |
55 | const groupTitle = regRet[1]
56 | const keyTitle = regRet[2]
57 | const value = regRet[3].trim()
58 |
59 | const groupEntry = Object.entries(admin.AdminConfig).find(
60 | ([ , group ]) => group.title === groupTitle
61 | )
62 |
63 | const cfgEntry = groupEntry
64 | ? Object.entries(groupEntry[1].cfg).find(([ _, item ]) => item.title === keyTitle)
65 | : null
66 |
67 | if (!groupEntry || !cfgEntry) {
68 | await renderConfig(e)
69 | return true
70 | }
71 |
72 | const [ groupName ] = groupEntry
73 | const [ cfgKey, cfgItem ] = cfgEntry
74 | switch (cfgItem.type) {
75 | case 'boolean': {
76 | const isOn = value === '开启'
77 | Config.modify(groupName, cfgKey, isOn)
78 | break
79 | }
80 | case 'number': {
81 | const number = checkNumberValue(Number(value), cfgItem.limit ?? '0-0')
82 | Config.modify(groupName, cfgKey, number)
83 | break
84 | }
85 | case 'string': {
86 | Config.modify(groupName, cfgKey, value)
87 | break
88 | }
89 | case 'array': {
90 | let list = Config[groupName]?.[cfgKey] || []
91 | if (/^添加/.test(value)) {
92 | const itemToAdd = value.replace(/^添加/, '').trim()
93 | if (!list.includes(itemToAdd)) {
94 | list.push(itemToAdd)
95 | }
96 | } else if (/^删除/.test(value)) {
97 | const itemToRemove = value.replace(/^删除/, '').trim()
98 | list = list.filter(item => item !== itemToRemove)
99 | } else {
100 | list = value.split(',').map(v => v.trim())
101 | }
102 | Config.modify(groupName, cfgKey, list)
103 | break
104 | }
105 | }
106 |
107 | await renderConfig(e)
108 | return true
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/apps/help.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 |
3 | import lodash from 'lodash'
4 | import MarkdownIt from 'markdown-it'
5 |
6 | import { Render, Version } from '#components'
7 | import { help } from '#models'
8 |
9 | export class Help extends plugin {
10 | constructor () {
11 | super({
12 | name: '柠糖表情:帮助',
13 | event: 'message',
14 | priority: -Infinity,
15 | rule: [
16 | {
17 | reg: /^#?(?:(柠糖)?表情|meme(?:-plugin)?)(?:命令|帮助|菜单|help|说明|功能|指令|使用说明)$/i,
18 | fnc: 'help'
19 | },
20 | {
21 | reg: /^#?(?:(柠糖)?表情|meme(?:-plugin)?)(?:版本|版本信息|version|versioninfo)$/i,
22 | fnc: 'versionInfo'
23 | }
24 | ]
25 | })
26 | }
27 |
28 | async help (e) {
29 | let helpGroup = []
30 | lodash.forEach(help.helpList.List, (group) => {
31 | if (group.auth && group.auth === 'master' && (!(e.isMaster || e.user_id.toString() === '3369906077'))) {
32 | return true
33 | }
34 | lodash.forEach(group.list, (help) => {
35 | let icon = help.icon * 1
36 | if (!icon) {
37 | help.css = 'display:none'
38 | } else {
39 | let x = (icon - 1) % 10
40 | let y = (icon - x - 1) / 10
41 | help.css = `background-position:-${x * 50}px -${y * 50}px`
42 | }
43 | })
44 |
45 | helpGroup.push(group)
46 | })
47 | const themeData = await help.Theme.getThemeData(help.helpCfg.Cfg)
48 | const img = await Render.render(
49 | 'help/index',
50 | {
51 | helpCfg: help.helpCfg.Cfg,
52 | helpGroup,
53 | ...themeData
54 | }
55 | )
56 | await e.reply(img)
57 | return true
58 | }
59 |
60 | async versionInfo (e) {
61 | const md = new MarkdownIt({ html: true })
62 | const makdown = md.render(await fs.readFile(`${Version.Plugin_Path}/CHANGELOG.md`, 'utf-8'))
63 | const img = await Render.render(
64 | 'help/version-info',
65 | {
66 | Markdown: makdown
67 | }
68 | )
69 | await e.reply(img)
70 | return true
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/apps/hijack.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 |
3 | import { Config } from '#components'
4 |
5 | (async () => {
6 | let dailyNoteByWidget
7 | try {
8 | dailyNoteByWidget = (await import('../../earth-k-plugin/apps/emoticon.js')).dailyNoteByWidget
9 | } catch (error) {
10 | logger.warn(chalk.cyan('[柠糖表情] 土块插件未加载,跳过劫持'))
11 | return
12 | }
13 |
14 | if (Config.other.hijackRes) {
15 | try {
16 | dailyNoteByWidget.prototype.accept = async function () {
17 | logger.debug(chalk.yellow('[柠糖表情:表情包] 已劫持土块插件表情包'))
18 | }
19 | } catch (error) {
20 | logger.error('[柠糖表情:表情包] 劫持土块插件表情包失败')
21 | }
22 | }
23 | })()
24 |
--------------------------------------------------------------------------------
/apps/imageTool/imageInfo.js:
--------------------------------------------------------------------------------
1 | import { Version } from '#components'
2 | import { imageTool, utils } from '#models'
3 | export class Imageinfo extends plugin {
4 | constructor () {
5 | super({
6 | name: '柠糖表情:查看图片信息',
7 | event: 'message',
8 | priority: -Infinity,
9 | rule: [
10 | {
11 | reg: /^#?(?:(?:柠糖)(?:表情|meme))?(?:查看)?(?:图片信息|imageinfo)$/i,
12 | fnc: 'image_info'
13 | }
14 | ]
15 | })
16 | }
17 |
18 | async image_info (e) {
19 | try {
20 | const image = await utils.get_image(e, 'url')
21 | if (!image) {
22 | return await e.reply('请发送图片', true)
23 | }
24 | const image_id = await utils.upload_image(image[0].image)
25 | const image_info = await imageTool.get_image_info(image_id)
26 | const replyMessage = [
27 | segment.image(`base64://${await imageTool.get_image(image_id, 'base64')}`),
28 | '图片信息:\n',
29 | `分辨率: ${image_info.width}x${image_info.height}\n`,
30 | `是否为动图: ${image_info.is_multi_frame}\n`
31 | ]
32 | if (image_info.is_multi_frame) {
33 | replyMessage.push(`帧数: ${image_info.frame_count}\n`)
34 | replyMessage.push(`动图平均帧率: ${image_info.average_duration}\n`)
35 | }
36 | await e.reply(replyMessage, true)
37 | } catch (error) {
38 | logger.error(error)
39 | await e.reply(`[${Version.Plugin_AliasName}]获取图片信息失败: ${error.message}`, true)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/info.js:
--------------------------------------------------------------------------------
1 | import { Config } from '#components'
2 | import { utils } from '#models'
3 |
4 | export class info extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:表情包详情',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?表情)详情\s*(.+)$/i,
13 | fnc: 'info'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async info (e) {
20 | if (!Config.meme.enable) return false
21 | try {
22 | const [ , searchKey ] = e.msg.match(this.rule[0].reg)
23 | const memeInfo = await utils.get_meme_info_by_keyword(searchKey) ?? await utils.get_meme_info(searchKey)
24 |
25 | if (!memeInfo) {
26 | throw new Error(`没有找到该表情${searchKey}信息`)
27 | }
28 |
29 | const {
30 | key: memeKey,
31 | keyWords: alias,
32 | min_images,
33 | max_images,
34 | min_texts,
35 | max_texts,
36 | default_texts: defText,
37 | options,
38 | tags
39 | } = memeInfo
40 | const presetList = await utils.get_preset_all_about_keywords_by_key(memeKey)
41 | const aliasArray = typeof alias === 'string' ? JSON.parse(alias) : (Array.isArray(alias) ? alias : [])
42 | const defTextArray = typeof defText === 'string' ? JSON.parse(defText) : (Array.isArray(defText) ? defText : [])
43 | const tagsArray = typeof tags === 'string' ? (JSON.parse(tags)).map((tag) => `[${tag}]`) : (Array.isArray(tags) ? tags : [])
44 | const optionsArray = typeof options === 'string' ? JSON.parse(options) : (Array.isArray(options) ? options : [])
45 | const optionArray = optionsArray.length > 0 ? optionsArray.map((opt) => `[${opt.name}: ${opt.description}]`).join('') : null
46 | const optionCmdArray = Array.isArray(presetList) ? presetList.map(cmd => `[${cmd}]`).join(' ') : null
47 |
48 | const replyMessage = [
49 | `名称: ${memeKey}\n`,
50 | `别名: ${aliasArray.map((alias) => `[${alias}]`).join(' ')}\n`,
51 | `图片数量: ${min_images === max_images ? min_images : `${min_images} ~ ${max_images ?? '[未知]'}`}\n`,
52 | `文本数量: ${min_texts === max_texts ? min_texts : `${min_texts} ~ ${max_texts ?? '[未知]'}`}\n`,
53 | `默认文本: ${defTextArray.length > 0 ? defTextArray.join('') : '[无]'}\n`,
54 | `标签: ${tagsArray.length > 0 ? tagsArray.join('') : '[无]'}`
55 | ]
56 | if (optionCmdArray) {
57 | replyMessage.push(`\n可选预设:\n${optionCmdArray}`)
58 | }
59 | if (optionArray) {
60 | replyMessage.push(`\n可选选项:\n${optionArray}`)
61 | }
62 |
63 | try {
64 | const previewImage = await utils.get_meme_preview(memeKey)
65 | if (previewImage) {
66 | replyMessage.push('\n预览图片:\n')
67 | replyMessage.push(segment.image(`base64://${previewImage}`))
68 | }
69 | } catch (error) {
70 | replyMessage.push('\n预览图片:\n')
71 | replyMessage.push('预览图获取失败')
72 | }
73 |
74 | await e.reply(replyMessage, { at: true })
75 | } catch (error) {
76 | logger.error(error)
77 | await e.reply(error.message)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/apps/list.js:
--------------------------------------------------------------------------------
1 | import { Config, Render, Version } from '#components'
2 | import { utils } from '#models'
3 |
4 | export class list extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:列表',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?(?:表情|(?:meme(?:s)?)))列表$/i,
13 | fnc: 'list'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async list (e) {
20 | if (!Config.meme.enable) return false
21 | try {
22 | const keys = await utils.get_meme_all_keys()
23 | if (!keys || keys.length === 0) {
24 | await e.reply(`[${Version.Plugin_AliasName}]没有找到表情列表, 请使用[#柠糖表情更新资源], 稍后再试`, true)
25 | return true
26 | }
27 | const tasks = keys.map(async (key) => {
28 | const keywords = await utils.get_meme_keyword(key) ?? []
29 | const params = await utils.get_meme_info(key)
30 |
31 | const min_texts = params?.min_texts ?? 0
32 | const min_images = params?.min_images ?? 0
33 | const options = params?.options ?? null
34 | const types = []
35 | if (min_texts >= 1) types.push('text')
36 | if (min_images >= 1) types.push('image')
37 | if (options !== null) types.push('option')
38 |
39 | if (keywords.length > 0) {
40 | return {
41 | name: keywords.join('/'),
42 | types
43 | }
44 | }
45 |
46 | return []
47 | })
48 | const memeList = (await Promise.all(tasks)).flat()
49 | const total = keys.length
50 |
51 | const img = await Render.render(
52 | 'list/index',
53 | {
54 | memeList,
55 | total
56 | }
57 | )
58 | await e.reply(img)
59 | return true
60 | } catch (error) {
61 | logger.error(error)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/apps/meme.js:
--------------------------------------------------------------------------------
1 | import { Config, Version } from '#components'
2 | import { make, utils } from '#models'
3 |
4 | let memeRegExp, presetRegExp
5 |
6 | /**
7 | * 生成正则表达式
8 | * @param {Function} getKeywords 获取关键词的函数
9 | * @returns {RegExp | null}
10 | */
11 | const createRegex = async (getKeywords) => {
12 | const keywords = (await getKeywords()) ?? []
13 | if (keywords.length === 0) return null
14 | const prefix = Config.meme.forceSharp ? '^#' : '^#?'
15 | const escapedKeywords = keywords.map((keyword) =>
16 | keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
17 | )
18 | const keywordsRegex = `(${escapedKeywords.join('|')})`
19 | return new RegExp(`${prefix}${keywordsRegex}(.*)`, 'i')
20 | }
21 |
22 | memeRegExp = await createRegex(async () => await utils.get_meme_all_keywords())
23 | presetRegExp = await createRegex(async () => await utils.get_preset_all_keywords())
24 |
25 | export class meme extends plugin {
26 | constructor () {
27 | super({
28 | name: '柠糖表情:表情包生成',
29 | event: 'message',
30 | priority: -Infinity,
31 | rule: []
32 | })
33 |
34 | if (memeRegExp) {
35 | this.rule.push({
36 | reg: memeRegExp,
37 | fnc: 'meme'
38 | })
39 | }
40 | if (presetRegExp) {
41 | this.rule.push({
42 | reg: presetRegExp,
43 | fnc: 'preset'
44 | })
45 | }
46 | }
47 |
48 | /**
49 | * 更新正则
50 | */
51 | async updateRegExp () {
52 | memeRegExp = await createRegex(async () => await utils.get_meme_all_keywords() ?? [])
53 | presetRegExp = await createRegex(async () => await utils.get_preset_all_keywords() ?? [])
54 | if (!memeRegExp && !presetRegExp) {
55 | logger.info(`[${Version.Plugin_AliasName}] 没有找到表情关键词, 请使用[#柠糖表情更新资源], 稍后再试`)
56 | return false
57 | }
58 |
59 | this.rule = [
60 | {
61 | reg: memeRegExp,
62 | fnc: 'meme'
63 | },
64 | {
65 | reg: presetRegExp,
66 | fnc: 'preset'
67 | }
68 | ]
69 |
70 | return true
71 | }
72 | async meme (e) {
73 | if (!Config.meme.enable) return false
74 | try {
75 | const [ , keyword, userText ] = e.msg.match(this.rule[0].reg)
76 | const key = await utils.get_meme_key_by_keyword(keyword)
77 | if (!key) return false
78 | const memeInfo = await utils.get_meme_info(key)
79 | const min_texts = memeInfo?.min_texts ?? 0
80 | const max_texts = memeInfo?.max_texts ?? 0
81 | const min_images = memeInfo?.min_images ?? 0
82 | const max_images = memeInfo?.max_images ?? 0
83 | const options = memeInfo?.options ?? null
84 | /* 检查用户权限 */
85 | if (!await this.checkUserAccess(e)) return false
86 |
87 | /* 检查禁用表情列表 */
88 | if (await this.checkBlacklisted(keyword)) return false
89 |
90 | /* 防误触发处理 */
91 | if (!await this.checkUserText(min_texts, max_texts, userText)) return false
92 |
93 | const res = await make.make_meme(
94 | e,
95 | key,
96 | min_texts,
97 | max_texts,
98 | min_images,
99 | max_images,
100 | options,
101 | userText,
102 | false
103 | )
104 | await e.reply([ segment.image(res) ], Config.meme.reply)
105 | } catch (error) {
106 | logger.error(error)
107 | if (Config.meme.errorReply) {
108 | const prefix = Config.meme?.prefix || Version.Plugin_AliasName
109 | return await e.reply(`[${prefix}]: 生成表情失败, 错误信息: ${error.message}`)
110 | }
111 | return true
112 | }
113 | }
114 |
115 | async preset (e) {
116 | if (!Config.meme.enable) return false
117 | try {
118 | const [ , keyword, userText ] = e.msg.match(this.rule[1].reg)
119 | const key = await utils.get_preset_key(keyword)
120 | if (!key) return false
121 | const memeInfo = await utils.get_meme_info(key)
122 | const min_texts = memeInfo?.min_texts ?? 0
123 | const max_texts = memeInfo?.max_texts ?? 0
124 | const min_images = memeInfo?.min_images ?? 0
125 | const max_images = memeInfo?.max_images ?? 0
126 | const options = memeInfo?.options ?? null
127 | /* 检查用户权限 */
128 | if (!await this.checkUserAccess(e)) return false
129 |
130 | /* 检查禁用表情列表 */
131 | if (await this.checkBlacklisted(keyword)) return false
132 |
133 | /* 防误触发处理 */
134 | if (!await this.checkUserText(min_texts, max_texts, userText)) return false
135 |
136 | const res = await make.make_meme(
137 | e,
138 | key,
139 | min_texts,
140 | max_texts,
141 | min_images,
142 | max_images,
143 | options,
144 | userText,
145 | true,
146 | keyword
147 | )
148 | await e.reply([ segment.image(res) ], Config.meme.reply)
149 | } catch (error) {
150 | logger.error(error)
151 | if (Config.meme.errorReply) {
152 | const prefix = Config.meme?.prefix || Version.Plugin_AliasName
153 | return await e.reply(`[${prefix}]: 生成表情失败, 错误信息: ${error.message}`)
154 | }
155 | return true
156 | }
157 | }
158 |
159 | /**
160 | * 权限检查
161 | * @param {Message} e 消息
162 | * @returns 是否有权限
163 | */
164 | async checkUserAccess (e) {
165 | if (Config.access.enable) {
166 | const userId = e.user_id
167 | if (Config.access.mode === 0 && !Config.access.userWhiteList.includes(userId)) {
168 | logger.info(`[${Version.Plugin_AliasName}] 用户 ${userId} 不在白名单中,跳过生成`)
169 | return false
170 | } else if (Config.access.mode === 1 && Config.access.userBlackList.includes(userId)) {
171 | logger.info(`[${Version.Plugin_AliasName}] 用户 ${userId} 在黑名单中,跳过生成`)
172 | return false
173 | }
174 | }
175 | return true
176 | }
177 |
178 | /**
179 | * 禁用表情检查
180 | * @param {string} keywordOrKey - 表情关键词或key
181 | * @returns 是否在禁用列表中
182 | */
183 | async checkBlacklisted (keywordOrKey) {
184 | if (!Config.access.blackListEnable || Config.access.blackList.length < 0) {
185 | return false
186 | }
187 | const key = await utils.get_meme_key_by_keyword(keywordOrKey)
188 | if (!key) {
189 | return false
190 | }
191 |
192 | const blacklistKeys = await Promise.all(
193 | Config.access.blackList.map(async item => {
194 | const convertedKey = await utils.get_meme_key_by_keyword(item)
195 | return convertedKey ?? item
196 | })
197 | )
198 |
199 | if (blacklistKeys.includes(key)) {
200 | logger.info(`[${Version.Plugin_AliasName}] 该表情 "${key}" 在禁用列表中,跳过生成`)
201 | return true
202 | }
203 |
204 | return false
205 | }
206 |
207 | /**
208 | * 防误触发处理
209 | */
210 | async checkUserText (min_texts, max_texts, userText) {
211 | if (min_texts === 0 && max_texts === 0 && userText) {
212 | const trimmedText = userText.trim()
213 | if (
214 | !/^(@\s*\d+\s*)+$/.test(trimmedText) &&
215 | !/^(#\S+\s+[^#]+(?:\s+#\S+\s+[^#]+)*)$/.test(trimmedText)
216 | ) {
217 | return false
218 | }
219 | }
220 | return true
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/apps/random.js:
--------------------------------------------------------------------------------
1 | import { Config, Version } from '#components'
2 | import { make, utils } from '#models'
3 |
4 | export class random extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:随机表情包',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?表情)?随机(?:表情|meme)(包)?$/i,
13 | fnc: 'random'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async random (e) {
20 | if (!Config.meme.enable) return false
21 | try {
22 | const memeKeys = await utils.get_meme_all_keys() ?? null
23 | if (!memeKeys || memeKeys.length === 0) {
24 | throw new Error('未找到可用的表情包')
25 | }
26 |
27 | for (let i = memeKeys.length - 1; i > 0; i--) {
28 | const j = Math.floor(Math.random() * (i + 1))
29 | ;[ memeKeys[i], memeKeys[j] ] = [ memeKeys[j], memeKeys[i] ]
30 | }
31 |
32 | for (const memeKey of memeKeys) {
33 | const memeInfo = await utils.get_meme_info(memeKey) ?? null
34 | if (!memeInfo) continue
35 | const min_texts = memeInfo.min_texts ?? 0
36 | const max_texts = memeInfo.max_texts ?? 0
37 | const min_images = memeInfo.min_images ?? 0
38 | const max_images = memeInfo.max_images ?? 0
39 | const options = memeInfo.options ?? null
40 | if (
41 | (min_texts === 1 && max_texts === 1) ||
42 | (min_images === 1 && max_images === 1) ||
43 | (min_texts === 1 && min_images === 1 && max_texts === 1 && max_images === 1)
44 | ) {
45 | try {
46 | let keyWords = await utils.get_meme_keyword(memeKey) ?? null
47 | keyWords = keyWords ? keyWords.map(word => `[${word}]`) : [ '[无]' ]
48 |
49 | const result = await make.make_meme(
50 | e,
51 | memeKey,
52 | min_texts,
53 | max_texts,
54 | min_images,
55 | max_images,
56 | options,
57 | '',
58 | false
59 | )
60 |
61 | let replyMessage = [
62 | ('本次随机表情信息如下:\n'),
63 | (`表情的名称: ${memeKey}\n`),
64 | (`表情的别名: ${keyWords}\n`)
65 | ]
66 | if (result) {
67 | replyMessage.push(segment.image(result))
68 | } else {
69 | throw new Error('表情生成失败,请重试!')
70 | }
71 | await e.reply(replyMessage)
72 | return true
73 | } catch (error) {
74 | throw new Error(error.message)
75 | }
76 | }
77 | }
78 |
79 | throw new Error('未找到有效的表情包')
80 | } catch (error) {
81 | logger.error(error.message)
82 | if (Config.meme?.errorReply) {
83 | const prefix = Config.meme?.prefix || Version.Plugin_AliasName
84 | await e.reply(`[${prefix}] 生成随机表情失败, 错误信息: ${error.message}`)
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/apps/search.js:
--------------------------------------------------------------------------------
1 | import { Config } from '#components'
2 | import { utils } from '#models'
3 |
4 | export class search extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:搜索',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?表情)搜索\s*(.+?)$/i,
13 | fnc: 'search'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async search (e) {
20 | if (!Config.meme.enable) return false
21 | try {
22 | const [ , searchKey ] = e.msg.match(this.rule[0].reg)
23 |
24 | /** 关键词搜索 */
25 | const keywords = await utils.get_meme_keywords_by_about(searchKey)
26 | /** 键值搜索 */
27 | const keys = await Promise.all(
28 | (await utils.get_meme_keys_by_about(searchKey) ?? []).map(key => utils.get_meme_keyword(key))
29 | )
30 |
31 | /** tag搜索 */
32 | const [ keyTags ] = await Promise.all([
33 | utils.get_meme_keys_by_about_tag(searchKey),
34 | utils.get_meme_keywords_by_about_tag(searchKey)
35 | ])
36 | const keyTagsKeywords = await Promise.all(
37 | (keyTags ?? []).map(key => utils.get_meme_keyword(key))
38 | )
39 |
40 | const tags = [ ...(keyTagsKeywords.filter(Boolean) ?? []) ]
41 |
42 | /** 预设表情搜索 */
43 | const preset = await utils.get_preset_all_about_keywords(searchKey) ?? await utils.get_preset_all_about_keywords_by_key(searchKey) ?? []
44 | const presetKeys = await Promise.all(
45 | preset.map(async (preset) => {
46 | const presetKey = await utils.get_preset_key(preset)
47 | return presetKey
48 | })
49 | )
50 | const presetKeywords = await Promise.all(
51 | presetKeys.map(async (presetKeys) => {
52 | const keywords = await utils.get_meme_keyword(String(presetKeys))
53 | return keywords ?? []
54 | })
55 | )
56 |
57 | /** 关键词搜索 */
58 | if (!keywords?.length && !keys?.length && !tags?.length && !presetKeywords?.length) {
59 | await e.reply(`没有找到${searchKey}相关的表情`)
60 | return true
61 | }
62 |
63 | const allResults = [ ...new Set([ ...(keywords ?? []), ...(keys ?? []), ...presetKeywords, ...(tags ?? []) ].flat()) ]
64 |
65 | const replyMessage = allResults
66 | .map((kw, index) => `${index + 1}. ${kw}`)
67 | .join('\n')
68 |
69 | await e.reply([ ('你可能在找以下表情:\n' + replyMessage) ], true)
70 | return true
71 | } catch (error) {
72 | await e.reply(`搜索出错了:${error.message}`)
73 | return false
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/apps/server/download.js:
--------------------------------------------------------------------------------
1 | import { server } from '#models'
2 | export class download extends plugin {
3 | constructor () {
4 | super({
5 | name: '柠糖表情:下载表情服务端资源',
6 | event: 'message',
7 | priority: -Infinity,
8 | rule: [
9 | {
10 | reg: /^#?(?:(?:柠糖)(?:表情|meme))(?:下载|更新)表情服务端资源$/i,
11 | fnc: 'download'
12 | }
13 | ]
14 | })
15 | }
16 |
17 | async download (e) {
18 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
19 | try {
20 | await e.reply('正在下载/更新表情服务端资源,请稍等...')
21 | const res = await server.download_server_resource()
22 | if (!res) {
23 | await e.reply('表情服务端资源下载失败')
24 | return
25 | }
26 | await e.reply('表情服务端资源下载成功')
27 | } catch (error) {
28 | logger.error(error)
29 | await e.reply('下载表情服务端资源失败, 请前往控制台查看日志')
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/apps/server/restart.js:
--------------------------------------------------------------------------------
1 | import { server } from '#models'
2 |
3 | export class restart extends plugin {
4 | constructor () {
5 | super({
6 | name: '柠糖表情:重启表情服务端',
7 | event: 'message',
8 | priority: -Infinity,
9 | rule: [
10 | {
11 | reg: /^#?(?:(?:柠糖)(?:表情|meme))(?:重启|重新启动)(?:表情)?(?:服务端)$/i,
12 | fnc: 'restart'
13 | }
14 | ]
15 | })
16 | }
17 |
18 | async restart (e) {
19 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
20 | try {
21 | await server.restart()
22 | await e.reply('表情服务端已重新启动成功')
23 | } catch (error) {
24 | logger.error(error)
25 | await e.reply('启动失败')
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/server/start.js:
--------------------------------------------------------------------------------
1 | import { server, utils } from '#models'
2 |
3 | export class start extends plugin {
4 | constructor () {
5 | super({
6 | name: '柠糖表情:启动表情服务端',
7 | event: 'message',
8 | priority: -Infinity,
9 | rule: [
10 | {
11 | reg: /^#?(?:(?:柠糖)?(?:表情|meme))(?:开启|启动)(?:表情)?(?:服务端)?$/i,
12 | fnc: 'start'
13 | }
14 | ]
15 | })
16 | }
17 |
18 | async start (e) {
19 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
20 | try {
21 | await server.start()
22 | await e.reply('表情服务端已启动成功\n地址:' + await utils.get_base_url())
23 | } catch (error) {
24 | logger.error(error)
25 | await e.reply('启动失败')
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/server/status.js:
--------------------------------------------------------------------------------
1 | import { Render, Version } from '#components'
2 | import { server } from '#models'
3 |
4 | export class status extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:服务端状态',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?(?:表情|meme))(?:服务端)(?:状态)?$/i,
13 | fnc: 'status'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async status (e) {
20 | const img = await Render.render('server/status', {
21 | version: Version.Plugin_Version,
22 | serverVersion: await server.get_meme_server_version() ?? '未知',
23 | status: await server.get_meme_server_version() ? '运行中' : '未运行',
24 | runtime: await server.get_meme_server_runtime(),
25 | memory: await server.get_meme_server_memory() ?? `${await server.get_meme_server_memory()} MB` ?? '未知',
26 | total: await server.get_meme_server_meme_total() ?? `${await server.get_meme_server_meme_total()} MB` ?? '未知'
27 | })
28 | await e.reply(img)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/server/stop.js:
--------------------------------------------------------------------------------
1 | import { server } from '#models'
2 |
3 | export class stop extends plugin {
4 | constructor () {
5 | super({
6 | name: '柠糖表情:停止表情服务端',
7 | event: 'message',
8 | priority: -Infinity,
9 | rule: [
10 | {
11 | reg: /^#?(?:(?:柠糖)(?:表情|meme))(?:停止|关闭)(?:表情)?(?:服务端)?$/i,
12 | fnc: 'start'
13 | }
14 | ]
15 | })
16 | }
17 |
18 | async start (e) {
19 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
20 | try {
21 | await server.stop()
22 | await e.reply('表情服务端已停止成功')
23 | } catch (error) {
24 | logger.error(error)
25 | await e.reply('表情服务端停止失败')
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/stat.js:
--------------------------------------------------------------------------------
1 | import { Config, Render } from '#components'
2 | import { db, utils } from '#models'
3 |
4 | export class stat extends plugin {
5 | constructor () {
6 | super({
7 | name: '柠糖表情:表情包详情',
8 | event: 'message',
9 | priority: -Infinity,
10 | rule: [
11 | {
12 | reg: /^#?(?:(?:柠糖)?表情)(?:调用)?统计$/i,
13 | fnc: 'stat'
14 | }
15 | ]
16 | })
17 | }
18 |
19 | async stat (e) {
20 | if (!Config.stat.enable) return await e.reply('统计功能未开启')
21 | let statsData
22 | if (e.isGroup) {
23 | statsData = await db.stat.getAllByGroupId(e.group_id)
24 | } else {
25 | statsData = await db.stat.getAll()
26 | }
27 | if (!statsData || statsData.length === 0) {
28 | return await e.reply('当前没有统计数据')
29 | }
30 | let total = 0
31 | const formattedStats = []
32 | const memeKeyMap = new Map()
33 |
34 | statsData.forEach(data => {
35 | const { memeKey, count } = data
36 | memeKeyMap.set(memeKey, (memeKeyMap.get(memeKey) ?? 0) + count)
37 | })
38 |
39 | await Promise.all([ ...memeKeyMap.entries() ].map(async ([ memeKey, count ]) => {
40 | total += count
41 | const allKeywords = [
42 | ...new Set([
43 | ...(await utils.get_meme_keyword(memeKey) ?? []),
44 | ...(await utils.get_preset_keyword(memeKey) ?? [])
45 | ])
46 | ]
47 | if (allKeywords?.length) {
48 | formattedStats.push({ keywords: allKeywords.join(', '), count })
49 | }
50 | }))
51 |
52 | const img = await Render.render('stat/index', {
53 | total,
54 | memeList: formattedStats
55 | })
56 |
57 | await e.reply(img)
58 | return true
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/apps/update.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 |
3 | import { Config, Version } from '#components'
4 | import { utils } from '#models'
5 |
6 | import pluginsLoader from '../../../lib/plugins/loader.js'
7 | import { update as Update } from '../../other/update.js'
8 | import { meme } from './meme.js'
9 |
10 | export class update extends plugin {
11 | constructor () {
12 | super({
13 | name: '柠糖表情:更新',
14 | event: 'message',
15 | priority: -Infinity,
16 | rule: [
17 | {
18 | reg: /^#?(柠糖表情|meme-plugin)(插件)?(强制)?更新$/i,
19 | fnc: 'update'
20 | },
21 | {
22 | reg: /^#?(柠糖表情|meme-plugin)更新日志$/i,
23 | fnc: 'updateLog'
24 | },
25 | {
26 | reg: /^#?(?:(?:柠糖)?表情)更新(?:表情(?:包)?)?(?:资源|数据)?$/i,
27 | fnc: 'updateRes'
28 | }
29 | ]
30 | })
31 |
32 | this.task = []
33 |
34 |
35 | if (Config.other.autoUpdateRes) {
36 | this.task.push({
37 | name: '柠糖表情:表情包数据每日更新',
38 | cron: Config.other.autoUpdateResCron,
39 | fnc: async () => {
40 | await this.updateRes(null, true)
41 | }
42 | })
43 | }
44 |
45 | }
46 |
47 | async update (e) {
48 | if (!(e.isMaster || e.user_id.toString() === '3369906077')) return
49 | const Type = e.msg.includes('强制') ? '#强制更新' : '#更新'
50 | if (e) e.msg = Type + Version.Plugin_Name
51 | const up = new Update(e)
52 | up.e = e
53 | return up.update()
54 | }
55 |
56 | async updateLog (e = this.e) {
57 | if (e) e.msg = '#更新日志' + Version.Plugin_Name
58 | const up = new Update(e)
59 | up.e = e
60 | return up.updateLog()
61 | }
62 |
63 | async updateRes (e, isTask = false) {
64 | if (!isTask && (!(e.isMaster || e.user_id.toString() === '3369906077'))) {
65 | await e.reply('只有主人才能更新表情包数据')
66 | return
67 | }
68 |
69 | if (!isTask && e) {
70 | await e.reply('开始更新表情包数据中, 请稍后...')
71 | }
72 |
73 | try {
74 | await utils.update_meme(true)
75 | await utils.update_preset(true)
76 | const Plugin = new meme()
77 | const pluginName = Plugin.name
78 | const pluginKey = pluginsLoader.priority.find((p) => {
79 | if (p.plugin) {
80 | return p.plugin.name === pluginName
81 | } else if (p.class) {
82 | return p.name === pluginName
83 | }
84 | return false
85 | })
86 | let pluginInfo
87 | if (pluginKey.plugin) {
88 | pluginInfo = pluginKey.plugin
89 | } else {
90 | pluginInfo = new pluginKey.class()
91 | }
92 | await pluginInfo.updateRegExp()
93 |
94 | if (!isTask && e) {
95 | await e.reply('表情包数据更新完成')
96 | }
97 | logger.mark(chalk.rgb(255, 165, 0)('✅ 表情包数据更新完成 🎉'))
98 | return true
99 | } catch (error) {
100 | if (!isTask && e) {
101 | await e.reply(`表情包数据更新失败: ${error.message}`)
102 | }
103 | logger.error(`表情包数据更新出错: ${error.message}`)
104 | return false
105 | }
106 | }
107 |
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/components/Config.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import path from 'node:path'
3 |
4 | import chokidar from 'chokidar'
5 | import _ from 'lodash'
6 |
7 | import cfg from '../../../lib/config/config.js'
8 | import { Version } from './Version.js'
9 | import { YamlReader } from './YamlReader.js'
10 |
11 | class Cfg {
12 | constructor () {
13 | this.config = {}
14 | this.watcher = {}
15 |
16 | this.dirCfgPath = `${Version.Plugin_Path}/config/config/`
17 | this.defCfgPath = `${Version.Plugin_Path}/config/defSet/`
18 |
19 | this.initCfg()
20 | }
21 |
22 | /** 初始化配置 */
23 | initCfg () {
24 | if (!fs.existsSync(this.dirCfgPath)) fs.mkdirSync(this.dirCfgPath, { recursive: true })
25 |
26 | fs.readdirSync(this.defCfgPath)
27 | .filter((file) => file.endsWith('.yaml'))
28 | .forEach((file) => {
29 | const name = path.basename(file, '.yaml')
30 | const userCfgPath = path.join(this.dirCfgPath, file)
31 | if (!fs.existsSync(userCfgPath)) fs.copyFileSync(path.join(this.defCfgPath, file), userCfgPath)
32 | this.watch(userCfgPath, name, 'config')
33 | })
34 | }
35 |
36 | /** 读取默认或用户配置 */
37 | getDefOrConfig (name) {
38 | return { ...this.getDefSet(name), ...this.getConfig(name) }
39 | }
40 |
41 | /** 默认配置 */
42 | getDefSet (name) {
43 | return this.getYaml('defSet', name)
44 | }
45 |
46 | /** 用户配置 */
47 | getConfig (name) {
48 | return this.getYaml('config', name)
49 | }
50 |
51 | /** 获取 YAML 配置 */
52 | getYaml (type, name) {
53 | let filePath = path.join(Version.Plugin_Path, 'config', type, `${name}.yaml`)
54 | let key = `${type}.${name}`
55 |
56 | if (this.config[key]) return this.config[key]
57 |
58 | this.config[key] = new YamlReader(filePath).jsonData
59 | this.watch(filePath, name, type)
60 |
61 | return this.config[key]
62 | }
63 |
64 | /** 监听配置文件 */
65 | watch (file, name, type = 'config') {
66 | let key = `${type}.${name}`
67 | if (this.watcher[key]) return
68 |
69 | const watcher = chokidar.watch(file, { persistent: true })
70 | this.watcher[key] = watcher
71 |
72 | watcher.on('change', _.debounce(async () => {
73 | const oldConfig = _.cloneDeep(this.config[key] || {})
74 |
75 | delete this.config[key]
76 | this.config[key] = new YamlReader(file).jsonData
77 |
78 | logger.mark(`[柠糖表情][修改配置文件][${type}][${name}]`)
79 |
80 | const changes = this.findDifference(oldConfig, this.config[key])
81 | for (const key in changes) {
82 | const value = changes[key]
83 |
84 | let target = { type: null }
85 |
86 | if (_.isObject(value.newValue) && value.oldValue === undefined) target.type = 'add'
87 | else if (value.newValue === undefined && _.isObject(value.oldValue)) target.type = 'del'
88 | else if (value.newValue === true && !value.oldValue) target.type = 'close'
89 | else if (value.newValue === false && value.oldValue) target.type = 'open'
90 | }
91 | }))
92 | }
93 |
94 | /** 获取所有配置 */
95 | getCfg () {
96 | return fs.readdirSync(this.defCfgPath)
97 | .filter((file) => file.endsWith('.yaml'))
98 | .reduce((configData, file) => {
99 | const name = path.basename(file, '.yaml')
100 | configData[name] = this.getDefOrConfig(name)
101 | return configData
102 | }, {})
103 | }
104 |
105 | /** 修改配置 */
106 | modify (name, key, value, type = 'config') {
107 | let filePath = path.join(Version.Plugin_Path, 'config', type, `${name}.yaml`)
108 | new YamlReader(filePath).set(key, value)
109 | delete this.config[`${type}.${name}`]
110 | }
111 |
112 | /** 对比两个对象的不同值 */
113 | findDifference (obj1, obj2) {
114 | return _.reduce(
115 | obj1,
116 | (result, value, key) => {
117 | if (!_.isEqual(value, obj2[key])) result[key] = { oldValue: value, newValue: obj2[key] }
118 | return result
119 | },
120 | _.reduce(
121 | obj2,
122 | (result, value, key) => {
123 | if (!(key in obj1)) result[key] = { oldValue: undefined, newValue: value }
124 | return result
125 | },
126 | {}
127 | )
128 | )
129 | }
130 | }
131 |
132 | export const Config = new Proxy(new Cfg(), {
133 | get (target, prop) {
134 | if (prop === 'masterQQ') return cfg.masterQQ
135 | if (prop in target) return target[prop]
136 | return target.getDefOrConfig(prop)
137 | }
138 | })
139 |
--------------------------------------------------------------------------------
/components/Data.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 |
3 | import { Version } from './Version.js'
4 |
5 | const getRoot = (root = '') => {
6 | if (root === 'root' || root === 'yunzai') {
7 | root = `${Version.Bot_Path}/`
8 | } else if (!root) {
9 | root = `${Version.Plugin_Path}/`
10 | }
11 | return root
12 | }
13 |
14 | export const Data = {
15 | /*
16 | * 根据指定的path依次检查与创建目录
17 | */
18 | async createDir (path = '', root = '', includeFile = false) {
19 | root = getRoot(root)
20 | let pathList = path.split('/')
21 | let nowPath = root
22 |
23 | await pathList.reduce(async (previousPromise, name, idx) => {
24 | await previousPromise
25 | name = name.trim()
26 | if (!includeFile && idx <= pathList.length - 1) {
27 | nowPath += name + '/'
28 | if (name) {
29 | try {
30 | await fs.mkdir(nowPath, { recursive: true })
31 | } catch (e) {
32 | }
33 | }
34 | }
35 | }, Promise.resolve())
36 | },
37 |
38 | /*
39 | * 读取JSON
40 | */
41 | async readJSON (file = '', root = '') {
42 | root = getRoot(root)
43 | try {
44 | const filePath = `${root}/${file}`
45 | await fs.access(filePath)
46 | const data = await fs.readFile(filePath, 'utf8')
47 | return JSON.parse(data)
48 | } catch (e) {
49 | console.error(`读取 JSON 文件失败: ${file}`, e)
50 | return {}
51 | }
52 | },
53 |
54 | /*
55 | * 写JSON
56 | */
57 | async writeJSON (file, data, space = '\t', root = '') {
58 | await Data.createDir(file, root, true)
59 | root = getRoot(root)
60 | delete data._res
61 | const jsonData = JSON.stringify(data, null, space)
62 | const filePath = `${root}/${file}`
63 | await fs.writeFile(filePath, jsonData)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/components/Render.js:
--------------------------------------------------------------------------------
1 | import puppeteer from '../../../lib/puppeteer/puppeteer.js'
2 | import { Config } from './Config.js'
3 | import { Version } from './Version.js'
4 |
5 | function scale (pct = 1) {
6 | const renderScale = Config.other.renderScale || 100
7 | const scale = Math.min(2, Math.max(0.5, renderScale / 100))
8 | pct = pct * scale
9 | return `style=transform:scale(${pct})`
10 | }
11 |
12 | export const Render = {
13 | /**
14 | *
15 | * @param {string} path html模板路径
16 | * @param {*} params 模板参数
17 | * @param {*} cfg 渲染参数
18 | * @param {boolean} multiPage 是否分页截图,默认false
19 | * @returns
20 | */
21 | async render (path, params) {
22 | path = path.replace(/.html$/, '')
23 | const savePath = `${path.replace('html/', '')}`
24 | const data = {
25 | _Plugin_AliasName: `${Version.Plugin_AliasName}`,
26 | _res_path: `${Version.Plugin_Path}/resources`.replace(/\\/g, '/'),
27 | _layout_path: `${Version.Plugin_Path}/resources/common/layout/`.replace(/\\/g, '/'),
28 | defaultLayout: `${Version.Plugin_Path}/resources/common/layout/default.html`.replace(/\\/g, '/'),
29 | sys: {
30 | scale: scale(params?.scale || 1)
31 | },
32 | copyright: `${Version.Bot_Name} ${Version.Bot_Version} & ${Version.Plugin_Name} ${Version.Plugin_Version}`,
33 | pageGotoParams: {
34 | waitUntil: 'networkidle0',
35 | timeout: 60000
36 | },
37 | tplFile: `${Version.Plugin_Path}/resources/${path}.html`,
38 | pluResPath: `${Version.Plugin_Path}/resources/`,
39 | saveId: path.split('/').pop(),
40 | imgType: 'jpeg',
41 | multiPage: true,
42 | multiPageHeight: 12000,
43 | ...params
44 | }
45 | return await puppeteer.screenshots(`${Version.Plugin_Name}/${savePath}`, data)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/components/Version.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { basename, dirname, join } from 'path'
3 | import { fileURLToPath } from 'url'
4 |
5 | import cfg from '../../../lib/config/config.js'
6 | import { Data } from './Data.js'
7 |
8 | const __filename = fileURLToPath(import.meta.url)
9 | const __dirname = dirname(__filename)
10 | const Path = process.cwd().replace(/\\/g, '/')
11 | const Plugin_Path = join(__dirname, '..').replace(/\\/g, '/')
12 | const Plugin_Name = basename(Plugin_Path)
13 |
14 | const pkg = await Data.readJSON('package.json', `${Plugin_Path}`)
15 |
16 | let BotName = cfg.package.name
17 |
18 | switch (BotName) {
19 | case 'miao-yunzai':
20 | BotName = 'Miao-Yunzai'
21 | break
22 | case 'yunzai':
23 | BotName = 'Yunzai-Bot'
24 | break
25 | case 'trss-yunzai':
26 | BotName = 'TRSS-Yunzai'
27 | break
28 | default:
29 | BotName = _.capitalize(BotName)
30 | }
31 |
32 |
33 | export const Version = {
34 | get Bot_Name () {
35 | return BotName
36 | },
37 | get Bot_Version () {
38 | return cfg.package.version
39 | },
40 | get Bot_Path () {
41 | return Path
42 | },
43 | get Plugin_Logs () {
44 | return changelogs
45 | },
46 | get Plugin_Path () {
47 | return Plugin_Path
48 | },
49 | get Plugin_Name () {
50 | return Plugin_Name
51 | },
52 | get Plugin_AliasName () {
53 | return '柠糖表情'
54 | },
55 | get Plugin_Version () {
56 | return pkg.version
57 | },
58 | get Plugin_Author () {
59 | return pkg.author
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/components/YamlReader.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 |
3 | import chokidar from 'chokidar'
4 | import _ from 'lodash'
5 | import YAML from 'yaml'
6 |
7 | export class YamlReader {
8 | constructor (yamlPath, isWatch = false) {
9 | this.yamlPath = yamlPath
10 | this.isWatch = isWatch
11 | this.isSave = false
12 | this.initYaml()
13 | }
14 |
15 | /** 初始化 YAML 解析 */
16 | initYaml () {
17 | if (!fs.existsSync(this.yamlPath)) fs.writeFileSync(this.yamlPath, '', 'utf8')
18 | this.document = YAML.parseDocument(fs.readFileSync(this.yamlPath, 'utf8')) || new YAML.Document()
19 |
20 | if (this.isWatch && !this.watcher) {
21 | this.watcher = chokidar.watch(this.yamlPath).on('change', _.debounce(() => {
22 | if (this.isSave) {
23 | this.isSave = false
24 | return
25 | }
26 | this.initYaml()
27 | }))
28 | }
29 | }
30 |
31 | /** 获取 YAML 转换后的 JSON 数据 */
32 | get jsonData () {
33 | return this.document?.toJSON() || {}
34 | }
35 |
36 | /** 检查是否包含 key */
37 | has (keyPath) {
38 | return this.document.hasIn(keyPath.split('.'))
39 | }
40 |
41 | /** 获取 key 的值 */
42 | get (keyPath) {
43 | return _.get(this.jsonData, keyPath)
44 | }
45 |
46 | /** 设置 key 的值 */
47 | set (keyPath, value) {
48 | this.document.setIn(keyPath.split('.'), value)
49 | this.save()
50 | }
51 |
52 | /** 删除 key */
53 | delete (keyPath) {
54 | this.document.deleteIn(keyPath.split('.'))
55 | this.save()
56 | }
57 |
58 | /** 数组添加数据 */
59 | addIn (keyPath, value) {
60 | let arr = this.get(keyPath) || []
61 | if (!Array.isArray(arr)) arr = []
62 | arr.push(value)
63 | this.set(keyPath, arr)
64 | }
65 |
66 | /** 保存 YAML 文件 */
67 | save () {
68 | this.isSave = true
69 | fs.writeFileSync(this.yamlPath, this.document.toString(), 'utf8')
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Config.js'
2 | export * from './Data.js'
3 | export * from './Render.js'
4 | export * from './Version.js'
5 | export * from './YamlReader.js'
--------------------------------------------------------------------------------
/config/defSet/access.yaml:
--------------------------------------------------------------------------------
1 | # 是否开启名单限制
2 | enable: false
3 |
4 | # 是否开启禁用表情列表
5 | blackListEnable: false
6 |
7 | # 名单限制模式(白名单:0,黑名单:1)
8 | mode: 0
9 |
10 | # 用户白名单
11 | userWhiteList: []
12 |
13 | # 用户黑名单
14 | userBlackList: []
15 |
16 | # 禁用表情列表
17 | blackList: []
18 |
--------------------------------------------------------------------------------
/config/defSet/meme.yaml:
--------------------------------------------------------------------------------
1 | # 是否设置为默认表情
2 | enable: true
3 |
4 | # 是否强制使用#触发
5 | forceSharp: false
6 |
7 | # 头像缓存
8 | cache: true
9 |
10 | # 是否开启引用回复
11 | reply: false
12 |
13 | # 是否开启默认使用用户昵称
14 | userName: false
15 |
16 | # 是否开启表情生成错误回复
17 | errorReply: true
18 |
19 | # 自定义错误回复前缀
20 | perfix: ''
--------------------------------------------------------------------------------
/config/defSet/other.yaml:
--------------------------------------------------------------------------------
1 | # 图片渲染精度
2 | renderScale: 100
3 |
4 | # 是否开启每日自动更新表情包数据
5 | autoUpdateRes: true
6 |
7 | # 自动更新资源Cron
8 | autoUpdateResCron: "0 0 2 * * ?"
9 |
10 | # 劫持土块表情包
11 | hijackRes: true
--------------------------------------------------------------------------------
/config/defSet/protect.yaml:
--------------------------------------------------------------------------------
1 | # 是否开启表情保护
2 | enable: false
3 |
4 | # 主人保护
5 | master: false
6 |
7 | # 是否开启用户保护
8 | userEnable: false
9 |
10 | # 其他用户保护列表
11 | user: []
12 |
13 | # 表情保护列表
14 | list: []
--------------------------------------------------------------------------------
/config/defSet/server.yaml:
--------------------------------------------------------------------------------
1 | # 自定义表情服务模式
2 | # 0: 使用远程服务
3 | # 1: 使用本地服务
4 | mode: 0
5 |
6 | # 自定义表情服务地址
7 | url: ''
8 |
9 | # 自定义表情服务端口
10 | # 仅在本地服务模式下生效
11 | port: 2255
12 |
13 | # 是否使用base64上传图片
14 | usebase64: false
15 |
16 | # 启动时检查服务是否运行
17 | # 如果启用则杀死该端口的进程,否则抛出一个错误
18 | kill: true
19 |
20 | # 请求最大重试次数
21 | retry: 5
22 |
23 | # 请求超时时间
24 | timeout: 5
25 |
26 | # 服务端下载代理镜像地址
27 | # 如: https://github.moeyy.xyz
28 | proxy_url: ''
29 |
30 | # 服务端资源下载代理镜像地址
31 | # 如: https://cdn.mengze.vip/gh/MemeCrafters/meme-generator-rs@
32 | download_url: ''
--------------------------------------------------------------------------------
/config/defSet/stat.yaml:
--------------------------------------------------------------------------------
1 | # 表情调用统计
2 | enable: true
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { createRequire } from 'node:module'
2 |
3 | const require = createRequire(import.meta.url)
4 | const simpleImportSort = require('eslint-plugin-simple-import-sort')
5 | const globals = require('globals')
6 | const neostandard = require('neostandard')
7 |
8 | export default (
9 | {
10 | ignores: [ 'eslint.config.js' ]
11 | },
12 | {
13 | languageOptions: {
14 | sourceType: 'module',
15 | globals: { ...globals.node }
16 | },
17 | plugins: {
18 | 'simple-import-sort': simpleImportSort
19 | },
20 | files: [ '**/*.js', 'eslint.config.js' ],
21 | rules: {
22 | ...neostandard.rules,
23 | 'no-prototype-builtins': 0,
24 | 'no-unsafe-optional-chaining': 0,
25 | 'no-useless-escape': 0,
26 | 'prefer-regex-literals': 0,
27 | 'line-comment-position': 'off',
28 | 'quotes': [ 1, 'single' ],
29 | 'camelcase': 'off',
30 | 'eqeqeq': [ 1, 'always' ],
31 | 'prefer-const': 'off',
32 | 'comma-dangle': [ 1, {
33 | arrays: 'never',
34 | objects: 'never',
35 | imports: 'never',
36 | exports: 'never',
37 | functions: 'never'
38 | } ],
39 | 'arrow-body-style': 'off',
40 | 'indent': [ 1, 2, { 'SwitchCase': 1 } ],
41 | 'space-before-function-paren': 1,
42 | 'semi': [ 2, 'never' ],
43 | 'no-trailing-spaces': 1,
44 | 'object-curly-spacing': [ 1, 'always' ],
45 | 'array-bracket-spacing': [ 1, 'always' ],
46 | 'no-multiple-empty-lines': [ 1, { max: 2, maxEOF: 0, maxBOF: 0 } ],
47 | 'simple-import-sort/imports': 'error',
48 | 'simple-import-sort/exports': 'error',
49 | 'comma-spacing': [ 1, { before: false, after: true } ],
50 | 'key-spacing': [ 1, { beforeColon: false, afterColon: true } ],
51 | 'space-infix-ops': 1,
52 | 'space-unary-ops': [ 1, {
53 | words: true,
54 | nonwords: false
55 | } ],
56 | 'space-before-blocks': [ 1, 'always' ],
57 | 'space-in-parens': [ 1, 'never' ],
58 | 'keyword-spacing': [ 1, {
59 | before: true,
60 | after: true,
61 | overrides: {
62 | if: { after: true }
63 | }
64 | } ],
65 | 'brace-style': [ 1, '1tbs' ]
66 | }
67 | }
68 | )
69 |
--------------------------------------------------------------------------------
/guoba.support.js:
--------------------------------------------------------------------------------
1 | import { guoba } from '#models'
2 |
3 | export function supportGuoba () {
4 | return {
5 | pluginInfo: guoba.pluginInfo,
6 | configInfo: guoba.configInfo
7 | }
8 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import path from 'node:path'
3 |
4 | import axios from 'axios'
5 | import chalk from 'chalk'
6 |
7 | import { Config, Version } from '#components'
8 | import { server, utils } from '#models'
9 |
10 | const startTime = Date.now()
11 | let apps
12 |
13 | let responseData = '加载失败'
14 | try {
15 | const response = await axios.get(
16 | `https://api.wuliya.cn/api/count?name=${Version.Plugin_Name}&type=json`,
17 | { timeout: 500 }
18 | )
19 | responseData = response.data.data
20 | } catch (error) {
21 | logger.warn('⚠️ 访问统计数据失败,超时或网络错误')
22 | }
23 | try {
24 | if (Number(Config.server.mode) === 1) {
25 | logger.info(chalk.bold.blue('🚀 启动表情服务端...'))
26 | await server.init_server(Config.server.port)
27 | logger.info(chalk.bold.green('🎉 表情服务端启动成功!'))
28 | }
29 | } catch (error) {
30 | logger.error(chalk.bold.red(`💥 表情服务端启动失败!错误详情:${error.message}`))
31 | }
32 | try {
33 | await utils.init()
34 | logger.info(chalk.bold.cyan(`[${Version.Plugin_AliasName}] 🎉 表情包数据初始化成功!`))
35 | } catch (error) {
36 | logger.error(chalk.bold.red(`[${Version.Plugin_AliasName}] 💥 表情包数据初始化失败!错误详情:${error.message}`))
37 | }
38 |
39 | async function getFiles (dir) {
40 | const files = await fs.readdir(dir, { withFileTypes: true })
41 | const jsFiles = []
42 |
43 | for (const file of files) {
44 | const filePath = path.resolve(dir, file.name)
45 | if (file.isDirectory()) {
46 | jsFiles.push(...await getFiles(filePath))
47 | } else if (file.isFile() && file.name.endsWith('.js')) {
48 | jsFiles.push(filePath)
49 | }
50 | }
51 |
52 | return jsFiles
53 | }
54 |
55 | try {
56 | const files = await getFiles(`${Version.Plugin_Path}/apps`)
57 |
58 | const ret = await Promise.allSettled(
59 | files.map(async (filePath) => {
60 | const startModuleTime = Date.now()
61 |
62 | try {
63 | const module = await import(`file://${filePath}`)
64 | const endModuleTime = Date.now()
65 | const loadTime = endModuleTime - startModuleTime
66 |
67 | logger.debug(
68 | chalk.rgb(0, 255, 255)(`[${Version.Plugin_AliasName}]`) +
69 | chalk.green(` 🚀 ${path.basename(filePath, '.js')}`) +
70 | chalk.rgb(255, 223, 0)(` 加载时间: ${loadTime} ms`)
71 | )
72 |
73 | return module
74 | } catch (error) {
75 | logger.error(
76 | chalk.bgRgb(255, 0, 0).white.bold(' ❌ 载入插件错误:') +
77 | chalk.redBright(` ${path.basename(filePath, '.js')} `) +
78 | ' 🚫'
79 | )
80 | logger.debug(chalk.red(`📄 错误详情: ${error.message}`))
81 |
82 | return null
83 | }
84 | })
85 | )
86 |
87 | apps = {}
88 |
89 | files.forEach((filePath, i) => {
90 | const name = path.basename(filePath, '.js')
91 |
92 | if (ret[i].status !== 'fulfilled' || !ret[i].value) {
93 | return
94 | }
95 |
96 | apps[name] = ret[i].value[Object.keys(ret[i].value)[0]]
97 | })
98 |
99 | const endTime = Date.now()
100 | const loadTime = endTime - startTime
101 |
102 | let loadTimeColor = chalk.green.bold
103 | if (loadTime < 500) {
104 | loadTimeColor = chalk.rgb(144, 238, 144).bold
105 | } else if (loadTime < 1000) {
106 | loadTimeColor = chalk.rgb(255, 215, 0).bold
107 | } else {
108 | loadTimeColor = chalk.red.bold
109 | }
110 |
111 | logger.info(chalk.bold.rgb(0, 255, 0)('========= 🌟🌟🌟 ========='))
112 | logger.info(
113 | chalk.bold.blue('📦 当前运行环境: ') +
114 | chalk.bold.white(`${Version.Bot_Name}`) +
115 | chalk.gray(' | ') +
116 | chalk.bold.green('🏷️ 运行版本: ') +
117 | chalk.bold.white(`${Version.Bot_Version}`) +
118 | chalk.gray(' | ') +
119 | chalk.bold.yellow('📊 运行插件总访问/运行次数: ') +
120 | chalk.bold.cyan(responseData)
121 | )
122 |
123 | logger.info(
124 | chalk.bold.rgb(255, 215, 0)(`✨ ${Version.Plugin_AliasName} `) +
125 | chalk.bold.rgb(255, 165, 0).italic(Version.Plugin_Version) +
126 | chalk.rgb(255, 215, 0).bold(' 载入成功 ^_^')
127 | )
128 | logger.info(loadTimeColor(`⏱️ 载入耗时:${loadTime} ms`))
129 | logger.info(chalk.cyan.bold('💬 雾里的小窝: 272040396'))
130 | logger.info(chalk.green.bold('========================='))
131 |
132 | } catch (error) {
133 | logger.error(chalk.red.bold(`❌ 初始化失败: ${error}`))
134 | }
135 |
136 | export { apps }
137 |
--------------------------------------------------------------------------------
/models/admin/index.js:
--------------------------------------------------------------------------------
1 | export const AdminConfig = {
2 | server: {
3 | title: '服务设置',
4 | cfg: {
5 | mode: {
6 | title: '服务模式',
7 | desc: '运行模式, 0为远程服务, 1为本地服务',
8 | type: 'number'
9 | },
10 | url: {
11 | title: '自定义地址',
12 | desc: '设置自定义表情服务地址',
13 | type: 'string'
14 | },
15 | port: {
16 | title: '自定义端口',
17 | desc: '设置自定义端口, 仅在本地服务模式下生效, 默认为2255',
18 | type: 'number'
19 | },
20 | retry: {
21 | title: '重试次数',
22 | desc: '重试次数, 默认为3次',
23 | type: 'number'
24 | },
25 | timeout: {
26 | title: '超时时间',
27 | desc: '超时时间,单位为秒',
28 | type: 'number'
29 | },
30 | proxy_url: {
31 | title: '代理地址',
32 | desc: '代理地址, 如: https://github.moeyy.xyz',
33 | type: 'string'
34 | },
35 | download_url: {
36 | title: '下载地址',
37 | desc: '下载地址, 如: https://cdn.mengze.vip/gh/MemeCrafters/meme-generator-rs@',
38 | type: 'string'
39 | }
40 | }
41 | },
42 | meme: {
43 | title: '表情设置',
44 | cfg: {
45 | enable: {
46 | title: '默认表情',
47 | desc: '是否设置为默认表情',
48 | type: 'boolean'
49 | },
50 | forceSharp: {
51 | title: '强制触发',
52 | desc: '是否强制使用#触发, 开启后必须使用#触发',
53 | type: 'boolean'
54 | },
55 | cache: {
56 | title: '缓存',
57 | desc: '是否开启头像缓存',
58 | type: 'boolean'
59 | },
60 | reply: {
61 | title: '引用回复',
62 | desc: '是否开启引用回复',
63 | type: 'boolean'
64 | },
65 | userName: {
66 | title: '用户昵称',
67 | desc: '是否开启使用用户昵称,不开则默认使用表情名称',
68 | type: 'boolean'
69 | },
70 | errorReply: {
71 | title: '错误回复',
72 | desc: '是否开启错误信息回复',
73 | type: 'boolean'
74 | },
75 | perfix: {
76 | title: '自定义前缀',
77 | desc: '设置自定义错误回复前缀',
78 | type: 'string'
79 | }
80 | }
81 | },
82 | access: {
83 | title: '名单设置',
84 | cfg: {
85 | enable: {
86 | title: '名单限制',
87 | desc: '是否开启名单限制',
88 | type: 'boolean'
89 | },
90 | blackListEnable: {
91 | title: '禁用表情列表',
92 | desc: '是否开启黑名单',
93 | type: 'boolean'
94 | },
95 | mode: {
96 | title: '名单模式',
97 | desc: '名单模式,仅在开启名单限制启用,0为白名单,1为黑名单',
98 | type: 'number'
99 | },
100 | userWhiteList: {
101 | title: '用户白名单',
102 | desc: '白名单,白名单模式时生效',
103 | type: 'array'
104 | },
105 | userBlackList: {
106 | title: '用户黑名单',
107 | desc: '用户黑名单,黑名单模式时生效',
108 | type: 'array'
109 | },
110 | blackList: {
111 | title: '黑名单表情列表',
112 | desc: '黑名单表情列表',
113 | type: 'array'
114 | }
115 | }
116 | },
117 | protect: {
118 | title: '表情保护设置',
119 | cfg: {
120 | enable: {
121 | title: '表情保护',
122 | desc: '是否开启表情保护',
123 | type: 'boolean'
124 | },
125 | master: {
126 | title: '主人保护',
127 | desc: '是否开启主人保护',
128 | type: 'boolean'
129 | },
130 | user: {
131 | title: '保护用户列表',
132 | desc: '设置要保护的用户,如123456',
133 | type: 'array'
134 | },
135 | list: {
136 | title: '表情保护列表',
137 | desc: '表情保护列表',
138 | type: 'array'
139 | }
140 | }
141 | },
142 | stat: {
143 | title: '统计设置',
144 | cfg: {
145 | enable: {
146 | title: '表情统计',
147 | desc: '是否开启表情统计',
148 | type: 'boolean'
149 | }
150 | }
151 | },
152 | other: {
153 | title: '其他设置',
154 | cfg: {
155 | renderScale: {
156 | title: '渲染精度',
157 | desc: '设置渲染精度',
158 | type: 'number',
159 | limit: '50-200'
160 | },
161 | autoUpdateRes: {
162 | title: '自动更新资源',
163 | desc: '是否自动更新表情包资源,开启后每日凌晨会自动更新',
164 | type: 'boolean'
165 | }
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/models/db/base.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import fs from 'fs/promises'
3 | import { col, DataTypes, fn, literal, Op, Sequelize } from 'sequelize'
4 |
5 | import { Version } from '#components'
6 | import { utils } from '#models'
7 | const dbPath = `${Version.Plugin_Path}/data`
8 | if (!await utils.exists(dbPath)) {
9 | await fs.mkdir(dbPath)
10 | }
11 |
12 | const sequelize = new Sequelize({
13 | dialect: 'sqlite',
14 | storage: `${dbPath}/data.db`,
15 | logging: false
16 | })
17 | /** 测试连接 */
18 | try {
19 | await sequelize.authenticate()
20 | logger.debug(chalk.bold.cyan(`[${Version.Plugin_AliasName}] 数据库连接成功`))
21 | } catch (error) {
22 | logger.error(chalk.bold.cyan(`[${Version.Plugin_AliasName}] 数据库连接失败: ${error}`))
23 | }
24 |
25 | export {
26 | col,
27 | DataTypes,
28 | fn,
29 | literal,
30 | Op,
31 | sequelize
32 | }
--------------------------------------------------------------------------------
/models/db/index.js:
--------------------------------------------------------------------------------
1 | export * as base from './base.js'
2 | export * as meme from './meme.js'
3 | export * as preset from './preset.js'
4 | export * as stat from './stat.js'
--------------------------------------------------------------------------------
/models/db/meme.js:
--------------------------------------------------------------------------------
1 | import { DataTypes, literal, Op, sequelize } from './base.js'
2 |
3 | export const table = sequelize.define('meme', {
4 | /**
5 | * 主键 ID
6 | * @type {number}
7 | */
8 | id: {
9 | type: DataTypes.INTEGER,
10 | allowNull: false,
11 | primaryKey: true,
12 | autoIncrement: true
13 | },
14 | /**
15 | * 唯一标识符
16 | * @type {string}
17 | */
18 | key: {
19 | type: DataTypes.STRING,
20 | allowNull: false,
21 | unique: true
22 | },
23 |
24 | /**
25 | * 关键字列表
26 | * @type {string[]}
27 | */
28 | keyWords: {
29 | type: DataTypes.JSON,
30 | allowNull: false
31 | },
32 |
33 | /**
34 | * 最小文本数量
35 | * @type {number}
36 | */
37 | min_texts: {
38 | type: DataTypes.INTEGER,
39 | allowNull: false
40 | },
41 |
42 | /**
43 | * 最大文本数量
44 | * @type {number}
45 | */
46 | max_texts: {
47 | type: DataTypes.INTEGER,
48 | allowNull: false
49 | },
50 |
51 | /**
52 | * 最小图片数量
53 | * @type {number}
54 | */
55 | min_images: {
56 | type: DataTypes.INTEGER,
57 | allowNull: false
58 | },
59 |
60 | /**
61 | * 最大图片数量
62 | * @type {number}
63 | */
64 | max_images: {
65 | type: DataTypes.INTEGER,
66 | allowNull: false
67 | },
68 |
69 | /**
70 | * 默认文本
71 | * @type {string[]}
72 | */
73 | default_texts: {
74 | type: DataTypes.JSON,
75 | allowNull: true
76 | },
77 |
78 | /**
79 | * 参数类型
80 | * @type {object}
81 | */
82 | options: {
83 | type: DataTypes.JSON,
84 | allowNull: true
85 | },
86 | /**
87 | * 标签
88 | * @type {string[]}
89 | */
90 | tags: {
91 | type: DataTypes.JSON,
92 | allowNull: true
93 | }
94 | }, {
95 | freezeTableName: true,
96 | defaultScope: {
97 | raw: true
98 | }
99 | })
100 |
101 | await table.sync()
102 |
103 | /**
104 | * 添加表情信息
105 | * @param {object} data 表情信息
106 | * - key 唯一标识符
107 | * - keyWords 关键字列表
108 | * - min_texts 最小文本数量
109 | * - max_texts 最大文本数量
110 | * - min_images 最小图片数量
111 | * - max_images 最大图片数量
112 | * - default_texts 默认文本
113 | * - options 参数类型
114 | * - tags 标签
115 | * @param {object} options 选项
116 | * - force 是否强制添加
117 | * @returns {Promise<[Model, boolean | null]>} 表情信息
118 | */
119 | export async function add ({
120 | key,
121 | keyWords,
122 | min_texts,
123 | max_texts,
124 | min_images,
125 | max_images,
126 | default_texts,
127 | options,
128 | tags
129 | }, {
130 | force = false
131 | }) {
132 | if (force) {
133 | await clear()
134 | }
135 | const data = {
136 | key,
137 | keyWords,
138 | min_texts,
139 | max_texts,
140 | min_images,
141 | max_images,
142 | default_texts,
143 | options,
144 | tags
145 | }
146 | return await table.upsert(data)
147 | }
148 |
149 | /**
150 | * 通过表情唯一标识符获取表情信息
151 | * @param {string }key 表情的唯一标识符
152 | * @returns 表情的信息
153 | */
154 | export async function get (key) {
155 | return await table.findOne({
156 | where: {
157 | key
158 | }
159 | })
160 | }
161 |
162 | /**
163 | * 通过表情唯一标识符模糊获取所有相关的表情信息
164 | * @param {string} key 表情的唯一标识符
165 | * @returns 表情的信息列表
166 | */
167 | export async function getKeysByAbout (key) {
168 | return await table.findAll({
169 | where: {
170 | key: {
171 | [Op.like]: `%${key}%`
172 | }
173 | }
174 | })
175 | }
176 |
177 | /**
178 | * 通过表情关键词获取表情信息
179 | * @param {string} keyword 表情的关键字
180 | * @returns 表情信息
181 | */
182 | export async function getByKeyWord (keyword) {
183 | return await table.findOne({
184 | where: literal(`json_extract(keyWords, '$') LIKE '%"${keyword}"%'`)
185 | })
186 | }
187 |
188 | /**
189 | * 通过表情关键词模糊获取所有相关的表情信息
190 | * @param {string} keywod 关键词
191 | * @returns 表情信息
192 | */
193 | export async function getKeyWordsByAbout (keyword) {
194 | return await table.findAll({
195 | where: literal(`json_extract(keyWords, '$') LIKE '%${keyword}%'`)
196 | })
197 | }
198 |
199 | /**
200 | * 通过表情标签获取表情信息
201 | * @param {string} tag 表情的标签
202 | * @returns 表情信息
203 | */
204 | export async function getByTag (tag) {
205 | return await table.findOne({
206 | where: literal(`json_extract(tags, '$') LIKE '%"${tag}"%'`)
207 | })
208 | }
209 |
210 | /**
211 | * 通过表情标签模糊获取所有相关的表情信息
212 | * @param {string} tag 标签关键词
213 | * @returns 表情信息列表
214 | */
215 | export async function getTagsByAbout (tag) {
216 | return await table.findAll({
217 | where: literal(`json_extract(tags, '$') LIKE '%${tag}%'`)
218 | })
219 | }
220 |
221 | /**
222 | * 获取表情信息列表
223 | * @returns 表情信息列表
224 | */
225 | export async function getAll () {
226 | return await table.findAll()
227 | }
228 |
229 | /**
230 | * 清空所有表情信息
231 | */
232 | export async function clear () {
233 | await table.destroy({
234 | truncate: true
235 | })
236 | await sequelize.query('DELETE FROM sqlite_sequence WHERE name = "meme"')
237 | }
--------------------------------------------------------------------------------
/models/db/preset.js:
--------------------------------------------------------------------------------
1 | import { DataTypes, Op, sequelize } from './base.js'
2 | /**
3 | * 定义 `preset` 表(包含 JSON 数据存储、关键字、参数、标签等)。
4 | */
5 | export const table = sequelize.define('preset', {
6 | /**
7 | * 主键Id
8 | * @type {number}
9 | */
10 | id: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | primaryKey: true,
14 | autoIncrement: true
15 | },
16 | /**
17 | * 表情的快捷指令
18 | * @type {string}
19 | */
20 | name: {
21 | type: DataTypes.STRING,
22 | allowNull: false
23 | },
24 | /**
25 | * 表情包的键值
26 | * 对应预设参数的表情的键值
27 | * @type {string}
28 | */
29 | key: {
30 | type: DataTypes.STRING,
31 | allowNull: false
32 | },
33 | /**
34 | * 对应表情选项名称
35 | * @type {string}
36 | */
37 | option_name: {
38 | type: DataTypes.STRING,
39 | allowNull: false
40 | },
41 | /**
42 | * 对应表情选项值
43 | * @type {string}
44 | */
45 | option_value: {
46 | type: DataTypes.STRING,
47 | allowNull: false
48 | }
49 | }, {
50 | freezeTableName: true,
51 | defaultScope: {
52 | raw: true
53 | }
54 | })
55 |
56 | await table.sync()
57 |
58 | /**
59 | * 添加或更新表情预设记录
60 | * @param {string} name - 表情的关键词
61 | * @param {string} key - 表情包键值
62 | * @param {string} option_name - 选项名称
63 | * @param {string | number} option_value - 选项值
64 | * @param {boolean} force - 是否强制创建新记录
65 | * @returns {Promise<[Model, boolean | null]>} 创建或更新后的记录对象
66 | */
67 | export async function add ({
68 | name,
69 | key,
70 | option_name,
71 | option_value
72 | }, {
73 | force = false
74 | }) {
75 | if (force) {
76 | await clear()
77 | }
78 | name = String(name)
79 | const data = {
80 | name,
81 | key,
82 | option_name,
83 | option_value
84 | }
85 | return await table.upsert(data)
86 | }
87 |
88 | /**
89 | * 通过表情唯一标识符获取表情快捷指令信息
90 | * @param {string} key 表情的唯一标识符
91 | * @returns {Promise } 表情的信息
92 | */
93 | export async function get (key) {
94 | return await table.findOne({
95 | where: {
96 | key
97 | }
98 | })
99 | }
100 |
101 | /**
102 | * 通过表情唯一标识符获取所有表情快捷指令信息
103 | * @param {string} key 表情的唯一标识符
104 | * @returns {Promise} 表情的信息
105 | */
106 | export async function getAbout (key) {
107 | return await table.findAll({
108 | where: {
109 | key
110 | }
111 | })
112 | }
113 |
114 | /**
115 | * 通过预设表情关键词获取表情信息
116 | * @param {string} keyword 表情关键词
117 | * @returns {Promise} 表情信息
118 | */
119 | export async function getByKeyWord (keyword) {
120 | return await table.findOne({
121 | where: {
122 | name: keyword
123 | }
124 | })
125 | }
126 |
127 | /**
128 | * 通过预设表情关键词获取所有相关表情信息
129 | * @param {string} keyword 表情关键词
130 | * @returns {Promise} 表情信息
131 | */
132 | export async function getByKeyWordAbout (keyword) {
133 | return await table.findAll({
134 | where: {
135 | name: {
136 | [Op.like]: `%${keyword}%`
137 | }
138 | }
139 | })
140 | }
141 |
142 | /**
143 | * 获取所有表情预设记录
144 | * @returns {Promise} 找到的记录对象数组
145 | */
146 | export async function getAll () {
147 | return await table.findAll()
148 | }
149 |
150 | /**
151 | * 通过表情唯一标识符删除对应的表情信息
152 | * @param {string} key 表情的唯一标识符
153 | * @returns {Promise} 删除成功与否
154 | */
155 | export async function remove (key) {
156 | return Boolean(await table.destroy({ where: { key } }))
157 | }
158 |
159 | /**
160 | * 清空所有表情预设记录
161 | */
162 | export async function clear () {
163 | await table.destroy({
164 | truncate: true
165 | })
166 | await sequelize.query('DELETE FROM sqlite_sequence WHERE name = "preset"')
167 | }
--------------------------------------------------------------------------------
/models/db/stat.js:
--------------------------------------------------------------------------------
1 | import { DataTypes, sequelize } from './base.js'
2 |
3 | const stat = sequelize.define('stat', {
4 | groupId: {
5 | type: DataTypes.STRING,
6 | allowNull: false,
7 | primaryKey: true,
8 | comment: '群组ID'
9 | },
10 | memeKey: {
11 | type: DataTypes.STRING,
12 | allowNull: false,
13 | primaryKey: true,
14 | comment: '表情ID'
15 | },
16 | count: {
17 | type: DataTypes.INTEGER,
18 | allowNull: false,
19 | defaultValue: 0,
20 | comment: '使用次数'
21 | }
22 | }, {
23 | freezeTableName: true,
24 | defaultScope: {
25 | raw: true
26 | }
27 | })
28 |
29 | await stat.sync()
30 |
31 | /**
32 | * 增加表情统计信息
33 | * @param groupId 群组ID
34 | * @param memeKey 表情ID
35 | */
36 | export async function add ({
37 | groupId,
38 | memeKey,
39 | count
40 | }) {
41 | const data = {
42 | groupId,
43 | memeKey,
44 | count
45 | }
46 | return await stat.upsert(data)
47 | }
48 |
49 | /**
50 | * 获取表情统计信息
51 | * @param groupId 群组ID
52 | * @param memeKey 表情ID
53 | * @returns 表情统计信息
54 | */
55 | export async function get ({
56 | groupId,
57 | memeKey
58 | }) {
59 | return await stat.findOne({
60 | where: {
61 | groupId,
62 | memeKey
63 | }
64 | })
65 | }
66 |
67 | /**
68 | * 获取所有表情统计信息
69 | * @returns 所有表情统计信息
70 | */
71 | export async function getAll () {
72 | return await stat.findAll()
73 | }
74 |
75 | /**
76 | * 获取指定群组的所有表情统计信息
77 | * @param groupId 群组ID
78 | * @returns 该群组的所有表情统计信息
79 | */
80 | export async function getAllByGroupId (groupId) {
81 | return await stat.findAll({
82 | where: {
83 | groupId
84 | }
85 | })
86 | }
87 |
--------------------------------------------------------------------------------
/models/guoba/configInfo.js:
--------------------------------------------------------------------------------
1 | import { getConfigData, schemas, setConfigData } from './schemas/index.js'
2 |
3 | export const configInfo = {
4 | schemas,
5 | getConfigData,
6 | setConfigData
7 | }
8 |
--------------------------------------------------------------------------------
/models/guoba/getMemeList.js:
--------------------------------------------------------------------------------
1 | import { meme } from '../db/index.js'
2 |
3 | export const getMemeList = async () => {
4 |
5 | const keywords = async () => {
6 | const res = await meme.getAll()
7 | return res.map((item) => JSON.parse(String(item.keyWords))).flat() ?? null
8 | }
9 | const keys = async (keyword) => {
10 | const res = await await meme.getByKeyWord(keyword)
11 | if (!res) return null
12 | return res.key
13 | }
14 | return await Promise.all(
15 | (await keywords() ?? []).map(async keyword => {
16 | const memeKey = await keys(keyword)
17 | return {
18 | label: keyword,
19 | value: memeKey
20 | }
21 | })
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/models/guoba/index.js:
--------------------------------------------------------------------------------
1 | export * from './configInfo.js'
2 | export * from './pluginInfo.js'
--------------------------------------------------------------------------------
/models/guoba/pluginInfo.js:
--------------------------------------------------------------------------------
1 | import { Version } from '#components'
2 |
3 |
4 | export const pluginInfo = {
5 | name: `${Version.Plugin_Name}`,
6 | title: `${Version.Plugin_AliasName}插件`,
7 | author: `@${Version.Plugin_Author}`,
8 | authorLink: `https://github.com/${Version.Plugin_Author}`,
9 | link: `https://github.com/${Version.Plugin_Author}/${Version.Plugin_Name}`,
10 | isV3: true,
11 | isV2: false,
12 | showInMenu: 'auto',
13 | description: '一个Yunzai-Bot V3的扩展插件, 提供表情包合成功能',
14 | icon: 'mdi:wallet-membership',
15 | iconColor: 'rgb(188, 202, 224)'
16 | }
--------------------------------------------------------------------------------
/models/guoba/schemas/access.js:
--------------------------------------------------------------------------------
1 | import { getMemeList } from '../getMemeList.js'
2 |
3 | export default [
4 | {
5 | component: 'SOFT_GROUP_BEGIN',
6 | label: '名单设置'
7 | },
8 | {
9 | field: 'access.enable',
10 | label: '名单限制',
11 | component: 'Switch',
12 | bottomHelpMessage: '是否开启名单限制'
13 | },
14 | {
15 | field: 'access.blackListEnable',
16 | label: '禁用表情列表',
17 | component: 'Switch',
18 | bottomHelpMessage: '是否开启禁用表情列表'
19 | },
20 | {
21 | field: 'access.mode',
22 | label: '名单模式',
23 | component: 'Select',
24 | bottomHelpMessage: '名单模式,仅在开启名单限制启用,0为白名单,1为黑名单',
25 | componentProps: {
26 | options: [
27 | {
28 | label: '白名单',
29 | value: 0
30 | },
31 | {
32 | label: '黑名单',
33 | value: 1
34 | }
35 | ]
36 | }
37 | },
38 | {
39 | field: 'access.userWhiteList',
40 | label: '用户白名单',
41 | component: 'GTags',
42 | bottomHelpMessage: '白名单,白名单模式时生效'
43 | },
44 | {
45 | field: 'access.userBlackList',
46 | label: '用户黑名单',
47 | component: 'GTags',
48 | bottomHelpMessage: '黑名单,黑名单模式时生效'
49 | },
50 | {
51 | field: 'access.blackList',
52 | label: '禁用表情列表',
53 | component: 'Select',
54 | bottomHelpMessage: '设置禁用表情列表,如骑',
55 | componentProps: {
56 | options: await getMemeList(),
57 | mode: 'multiple'
58 | }
59 | }
60 | ]
--------------------------------------------------------------------------------
/models/guoba/schemas/index.js:
--------------------------------------------------------------------------------
1 | import { Config } from '#components'
2 |
3 | import access from './access.js'
4 | import meme from './meme.js'
5 | import other from './other.js'
6 | import protect from './protect.js'
7 | import server from './server.js'
8 | import stat from './stat.js'
9 |
10 | export const schemas = [
11 | server,
12 | meme,
13 | access,
14 | protect,
15 | stat,
16 | other
17 | ].flat()
18 |
19 | export function getConfigData () {
20 | return {
21 | server: Config.server,
22 | meme: Config.meme,
23 | access: Config.access,
24 | protect: Config.protect,
25 | stat: Config.stat,
26 | other: Config.other
27 | }
28 | }
29 |
30 | export function setConfigData (data, { Result }) {
31 | for (let key in data) {
32 | Config.modify(...key.split('.'), data[key])
33 | }
34 | return Result.ok({}, '保存成功辣ε(*´・ω・)з')
35 | }
36 |
--------------------------------------------------------------------------------
/models/guoba/schemas/meme.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | component: 'SOFT_GROUP_BEGIN',
4 | label: '表情设置'
5 | },
6 | {
7 | field: 'meme.enable',
8 | label: '表情',
9 | component: 'Switch',
10 | bottomHelpMessage: '是否设置当前插件的表情功能为默认表情'
11 | },
12 | {
13 | field: 'meme.cache',
14 | label: '缓存',
15 | component: 'Switch',
16 | bottomHelpMessage: '是否开启头像缓存'
17 | },
18 | {
19 | field: 'meme.reply',
20 | label: '引用回复',
21 | component: 'Switch',
22 | bottomHelpMessage: '是否开启引用回复'
23 | },
24 | {
25 | field: 'meme.forceSharp',
26 | label: '强制触发',
27 | component: 'Switch',
28 | bottomHelpMessage: '是否强制使用#触发, 开启后必须使用#触发'
29 | },
30 | {
31 | field: 'meme.errorReply',
32 | label: '错误回复',
33 | component: 'Switch',
34 | bottomHelpMessage: '是否开启错误信息回复'
35 | },
36 | {
37 | field: 'meme.perfix',
38 | label: '前缀',
39 | component: 'Input',
40 | bottomHelpMessage: '自定义错误输出前缀'
41 | }
42 | ]
--------------------------------------------------------------------------------
/models/guoba/schemas/other.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | component: 'SOFT_GROUP_BEGIN',
4 | label: '其他设置'
5 | },
6 | {
7 | field: 'other.renderScale',
8 | label: '渲染精度',
9 | component: 'InputNumber',
10 | bottomHelpMessage: '可选值50~200,建议100。设置高精度会提高图片的精细度,但因图片较大可能会影响渲染与发送速度',
11 | required: true,
12 | componentProps: {
13 | min: 50,
14 | max: 200,
15 | placeholder: '请输入渲染精度'
16 | }
17 | },
18 | {
19 | field: 'other.autoUpdateRes',
20 | label: '自动更新资源',
21 | component: 'Switch',
22 | bottomHelpMessage: '是否自动更新表情包资源,开启后每日凌晨会自动更新'
23 | },
24 | {
25 | field: 'other.autoUpdateResCron',
26 | label: '自动更新资源Cron表达式',
27 | bottomHelpMessage: '定时自动更新资源Cron表达式,重启生效',
28 | component: 'EasyCron',
29 | componentProps: {
30 | placeholder: '请输入Cron表达式'
31 | }
32 | },
33 | {
34 | field: 'other.autoUpdate',
35 | label: '自动更新',
36 | component: 'Switch',
37 | bottomHelpMessage: '是否开启自动更新'
38 | },
39 | {
40 | field: 'other.autoUpdateCron',
41 | label: '自动更新Cron表达式',
42 | bottomHelpMessage: '定时自动更新Cron表达式,重启生效',
43 | component: 'EasyCron',
44 | componentProps: {
45 | placeholder: '请输入Cron表达式'
46 | }
47 | },
48 | {
49 | field: 'other.hijackRes',
50 | label: '劫持土块表情包',
51 | component: 'Switch',
52 | bottomHelpMessage: '是否开启劫持土块表情包'
53 | }
54 | ]
--------------------------------------------------------------------------------
/models/guoba/schemas/protect.js:
--------------------------------------------------------------------------------
1 | import { getMemeList } from '../getMemeList.js'
2 |
3 | export default [
4 | {
5 | component: 'SOFT_GROUP_BEGIN',
6 | label: '表情保护设置'
7 | },
8 | {
9 | field: 'protect.enable',
10 | label: '表情保护',
11 | component: 'Switch',
12 | bottomHelpMessage: '是否开启表情保护'
13 | },
14 | {
15 | field: 'protect.master',
16 | label: '主人保护',
17 | component: 'Switch',
18 | bottomHelpMessage: '是否开启主人保护'
19 | },
20 | {
21 | field: 'protect.userEnable',
22 | label: '用户保护',
23 | component: 'Switch',
24 | bottomHelpMessage: '是否开启用户保护'
25 | },
26 | {
27 | field: 'protect.user',
28 | label: '保护用户列表',
29 | component: 'GTags',
30 | bottomHelpMessage: '设置要保护的用户,如123456'
31 | },
32 | {
33 | field: 'protect.list',
34 | label: '保护表情列表',
35 | component: 'Select',
36 | bottomHelpMessage: '设置要保护表情列表,如骑',
37 | componentProps: {
38 | options: await getMemeList(),
39 | mode: 'multiple'
40 | }
41 | }
42 | ]
--------------------------------------------------------------------------------
/models/guoba/schemas/server.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | component: 'SOFT_GROUP_BEGIN',
4 | label: '服务设置'
5 | },
6 | {
7 | field: 'server.mode',
8 | label: '服务模式',
9 | component: 'Select',
10 | bottomHelpMessage: '服务模式, 0为远程服务, 1为本地服务',
11 | componentProps: {
12 | options: [
13 | {
14 | label: '0远程服务',
15 | value: 0
16 | },
17 | {
18 | label: '本地服务',
19 | value: 1
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | field: 'server.url',
26 | label: '自定义地址',
27 | component: 'Input',
28 | bottomHelpMessage: '自定义表情包地址,为空时使用插件自带'
29 | },
30 | {
31 | field: 'server.port',
32 | label: '自定义端口',
33 | component: 'InputNumber',
34 | defaultValue: 2255,
35 | componentProps: {
36 | min: 1,
37 | max: 65535
38 | }
39 | },
40 | {
41 | field: 'server.usebase64',
42 | label: 'base64上传',
43 | component: 'Switch',
44 | bottomHelpMessage: '是否开启使用base64上传图片'
45 | },
46 | {
47 | field: 'server.retry',
48 | label: '重试次数',
49 | component: 'InputNumber',
50 | bottomHelpMessage: '最大次数,用于请求重试'
51 | },
52 | {
53 | field: 'server.timeout',
54 | label: '超时时间',
55 | component: 'InputNumber',
56 | bottomHelpMessage: '超时时间,单位为秒'
57 | },
58 | {
59 | field: 'server.proxy_url',
60 | label: '代理地址',
61 | component: 'Input',
62 | bottomHelpMessage: '代理地址,如: https://github.moeyy.xyz'
63 | },
64 | {
65 | field: 'server.download_url',
66 | label: '下载地址',
67 | component: 'Input',
68 | bottomHelpMessage: '下载地址,如: https://cdn.mengze.vip/gh/MemeCrafters/meme-generator-rs@'
69 | }
70 | ]
--------------------------------------------------------------------------------
/models/guoba/schemas/stat.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | component: 'SOFT_GROUP_BEGIN',
4 | label: '表情统计设置'
5 | },
6 | {
7 | field: 'stat.enable',
8 | label: '表情统计',
9 | component: 'Switch',
10 | bottomHelpMessage: '是否开启表情调用统计功能'
11 | }
12 | ]
--------------------------------------------------------------------------------
/models/help/config.js:
--------------------------------------------------------------------------------
1 | import { Version } from '#components'
2 |
3 | export const Cfg = {
4 | title: `${Version.Plugin_AliasName}帮助`,
5 | subTitle: Version.Plugin_Name,
6 | columnCount: 3,
7 | colWidth: 265,
8 | theme: 'all',
9 | style: {
10 | fontColor: '#d3bc8e',
11 | descColor: '#eee',
12 | contBgColor: 'rgba(6, 21, 31, .5)',
13 | contBgBlur: 3,
14 | headerBgColor: 'rgba(6, 21, 31, .4)',
15 | rowBgColor1: 'rgba(6, 21, 31, .2)'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/models/help/index.js:
--------------------------------------------------------------------------------
1 | export * as helpCfg from './config.js'
2 | export * as helpList from './list.js'
3 | export * as Theme from './theme.js'
--------------------------------------------------------------------------------
/models/help/list.js:
--------------------------------------------------------------------------------
1 | export const List = [
2 | {
3 | group: '[]内为必填项,{}内为可选项, #均为可选'
4 | },
5 | {
6 | group: '表情命令',
7 | list: [
8 | {
9 | icon: 161,
10 | title: '#柠糖表情列表',
11 | desc: '获取表情列表'
12 | },
13 | {
14 | icon: 141,
15 | title: '#柠糖表情统计',
16 | desc: '获取表情统计'
17 | },
18 | {
19 | icon: 90,
20 | title: '#柠糖表情搜索xx',
21 | desc: '搜指定的表情'
22 | },
23 | {
24 | icon: 75,
25 | title: '#柠糖表情详情xx',
26 | desc: '获取指定表情详情'
27 | },
28 | {
29 | icon: 72,
30 | title: '#柠糖表情统计',
31 | desc: '获取表情统计'
32 | },
33 | {
34 | icon: 71,
35 | title: 'xx',
36 | desc: '如喜报xx (参数使用#参数名 参数值,, 多段文本使用/, 指定用户头像使用@+qq号)'
37 | }
38 | ]
39 | },
40 | {
41 | group: '图片操作命令',
42 | list: [
43 | {
44 | icon: 71,
45 | title: '#图片信息',
46 | desc: '获取图片信息'
47 | },
48 | {
49 | icon: 157,
50 | title: '#水平翻转',
51 | desc: '水平翻转图片'
52 | },
53 | {
54 | icon: 158,
55 | title: '#垂直翻转',
56 | desc: '垂直翻转图片'
57 | },
58 | {
59 | icon: 158,
60 | title: '#灰度化',
61 | desc: '灰度化图片'
62 | },
63 | {
64 | icon: 158,
65 | title: '#反色',
66 | desc: '反色图片'
67 | },
68 | {
69 | icon: 159,
70 | title: '#旋转 xx',
71 | desc: '旋转图片xx度'
72 | },
73 | {
74 | icon: 160,
75 | title: '#缩放 xx',
76 | desc: '缩放图片xx度'
77 | },
78 | {
79 | icon: 161,
80 | title: '#裁剪 xx,xx,xx,xx',
81 | desc: '裁剪图片xx度'
82 | },
83 | {
84 | icon: 132,
85 | title: '#水平拼接',
86 | desc: '水平拼接图片,需多张图片'
87 | },
88 | {
89 | icon: 132,
90 | title: '#垂直拼接',
91 | desc: '垂直拼接图片,需多张图片'
92 | },
93 | {
94 | icon: 123,
95 | title: '#gif分解',
96 | desc: '分解gif图片'
97 | },
98 | {
99 | icon: 123,
100 | title: '#gif合成',
101 | desc: '合成gif图片,需多张图片'
102 | },
103 | {
104 | icon: 123,
105 | title: '#gif变速xxxS',
106 | desc: '变速gif图片'
107 | }
108 | ]
109 | },
110 | {
111 | group: '服务端管理命令',
112 | auth: 'master',
113 | list: [
114 | {
115 | icon: 35,
116 | title: '#柠糖表情下载表情服务端资源',
117 | desc: '下载表情服务端资源'
118 | },
119 | {
120 | icon: 35,
121 | title: '#柠糖表情下载/更新表情服务端资源',
122 | desc: '下载/更新表情服务端资源'
123 | },
124 | {
125 | icon: 934,
126 | title: '#柠糖表情启动表情服务端',
127 | desc: '启动表情服务端'
128 | },
129 | {
130 | icon: 34,
131 | title: '#柠糖表情关闭表情服务端',
132 | desc: '关闭表情服务端'
133 | },
134 | {
135 | icon: 34,
136 | title: '#柠糖表情重启表情服务端',
137 | desc: '重启表情服务端'
138 | },
139 | {
140 | icon: 34,
141 | title: '#柠糖表情服务端状态',
142 | desc: '查看表情服务端状态'
143 | }
144 | ]
145 | },
146 | {
147 | group: '管理命令,仅主人可用',
148 | auth: 'master',
149 | list: [
150 | {
151 | icon: 95,
152 | title: '#柠糖表情{插件}{强制}更新',
153 | desc: '更新插件本体'
154 | },
155 | {
156 | icon: 81,
157 | title: '#柠糖表情({强制}更新资源',
158 | desc: '更新表情资源'
159 | },
160 | {
161 | icon: 85,
162 | title: '#柠糖表情设置',
163 | desc: '管理命令'
164 | }
165 | ]
166 | }
167 | ]
168 |
--------------------------------------------------------------------------------
/models/help/theme.js:
--------------------------------------------------------------------------------
1 | import { Version } from '#components'
2 |
3 | import { Cfg } from './config.js'
4 |
5 | export async function getThemeCfg () {
6 | const resPath = `${Version.Plugin_Path}/resources/help/theme`
7 | const mainImagePath = `${resPath}/main.webp`
8 | const bgImagePath = `${resPath}/bg.webp`
9 |
10 | return {
11 | main: mainImagePath,
12 | bg: bgImagePath,
13 | style: Cfg.style
14 | }
15 | }
16 | export async function getThemeData (diyStyle) {
17 | const helpConfig = Object.assign({}, diyStyle)
18 | const colCount = Math.min(5, Math.max(parseInt(helpConfig?.colCount) || 3, 2))
19 | const colWidth = Math.min(500, Math.max(100, parseInt(helpConfig?.colWidth) || 265))
20 | const width = Math.min(2500, Math.max(800, colCount * colWidth + 30))
21 | const theme = await getThemeCfg()
22 | const themeStyle = theme.style || {}
23 | const ret = [ `
24 | body { background-image: url(${theme.bg}); width: ${width}px; }
25 | .container { background-image: url(${theme.main}); width: ${width}px; }
26 | .help-table .td, .help-table .th { width: ${100 / colCount}%; }
27 | ` ]
28 |
29 | const defFnc = function (...args) {
30 | for (const idx in args) {
31 | if (args[idx] !== undefined) {
32 | return args[idx]
33 | }
34 | }
35 | }
36 |
37 | const css = function (sel, cssProperty, key, def, fn) {
38 | let val = defFnc(themeStyle[key], diyStyle[key], def)
39 | if (fn) {
40 | val = fn(val)
41 | }
42 | ret.push(`${sel} { ${cssProperty}: ${val}; }`)
43 | }
44 |
45 | css('.help-title,.help-group', 'color', 'fontColor', '#ceb78b', undefined)
46 | css('.help-title,.help-group', 'text-shadow', 'fontShadow', 'none', undefined)
47 | css('.help-desc', 'color', 'descColor', '#eee', undefined)
48 | css('.cont-box', 'background', 'contBgColor', 'rgba(43, 52, 61, 0.8)', undefined)
49 | css('.cont-box', 'backdrop-filter', 'contBgBlur', 3, function (n) {
50 | return diyStyle.bgBlur === false ? 'none' : `blur(${n}px)`
51 | })
52 | css('.help-group', 'background', 'headerBgColor', 'rgba(34, 41, 51, .4)', undefined)
53 | css('.help-table .tr:nth-child(odd)', 'background', 'rowBgColor1', 'rgba(34, 41, 51, .2)', undefined)
54 | css('.help-table .tr:nth-child(even)', 'background', 'rowBgColor2', 'rgba(34, 41, 51, .4)', undefined)
55 |
56 | return {
57 | style: ``,
58 | colCount
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/models/imageTool/index.js:
--------------------------------------------------------------------------------
1 | export * from './tools.js'
--------------------------------------------------------------------------------
/models/imageTool/tools.js:
--------------------------------------------------------------------------------
1 | import { utils } from '#models'
2 |
3 | /**
4 | * 获取图片
5 | * @param image_id 图片 ID
6 | * @param type 返回类型 base64 或 buffer 默认 base64
7 | * @returns 图片
8 | */
9 | export const get_image = async (image_id, type = 'base64') => {
10 | try {
11 | const url = await utils.get_base_url()
12 | const res = await utils.Request.get(`${url}/image/${image_id}`, {}, {}, 'arraybuffer')
13 | switch (type) {
14 | case 'buffer':
15 | return res.data
16 | case 'base64':
17 | default:
18 | return await utils.getImageBase64(res.data)
19 | }
20 | } catch (error) {
21 | throw new Error(`获取图片失败: ${erro.message}`)
22 | }
23 | }
24 | /**
25 | * 获取图片信息
26 | * @param image_id 图片 ID
27 | * @returns 图片信息
28 | */
29 | export const get_image_info = async (image_id) => {
30 | try {
31 | const data = {
32 | image_id
33 | }
34 | const url = await utils.get_base_url()
35 | const res = await utils.Request.post(`${url}/tools/image_operations/inspect`, data)
36 | return res.data
37 | } catch (error) {
38 | throw new Error(`获取图片信息失败: ${error.message}`)
39 | }
40 | }
41 |
42 | /**
43 | * 水平翻转图片
44 | * @param image_id 图片 ID
45 | * @returns 图片 ID
46 | */
47 | export const flip_horizontal = async (image_id) => {
48 | try {
49 | const data = {
50 | image_id
51 | }
52 | const url = await utils.get_base_url()
53 | const res = await utils.Request.post(`${url}/tools/image_operations/flip_horizontal`, data)
54 | return res.data.image_id
55 | } catch (error) {
56 | throw new Error(`水平翻转图片失败: ${error.message}`)
57 | }
58 | }
59 |
60 | /**
61 | * 垂直翻转图片
62 | * @param image_id 图片id
63 | * @returns 图片id
64 | */
65 | export const flip_vertical = async (image_id) => {
66 | try {
67 | const data = {
68 | image_id
69 | }
70 | const url = await utils.get_base_url()
71 | const res = await utils.Request.post(`${url}/tools/image_operations/flip_vertical`, data)
72 | return res.data.image_id
73 | } catch (error) {
74 | throw new Error(`垂直翻转图片失败: ${error.message}`)
75 | }
76 | }
77 |
78 | /**
79 | * 旋转图片
80 | * @param image_id 图片id
81 | * @param degrees 旋转角度
82 | * @returns 图片id
83 | */
84 | export const rotate = async (image_id, degrees) => {
85 | try {
86 | const data = {
87 | image_id,
88 | degrees
89 | }
90 | const url = await utils.get_base_url()
91 | const res = await utils.Request.post(`${url}/tools/image_operations/rotate`, data)
92 | return res.data.image_id
93 | } catch (error) {
94 | throw new Error(`旋转图片失败: ${error.message}`)
95 | }
96 | }
97 |
98 | /**
99 | * 缩放图片
100 | * @param image_id 图片id
101 | * @param width 宽度
102 | * @param height 高度
103 | * @returns 图片id
104 | */
105 | export const resize = async (image_id, width, height) => {
106 | try {
107 | const data = {
108 | image_id,
109 | width,
110 | height
111 | }
112 | const url = await utils.get_base_url()
113 | const res = await utils.Request.post(`${url}/tools/image_operations/resize`, data)
114 | return res.data.image_id
115 | } catch (error) {
116 | throw new Error(`缩放图片失败: ${error.message}`)
117 | }
118 | }
119 |
120 | /**
121 | * 裁剪图片
122 | * @param image_id 图片id
123 | * @param left 左
124 | * @param top 上
125 | * @param right 右
126 | * @param bottom 下
127 | * @returns 图片id
128 | */
129 | export const crop = async (image_id, left, top, right, bottom) => {
130 | try {
131 | const data = {
132 | image_id,
133 | left,
134 | top,
135 | right,
136 | bottom
137 | }
138 | const url = await utils.get_base_url()
139 | const res = await utils.Request.post(`${url}/tools/image_operations/crop`, data)
140 | return res.data.image_id
141 | } catch (error) {
142 | throw new Error(`裁剪图片失败: ${error.message}`)
143 | }
144 | }
145 |
146 | /**
147 | * 灰度化图片
148 | * @param image_id 图片id
149 | * @returns 图片id
150 | */
151 | export const grayscale = async (image_id) => {
152 | try {
153 | const data = {
154 | image_id
155 | }
156 | const url = await utils.get_base_url()
157 | const res = await utils.Request.post(`${url}/tools/image_operations/grayscale`, data)
158 | return res.data.image_id
159 | } catch (error) {
160 | throw new Error(`灰度化图片失败: ${error.message}`)
161 | }
162 | }
163 |
164 | /**
165 | * 反色图片
166 | * @param image_id 图片id
167 | * @returns 图片id
168 | */
169 | export const invert = async (image_id) => {
170 | try {
171 | const data = {
172 | image_id
173 | }
174 | const url = await utils.get_base_url()
175 | const res = await utils.Request.post(`${url}/tools/image_operations/invert`, data)
176 | return res.data.image_id
177 | } catch (error) {
178 | throw new Error(`反色图片失败: ${error.message}`)
179 | }
180 | }
181 |
182 | /**
183 | * 水平拼接图片
184 | * @param image_ids 图片id数组
185 | * @returns 图片id
186 | */
187 | export const merge_horizontal = async (image_ids) => {
188 | try {
189 | const data = {
190 | image_ids
191 | }
192 | const url = await utils.get_base_url()
193 | const res = await utils.Request.post(`${url}/tools/image_operations/merge_horizontal`, data)
194 | return res.data.image_id
195 | } catch (error) {
196 | throw new Error(`水平拼接图片失败: ${error.message}`)
197 | }
198 | }
199 |
200 | /**
201 | * 垂直拼接图片
202 | * @param image_ids 图片id数组
203 | * @returns 图片id
204 | */
205 | export const merge_vertical = async (image_ids) => {
206 | try {
207 | const data = {
208 | image_ids
209 | }
210 | const url = await utils.get_base_url()
211 | const res = await utils.Request.post(`${url}/tools/image_operations/merge_vertical`, data)
212 | return res.data.image_id
213 | } catch (error) {
214 | throw new Error(`垂直拼接图片失败: ${error.message}`)
215 | }
216 | }
217 |
218 | /**
219 | * gif分解
220 | * @param image_id 图片id
221 | * @returns 图片id数组
222 | */
223 | export const gif_split = async (image_id) => {
224 | try {
225 | const data = {
226 | image_id
227 | }
228 | const url = await utils.get_base_url()
229 | const res = await utils.Request.post(`${url}/tools/image_operations/gif_split`, data)
230 | return res.data.image_ids
231 | } catch (error) {
232 | throw new Error(`gif分解失败: ${error.message}`)
233 | }
234 | }
235 |
236 | /**
237 | * gif合成
238 | * @param image_ids 图片id数组
239 | * @returns 图片id
240 | */
241 | export const gif_merge = async (image_ids) => {
242 | try {
243 | const data = {
244 | image_ids
245 | }
246 | const url = await utils.get_base_url()
247 | const res = await utils.Request.post(`${url}/tools/image_operations/gif_merge`, data)
248 | return res.data.image_id
249 | } catch (error) {
250 | throw new Error(`gif合成失败: ${error.message}`)
251 | }
252 | }
253 |
254 | /**
255 | * gif反转
256 | * @param image_id 图片id
257 | * @returns 图片id
258 | */
259 | export const gif_reverse = async (image_id) => {
260 | try {
261 | const data = {
262 | image_id
263 | }
264 | const url = await utils.get_base_url()
265 | const res = await utils.Request.post(`${url}/tools/image_operations/gif_reverse`, data)
266 | return res.data.image_id
267 | } catch (error) {
268 | throw new Error(`gif反转失败: ${error.message}`)
269 | }
270 | }
271 |
272 | /**
273 | * gif变速帧率
274 | * @param image_id 图片id
275 | * @param duration 图片帧率间隔
276 | * @returns 图片id
277 | */
278 | export const gif_change_duration = async (image_id, duration) => {
279 | try {
280 | const data = {
281 | image_id,
282 | duration
283 | }
284 | const url = await utils.get_base_url()
285 | const res = await utils.Request.post(`${url}/tools/image_operations/gif_change_duration`, data)
286 | return res.data.image_id
287 | } catch (error) {
288 | throw new Error(`gif变速帧率失败: ${error.message}`)
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/models/index.js:
--------------------------------------------------------------------------------
1 | export * as admin from './admin/index.js'
2 | export * as db from './db/index.js'
3 | export * as guoba from './guoba/index.js'
4 | export * as help from './help/index.js'
5 | export * as imageTool from './imageTool/index.js'
6 | export * as make from './make/index.js'
7 | export * as server from './server/index.js'
8 | export * as utils from './utils/index.js'
--------------------------------------------------------------------------------
/models/make/images.js:
--------------------------------------------------------------------------------
1 | import { Config } from '#components'
2 | import { utils } from '#models'
3 |
4 | export async function handleImages (
5 | e,
6 | memeKey,
7 | min_images,
8 | max_images,
9 | allUsers,
10 | quotedUser,
11 | userText,
12 | formdata
13 | ) {
14 | let images = []
15 | const getType = Config.server.usebase64 ? 'base64' : 'url'
16 | const AvatarUploadType = Config.server.usebase64
17 | ? 'data'
18 | : Number(Config.server.mode) === 1 && Config.meme.cache
19 | ? 'path'
20 | : 'url'
21 | const uploadType = Config.server.usebase64 ? 'data' : 'url'
22 | const messageImages = await utils.get_image(e, getType)
23 | let userAvatars = []
24 |
25 | const imagePromises = messageImages.map(async (msgImage) => {
26 | const [ image, name ] = await Promise.all([
27 | utils.upload_image(msgImage.image, uploadType),
28 | utils.get_user_name(e, msgImage.userId)
29 | ])
30 | return {
31 | name,
32 | id: image
33 | }
34 | })
35 | images = await Promise.all(imagePromises)
36 |
37 | if (allUsers.length > 0) {
38 | let avatar = await utils.get_user_avatar(e, allUsers[0], getType)
39 | if (!avatar) {
40 | return {
41 | success: false,
42 | message: '获取用户头像失败'
43 | }
44 | }
45 |
46 | const image = await utils.upload_image(avatar.avatar, AvatarUploadType)
47 |
48 | if (image) {
49 | userAvatars.push({
50 | name: await utils.get_user_name(e, avatar.userId),
51 | id: image
52 | })
53 | }
54 | }
55 |
56 | /** 获取引用消息的头像 */
57 | if (messageImages.length === 0 && quotedUser) {
58 | let avatar = await utils.get_user_avatar(e, quotedUser, getType)
59 | if (!avatar) {
60 | return {
61 | success: false,
62 | message: '获取用户头像失败'
63 | }
64 | }
65 |
66 | const image = await utils.upload_image(avatar.avatar, AvatarUploadType)
67 |
68 | if (image) {
69 | userAvatars.push({
70 | name: await utils.get_user_name(e, avatar.userId),
71 | id: image
72 | })
73 | }
74 | }
75 |
76 | /**
77 | * 特殊处理:当 min_images === 1 时,因没有多余的图片,表情保护功能会失效
78 | */
79 | if (min_images === 1 && messageImages.length === 0) {
80 | let avatar = await utils.get_user_avatar(e, e.user_id, getType)
81 | if (!avatar) {
82 | return {
83 | success: false,
84 | message: '获取用户头像失败'
85 | }
86 | }
87 |
88 | const image = await utils.upload_image(avatar.avatar, AvatarUploadType)
89 |
90 | if (image) {
91 | userAvatars.push({
92 | name: await utils.get_user_name(e, avatar.userId),
93 | id: image
94 | })
95 | }
96 | }
97 |
98 | if (images.length + userAvatars.length < min_images) {
99 | let avatar = await utils.get_user_avatar(e, e.user_id, getType)
100 | if (!avatar) {
101 | return {
102 | success: false,
103 | message: '获取用户头像失败'
104 | }
105 | }
106 |
107 | const image = await utils.upload_image(avatar.avatar, AvatarUploadType)
108 |
109 | if (image) {
110 | userAvatars.unshift({
111 | name: await utils.get_user_name(e, avatar.userId),
112 | id: image
113 | })
114 | }
115 | }
116 |
117 | /** 表情保护逻辑 */
118 | if (Config.protect.enable) {
119 | const protectList = Config.protect.list
120 | if (protectList.length > 0) {
121 | /** 处理表情保护列表可能含有关键词 */
122 | const memeKeys = await Promise.all(protectList.map(async item => {
123 | const key = await utils.get_meme_key_by_keyword(item)
124 | return key ?? item
125 | }))
126 | if (memeKeys.includes(memeKey)) {
127 | const allProtectedUsers = [ ...allUsers, ...(quotedUser ? [ quotedUser ] : []) ]
128 |
129 | if (allProtectedUsers.length > 0) {
130 | const masterQQArray = Array.isArray(Config.masterQQ)
131 | ? Config.masterQQ.map(String)
132 | : [ String(Config.masterQQ) ]
133 | /** 优先检查引用消息的用户 */
134 | const protectUser = quotedUser ??
135 | (allProtectedUsers.length === 1 ? allProtectedUsers[0] : allProtectedUsers[1])
136 | if (!e.isMaster) {
137 | if (Config.protect.master) {
138 | if (masterQQArray.includes(protectUser)) {
139 | userAvatars.reverse()
140 | }
141 | }
142 | if (Config.protect.userEnable) {
143 | const protectUsers = Array.isArray(Config.protect.user)
144 | ? Config.protect.user.map(String)
145 | : [ String(Config.protect.user) ]
146 | if (protectUsers.includes(protectUser)) {
147 | userAvatars.reverse()
148 | }
149 | }
150 | }
151 | }
152 | }
153 | }
154 | }
155 |
156 | images = [ ...userAvatars, ...images ].slice(0, max_images)
157 | formdata['images'] = images
158 |
159 | return images.length < min_images
160 | ? {
161 | success: false,
162 | message: min_images === max_images
163 | ? `该表情需要${min_images}张图片`
164 | : `该表情至少需要 ${min_images} ~ ${max_images} 张图片`
165 | }
166 | : {
167 | success: true,
168 | text: userText
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/models/make/index.js:
--------------------------------------------------------------------------------
1 | import { Config } from '#components'
2 | import { db, utils } from '#models'
3 |
4 | import { handleImages } from './images.js'
5 | import { handleOption } from './options.js'
6 | import { handleTexts } from './texts.js'
7 |
8 | export async function make_meme (
9 | e,
10 | memekey,
11 | min_texts,
12 | max_texts,
13 | min_images,
14 | max_images,
15 | options,
16 | userText,
17 | isPreset,
18 | PresetKeyWord
19 | ) {
20 | try {
21 | const getquotedUser = async (e)=> {
22 | let source = null
23 | if (e.reply_id) {
24 | source = await e.getReply()
25 | } else if (e.source) {
26 | if (e.isGroup) {
27 | source = await Bot[e.self_id].pickGroup(e.group_id).getChatHistory(e.reply_id ?? e.source.seq, 1)
28 | } else if (e.isPrivate) {
29 | source = await Bot[e.self_id].pickFriend(e.user_id).getChatHistory((Math.floor(Date.now() / 1000)), 1)
30 | }
31 | }
32 | if (source) {
33 | const sourceArray = Array.isArray(source) ? source : [ source ]
34 | return sourceArray[0].sender.user_id.toString()
35 | }
36 | return null
37 | }
38 |
39 | const quotedUser = await getquotedUser(e)
40 | const allUsers = [
41 | ...new Set([
42 | ...e.message
43 | .filter(m => m?.type === 'at')
44 | .map(at => at?.qq?.toString() ?? ''),
45 | ...[ ...(userText?.matchAll(/@\s*(\d+)/g) ?? []) ].map(match => match[1] ?? '')
46 | ])
47 | ].filter(id => id && id !== quotedUser)
48 |
49 | let formdata = {
50 | images: [],
51 | texts: [],
52 | options: {}
53 | }
54 |
55 | if (options) {
56 | const option = await handleOption(e, memekey, userText, formdata, isPreset, PresetKeyWord)
57 | if (!option.success) {
58 | throw new Error(option.message)
59 | }
60 | userText = option.text
61 | }
62 |
63 | if (min_texts > 0 && max_texts > 0) {
64 | const text = await handleTexts(e, memekey, min_texts, max_texts, userText, formdata)
65 | if (!text.success) {
66 | throw new Error(text.message)
67 | }
68 | }
69 |
70 | if (min_images > 0 && max_images > 0) {
71 | const image = await handleImages(e, memekey, min_images, max_images, allUsers, quotedUser, userText, formdata)
72 | if (!image.success) {
73 | throw new Error(image.message)
74 | }
75 | }
76 | const response = await utils.make_meme(memekey, formdata)
77 | const basedata = await utils.getImageBase64(response)
78 | if (Config.stat.enable && e.isGroup) {
79 | const groupStart = (await db.stat.get({
80 | groupId: e.group_id,
81 | memeKey: memekey
82 | }))?.count ?? 0
83 | await db.stat.add({
84 | groupId: e.group_id,
85 | memeKey: memekey,
86 | count: Number(groupStart) + 1
87 | })
88 | }
89 | return `base64://${basedata}`
90 | } catch (error) {
91 | logger.error(error)
92 | throw new Error(error.message)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/models/make/options.js:
--------------------------------------------------------------------------------
1 | import { utils } from '#models'
2 | export async function handleOption (
3 | e,
4 | memekey,
5 | userText,
6 | formdata,
7 | isPreset,
8 | PresetKeyWord
9 | ) {
10 | let options = {}
11 | const optionsMatches = userText.match(/#(\S+)\s+([^#]+)/g)
12 | const optionArray = []
13 |
14 | if (isPreset) {
15 | const presetInfo = await utils.get_preset_info_by_keyword(PresetKeyWord)
16 | if (!presetInfo) {
17 | return {
18 | success: false,
19 | message: '获取预设信息失败'
20 | }
21 | }
22 | optionArray.push({
23 | name: presetInfo.option_name,
24 | value: presetInfo.option_value
25 | })
26 | }
27 | if (optionsMatches) {
28 | for (const match of optionsMatches) {
29 | const [ , name, value ] = match.match(/#(\S+)\s+([^#]+)/) ?? null
30 | if (name && value) {
31 | optionArray.push({
32 | name: name.trim(),
33 | value: value.trim()
34 | })
35 | }
36 | }
37 | }
38 |
39 | const optionsInfo = (await utils.get_meme_info(memekey))?.options ?? null
40 | if (!optionsInfo) {
41 | return {
42 | success: false,
43 | message: '获取选项信息失败'
44 | }
45 | }
46 |
47 | const optionsArray = Array.isArray(optionsInfo)
48 | ? optionsInfo
49 | : JSON.parse(optionsInfo)
50 |
51 | for (const option of optionArray) {
52 | const supportedOption = optionsArray.find(
53 | (opt) => opt.name === option.name
54 | )
55 | if (!supportedOption) {
56 | return {
57 | success: false,
58 | message: `该表情不支持参数:${option.name}`
59 | }
60 | }
61 |
62 | const result = convertOptionValue(option, supportedOption)
63 | if (!result.success) {
64 | return {
65 | success: false,
66 | message: result.message
67 | }
68 | }
69 |
70 | options[option.name] = result.value
71 | }
72 |
73 | formdata['options'] = options
74 | return {
75 | success: true,
76 | text: userText.replace(/#(\S+)\s+([^#]+)/g, '').trim()
77 | }
78 | }
79 |
80 | /**
81 | * 转换选项值为指定类型
82 | *
83 | * @param option - 选项对象
84 | * @param option.name - 选项名称
85 | * @param option.value - 选项值(字符串形式)
86 | * @param supportedOption - 支持的选项类型定义
87 | * @returns 转换结果对象
88 | */
89 | function convertOptionValue (option, supportedOption) {
90 | let convertedValue
91 |
92 | switch (supportedOption.type) {
93 | case 'boolean': {
94 | const boolValue = option.value.toLowerCase()
95 | if ([ 'true', '真', '是', 'yes', '1' ].includes(boolValue)) {
96 | convertedValue = true
97 | } else if ([ 'false', '假', '否', 'no', '0' ].includes(boolValue)) {
98 | convertedValue = false
99 | } else {
100 | return {
101 | success: false,
102 | message: `参数 ${option.name} 需要是布尔值`
103 | }
104 | }
105 | return { success: true, value: convertedValue }
106 | }
107 |
108 | case 'integer': {
109 | const intValue = parseInt(option.value)
110 | if (isNaN(intValue)) {
111 | return {
112 | success: false,
113 | message: `参数 ${option.name} 需要是整数`
114 | }
115 | }
116 | if (
117 | supportedOption.minimum !== null &&
118 | intValue < supportedOption.minimum
119 | ) {
120 | return {
121 | success: false,
122 | message: `参数 ${option.name} 不能小于 ${supportedOption.minimum}`
123 | }
124 | }
125 | if (
126 | supportedOption.maximum !== null &&
127 | intValue > supportedOption.maximum
128 | ) {
129 | return {
130 | success: false,
131 | message: `参数 ${option.name} 不能大于 ${supportedOption.maximum}`
132 | }
133 | }
134 | return {
135 | success: true,
136 | value: intValue
137 | }
138 | }
139 |
140 | case 'float': {
141 | const floatValue = parseFloat(option.value)
142 | if (isNaN(floatValue)) {
143 | return {
144 | success: false,
145 | message: `参数 ${option.name} 需要是数字`
146 | }
147 | }
148 | if (
149 | supportedOption.minimum !== null &&
150 | floatValue < supportedOption.minimum
151 | ) {
152 | return {
153 | success: false,
154 | message: `参数 ${option.name} 不能小于 ${supportedOption.minimum}`
155 | }
156 | }
157 | if (
158 | supportedOption.maximum !== null &&
159 | floatValue > supportedOption.maximum
160 | ) {
161 | return {
162 | success: false,
163 | message: `参数 ${option.name} 不能大于 ${supportedOption.maximum}`
164 | }
165 | }
166 | return {
167 | success: true,
168 | value: floatValue
169 | }
170 | }
171 |
172 | case 'string':
173 | convertedValue = option.value
174 | if (
175 | supportedOption.choices &&
176 | !supportedOption.choices.includes(convertedValue)
177 | ) {
178 | return {
179 | success: false,
180 | message: `参数 ${
181 | option.name
182 | } 的值必须是: ${supportedOption.choices.join(', ')} 之一`
183 | }
184 | }
185 | return {
186 | success: true,
187 | value: convertedValue
188 | }
189 |
190 | default:
191 | return {
192 | success: false,
193 | message: `不支持的参数类型:${supportedOption.type}`
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/models/make/texts.js:
--------------------------------------------------------------------------------
1 | import { utils } from '#models'
2 |
3 | export async function handleTexts (
4 | e,
5 | memekey,
6 | min_texts,
7 | max_texts,
8 | userText,
9 | formdata
10 | ) {
11 | const texts = []
12 |
13 | /** 用户输入的文本 */
14 | if (userText) {
15 | const splitTexts = userText.split('/').map((text) => text.trim())
16 | for (const text of splitTexts) {
17 | if (text) {
18 | texts.push(text)
19 | }
20 | }
21 | }
22 |
23 | const memeInfo = await utils.get_meme_info(memekey)
24 | const default_texts = memeInfo?.default_texts ? JSON.parse(String(memeInfo.default_texts)) : []
25 | if (
26 | texts.length < min_texts &&
27 | default_texts
28 | ) {
29 | while (texts.length < min_texts) {
30 | const randomIndex = Math.floor(Math.random() * default_texts.length)
31 | texts.push(default_texts[randomIndex])
32 | }
33 | }
34 |
35 | formdata['texts'] = texts
36 |
37 | return texts.length < min_texts
38 | ? {
39 | success: false,
40 | message: min_texts === max_texts
41 | ? `该表情需要${min_texts}个文本`
42 | : `该表情至少需要 ${min_texts} ~ ${max_texts} 个文本`
43 | }
44 | : {
45 | success: true,
46 | texts: userText
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/models/server/index.js:
--------------------------------------------------------------------------------
1 | export * from './manger.js'
2 | export * from './utils.js'
3 |
--------------------------------------------------------------------------------
/models/server/manger.js:
--------------------------------------------------------------------------------
1 | import { spawn } from 'node:child_process'
2 | import fs from 'node:fs/promises'
3 | import os from 'node:os'
4 | import path from 'node:path'
5 |
6 | import TOML from 'smol-toml'
7 |
8 | import { Config } from '#components'
9 | import { utils } from '#models'
10 |
11 | import { checkPort, get_meme_server_name, get_meme_server_path, kil_meme_server } from './utils.js'
12 |
13 | let serverProcess = null
14 |
15 | const config = {
16 | meme: {
17 | load_builtin_memes: true,
18 | load_external_memes: false,
19 | meme_disabled_list: []
20 | },
21 | resource: {
22 | resource_url: 'https://cdn.jsdelivr.net/gh/MemeCrafters/meme-generator-rs@',
23 | download_fonts: true
24 | },
25 | font: {
26 | use_local_fonts: true,
27 | default_font_families: [ 'Noto Sans SC', 'Noto Color Emoji' ]
28 | },
29 | encoder: {
30 | gif_max_frames: 200,
31 | gif_encode_speed: 1
32 | },
33 | api: {
34 | baidu_trans_appid: '',
35 | baidu_trans_apikey: ''
36 | },
37 | server: {
38 | host: '0.0.0.0',
39 | port: 2255
40 | }
41 | }
42 |
43 | /**
44 | * 启动表情服务端
45 | * @param port - 端口
46 | * @returns 启动结果
47 | */
48 |
49 | export async function start (port = 2255) {
50 | try {
51 | const configDir = path.join(os.homedir(), '.meme_generator')
52 | const configPath = path.join(configDir, 'config.toml')
53 | if (!await utils.exists(configDir)) {
54 | await fs.mkdir(configDir)
55 | }
56 | if (!await utils.exists(configPath)) {
57 | const defaultConfig = TOML.stringify(config)
58 | await fs.writeFile(configPath, defaultConfig)
59 | }
60 | const configContentBuffer = await fs.readFile(configPath)
61 | const configContent = configContentBuffer?.toString().trim() || TOML.stringify(config)
62 | const configData = TOML.parse(configContent)
63 | const download_url = 'https://cdn.jsdelivr.net/gh/MemeCrafters/meme-generator-rs@'
64 | const base_url = await utils.isAbroad() ? download_url : 'https://cdn.mengze.vip/gh/MemeCrafters/meme-generator-rs@'
65 | const url = Config.server.download_url?.trim() ? Config.server.download_url.replace(/\/+$/, '') : base_url
66 | configData.server.port = port
67 | configData.resource.resource_url = url
68 | const newConfigData = TOML.stringify(configData)
69 | await fs.writeFile(configPath, newConfigData)
70 | const memeServerPath = path.join(`${get_meme_server_path()}/${get_meme_server_name()}`)
71 | if (!memeServerPath) {
72 | throw new Error('未找到表情服务端文件')
73 | }
74 | const available = await checkPort(port)
75 | if (available) {
76 | if (Config.server.kill) {
77 | await kil_meme_server(port)
78 | } else {
79 | throw new Error(`[meme-server] 端口${port}已被占用, 请检查并稍后重启`)
80 | }
81 | }
82 | serverProcess = spawn(memeServerPath, [ 'run' ], { stdio: 'inherit' })
83 | serverProcess.on('error', (error) => {
84 | logger.error(error)
85 | throw new Error(`启动服务器失败: ${(error).message}`)
86 | })
87 | } catch (error) {
88 | logger.error(error)
89 | throw new Error(`启动服务器失败: ${error.message}`)
90 | }
91 | }
92 |
93 | /**
94 | * 停止表情服务端
95 | * @returns 停止结果
96 | */
97 | export async function stop () {
98 | try {
99 | if (serverProcess) {
100 | await new Promise((resolve, reject) => {
101 | serverProcess.on('exit', resolve)
102 | serverProcess.on('error', reject)
103 | serverProcess.kill()
104 | })
105 | serverProcess = null
106 | } else {
107 | throw new Error('表情服务端未运行')
108 | }
109 | } catch (error) {
110 | logger.error(error)
111 | throw new Error(`停止服务器失败: ${error.message}`)
112 | }
113 | }
114 |
115 | /**
116 | * 重启表情服务端
117 | * @returns 重启结果
118 | */
119 | export async function restart () {
120 | try {
121 | if (serverProcess) {
122 | await stop()
123 | }
124 | await start()
125 | } catch (error) {
126 | logger.error(error)
127 | throw new Error(`重启服务器失败: ${error.message}`)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/models/server/utils.js:
--------------------------------------------------------------------------------
1 | import { exec as childExec, spawn } from 'node:child_process'
2 | import fs from 'node:fs/promises'
3 | import net from 'node:net'
4 | import os from 'node:os'
5 | import path from 'node:path'
6 | import { promisify } from 'node:util'
7 |
8 | import AdmZip from 'adm-zip'
9 |
10 | import { Config, Version } from '#components'
11 | import { utils } from '#models'
12 |
13 | import { start } from './manger.js'
14 |
15 | const exec = promisify(childExec)
16 | /**
17 | * 格式化日期时间
18 | */
19 | function formatRuntime (diffMs) {
20 | const seconds = Math.floor(diffMs / 1000)
21 | const minutes = Math.floor(seconds / 60)
22 | const hours = Math.floor(minutes / 60)
23 | const days = Math.floor(hours / 24)
24 |
25 | const remainingHours = hours % 24
26 | const remainingMinutes = minutes % 60
27 | const remainingSeconds = seconds % 60
28 |
29 | let runtime = ''
30 | if (days > 0) runtime += `${days}天`
31 | if (remainingHours > 0) runtime += `${remainingHours}小时`
32 | if (remainingMinutes > 0) runtime += `${remainingMinutes}分钟`
33 | runtime += `${remainingSeconds}秒`
34 |
35 | return runtime
36 | }
37 |
38 | /*
39 | * 获取本地IP地址
40 | * @returns 本地IP地址
41 | */
42 | export async function get_local_ip () {
43 | const interfaces = os.networkInterfaces()
44 |
45 | for (const devName in interfaces) {
46 | const iface = interfaces[devName]
47 | if (!iface) continue
48 |
49 | for (const alias of iface) {
50 | if (!alias) continue
51 |
52 | if (
53 | alias.family === 'IPv4' &&
54 | alias.address !== '127.0.0.1' &&
55 | !alias.internal
56 | ) {
57 | return alias.address
58 | }
59 | }
60 | }
61 |
62 | return Promise.resolve('127.0.0.1')
63 | }
64 |
65 |
66 | /**
67 | * 重启表情服务端
68 | * @returns 重启结果
69 | */
70 |
71 | const save_path = path.join(Version.Plugin_Path, 'data', 'server')
72 | /**
73 | * 下载表情服务端
74 | * 自动根据当前系统类型下载对应的表情服务端并保存到本地
75 | */
76 | export async function download_server () {
77 | try {
78 | let file_name
79 | if (!await utils.exists(save_path)) {
80 | await fs.mkdir(save_path)
81 | }
82 | const type = os.type()
83 | const arch = os.arch()
84 | switch (type) {
85 | case 'Windows_NT':
86 | file_name = 'meme-generator-cli-windows-x86_64.zip'
87 | break
88 | case 'Linux':
89 | switch (arch) {
90 | case 'x64':
91 | file_name = 'meme-generator-cli-linux-x86_64.zip'
92 | break
93 | case 'arm64':
94 | file_name = 'meme-generator-cli-linux-aarch64.zip'
95 | break
96 | default:
97 | throw new Error('不支持的架构')
98 | }
99 | break
100 | case 'Darwin':
101 | switch (arch) {
102 | case 'x64':
103 | file_name = 'meme-generator-cli-macos-x86_64.zip'
104 | break
105 | case 'arm64':
106 | file_name = 'meme-generator-cli-macos-aarch64.zip'
107 | break
108 | default:
109 | throw new Error('不支持的架构')
110 | }
111 | break
112 | default:
113 | throw new Error('不支持的操作系统')
114 | }
115 | const github_url = 'https://github.com'
116 | const base_url = await utils.isAbroad() ? github_url : `https://github.moeyy.xyz/${github_url}`
117 | const url = Config.server.proxy_url?.trim() ? `${Config.server.proxy_url.replace(/\/+$/, '')}/${github_url}` : base_url
118 | const release_url = `${url}/MemeCrafters/meme-generator-rs/releases/latest/download/${file_name}`
119 | const res = await utils.Request.get(release_url, null, null, 'arraybuffer')
120 | if (!res.success) {
121 | throw new Error('下载表情服务端文件失败')
122 | } else if (res.status) {
123 | logger.info('下载表情服务端文件成功')
124 | }
125 | logger.debug('下载表情服务端文件成功')
126 | logger.debug('开始解压表情服务端文件')
127 | const zip = new AdmZip(res.data)
128 | zip.extractAllTo(save_path, true)
129 | logger.debug('解压表情服务端文件成功')
130 | } catch (error) {
131 | logger.error(error)
132 | throw new Error('下载表情服务端文件失败:' + error.message)
133 | }
134 | }
135 |
136 | /**
137 | * 下载表情服务端资源
138 | * @returns 更新结果
139 | */
140 | export async function download_server_resource () {
141 | try {
142 | const server_path = path.join(`${get_meme_server_path()}/${get_meme_server_name()}`)
143 | if (!server_path) throw new Error('表情服务端文件不存在')
144 |
145 | return new Promise((resolve, reject) => {
146 | const downloadProcess = spawn(server_path, [ 'download' ], { stdio: 'inherit' })
147 |
148 | downloadProcess.on('error', (error) => {
149 | logger.error(error)
150 | reject(new Error('下载表情服务端资源失败'))
151 | })
152 |
153 | downloadProcess.on('close', (code) => {
154 | if (code !== 0) {
155 | reject(new Error('下载表情服务端资源失败'))
156 | } else {
157 | logger.info('下载表情服务端资源成功')
158 | resolve(true)
159 | }
160 | })
161 | })
162 | } catch (error) {
163 | logger.error(error)
164 | throw new Error('更新表情服务端资源失败: ' + error.message)
165 | }
166 | }
167 |
168 | /**
169 | * 获取表情服务端的名称
170 | * @returns 表情服务端的名称
171 | */
172 | export function get_meme_server_name () {
173 | let name
174 | try {
175 | const type = os.type()
176 | switch (type) {
177 | case 'Windows_NT':
178 | name = 'meme.exe'
179 | break
180 | case 'Linux':
181 | case 'Darwin':
182 | name = 'meme'
183 | break
184 | default:
185 | throw new Error('不支持的操作系统')
186 | }
187 | return name
188 | } catch (error) {
189 | logger.error(error)
190 | return null
191 | }
192 | }
193 |
194 | /**
195 | * 获取表情服务端的路径
196 | * @returns 表情服务端的路径
197 | */
198 | export function get_meme_server_path () {
199 | return path.join(save_path)
200 | }
201 |
202 | /**
203 | * 获取表情服务端的版本
204 | * @returns 表情服务端的版本
205 | */
206 | export async function get_meme_server_version () {
207 | try {
208 | const url = await utils.get_base_url()
209 | const res = await utils.Request.get(`${url}/meme/version`)
210 | return res.data
211 | } catch (error) {
212 | logger.error(error)
213 | return null
214 | }
215 | }
216 |
217 | /**
218 | * 获取表情服务端的进程ID
219 | * @returns 表情服务端的进程ID
220 | */
221 | export async function get_meme_server_pid () {
222 | try {
223 | const type = os.type()
224 | let command
225 | let args
226 |
227 | switch (type) {
228 | case 'Windows_NT':
229 | {
230 | command = 'wmic'
231 | const serverPath = path.join(get_meme_server_path() ?? '', get_meme_server_name() ?? '')
232 | args = [ 'process', 'where', `"ExecutablePath='${serverPath.replace(/\\/g, '\\\\')}'"`, 'get', 'ProcessId', '/value' ]
233 | break
234 | }
235 | case 'Linux':
236 | case 'Darwin':
237 | {
238 | command = 'pgrep'
239 | const serverPath = path.join(get_meme_server_path() ?? '', get_meme_server_name() ?? '')
240 | args = [ '-f', serverPath ?? '' ]
241 | break
242 | }
243 | default:
244 | throw new Error('不支持的操作系统')
245 | }
246 |
247 | const { stdout, stderr } = await exec(`${command} ${args.join(' ')}`)
248 | if (stderr) throw new Error('获取表情服务端进程ID失败')
249 | let pid
250 |
251 | if (type === 'Windows_NT') {
252 | const pidMatch = stdout.match(/ProcessId=(\d+)/)
253 | if (!pidMatch) {
254 | throw new Error('无法获取进程ID')
255 | }
256 | pid = pidMatch[1]
257 | } else {
258 | pid = stdout.trim().split('\n')[0]
259 | }
260 | if (!pid) {
261 | throw new Error('无法获取进程ID')
262 | }
263 | return pid
264 | } catch (error) {
265 | logger.error(error)
266 | throw new Error('获取表情服务端进程ID失败:' + error.message)
267 | }
268 | }
269 |
270 | /**
271 | * 获取表情服务端的运行时间
272 | * @returns 表情服务端的运行时间
273 | */
274 | export async function get_meme_server_runtime () {
275 | try {
276 | const pid = await get_meme_server_pid()
277 |
278 | const type = os.type()
279 | let command
280 | let args
281 |
282 | switch (type) {
283 | case 'Windows_NT':
284 | command = 'wmic'
285 | args = [ 'process', 'where', `processid=${pid}`, 'get', 'CreationDate', '/value' ]
286 | break
287 | case 'Linux':
288 | case 'Darwin':
289 | command = 'ps'
290 | args = [ '-p', pid, '-o', 'etime=' ]
291 | break
292 | default:
293 | throw new Error('不支持的操作系统')
294 | }
295 |
296 | const { stdout, stderr } = await exec(`${command} ${args.join(' ')}`)
297 | if (stderr) throw new Error('获取表情服务端运行时间失败')
298 | let runtime = ''
299 |
300 | if (type === 'Windows_NT') {
301 | const match = stdout.match(/CreationDate=(\d+)/)
302 | if (match) {
303 | const wmicDate = match[1]
304 | const year = parseInt(wmicDate.substring(0, 4))
305 | const month = parseInt(wmicDate.substring(4, 6)) - 1
306 | const day = parseInt(wmicDate.substring(6, 8))
307 | const hours = parseInt(wmicDate.substring(8, 10))
308 | const minutes = parseInt(wmicDate.substring(10, 12))
309 | const seconds = parseInt(wmicDate.substring(12, 14))
310 | const creationDate = new Date(year, month, day, hours, minutes, seconds)
311 | const now = new Date()
312 | const diffMs = now.getTime() - creationDate.getTime()
313 | runtime = formatRuntime(diffMs)
314 | } else {
315 | throw new Error('无法解析WMIC输出的创建日期')
316 | }
317 | } else {
318 | const etime = stdout.trim()
319 | const etimeMatch = etime.match(/(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)/)
320 | if (etimeMatch) {
321 | const [ , days, possibleHours, minutes, seconds ] = etimeMatch
322 | let hours = 0
323 | if (possibleHours) {
324 | hours = parseInt(possibleHours)
325 | }
326 | const diffMs = ((parseInt(days || '0') * 24 * 60 * 60) +
327 | (hours * 60 * 60) +
328 | parseInt(minutes) * 60 +
329 | parseInt(seconds)) * 1000
330 | runtime = formatRuntime(diffMs)
331 | }
332 | if (!etimeMatch) {
333 | throw new Error('无法获取进程运行时间')
334 | }
335 | if (!etimeMatch) {
336 | throw new Error('无法获取进程运行时间')
337 | }
338 | }
339 |
340 | return runtime
341 | } catch (error) {
342 | logger.error(error)
343 | return '未知'
344 | }
345 | }
346 |
347 | export async function get_meme_server_memory () {
348 | try {
349 | const pid = await get_meme_server_pid()
350 | const type = os.type()
351 | let command
352 | let args
353 |
354 | switch (type) {
355 | case 'Windows_NT':
356 | command = 'wmic'
357 | args = [ 'process', 'where', `processid=${pid}`, 'get', 'WorkingSetSize', '/value' ]
358 | break
359 | case 'Linux':
360 | case 'Darwin':
361 | command = 'ps'
362 | args = [ '-p', pid, '-o', 'rss=' ]
363 | break
364 | default:
365 | throw new Error('不支持的操作系统')
366 | }
367 |
368 | const { stdout, stderr } = await exec(`${command} ${args.join(' ')}`, { log: true })
369 | if (stderr) throw new Error('获取表情服务端内存使用失败')
370 | let memoryInMB
371 |
372 | if (type === 'Windows_NT') {
373 | const match = stdout.match(/WorkingSetSize=(\d+)/)
374 | if (match) {
375 | const memoryStr = match[1]
376 | memoryInMB = parseInt(memoryStr) / (1024 * 1024)
377 | } else {
378 | throw new Error('获取表情服务端内存使用失败')
379 | }
380 | } else {
381 | memoryInMB = parseInt(stdout.trim()) / 1024
382 | }
383 |
384 | return memoryInMB % 1 === 0 ? memoryInMB.toFixed(0) : memoryInMB.toFixed(2)
385 | } catch (error) {
386 | logger.error(error)
387 | return '未知'
388 | }
389 | }
390 |
391 | /**
392 | * 获取表情服务端的表情包总数
393 | * @returns 表情服务端的表情包总数
394 | */
395 | export async function get_meme_server_meme_total () {
396 | try {
397 | const url = await utils.get_base_url()
398 | const res = await utils.Request.get(`${url}/meme/keys`)
399 | return res.data.length
400 | } catch (error) {
401 | logger.error(error)
402 | return '未知'
403 | }
404 | }
405 |
406 | /**
407 | * 检查指定的端口是否被占用
408 | * 后续将改成karin内置函数
409 | * @param port 监听端口
410 | * @returns 被占用则返回 true,否则返回 false
411 | */
412 | export async function checkPort (port) {
413 | return new Promise(resolve => {
414 | const server = net.createServer()
415 |
416 | server.once('error', () => {
417 | server.close()
418 | resolve(false)
419 | })
420 |
421 | server.once('listening', () => {
422 | server.close()
423 | resolve(true)
424 | })
425 |
426 | server.listen(port, '127.0.0.1')
427 | })
428 | }
429 |
430 | /**
431 | * 杀死表情服务端进程
432 | * @param port - 端口
433 | * @returns 杀死结果
434 | */
435 | export async function kil_meme_server (port) {
436 | const isWin = os.type() === 'Windows_NT'
437 | const getPid = async (port) => {
438 | const command = isWin ? `netstat -ano | findstr :${port}` : `lsof -i:${port} | grep LISTEN | awk '{print $2}'`
439 | const { stdout } = await exec(command)
440 | if (!stdout) return null
441 | if (isWin) {
442 | const pid = stdout.toString().split(/\s+/).filter(Boolean).pop()
443 | return isNaN(Number(pid)) ? null : Number(pid)
444 | }
445 |
446 | return isNaN(Number(stdout)) ? null : Number(stdout)
447 | }
448 | const pid = await getPid(Number(port))
449 | if (!pid) return false
450 | const command = isWin
451 | ? `taskkill /F /PID ${pid}`
452 | : `kill -9 ${pid}`
453 | const { stderr } = await exec(command)
454 | if (stderr) return false
455 | return true
456 | }
457 |
458 | /**
459 | * 初始化表情服务端
460 | * @param port - 端口
461 | * @returns 初始化结果
462 | */
463 | export async function init_server (port = 2255) {
464 | try {
465 | const server_path = path.join(`${get_meme_server_path()}/${get_meme_server_name()}`)
466 | if (!await utils.exists(server_path)) await download_server()
467 | const type = os.type()
468 | if (type === 'Linux') {
469 | await exec('chmod +x ' + server_path)
470 | }
471 | const resource_path = path.join(os.homedir(), '.meme_generator', 'resources')
472 | if (!await utils.exists(resource_path)) {
473 | logger.info('表情服务端资源不存在,请稍后使用[#柠糖表情下载表情服务端资源]命令下载')
474 | }
475 | await start(port)
476 | } catch (error) {
477 | logger.error(error)
478 | throw new Error('初始化表情服务端失败:' + error.message)
479 | }
480 | }
481 |
--------------------------------------------------------------------------------
/models/utils/common.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import os from 'node:os'
3 | import path from 'node:path'
4 |
5 | import { Config, Version } from '#components'
6 | import { server } from '#models'
7 |
8 | import Request from './request.js'
9 | import { exists } from './tools.js'
10 | /**
11 | * 获取图片 Buffer
12 | * @param {string | Buffer} image - 图片地址或 Buffer
13 | * @returns {Promise} - 返回图片的 Buffer 数据
14 | * @throws {Error} - 如果图片地址为空或请求失败,则抛出异常
15 | */
16 | export async function getImageBuffer (image) {
17 | try {
18 | if (!image) throw new Error('图片地址不能为空')
19 |
20 | if (Buffer.isBuffer(image)) {
21 | return image
22 | }
23 |
24 | const response = await Request.get(image, null, null, 'arraybuffer')
25 | if (!response.success) {
26 | throw new Error(response.msg)
27 | }
28 | return response.data
29 | } catch (error) {
30 | logger.error(`获取图片Buffer失败: ${error.message}`)
31 | }
32 | }
33 |
34 | /**
35 | * 获取图片 Base64 字符串
36 | * @param {string | Buffer} image - 图片的 URL、Buffer 或 Base64 字符串
37 | * @returns {Promise} - 返回 Base64 编码的图片字符串,可能包含 `base64://` 前缀
38 | * @throws {Error} - 如果图片地址为空或处理失败,则抛出异常
39 | */
40 | export async function getImageBase64 (image) {
41 | try {
42 | if (!image) {
43 | logger.error('图片地址不能为空')
44 | }
45 |
46 | if (typeof image === 'string') return image
47 |
48 | if (Buffer.isBuffer(image)) {
49 | return image.toString('base64')
50 | }
51 |
52 | const buffer = await getImageBuffer(image)
53 | return buffer.toString('base64')
54 | } catch (error) {
55 | logger.error(`获取图片Base64失败: ${error.message}`)
56 | }
57 | }
58 |
59 | /**
60 | * 获取基础 URL
61 | * @returns {Promise} 基础 URL
62 | */
63 | export async function get_base_url () {
64 | try {
65 | let base_url
66 | if (!Config.server.url && !(Config.server.mode === 1))
67 | throw new Error('请先使用未配置表情包API或使用本地服务')
68 | switch (Number(Config.server.mode)) {
69 | case 0:
70 | base_url = Config.server.url.replace(/\/+$/, '')
71 | break
72 | case 1: {
73 | const resources_path = path.join(
74 | os.homedir(),
75 | '.meme_generator',
76 | 'resources'
77 | )
78 | if (!(await exists(resources_path))) {
79 | throw new Error('请先使用[#柠糖表情下载表情服务端资源]')
80 | }
81 | base_url = `http://${await server.get_local_ip()}:${
82 | Config.server.port
83 | }`
84 | break
85 | }
86 | default:
87 | throw new Error('请检查服务器模式')
88 | }
89 |
90 | return Promise.resolve(base_url)
91 | } catch (error) {
92 | logger.error(error)
93 | throw new Error(error.message)
94 | }
95 | }
96 |
97 | /**
98 | * 异步判断是否在海外环境
99 | * @returns {Promise} 如果在海外环境返回 true,否则返回 false
100 | */
101 | export async function isAbroad () {
102 | const urls = [
103 | 'https://blog.cloudflare.com/cdn-cgi/trace',
104 | 'https://developers.cloudflare.com/cdn-cgi/trace',
105 | 'https://hostinger.com/cdn-cgi/trace',
106 | 'https://ahrefs.com/cdn-cgi/trace'
107 | ]
108 |
109 | try {
110 | const responses = await Promise.all(
111 | urls.map((url) => Request.get(url, null, null, 'text'))
112 | )
113 | const traceTexts = responses.map((res) => res.data).filter(Boolean)
114 | const traceLines = traceTexts
115 | .flatMap((text) => text.split('\n').filter((line) => line))
116 | .map((line) => line.split('='))
117 |
118 | const traceMap = Object.fromEntries(traceLines)
119 | return traceMap.loc !== 'CN'
120 | } catch (error) {
121 | throw new Error(`获取 IP 所在地区出错: ${error.message}`)
122 | }
123 | }
124 |
125 | /**
126 | * 获取用户头像
127 | * @param {Message} e 消息事件
128 | * @param {string}userId 用户ID
129 | * @param {'url' | 'base64'}type 返回类型 url 或 base64
130 | * @returns {Promise<{userId:string,avatar:string} | null>}用户头像
131 | */
132 |
133 | export async function get_user_avatar (e, userId, type = 'url') {
134 | try {
135 | if (!e) throw new Error('消息事件不能为空')
136 | if (!userId) throw new Error('用户ID不能为空')
137 |
138 | const getAvatarUrl = async (e, userId) => {
139 | let avatarUrl
140 |
141 | try {
142 | if (e.isGroup) {
143 | const member = e.bot.pickMember(e.group_id, userId)
144 | avatarUrl = await member.getAvatarUrl()
145 | } else if (e.isPrivate) {
146 | const friend = e.bot.pickFriend(userId)
147 | avatarUrl = await friend.getAvatarUrl()
148 | }
149 | } catch (err) {}
150 | if (!avatarUrl) {
151 | throw new Error(`获取用户头像地址失败: ${userId}`)
152 | }
153 | return avatarUrl
154 | }
155 |
156 | const avatarDir = path.join(Version.Plugin_Path, 'data', 'avatar')
157 | const cachePath = path.join(avatarDir, `${userId}.png`).replace(/\\/g, '/')
158 |
159 | if (
160 | Config.meme.cache &&
161 | Number(Config.server.mode) === 1 &&
162 | (await exists(cachePath))
163 | ) {
164 | const avatarUrl = await getAvatarUrl(e, userId)
165 | if (!avatarUrl) throw new Error(`获取用户头像失败: ${userId}`)
166 | const headRes = await Request.head(avatarUrl)
167 | const lastModified = headRes.data['last-modified']
168 | const cacheStat = await fs.stat(cachePath)
169 |
170 | if (new Date(lastModified) <= cacheStat.mtime) {
171 | switch (type) {
172 | case 'base64': {
173 | const data = await fs.readFile(cachePath)
174 | if (!data) throw new Error(`通过缓存获取用户头像失败: ${userId}`)
175 | return {
176 | userId,
177 | avatar: data.toString('base64')
178 | }
179 | }
180 | case 'url':
181 | default:
182 | return {
183 | userId,
184 | avatar: cachePath
185 | }
186 | }
187 | }
188 | }
189 |
190 | const avatarUrl = await getAvatarUrl(e, userId)
191 | if (!avatarUrl) throw new Error(`获取用户头像失败: ${userId}`)
192 |
193 | if (
194 | Config.meme.cache &&
195 | Number(Config.server.mode) === 1 &&
196 | !(await exists(avatarDir))
197 | ) {
198 | await fs.mkdir(avatarDir)
199 | }
200 |
201 | const res = await Request.get(avatarUrl, null, null, 'arraybuffer')
202 | const avatarData = res.data
203 |
204 | if (Config.meme.cache && Number(Config.server.mode) === 1) {
205 | await fs.writeFile(cachePath, avatarData)
206 | }
207 |
208 | switch (type) {
209 | case 'base64':
210 | return {
211 | userId,
212 | avatar: avatarData.toString('base64')
213 | }
214 | case 'url':
215 | default:
216 | return {
217 | userId,
218 | avatar:
219 | Config.meme.cache && Number(Config.server.mode) === 1
220 | ? cachePath
221 | : avatarUrl
222 | }
223 | }
224 | } catch (error) {
225 | logger.error(error)
226 | return null
227 | }
228 | }
229 |
230 | /**
231 | * 获取用户昵称
232 | * @param {Message} e 消息事件
233 | * @param {string} userId 用户 ID
234 | * @returns 用户昵称
235 | */
236 | export async function get_user_name (e, userId) {
237 | try {
238 | if (!e) throw new Error('消息事件不能为空')
239 | if (!userId) throw new Error('用户ID不能为空')
240 | userId = String(userId)
241 | let nickname
242 | let userInfo
243 | if (e.isGroup) {
244 | const memberInfo = Bot[e.self_id].pickMember(e.group_id, userId)
245 | userInfo = await memberInfo.getInfo()
246 | nickname = userInfo.card?.trim() || userInfo.nickname?.trim() || null
247 | } else if (e.isPrivate) {
248 | const friendInfo = Bot[e.self_id].pickFriend(userId)
249 | userInfo = await friendInfo.getInfo()
250 | nickname = userInfo.nickname.trim() ?? null
251 | } else {
252 | nickname = e.sender.nickname.trim() ?? null
253 | }
254 | if (!nickname) throw new Error('获取用户昵称失败')
255 | return nickname
256 | } catch (error) {
257 | logger.error(`获取用户昵称失败: ${error}`)
258 | return '未知'
259 | }
260 | }
261 |
262 | /**
263 | * 获取图片
264 | * @param {Message} e 消息事件
265 | * @param {'url' | 'base64'} type 返回类型 url 或 base64
266 | * @returns {Promise>} 图片数组信息
267 | */
268 | export async function get_image (e, type = 'url') {
269 | if (!e) throw new Error('消息事件不能为空')
270 | const imagesInMessage = e.message
271 | .filter((m) => m.type === 'image')
272 | .map((img) => ({
273 | userId: e.sender.user_id,
274 | url: img.url
275 | }))
276 |
277 | const replyId = e.reply_id ?? e.message.find((m) => m.type === 'reply')?.id
278 |
279 | const tasks = []
280 |
281 | let quotedImages = []
282 | let source = null
283 | if (e.getReply) {
284 | source = await e.getReply()
285 | } else if (replyId) {
286 | if (e.isGroup) {
287 | source = await Bot[e.self_id]
288 | .pickGroup(e.group_id)
289 | .getChatHistory(e.source.seq || replyId, 1)
290 | } else if (e.isPrivate) {
291 | source = await Bot[e.self_id]
292 | .pickFriend(e.user_id)
293 | .getChatHistory(Math.floor(Date.now() / 1000), 1)
294 | }
295 | }
296 |
297 | /**
298 | * 提取引用消息中的图片
299 | */
300 | if (source) {
301 | const sourceArray = Array.isArray(source) ? source : [ source ]
302 |
303 | quotedImages = sourceArray.flatMap(({ message, sender }) =>
304 | message
305 | .filter((element) => element.type === 'image')
306 | .map((element) => ({
307 | userId: sender.user_id,
308 | url: element.url
309 | }))
310 | )
311 | }
312 |
313 | /**
314 | * 处理引用消息中的图片
315 | */
316 | if (quotedImages.length > 0) {
317 | for (const item of quotedImages) {
318 | switch (type) {
319 | case 'url':
320 | tasks.push(
321 | Promise.resolve({
322 | userId: item.userId,
323 | image: item.url.toString()
324 | })
325 | )
326 | break
327 | case 'base64':
328 | default:
329 | tasks.push(
330 | Promise.resolve({
331 | userId: item.userId,
332 | image: await getImageBase64(item.url)
333 | })
334 | )
335 | break
336 | }
337 | }
338 | }
339 |
340 | /**
341 | * 处理消息中的图片
342 | */
343 | if (imagesInMessage.length > 0) {
344 | const imagePromises = imagesInMessage.map(async (item) => {
345 | switch (type) {
346 | case 'url':
347 | return {
348 | userId: item.userId,
349 | image: item.url.toString()
350 | }
351 | case 'base64':
352 | default:
353 | return {
354 | userId: item.userId,
355 | image: await getImageBase64(item.url)
356 | }
357 | }
358 | })
359 | tasks.push(...imagePromises)
360 | }
361 |
362 | const results = await Promise.allSettled(tasks)
363 | const images = results
364 | .filter((res) => res.status === 'fulfilled' && Boolean(res.value))
365 | .map((res) => res.value)
366 |
367 | return images
368 | }
369 |
--------------------------------------------------------------------------------
/models/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './common.js'
2 | export * from './preset.js'
3 | export { default as Request } from './request.js'
4 | export * from './tools.js'
--------------------------------------------------------------------------------
/models/utils/preset.js:
--------------------------------------------------------------------------------
1 | const preset = [
2 | {
3 | name: '狠撅',
4 | key: 'do',
5 | option_name: 'fps',
6 | option_value: 50
7 | },
8 | {
9 | name: '狠骑',
10 | key: 'qi',
11 | option_name: 'fps',
12 | option_value: 50
13 | },
14 | /** ba说 */
15 | {
16 | name: '心奈说',
17 | key: 'ba_say',
18 | option_name: 'character',
19 | option_value: 'kokona'
20 | },
21 | {
22 | name: '爱丽丝说',
23 | key: 'ba_say',
24 | option_name: 'character',
25 | option_value: 'arisu'
26 | },
27 | {
28 | name: '泉奈说',
29 | key: 'ba_say',
30 | option_name: 'character',
31 | option_value: 'izuna'
32 | },
33 | {
34 | name: 'key说',
35 | key: 'ba_say',
36 | option_name: 'character',
37 | option_value: 'key'
38 | },
39 | {
40 | name: '玛丽说',
41 | key: 'ba_say',
42 | option_name: 'character',
43 | option_value: 'mari'
44 | },
45 | {
46 | name: '濑名说',
47 | key: 'ba_say',
48 | option_name: 'character',
49 | option_value: 'sena'
50 | },
51 | {
52 | name: '优香说',
53 | key: 'ba_say',
54 | option_name: 'character',
55 | option_value: 'yuuka'
56 | },
57 | /** 原神吃 */
58 | {
59 | name: '八重神子吃',
60 | key: 'genshin_eat',
61 | option_name: 'character',
62 | option_value: 'nilou'
63 | },
64 | {
65 | name: '神子吃',
66 | key: 'genshin_eat',
67 | option_name: 'character',
68 | option_value: 'nilou'
69 | },
70 | {
71 | name: '八重吃',
72 | key: 'genshin_eat',
73 | option_name: 'character',
74 | option_value: 'nilou'
75 | },
76 | {
77 | name: '胡桃吃',
78 | key: 'genshin_eat',
79 | option_name: 'character',
80 | option_value: 'hutao'
81 | },
82 | {
83 | name: '妮露吃',
84 | key: 'genshin_eat',
85 | option_name: 'character',
86 | option_value: 'nilou'
87 | },
88 | {
89 | name: '可莉吃',
90 | key: 'genshin_eat',
91 | option_name: 'character',
92 | option_value: 'klee'
93 | },
94 | {
95 | name: '刻晴吃',
96 | key: 'genshin_eat',
97 | option_name: 'character',
98 | option_value: 'keqing'
99 | },
100 | {
101 | name: '钟离吃',
102 | key: 'genshin_eat',
103 | option_name: 'character',
104 | option_value: 'zhongli'
105 | },
106 | /** 世界计划 */
107 | {
108 | name: '爱莉说',
109 | key: 'pjsk',
110 | option_name: 'character',
111 | option_value: 'airi'
112 | },
113 | {
114 | name: '彰人说',
115 | key: 'pjsk',
116 | option_name: 'character',
117 | option_value: 'akito'
118 | },
119 | {
120 | name: '杏说',
121 | key: 'pjsk',
122 | option_name: 'character',
123 | option_value: 'an'
124 | },
125 | {
126 | name: '梦说',
127 | key: 'pjsk',
128 | option_name: 'character',
129 | option_value: 'emu'
130 | },
131 | {
132 | name: '绘名说',
133 | key: 'pjsk',
134 | option_name: 'character',
135 | option_value: 'ena'
136 | },
137 | {
138 | name: '遥说',
139 | key: 'pjsk',
140 | option_name: 'character',
141 | option_value: 'haruka'
142 | },
143 | {
144 | name: '穗波说',
145 | key: 'pjsk',
146 | option_name: 'character',
147 | option_value: 'honami'
148 | },
149 | {
150 | name: '一歌说',
151 | key: 'pjsk',
152 | option_name: 'character',
153 | option_value: 'ichika'
154 | },
155 | {
156 | name: 'KAITO说',
157 | key: 'pjsk',
158 | option_name: 'character',
159 | option_value: 'kaito'
160 | },
161 | {
162 | name: '奏说',
163 | key: 'pjsk',
164 | option_name: 'character',
165 | option_value: 'kanade'
166 | },
167 | {
168 | name: '心羽说',
169 | key: 'pjsk',
170 | option_name: 'character',
171 | option_value: 'kohane'
172 | },
173 | {
174 | name: '连说',
175 | key: 'pjsk',
176 | option_name: 'character',
177 | option_value: 'len'
178 | },
179 | {
180 | name: '流歌说',
181 | key: 'pjsk',
182 | option_name: 'character',
183 | option_value: 'luka'
184 | },
185 | {
186 | name: '真冬说',
187 | key: 'pjsk',
188 | option_name: 'character',
189 | option_value: 'mafuyu'
190 | },
191 | {
192 | name: 'MEIKO说',
193 | key: 'pjsk',
194 | option_name: 'character',
195 | option_value: 'meiko'
196 | },
197 | {
198 | name: '初音未来说',
199 | key: 'pjsk',
200 | option_name: 'character',
201 | option_value: 'miku'
202 | },
203 | {
204 | name: '实乃理说',
205 | key: 'pjsk',
206 | option_name: 'character',
207 | option_value: 'minori'
208 | },
209 | {
210 | name: '瑞希说',
211 | key: 'pjsk',
212 | option_name: 'character',
213 | option_value: 'mizuki'
214 | },
215 | {
216 | name: '宁宁说',
217 | key: 'pjsk',
218 | option_name: 'character',
219 | option_value: 'nene'
220 | },
221 | {
222 | name: '铃说',
223 | key: 'pjsk',
224 | option_name: 'character',
225 | option_value: 'rin'
226 | },
227 | {
228 | name: '类说',
229 | key: 'pjsk',
230 | option_name: 'character',
231 | option_value: 'rui'
232 | },
233 | {
234 | name: '咲希说',
235 | key: 'pjsk',
236 | option_name: 'character',
237 | option_value: 'saki'
238 | },
239 | {
240 | name: '志步说',
241 | key: 'pjsk',
242 | option_name: 'character',
243 | option_value: 'shiho'
244 | },
245 | {
246 | name: '雫说',
247 | key: 'pjsk',
248 | option_name: 'character',
249 | option_value: 'shizuku'
250 | },
251 | {
252 | name: '冬弥说',
253 | key: 'pjsk',
254 | option_name: 'character',
255 | option_value: 'touya'
256 | },
257 | {
258 | name: '司说',
259 | key: 'pjsk',
260 | option_name: 'character',
261 | option_value: 'tsukasa'
262 | }
263 | ]
264 | export { preset }
--------------------------------------------------------------------------------
/models/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import axiosRetry from 'axios-retry'
3 |
4 | import { Config, Version } from '#components'
5 |
6 | class Request {
7 | axiosInstance
8 |
9 | constructor () {
10 | this.axiosInstance = axios.create({
11 | timeout: Config.server.timeout * 1000,
12 | headers: {
13 | 'User-Agent': `${Version.Plugin_Name}/v${Version.Plugin_Version}`
14 | }
15 | })
16 |
17 | // 配置重试机制
18 | axiosRetry(this.axiosInstance, {
19 | retries: Config.server.retry,
20 | retryDelay: () => 0,
21 | shouldResetTimeout: true,
22 | retryCondition: (error) => {
23 | if (error.response) {
24 | return error.response.status === 500
25 | }
26 | return axiosRetry.isNetworkOrIdempotentRequestError(error)
27 | }
28 | })
29 | }
30 |
31 | /**
32 | * 发送请求
33 | * @param {'get' | 'post' | 'head'} method 请求方法 get post head
34 | * @param {string} url 请求地址
35 | * @param {any} data 请求数据
36 | * @param {Record | null} params 请求参数
37 | * @param {Record | null} headers 请求头
38 | * @param {'json' | 'arraybuffer'} responseType 响应类型
39 | * @returns 响应数据
40 | */
41 | async request (
42 | method,
43 | url,
44 | data,
45 | params,
46 | headers,
47 | responseType = 'json'
48 | ) {
49 | const config = {
50 | params,
51 | headers,
52 | responseType
53 | }
54 |
55 | try {
56 | let response
57 | switch (method.toLowerCase()) {
58 | case 'get':
59 | response = await this.axiosInstance.get(url, config)
60 | break
61 | case 'post':
62 | response = await this.axiosInstance.post(url, data, config)
63 | break
64 | case 'head':
65 | response = await this.axiosInstance.head(url, config)
66 | return {
67 | success: response.status >= 200 && response.status < 500,
68 | statusCode: response.status,
69 | data: Object.fromEntries(
70 | Object.entries(response.headers).map(([ k, v ]) => [ k.toLowerCase(), v ])
71 | ),
72 | msg: response.status >= 200 && response.status < 500 ? '请求成功' : '请求失败'
73 | }
74 | default:
75 | throw new Error('暂不支持该请求方法')
76 | }
77 | return {
78 | success: response.status >= 200 && response.status < 500,
79 | statusCode: response.status,
80 | data: response.data,
81 | msg: response.status >= 200 && response.status < 500 ? '请求成功' : '请求失败'
82 | }
83 | } catch (error) {
84 | logger.error(error)
85 | const axiosError = error
86 | const errorMessage = this.handleError(axiosError)
87 | return {
88 | success: false,
89 | statusCode: axiosError.response?.status ?? 500,
90 | data: null,
91 | msg: errorMessage
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * 发送 GET 请求
98 | * @param {string} url 请求地址
99 | * @param {Record | null} params 请求参数
100 | * @param {Record | null} headers 请求头
101 | * @param {'json' | 'arraybuffer'}responseType 响应类型
102 | * @returns 响应数据
103 | */
104 | async get (
105 | url,
106 | params,
107 | headers,
108 | responseType = 'json'
109 | ) {
110 | return this.request('get', url, null, params, headers, responseType)
111 | }
112 |
113 | /**
114 | * 发送 HEAD 请求
115 | * @param {string} url 请求地址
116 | * @param {Record | null} params 请求参数
117 | * @param {Record | null} headers 请求头
118 | * @param {'json' | 'arraybuffer'}responseType 响应类型
119 | * @returns 响应数据
120 | */
121 | async head (
122 | url,
123 | params,
124 | headers,
125 | responseType = 'json'
126 | ) {
127 | return this.request('head', url, null, params, headers, responseType)
128 | }
129 |
130 | /**
131 | * 发送 POST 请求
132 | * @param {string} url 请求地址
133 | * @param {any} data 请求数据
134 | * @param {Record | null} headers 请求头
135 | * @param {'json' | 'arraybuffer'} responseType 响应类型
136 | * @returns 响应数据
137 | */
138 | async post (
139 | url,
140 | data,
141 | headers,
142 | responseType = 'json'
143 | ) {
144 | return this.request('post', url, data, null, headers, responseType)
145 | }
146 |
147 | /**
148 | * 处理错误
149 | * @param error 错误对象
150 | * @returns 错误信息
151 | */
152 | handleError (error) {
153 | if (axios.isAxiosError(error)) {
154 | let errorMessage
155 |
156 | if (error.code === 'ECONNABORTED') {
157 | errorMessage = '请求超时,请检查网络连接'
158 | } else if (error.code === 'ERR_NETWORK') {
159 | errorMessage = '网络连接异常, 请检查网络连接'
160 | } else if (error.response?.data) {
161 | if (Buffer.isBuffer(error.response.data)) {
162 | errorMessage = error.response.data.toString()
163 | } else if (typeof error.response.data === 'string') {
164 | errorMessage = error.response.data
165 | } else {
166 | errorMessage = JSON.stringify(error.response.data)
167 | }
168 | } else if (error.response?.statusText) {
169 | errorMessage = error.response.statusText.toString()
170 | } else if (error.message) {
171 | errorMessage = error.message
172 | } else {
173 | errorMessage = '未知网络错误'
174 | }
175 |
176 | return errorMessage
177 | } else {
178 | return error?.message ?? error
179 | }
180 | }
181 | }
182 |
183 | export default new Request()
--------------------------------------------------------------------------------
/models/utils/tools.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 |
3 | import { db, imageTool } from '#models'
4 |
5 | import { get_base_url } from './common.js'
6 | import { preset } from './preset.js'
7 | import Request from './request.js'
8 |
9 | /**
10 | * 异步判断文件是否存在
11 | * @param {string} path
12 | * @returns {Promise} 是否存在,true存在,false不存在
13 | */
14 | export async function exists (path) {
15 | try {
16 | await fs.access(path)
17 | return true
18 | } catch (e) {
19 | return false
20 | }
21 | }
22 |
23 |
24 | /** 初始化数据 */
25 | export async function init () {
26 | await update_meme()
27 | await update_preset()
28 | }
29 |
30 | /**
31 | * 更新表情数据
32 | * @param {boolean} force 是否强制更新
33 | * @returns 初始化结果
34 | */
35 | export async function update_meme (force = false) {
36 | try {
37 | const keys = await get_meme_all_keys()
38 | if (keys && keys.length > 0 && !force) return
39 | const url = await get_base_url()
40 | const res = await Request.get(`${url}/meme/infos`)
41 | if (!res.success) throw new Error(res.msg)
42 | if (res.data && Array.isArray(res.data)) {
43 | await Promise.all(res.data.map(meme => {
44 | const {
45 | key,
46 | keywords: keyWords,
47 | params: {
48 | min_texts,
49 | max_texts,
50 | min_images,
51 | max_images,
52 | default_texts,
53 | options
54 | },
55 | tags
56 | } = meme
57 |
58 | return add_meme({
59 | key,
60 | keyWords: keyWords?.length ? keyWords : null,
61 | min_texts,
62 | max_texts,
63 | min_images,
64 | max_images,
65 | default_texts: default_texts?.length ? default_texts : null,
66 | options: options?.length ? options : null,
67 | tags: tags?.length ? tags : null
68 | }, {
69 | force
70 | })
71 | }))
72 | }
73 | } catch (error) {
74 | logger.error(`初始化表情数据失败: ${error}`)
75 | }
76 | }
77 |
78 | /**
79 | * 更新预设数据
80 | * @param {boolean} force 是否强制更新
81 | * @returns 初始化结果
82 | */
83 | export async function update_preset (force = false) {
84 | try {
85 | const keys = await get_preset_all_keys()
86 | if (keys && keys.length > 0 && !force) return
87 | await Promise.all(
88 | preset.map(async (preset) => {
89 | const memeExists = await db.meme.get(preset.key)
90 | if (!memeExists && !force) return
91 | await db.preset.add({
92 | name: preset.name,
93 | key: preset.key,
94 | option_name: preset.option_name,
95 | option_value: preset.option_value
96 | }, {
97 | force
98 | })
99 | })
100 | )
101 | } catch (error) {
102 | logger.error(`初始化预设数据失败: ${error}`)
103 | }
104 | }
105 |
106 | /**
107 | * 添加表情
108 | * @param data 表情数据
109 | * - key 表情的唯一标识符
110 | * - keyWords 表情的关键词列表
111 | * - min_texts 表情最少的文本数
112 | * - max_texts 表情最多的文本数
113 | * - min_images 表情最少的图片数
114 | * - max_images 表情最多的图片数
115 | * - default_texts 表情的默认文本列表
116 | * - options 表情的参数类型
117 | * - tags 表情的标签列表
118 | * @param force 是否强制更新
119 | * @returns 添加结果
120 | */
121 | export async function add_meme ({
122 | key,
123 | keyWords,
124 | min_texts,
125 | max_texts,
126 | min_images,
127 | max_images,
128 | default_texts,
129 | options,
130 | tags
131 | }, {
132 | force = false
133 | }) {
134 | const data = {
135 | key,
136 | keyWords,
137 | min_texts,
138 | max_texts,
139 | min_images,
140 | max_images,
141 | default_texts,
142 | options,
143 | tags
144 | }
145 | return await db.meme.add(data, { force })
146 | }
147 |
148 | /**
149 | * 获取所有预设的键值信息
150 | * @returns {Promise} 键值信息列表
151 | */
152 | export async function get_preset_all_keys () {
153 | const res = await db.preset.getAll()
154 | return res.map(preset => preset.key).flat() ?? null
155 | }
156 |
157 | /**
158 | * 获取所有预设表情的关键词信息
159 | * @returns {Promise} 关键词信息列表
160 | */
161 | export async function get_preset_all_keywords () {
162 | const res = await db.preset.getAll()
163 | return res.map(preset => preset.name).flat() ?? null
164 | }
165 |
166 | /**
167 | * 通过关键词获取预设表情的键值
168 | * @param keyword 关键词
169 | * @returns 键值
170 | */
171 | export async function get_preset_key (keyword) {
172 | const res = await get_preset_info_by_keyword(keyword)
173 | if (!res) return null
174 | return res.key
175 | }
176 |
177 | /**
178 | * 通过表情的键值获取预设表情的关键词
179 | * @param key 表情的唯一标识符
180 | * @returns 预设表情信息
181 | */
182 | export async function get_preset_keyword (key) {
183 | const res = await db.preset.getAll()
184 | const filteredOptions = res
185 | .filter(preset => preset.key === key)
186 | .map(preset => preset.name)
187 | return filteredOptions.length > 0 ? filteredOptions : null
188 | }
189 | /**
190 | * 获取指定的预设表情信息
191 | * @param {string} key 表情的唯一标识符
192 | * @returns {Promise} 预设表情信息
193 | */
194 | export async function get_preset_info (key) {
195 | return await db.preset.get(key)
196 | }
197 |
198 | /**
199 | * 通过关键词获取预设表情信息
200 | * @param keyword 表情关键词
201 | * @returns 预设表情信息
202 | */
203 | export async function get_preset_info_by_keyword (keyword) {
204 | return await db.preset.getByKeyWord(keyword)
205 | }
206 |
207 | /**
208 | * 获取所有相关预设表情的键值
209 | * @param keyword 表情的关键词
210 | * @returns 所有相关预设表情的键值列表
211 | */
212 | export async function get_preset_all_about_keywords (keyword) {
213 | const res = await db.preset.getByKeyWordAbout(keyword)
214 | return res.map(preset => preset.name).flat() ?? null
215 | }
216 |
217 | /**
218 | * 获取所有相关预设表情的关键词
219 | * @param key 表情的唯一标识符
220 | * @returns 所有相关预设表情的键值列表
221 | */
222 | export async function get_preset_all_about_keywords_by_key (key) {
223 | const res = await db.preset.getAbout(key)
224 | return res.map(preset => preset.name).flat() ?? null
225 | }
226 |
227 | /**
228 | * 获取所有表情的键值信息
229 | * @returns 键值信息列表
230 | */
231 | export async function get_meme_all_keys () {
232 | const res = await db.meme.getAll()
233 | return res.map(meme => meme.key).flat() ?? null
234 | }
235 |
236 | /**
237 | * 通过关键词获取表情键值
238 | * @param {string} keyword 表情关键词
239 | * @returns {Promise} 表情键值
240 | */
241 | export async function get_meme_key_by_keyword (keyword) {
242 | const res = await get_meme_info_by_keyword(keyword)
243 | if (!res) return null
244 | return res.key
245 | }
246 |
247 | /**
248 | * 通过键值获取表情的标签信息
249 | * @param {string} tag 表情的tag
250 | * @returns 表情的标签信息
251 | */
252 | export async function get_meme_key_by_tag (tag) {
253 | const res = await db.meme.getByTag(tag)
254 | if (!res) return null
255 | return JSON.parse(String(res.key))
256 | }
257 |
258 | /**
259 | * 获取所有所有相关表情的键值
260 | * @param {string} key 表情的唯一标识符
261 | * @returns {Promise} 所有相关表情的键值列表
262 | */
263 | export async function get_meme_keys_by_about (key) {
264 | const res = await db.meme.getKeysByAbout(key)
265 | return res.map(meme => meme.key).flat() ?? null
266 | }
267 |
268 | /**
269 | * 获取所有所有相关表情的键值
270 | * @param {string} tag 表情的标签
271 | * @returns 所有相关表情的键值列表
272 | */
273 | export async function get_meme_keys_by_about_tag (tag) {
274 | const res = await db.meme.getTagsByAbout(tag)
275 | return res.map(meme => meme.key).flat() ?? null
276 | }
277 |
278 | /**
279 | * 获取所有表情的关键词信息
280 | * @returns {Promise } 关键词信息列表
281 | */
282 | export async function get_meme_all_keywords () {
283 | const res = await db.meme.getAll()
284 | return res.map((item) => JSON.parse(String(item.keyWords))).flat() ?? null
285 | }
286 |
287 | /**
288 | * 通过键值获取表情的关键词信息
289 | * @param {string} key 表情的唯一标识符
290 | * @returns {Promise } 表情的关键词信息
291 | */
292 | export async function get_meme_keyword (key) {
293 | const res = await get_meme_info(key)
294 | if (!res) return null
295 | return JSON.parse(String(res.keyWords))
296 | }
297 |
298 | /**
299 | * 通过关键词获取表情的标签信息
300 | * @param {string} tag 表情的标签
301 | * @returns 表情的标签信息
302 | */
303 | export async function get_meme_keyword_by_tag (tag) {
304 | const res = await db.meme.getByTag(tag)
305 | if (!res) return null
306 | return JSON.parse(String(res.keyWords))
307 | }
308 |
309 | /**
310 | * 获取所有相关表情的关键词信息
311 | * @param {string} keyword 表情关键词
312 | * @returns {Promise} 所有相关表情的关键词列表
313 | */
314 | export async function get_meme_keywords_by_about (keyword) {
315 | const res = await db.meme.getKeyWordsByAbout(keyword)
316 | return res.map((item) => JSON.parse(String(item.keyWords))).flat() ?? null
317 | }
318 |
319 | /**
320 | * 获取所有相关表情的关键词信息
321 | * @param {string} tag 表情的标签
322 | * @returns 所有相关表情的关键词列表
323 | */
324 | export async function get_meme_keywords_by_about_tag (tag) {
325 | const res = await db.meme.getTagsByAbout(tag)
326 | return res.map((item) => JSON.parse(String(item.keyWords))).flat() ?? null
327 | }
328 |
329 | /**
330 | * 获取所有表情的标签信息
331 | * @returns 标签信息列表
332 | */
333 | export async function get_meme_all_tags () {
334 | const res = await db.meme.getAll()
335 | return res.map((item) => JSON.parse(String(item.tags))).flat() ?? null
336 | }
337 | /**
338 | * 获取表情信息
339 | * @param {string} key 表情唯一标识符
340 | * @returns {Promise} 表情信息
341 | */
342 | export async function get_meme_info (key) {
343 | return await db.meme.get(key) ?? null
344 | }
345 |
346 | /**
347 | * 通过关键词获取表情信息
348 | * @param {string} keyword 表情关键词
349 | * @returns {Promise } 表情信息
350 | */
351 | export async function get_meme_info_by_keyword (keyword) {
352 | return await db.meme.getByKeyWord(keyword) ?? null
353 | }
354 |
355 | /**
356 | * 上传图片
357 | * @param {Buffer | string} image 图片数据
358 | * @param {'url' | 'path' | 'data'} type 上传的图片类型
359 | * - url 图片的网络地址
360 | * - path 图片的本地路径
361 | * - data 图片的base64数据
362 | * @param {Record} headers 请求头,仅在type为url时生效
363 | * @returns {Promise} image_id 图片的唯一标识符
364 | */
365 | export async function upload_image (
366 | image,
367 | type = 'url',
368 | headers
369 | ) {
370 | try {
371 | const url = await get_base_url()
372 | let data
373 | switch (type) {
374 | case 'url':
375 | data = {
376 | type: 'url',
377 | url: image,
378 | ...(headers && { headers })
379 | }
380 | break
381 | case 'path':
382 | data = {
383 | type: 'path',
384 | path: image
385 | }
386 | break
387 | case 'data':
388 | data = {
389 | type: 'data',
390 | data: Buffer.isBuffer(image) ? image.toString('base64') : image
391 | }
392 | break
393 | }
394 | const res = await Request.post(`${url}/image/upload`, data, {}, 'json')
395 | if (!res.success) throw new Error('图片上传失败')
396 | return res.data.image_id
397 | } catch (error) {
398 | logger.error(error)
399 | throw new Error(error.message)
400 | }
401 | }
402 | /**
403 | * 获取表情预览地址
404 | * @param {string} key 表情唯一标识符
405 | * @returns {Promise} 表情数据
406 | */
407 | export async function get_meme_preview (key) {
408 | try {
409 | const url = await get_base_url()
410 | const res = await Request.get(`${url}/memes/${key}/preview`)
411 | if (!res.success) throw new Error(res.msg)
412 | const image = await imageTool.get_image(res.data.image_id, 'base64')
413 | return image
414 | } catch (error) {
415 | logger.error(error)
416 | throw new Error(error.message)
417 | }
418 | }
419 |
420 | /**
421 | * 生成表情图片
422 | * @param {string} memekey 表情唯一标识符
423 | * @param {any} data 表情数据
424 | * @returns {Promise} 表情图片数据
425 | */
426 | export async function make_meme (memekey, data) {
427 | try {
428 | const url = await get_base_url()
429 | const res = await Request.post(`${url}/memes/${memekey}`, data, null, 'json')
430 | if (!res.success) {
431 | throw new Error(res.msg)
432 | }
433 | const image = await imageTool.get_image(res.data.image_id)
434 | if (!image) throw new Error('获取图片失败')
435 | return image
436 | } catch (error) {
437 | logger.error(error)
438 | throw new Error(error.message)
439 | }
440 | }
441 |
442 | /**
443 | * 向指定的群或好友发送文件
444 | * @param type 发送的类型
445 | * - group 为群
446 | * - private 为好友
447 | * @param botId 机器人的id
448 | * @param id 群或好友的id
449 | * @param file 文件路径
450 | * @param name 文件名称
451 | * @returns 发送结果
452 | */
453 | export async function send_file (type, botId, id, file, name) {
454 | try {
455 | const bot = Bot[botId]
456 | if (type === 'group') {
457 | await bot.pickGroup(id).fs.upload(file, '/', name)
458 | } else if (type === 'private') {
459 | await bot.pickFriend(id).sendMsg([ segment.file(file, name) ])
460 | } else {
461 | throw new Error('type 必须为 group 或 private')
462 | }
463 | } catch (error) {
464 | throw new Error(`向${type === 'group' ? '群' : '好友'} ${id} 发送文件失败: ${error.message}`)
465 | }
466 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meme-plugin",
3 | "version": "2.0.1",
4 | "author": "CandriaJS",
5 | "type": "module",
6 | "dependencies": {
7 | "adm-zip": "^0.5.16",
8 | "axios": "^1.8.4",
9 | "axios-retry": "4.5.0",
10 | "markdown-it": "^14.1.0",
11 | "sequelize": "^6.37.7",
12 | "smol-toml": "^1.3.4",
13 | "sqlite3": "5.1.6"
14 | },
15 | "devDependencies": {
16 | "eslint": "^9.25.0",
17 | "eslint-plugin-simple-import-sort": "^12.1.1",
18 | "globals": "^15.15.0",
19 | "neostandard": "^0.12.1"
20 | },
21 | "scripts": {
22 | "lint": "eslint .",
23 | "lint:fix": "eslint . --fix"
24 | },
25 | "imports": {
26 | "#components": "./components/index.js",
27 | "#models": "./models/index.js"
28 | },
29 | "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a"
30 | }
31 |
--------------------------------------------------------------------------------
/resources/admin/imgs/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/admin/imgs/bg.webp
--------------------------------------------------------------------------------
/resources/admin/imgs/cfg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/admin/imgs/cfg.webp
--------------------------------------------------------------------------------
/resources/admin/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | transform: scale(1);
3 | width: 660px;
4 | }
5 | .container {
6 | background: url("./imgs/bg.webp") #000144 left top repeat-y;
7 | background-size: 700px auto;
8 | width: 660px;
9 | }
10 | .head-box {
11 | margin: 0 0 80px 0;
12 | }
13 | .cfg-box {
14 | border-radius: 15px;
15 | margin-top: 20px;
16 | margin-bottom: 20px;
17 | padding: 5px 15px;
18 | overflow: hidden;
19 | background: #f5f5f5;
20 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
21 | position: relative;
22 | background: rgba(35, 38, 57, 0.8);
23 | }
24 | .cfg-group {
25 | color: #ceb78b;
26 | font-size: 18px;
27 | font-weight: bold;
28 | padding: 10px 20px;
29 | }
30 | .cfg-li {
31 | border-radius: 18px;
32 | min-height: 36px;
33 | position: relative;
34 | overflow: hidden;
35 | margin-bottom: 10px;
36 | background: rgba(203, 196, 190, 0);
37 | }
38 | .cfg-line {
39 | color: #4e5769;
40 | line-height: 36px;
41 | padding-left: 20px;
42 | font-weight: bold;
43 | border-radius: 16px;
44 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
45 | background: url("./imgs/cfg.webp") right top #cbc4be no-repeat;
46 | background-size: auto 36px;
47 | }
48 | .cfg-hint {
49 | font-size: 12px;
50 | font-weight: normal;
51 | margin-top: 3px;
52 | margin-bottom: -3px;
53 | }
54 | .cfg-status {
55 | position: absolute;
56 | top: 0;
57 | right: 0;
58 | height: 36px;
59 | width: 160px;
60 | text-align: center;
61 | line-height: 36px;
62 | font-size: 16px;
63 | color: #495366;
64 | font-weight: bold;
65 | border-radius: 0 16px 16px 0;
66 | }
67 | .cfg-status.status-off {
68 | color: #a95151;
69 | }
70 | .cfg-desc {
71 | font-size: 12px;
72 | color: #cbc4be;
73 | margin: 5px 0 5px 20px;
74 | }
75 | /*# sourceMappingURL=index.css.map */
76 |
--------------------------------------------------------------------------------
/resources/admin/index.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}}
2 | {{block 'css'}}
3 |
4 | {{/block}}
5 | {{block 'main'}}
6 |
7 |
8 |
9 |
{{title}}管理面板
10 |
#{{title}}设置
11 |
12 |
13 |
14 | {{each schema cfgGroup cfgKey}}
15 |
16 |
{{cfgGroup.title}}
17 |
18 | {{each cfgGroup.cfg cfgItem cfgItemKey}}
19 | -
20 |
21 |
22 | #{{title}}设置 {{cfgGroup.title}} {{cfgItem.title}}
23 | {{if cfgItem.type === 'number'}} 数值范围:{{cfgItem.limit}}
24 | {{else if cfgItem.type === 'string'}} 请输入文本
25 | {{else if cfgItem.type === 'array'}} 添加/删除项目
26 | {{else}} 开启/关闭
27 | {{/if}}
28 |
29 |
34 | {{if cfgItem.type === 'number'}}
35 | {{cfg[cfgKey][cfgItemKey] || '未设置'}}
36 | {{else if cfgItem.type === 'string'}}
37 | {{if cfg[cfgKey][cfgItemKey]}}已设置{{else}}未设置{{/if}}
38 | {{else if cfgItem.type === 'array'}}
39 | {{if cfg[cfgKey][cfgItemKey] && cfg[cfgKey][cfgItemKey].length > 0}}
40 | 已配置 {{cfg[cfgKey][cfgItemKey].length}} 项
41 | {{else}}
42 | 未配置
43 | {{/if}}
44 | {{else}}
45 | {{if cfg[cfgKey][cfgItemKey]}}已开启{{else}}已关闭{{/if}}
46 | {{/if}}
47 |
48 |
49 | {{if cfgItem.desc}}
50 | {{cfgItem.desc}}
51 | {{/if}}
52 |
53 | {{/each}}
54 |
55 |
56 | {{/each}}
57 |
58 | {{/block}}
59 |
--------------------------------------------------------------------------------
/resources/code/imgs/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/code/imgs/bg.webp
--------------------------------------------------------------------------------
/resources/code/index.css:
--------------------------------------------------------------------------------
1 | .container {
2 | background-image: url('./imgs/bg.webp');
3 | background-size: cover;
4 | background-repeat: no-repeat;
5 | background-position: center;
6 | }
7 | .info_box {
8 | padding: 20px;
9 | background: linear-gradient(135deg, rgba(50, 50, 70, 0.95), rgba(35, 38, 57, 0.95));
10 | border-radius: 16px;
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | box-sizing: border-box;
15 | min-width: 90%;
16 | max-width: 100%;
17 | margin: 20px auto;
18 | border: 1px solid rgba(255, 255, 255, 0.15);
19 | font-family: 'Arial', sans-serif;
20 | }
21 |
22 | .label {
23 | font-size: 30px;
24 | font-weight: bold;
25 | color: #ffffff;
26 | text-align: center;
27 | margin-bottom: 16px;
28 | text-shadow: 0 3px 6px rgba(0, 0, 0, 0.5);
29 | letter-spacing: 1px;
30 | }
31 |
32 | .total {
33 | font-size: 20px;
34 | font-weight: 500;
35 | color: #ffffff;
36 | text-align: center;
37 | margin-bottom: 16px;
38 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
39 | }
40 |
41 | .cfg-box {
42 | display: flex;
43 | flex-direction: column;
44 | gap: 12px;
45 | background: rgba(255, 255, 255, 0.1);
46 | border-radius: 12px;
47 | padding: 20px;
48 | width: 100%;
49 | box-sizing: border-box;
50 | border: 1px solid rgba(255, 255, 255, 0.2);
51 | }
52 |
53 | .cfg-ul {
54 | list-style: none;
55 | padding: 0;
56 | margin: 0;
57 | display: flex;
58 | flex-direction: column;
59 | gap: 16px;
60 | }
61 |
62 | .cfg-li {
63 | display: flex;
64 | flex-direction: column;
65 | gap: 8px;
66 | padding: 12px 16px;
67 | background: rgba(255, 255, 255, 0.2);
68 | border-radius: 8px;
69 | border: 1px solid rgba(255, 255, 255, 0.2);
70 | }
71 |
72 | .cfg-li span:first-child {
73 | font-size: 16px;
74 | font-weight: bold;
75 | color: #b0c4de;
76 | }
77 |
78 | .cfg-li span:last-child {
79 | font-size: 18px;
80 | font-weight: 400;
81 | color: #ffffff;
82 | line-height: 1.6;
83 | word-break: break-word;
84 | white-space: pre-wrap;
85 | }
86 |
87 | .cfg-li span:last-child.highlight {
88 | color: #ffd700;
89 | font-weight: 600;
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/resources/code/index.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}}
2 |
3 | {{block 'css'}}
4 |
5 | {{/block}}
6 |
7 | {{block 'main'}}
8 |
9 |
柠糖表情更新推送
10 |
当前分支:{{branchName}}
11 |
12 |
13 | -
14 | 提交者:
15 | {{commitInfo.committer}}
16 |
17 | -
18 | 提交时间:
19 | {{commitInfo.commitTime}}
20 |
21 | -
22 | 提交标题:
23 | {{commitInfo.title}}
24 |
25 | -
26 | 提交内容:
27 | {{commitInfo.content}}
28 |
29 |
30 |
31 |
32 | {{/block}}
33 |
--------------------------------------------------------------------------------
/resources/common/common.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Number";
3 | src: url("https://static.wuliya.cn/static/font/TTTGB-Number.woff") format("woff");
4 | font-display: swap
5 | }
6 | @font-face {
7 | font-family: "YS";
8 | src: url("https://static.wuliya.cn/static/font/HYWH.woff") format("woff");
9 | font-display: swap
10 | }
11 | @font-face {
12 | font-family: "Helvetica";
13 | src: url("https://static.wuliya.cn/static/font/HELVETI.woff") format("woff");
14 | font-display: swap
15 | }
16 | @font-face {
17 | font-family: "NotoColorEmoji";
18 | src: url("https://static.wuliya.cn/static/font/NotoColorEmoji-Regular.ttf") format("truetype");
19 | font-display: swap
20 | }
21 |
22 | * {
23 | margin: 0;
24 | padding: 0;
25 | box-sizing: border-box;
26 | -webkit-user-select: none;
27 | -moz-user-select: none;
28 | user-select: none;
29 | }
30 |
31 | body {
32 | color: #1e1f20;
33 | font-family: Number, YS, PingFangSC-Medium, NotoColorEmoji, sans-serif;
34 | transform-origin: 0 0;
35 | width: 100%;
36 | }
37 |
38 | .container {
39 | width: 1800px;
40 | padding: 20px 15px 10px 15px;
41 | background-size: contain;
42 | }
43 |
44 | .head-box {
45 | border-radius: 15px;
46 | padding: 10px 20px;
47 | position: relative;
48 | color: #fff;
49 | margin-top: 30px;
50 | }
51 |
52 | .head-box .title {
53 | font-family: Helvetica;
54 | font-style: italic;
55 | font-weight: bold;
56 | font-size: 36px;
57 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, 0.9);
58 | }
59 |
60 | .head-box .title .label {
61 | display: inline-block;
62 | margin-left: 10px;
63 | }
64 |
65 | .head-box .genshin_logo {
66 | position: absolute;
67 | top: 1px;
68 | right: 15px;
69 | width: 97px;
70 | }
71 |
72 | .head-box .label {
73 | font-size: 16px;
74 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, 0.9);
75 | }
76 |
77 | .head-box .label span {
78 | color: #d3bc8e;
79 | padding: 0 2px;
80 | }
81 |
82 | .notice {
83 | color: #888;
84 | font-size: 12px;
85 | text-align: right;
86 | padding: 12px 5px 5px;
87 | }
88 |
89 | .notice-center {
90 | color: #fff;
91 | text-align: center;
92 | margin-bottom: 10px;
93 | text-shadow: 1px 1px 1px #333;
94 | }
95 |
96 | .copyright {
97 | font-size: 15px;
98 | font-family: Helvetica;
99 | font-weight: bold;
100 | text-align: center;
101 | color: #fff;
102 | position: relative;
103 | padding-left: 10px;
104 | text-shadow: 1px 1px 1px #000;
105 | margin: 10px 0;
106 | }
107 | .copyright .version {
108 | color: #d3bc8e;
109 | display: inline-block;
110 | padding: 0 3px;
111 | }
112 |
113 | .copyright .commit_id_old {
114 | color: #808080;
115 | }
116 |
117 | .copyright .tip {
118 | color: #ff0000;
119 | }
120 |
--------------------------------------------------------------------------------
/resources/common/layout/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{_Plugin_AliasName}}
14 | {{block 'css'}} {{/block}}
15 |
16 |
17 |
18 |
19 | {{block 'main'}}{{/block}}
20 |
{{@copyright}}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/resources/help/icon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/help/icon.webp
--------------------------------------------------------------------------------
/resources/help/index.css:
--------------------------------------------------------------------------------
1 | .head-box {
2 | margin: 60px 0 0 0;
3 | padding-bottom: 0;
4 | }
5 | .head-box .title {
6 | font-size: 50px;
7 | }
8 | .cont-box {
9 | border-radius: 15px;
10 | margin-top: 20px;
11 | margin-bottom: 20px;
12 | overflow: hidden;
13 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
14 | position: relative;
15 | }
16 | .help-group {
17 | font-size: 18px;
18 | font-weight: bold;
19 | padding: 15px 15px 10px 20px;
20 | }
21 | .help-table {
22 | text-align: center;
23 | border-collapse: collapse;
24 | margin: 0;
25 | border-radius: 0 0 10px 10px;
26 | display: table;
27 | overflow: hidden;
28 | width: 100%;
29 | color: #fff;
30 | }
31 | .help-table .tr {
32 | display: table-row;
33 | }
34 | .help-table .td,
35 | .help-table .th {
36 | font-size: 14px;
37 | display: table-cell;
38 | box-shadow: 0 0 1px 0 #888 inset;
39 | padding: 12px 0 12px 50px;
40 | line-height: 24px;
41 | position: relative;
42 | text-align: left;
43 | }
44 | .help-table .tr:last-child .td {
45 | padding-bottom: 12px;
46 | }
47 | .help-table .th {
48 | background: rgba(34, 41, 51, 0.5);
49 | }
50 | .help-icon {
51 | width: 40px;
52 | height: 40px;
53 | display: block;
54 | position: absolute;
55 | background: url("icon.webp") 0 0 no-repeat;
56 | background-size: 500px auto;
57 | border-radius: 5px;
58 | left: 6px;
59 | top: 12px;
60 | transform: scale(0.85);
61 | }
62 | .help-title {
63 | display: block;
64 | color: #d3bc8e;
65 | font-size: 16px;
66 | line-height: 24px;
67 | }
68 | .help-desc {
69 | display: block;
70 | color: #eee;
71 | font-size: 13px;
72 | line-height: 18px;
73 | }
74 | /*# sourceMappingURL=index.css.map */
75 |
--------------------------------------------------------------------------------
/resources/help/index.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}} {{block 'css'}}
2 |
3 | {{@style}}
4 | {{/block}}
5 | {{block 'main'}}
6 |
7 |
8 |
9 |
{{helpCfg.title}}
10 |
{{helpCfg.subTitle}}
11 |
12 |
13 |
14 | {{each helpGroup group}} {{set len = group?.list?.length || 0 }}
15 |
16 |
{{group.group}}
17 | {{if len > 0}}
18 |
19 |
20 | {{each group.list help idx}}
21 |
22 | {{/if}}
23 | {{help.title}}
24 | {{help.desc}}
25 |
26 | {{if idx%colCount === colCount-1 && idx>0 && idx< len-1}}
27 |
28 |
29 | {{/if}}
30 | {{/each}}
31 | {{set remainder = (colCount - (len % colCount)) % colCount}}
32 | {{if remainder > 0}}
33 | {{each [...Array(remainder)] item}}
34 |
35 | {{/each}}
36 | {{/if}}
37 |
38 |
39 | {{/if}}
40 |
41 |
42 | {{/each}} {{/block}}
43 |
--------------------------------------------------------------------------------
/resources/help/theme/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/help/theme/bg.webp
--------------------------------------------------------------------------------
/resources/help/theme/main.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/help/theme/main.webp
--------------------------------------------------------------------------------
/resources/help/version-info.css:
--------------------------------------------------------------------------------
1 |
2 | .markdown-body {
3 | box-sizing: border-box;
4 | min-width: 200px;
5 | margin: 0 auto;
6 | padding: 45px;
7 | }
8 |
9 | @media (max-width: 767px) {
10 | .markdown-body {
11 | padding: 15px;
12 | }
13 | }
--------------------------------------------------------------------------------
/resources/help/version-info.html:
--------------------------------------------------------------------------------
1 |
2 | {{block 'css'}}
3 |
8 |
13 | {{/block}}
14 | {{block 'main'}}
15 |
16 | {{@Markdown}}
17 |
18 | {{/block}}
19 |
--------------------------------------------------------------------------------
/resources/list/imgs/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/list/imgs/bg.webp
--------------------------------------------------------------------------------
/resources/list/imgs/icons/image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/list/imgs/icons/option.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/list/imgs/icons/text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/list/index.css:
--------------------------------------------------------------------------------
1 | .container {
2 | background-image: url('./imgs/bg.webp');
3 | background-size: 100% 100%;
4 | background-repeat: no-repeat;
5 | background-position: center;
6 | background-attachment: fixed;
7 | min-height: 100vh;
8 | }
9 |
10 | .meme-container {
11 | max-width: 1600px;
12 | margin: 0 auto;
13 | padding: 2rem;
14 | font-family: 'Helvetica Neue', Arial, sans-serif;
15 | }
16 |
17 | .meme-header {
18 | text-align: center;
19 | margin-bottom: 2rem;
20 | padding: 1.5rem;
21 | background: rgba(255, 255, 255, 0.9);
22 | border-radius: 12px;
23 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
24 | }
25 |
26 | .meme-title {
27 | font-size: 2rem;
28 | font-weight: 600;
29 | color: #333;
30 | margin-bottom: 0.5rem;
31 | }
32 |
33 | .meme-count {
34 | font-size: 1rem;
35 | color: #666;
36 | }
37 |
38 | .meme-grid {
39 | display: grid;
40 | grid-template-columns: repeat(3, 1fr);
41 | gap: 1.5rem;
42 | padding: 1rem;
43 | }
44 |
45 | .meme-column {
46 | background: white;
47 | border-radius: 10px;
48 | padding: 1rem;
49 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
50 | }
51 |
52 | .meme-item {
53 | display: flex;
54 | justify-content: space-between;
55 | align-items: center;
56 | padding: 0.85rem 1.2rem;
57 | margin-bottom: 0.75rem;
58 | background: #f8f9fa;
59 | border-radius: 8px;
60 | overflow: hidden;
61 | }
62 |
63 | .meme-name {
64 | font-size: 1rem;
65 | color: #333;
66 | white-space: nowrap;
67 | overflow: hidden;
68 | text-overflow: ellipsis;
69 | margin-right: 1rem;
70 | max-width: calc(100% - 80px);
71 | }
72 |
73 | .meme-icons {
74 | display: flex;
75 | gap: 0.75rem;
76 | flex-shrink: 0;
77 | }
78 |
79 |
80 | .meme-name {
81 | font-size: 0.95rem;
82 | color: #333;
83 | }
84 |
85 | .meme-icons {
86 | display: flex;
87 | gap: 0.5rem;
88 | }
89 |
90 | .icon {
91 | width: 18px;
92 | height: 18px;
93 | opacity: 0.7;
94 | }
95 |
96 | .meme-empty {
97 | text-align: center;
98 | color: #666;
99 | padding: 2rem;
100 | }
101 |
--------------------------------------------------------------------------------
/resources/list/index.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}}
2 |
3 | {{block 'css'}}
4 |
5 | {{/block}}
6 |
7 | {{block 'main'}}
8 |
9 |
13 |
14 |
15 | {{if memeList.length > 0}}
16 | {{each memeList item index}}
17 | {{if index % 30 === 0}}
18 | {{if index !== 0}}
{{/if}}
19 |
20 |
21 | {{/if}}
22 | -
23 |
24 | {{index + 1}}. {{item.name || '未知表情'}}
25 |
26 |
27 | {{if item.types && item.types.includes('text')}}
28 |

29 | {{/if}}
30 | {{if item.types && item.types.includes('image')}}
31 |

32 | {{/if}}
33 | {{if item.types && item.types.includes('option')}}
34 |

35 | {{/if}}
36 |
37 |
38 | {{/each}}
39 |
40 |
41 | {{else}}
42 |
暂无表情数据
43 | {{/if}}
44 |
45 |
46 | {{/block}}
47 |
--------------------------------------------------------------------------------
/resources/server/imgs/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/server/imgs/bg.webp
--------------------------------------------------------------------------------
/resources/server/status.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-image: url('./imgs/bg.webp');
3 | }
4 | .info_box {
5 | background: rgba(255, 255, 255, 0.8);
6 | border-radius: 15px;
7 | padding: 20px;
8 | margin: 20px;
9 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
10 | }
11 |
12 | .head-box {
13 | text-align: center;
14 | margin-bottom: 30px;
15 | }
16 |
17 | .head-box .title {
18 | font-size: 24px;
19 | font-weight: bold;
20 | color: #333;
21 | margin-bottom: 10px;
22 | }
23 |
24 | .head-box .label {
25 | font-size: 16px;
26 | color: #666;
27 | }
28 |
29 | .card-container {
30 | max-width: 1200px;
31 | margin: 20px auto;
32 | padding: 20px;
33 | }
34 |
35 | .header-card {
36 | background: linear-gradient(135deg, #6e8efb, #a777e3);
37 | border-radius: 15px;
38 | padding: 30px;
39 | text-align: center;
40 | color: white;
41 | margin-bottom: 30px;
42 | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
43 | }
44 |
45 | .header-card .title {
46 | font-size: 28px;
47 | font-weight: bold;
48 | margin-bottom: 10px;
49 | }
50 |
51 | .header-card .subtitle {
52 | font-size: 16px;
53 | opacity: 0.9;
54 | }
55 |
56 | .status-grid {
57 | display: grid;
58 | grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
59 | gap: 20px;
60 | }
61 |
62 | .status-card {
63 | background: white;
64 | border-radius: 12px;
65 | padding: 20px;
66 | text-align: center;
67 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
68 | transition: all 0.3s ease;
69 | }
70 |
71 | .status-card:hover {
72 | transform: translateY(-5px);
73 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
74 | }
75 |
76 | .card-icon {
77 | font-size: 32px;
78 | margin-bottom: 15px;
79 | }
80 |
81 | .card-label {
82 | color: #666;
83 | font-size: 14px;
84 | margin-bottom: 8px;
85 | }
86 |
87 | .card-value {
88 | color: #333;
89 | font-size: 20px;
90 | font-weight: bold;
91 | }
92 |
93 | @media (max-width: 768px) {
94 | .status-grid {
95 | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
96 | gap: 15px;
97 | }
98 |
99 | .header-card {
100 | padding: 20px;
101 | }
102 |
103 | .header-card .title {
104 | font-size: 24px;
105 | }
106 | }
107 |
108 | .status-item {
109 | background: rgba(255, 255, 255, 0.9);
110 | border-radius: 10px;
111 | padding: 15px;
112 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
113 | transition: transform 0.2s;
114 | }
115 |
116 | .status-item:hover {
117 | transform: translateY(-2px);
118 | }
119 |
120 | .status-label {
121 | font-size: 14px;
122 | color: #666;
123 | margin-bottom: 8px;
124 | }
125 |
126 | .status-value {
127 | font-size: 18px;
128 | color: #333;
129 | font-weight: bold;
130 | }
--------------------------------------------------------------------------------
/resources/server/status.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}}
2 |
3 | {{block 'css'}}
4 |
5 | {{/block}}
6 |
7 | {{block 'main'}}
8 |
9 |
13 |
14 |
15 |
16 |
17 |
📦
18 |
插件版本
19 |
{{version}}
20 |
21 |
22 |
23 |
🔖
24 |
服务端版本
25 |
{{serverVersion}}
26 |
27 |
28 |
29 |
🚥
30 |
运行状态
31 |
{{status}}
32 |
33 |
34 |
35 |
⏰
36 |
启动时间
37 |
{{runtime}}
38 |
39 |
40 |
41 |
💾
42 |
内存使用
43 |
{{memory}} MB
44 |
45 |
46 |
47 |
🖼️
48 |
表情总数
49 |
{{total}}
50 |
51 |
52 |
53 |
54 | {{/block}}
55 |
--------------------------------------------------------------------------------
/resources/stat/imgs/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CandriaJS/meme-plugin/9120261a4bf8057b77fbc911e955057fac8628df/resources/stat/imgs/bg.webp
--------------------------------------------------------------------------------
/resources/stat/index.css:
--------------------------------------------------------------------------------
1 | .stat-container {
2 | background-image: url('./imgs/bg.webp');
3 | background-size: cover;
4 | background-repeat: no-repeat;
5 | background-position: center;
6 | padding: 20px;
7 | border-radius: 20px;
8 | text-align: center;
9 | box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
10 | border: 1px solid rgba(0, 0, 0, 0.1);
11 | backdrop-filter: blur(10px);
12 | }
13 |
14 | .stat-title {
15 | font-size: 36px;
16 | font-weight: 800;
17 | color: rgb(35, 65, 100);
18 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
19 | margin-bottom: 20px;
20 | letter-spacing: 1px;
21 | position: relative;
22 | display: inline-block;
23 | padding-bottom: 8px;
24 | }
25 |
26 | .stat-title::after {
27 | content: '';
28 | position: absolute;
29 | bottom: 0;
30 | left: 50%;
31 | transform: translateX(-50%);
32 | width: 60px;
33 | height: 3px;
34 | background: rgba(70, 130, 180, 0.4);
35 | border-radius: 2px;
36 | }
37 |
38 | .stat-total {
39 | font-size: 18px;
40 | font-weight: 600;
41 | color: rgb(70, 110, 150);
42 | text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.05);
43 | margin-bottom: 25px;
44 | }
45 |
46 | .stat-list {
47 | display: grid;
48 | grid-template-columns: repeat(5, 1fr);
49 | gap: 1rem;
50 | padding: 1rem;
51 | background: rgba(255, 255, 255, 0.85);
52 | border-radius: 12px;
53 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
54 | }
55 |
56 | .stat-item {
57 | display: flex;
58 | justify-content: space-between;
59 | align-items: center;
60 | padding: 12px 16px;
61 | background: linear-gradient(to right, #f8f9fa, #ffffff);
62 | border-left: 3px solid #4a6e91;
63 | border-radius: 8px;
64 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
65 | }
66 |
67 | .stat-keyword {
68 | font-size: 16px;
69 | font-weight: 600;
70 | color: #4a6e91;
71 | flex: 1;
72 | text-align: left;
73 | }
74 |
75 | .stat-count {
76 | font-size: 14px;
77 | font-weight: bold;
78 | color: #558fc8;
79 | margin-left: 12px;
80 | white-space: nowrap;
81 | background-color: rgba(85, 143, 200, 0.1);
82 | padding: 2px 8px;
83 | border-radius: 12px;
84 | }
--------------------------------------------------------------------------------
/resources/stat/index.html:
--------------------------------------------------------------------------------
1 | {{extend defaultLayout}}
2 |
3 | {{block 'css'}}
4 |
5 | {{/block}}
6 |
7 | {{block 'main'}}
8 |
9 |
柠糖表情统计
10 |
表情调用总次数:{{total}}
11 |
12 | {{each memeList item}}
13 |
14 |
{{item.keywords}}
15 |
{{item.count}} 次
16 |
17 | {{/each}}
18 |
19 |
20 | {{/block}}
--------------------------------------------------------------------------------