├── .bunfig.toml ├── .changelogrc.js ├── .commitlintrc.js ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug_report.yml │ ├── 2_feature_request.yml │ ├── 3_question.yml │ └── other.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── issue-auto-comments.yml │ ├── issue-check-inactive.yml │ ├── issue-close-require.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .releaserc.js ├── .remarkrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── api ├── index.ts └── v1 │ └── runner.ts ├── docs └── api.md ├── package.json ├── public └── index.html ├── src ├── cors.ts ├── edge.ts ├── gateway.ts ├── index.ts └── node.ts ├── tests ├── __snapshots__ │ └── edge.test.ts.snap ├── edge.test.ts └── gateway.test.ts ├── tsconfig.json ├── types ├── pluginItem.ts ├── plugins.ts └── schema.ts └── vitest.config.ts /.bunfig.toml: -------------------------------------------------------------------------------- 1 | [install.lockfile] 2 | 3 | save = false 4 | -------------------------------------------------------------------------------- /.changelogrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lobehub/lint').changelog; 2 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lobehub/lint').commitlint; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # relpace url with your internal plugins index url 2 | PLUGINS_INDEX_URL=https://example.com/index.json 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Eslintignore for LobeHub 2 | ################################################################ 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # ci 8 | coverage 9 | .coverage 10 | 11 | # test 12 | jest* 13 | _test_ 14 | __test__ 15 | 16 | # umi 17 | .umi 18 | .umi-production 19 | .umi-test 20 | .dumi/tmp* 21 | !.dumirc.ts 22 | 23 | # production 24 | dist 25 | es 26 | lib 27 | logs 28 | 29 | # misc 30 | # add other ignore file below 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const config = require('@lobehub/lint').eslint; 2 | 3 | config.rules['no-extra-boolean-cast'] = 0; 4 | config.rules['unicorn/no-array-for-each'] = 0; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | cjs: { output: 'dist' }, 5 | }); 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 反馈缺陷 Bug Report' 2 | description: '反馈一个问题缺陷 | Report an bug' 3 | title: '[Bug] ' 4 | labels: '🐛 Bug' 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '💻 系统环境 | Operating System' 9 | options: 10 | - Windows 11 | - macOS 12 | - Ubuntu 13 | - Other Linux 14 | - Other 15 | validations: 16 | required: true 17 | - type: dropdown 18 | attributes: 19 | label: '🌐 浏览器 | Browser' 20 | options: 21 | - Chrome 22 | - Edge 23 | - Safari 24 | - Firefox 25 | - Other 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: '🐛 问题描述 | Bug Description' 31 | description: A clear and concise description of the bug. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: '🚦 期望结果 | Expected Behavior' 37 | description: A clear and concise description of what you expected to happen. 38 | - type: textarea 39 | attributes: 40 | label: '📷 复现步骤 | Recurrence Steps' 41 | description: A clear and concise description of how to recurrence. 42 | - type: textarea 43 | attributes: 44 | label: '📝 补充信息 | Additional Information' 45 | description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 功能需求 Feature Request' 2 | description: '需求或建议 | Suggest an idea' 3 | title: '[Request] ' 4 | labels: '🌠 Feature Request' 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 需求描述 | Feature Description' 9 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 解决方案 | Proposed Solution' 15 | description: Describe the solution you'd like in a clear and concise manner. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 补充信息 | Additional Information' 21 | description: Add any other context about the problem here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_question.yml: -------------------------------------------------------------------------------- 1 | name: '😇 疑问或帮助 Help Wanted' 2 | description: '疑问或需要帮助 | Need help' 3 | title: '[Question] ' 4 | labels: '😇 Help Wanted' 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🧐 问题描述 | Proposed Solution' 9 | description: A clear and concise description of the proplem. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '📝 补充信息 | Additional Information' 15 | description: Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📝 其他 Other' 3 | about: '其他问题 | Other issues' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - [ ] ✨ feat 6 | - [ ] 🐛 fix 7 | - [ ] ♻️ refactor 8 | - [ ] 💄 style 9 | - [ ] 🔨 chore 10 | - [ ] 📝 docs 11 | 12 | #### 🔀 变更说明 | Description of Change 13 | 14 | 15 | 16 | #### 📝 补充信息 | Additional Information 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/issue-auto-comments.yml: -------------------------------------------------------------------------------- 1 | name: Issue Auto Comment 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | - closed 7 | - assigned 8 | pull_request_target: 9 | types: 10 | - opened 11 | - closed 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | run: 18 | permissions: 19 | issues: write # for actions-cool/issues-helper to update issues 20 | pull-requests: write # for actions-cool/issues-helper to update PRs 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Auto Comment on Issues Opened 24 | uses: wow-actions/auto-comment@v1 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 27 | issuesOpened: | 28 | 👀 @{{ author }} 29 | Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible. 30 | Please make sure you have given us as much context as possible.\ 31 | 非常感谢您提交 issue。我们会尽快调查此事,并尽快回复您。 请确保您已经提供了尽可能多的背景信息。 32 | - name: Auto Comment on Issues Closed 33 | uses: wow-actions/auto-comment@v1 34 | with: 35 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 36 | issuesClosed: | 37 | ✅ @{{ author }} 38 |
39 | This issue is closed, If you have any questions, you can comment and reply.\ 40 | 此问题已经关闭。如果您有任何问题,可以留言并回复。 41 | - name: Auto Comment on Pull Request Opened 42 | uses: wow-actions/auto-comment@v1 43 | with: 44 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 45 | pullRequestOpened: | 46 | 👍 @{{ author }} 47 |
48 | Thank you for raising your pull request and contributing to our Community 49 | Please make sure you have followed our contributing guidelines. We will review it as soon as possible. 50 | If you encounter any problems, please feel free to connect with us.\ 51 | 非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。 52 | 如果您遇到任何问题,请随时与我们联系。 53 | - name: Auto Comment on Pull Request Merged 54 | uses: actions-cool/pr-welcome@main 55 | if: github.event.pull_request.merged == true 56 | with: 57 | token: ${{ secrets.GH_TOKEN }} 58 | comment: | 59 | ❤️ Great PR @${{ github.event.pull_request.user.login }} ❤️ 60 |
61 | The growth of project is inseparable from user feedback and contribution, thanks for your contribution!\ 62 | 项目的成长离不开用户反馈和贡献,感谢您的贡献! 63 | emoji: 'hooray' 64 | pr-emoji: '+1, heart' 65 | - name: Remove inactive 66 | if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login 67 | uses: actions-cool/issues-helper@v3 68 | with: 69 | actions: 'remove-labels' 70 | token: ${{ secrets.GH_TOKEN }} 71 | issue-number: ${{ github.event.issue.number }} 72 | labels: 'Inactive' 73 | -------------------------------------------------------------------------------- /.github/workflows/issue-check-inactive.yml: -------------------------------------------------------------------------------- 1 | name: Issue Check Inactive 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 */15 * *' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | issue-check-inactive: 12 | permissions: 13 | issues: write # for actions-cool/issues-helper to update issues 14 | pull-requests: write # for actions-cool/issues-helper to update PRs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: check-inactive 18 | uses: actions-cool/issues-helper@v3 19 | with: 20 | actions: 'check-inactive' 21 | token: ${{ secrets.GH_TOKEN }} 22 | inactive-label: 'Inactive' 23 | inactive-day: 30 24 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-require.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Require 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | issue-close-require: 12 | permissions: 13 | issues: write # for actions-cool/issues-helper to update issues 14 | pull-requests: write # for actions-cool/issues-helper to update PRs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: need reproduce 18 | uses: actions-cool/issues-helper@v3 19 | with: 20 | actions: 'close-issues' 21 | token: ${{ secrets.GH_TOKEN }} 22 | labels: '✅ Fixed' 23 | inactive-day: 3 24 | body: | 25 | 👋 @{{ github.event.issue.user.login }} 26 |
27 | Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ 28 | 由于该 issue 被标记为已修复,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 29 | - name: need reproduce 30 | uses: actions-cool/issues-helper@v3 31 | with: 32 | actions: 'close-issues' 33 | token: ${{ secrets.GH_TOKEN }} 34 | labels: '🤔 Need Reproduce' 35 | inactive-day: 3 36 | body: | 37 | 👋 @{{ github.event.issue.user.login }} 38 |
39 | Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ 40 | 由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 41 | - name: need reproduce 42 | uses: actions-cool/issues-helper@v3 43 | with: 44 | actions: 'close-issues' 45 | token: ${{ secrets.GH_TOKEN }} 46 | labels: "🙅🏻‍♀️ WON'T DO" 47 | inactive-day: 3 48 | body: | 49 | 👋 @{{ github.event.issue.user.login }} 50 |
51 | Since the issue was labeled with `🙅🏻‍♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\ 52 | 由于该 issue 被标记为暂不处理,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - alpha 7 | - beta 8 | - rc 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Install bun 18 | uses: oven-sh/setup-bun@v1 19 | 20 | - name: Install deps 21 | run: bun i 22 | 23 | - name: CI 24 | run: bun run ci 25 | 26 | - name: Test 27 | run: bun run test 28 | 29 | - name: build 30 | run: bun run build 31 | 32 | - name: release 33 | run: bun run release 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - '!main' 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Install bun 15 | uses: oven-sh/setup-bun@v1 16 | 17 | - name: Install deps 18 | run: bun i 19 | 20 | - name: CI 21 | run: bun run ci 22 | 23 | - name: Test and coverage 24 | run: bun run test:coverage 25 | 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v3 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore for LobeHub 2 | ################################################################ 3 | 4 | # general 5 | .DS_Store 6 | .idea 7 | .vscode 8 | .history 9 | .temp 10 | .env.local 11 | venv 12 | temp 13 | tmp 14 | 15 | # dependencies 16 | node_modules 17 | *.log 18 | *.lock 19 | package-lock.json 20 | 21 | # ci 22 | coverage 23 | .coverage 24 | .eslintcache 25 | .stylelintcache 26 | 27 | # production 28 | dist 29 | es 30 | lib 31 | logs 32 | test-output 33 | 34 | # umi 35 | .umi 36 | .umi-production 37 | .umi-test 38 | .dumi/tmp* 39 | 40 | # husky 41 | .husky/prepare-commit-msg 42 | 43 | # misc 44 | # add other ignore file below 45 | bun.lockb 46 | build 47 | .env 48 | .vercel 49 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # lobe-commit as a commit hook 3 | if npx -v >&/dev/null 4 | then 5 | exec < /dev/tty 6 | npx -c "lobe-commit --hook $1 $2" 7 | else 8 | exec < /dev/tty 9 | lobe-commit --hook $1 $2 10 | fi -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | lockfile=false 2 | resolution-mode=highest 3 | public-hoist-pattern[]=*@umijs/lint* 4 | public-hoist-pattern[]=*changelog* 5 | public-hoist-pattern[]=*commitlint* 6 | public-hoist-pattern[]=*eslint* 7 | public-hoist-pattern[]=*postcss* 8 | public-hoist-pattern[]=*prettier* 9 | public-hoist-pattern[]=*remark* 10 | public-hoist-pattern[]=*semantic-release* 11 | public-hoist-pattern[]=*stylelint* 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettierignore for LobeHub 2 | ################################################################ 3 | 4 | # general 5 | .DS_Store 6 | .editorconfig 7 | .idea 8 | .vscode 9 | .history 10 | .temp 11 | .env.local 12 | .husky 13 | .npmrc 14 | .gitkeep 15 | venv 16 | temp 17 | tmp 18 | LICENSE 19 | 20 | # dependencies 21 | node_modules 22 | *.log 23 | *.lock 24 | package-lock.json 25 | 26 | # ci 27 | coverage 28 | .coverage 29 | .eslintcache 30 | .stylelintcache 31 | test-output 32 | __snapshots__ 33 | *.snap 34 | 35 | # production 36 | dist 37 | es 38 | lib 39 | logs 40 | 41 | # umi 42 | .umi 43 | .umi-production 44 | .umi-test 45 | .dumi/tmp* 46 | 47 | # ignore files 48 | .*ignore 49 | 50 | # docker 51 | docker 52 | Dockerfile* 53 | 54 | # image 55 | *.webp 56 | *.gif 57 | *.png 58 | *.jpg 59 | 60 | # misc 61 | # add other ignore file below 62 | .env.example 63 | .env 64 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lobehub/lint').prettier; 2 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lobehub/lint').semanticRelease; 2 | -------------------------------------------------------------------------------- /.remarkrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@lobehub/lint').remarklint; 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | ## [Version 1.9.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.2...v1.9.0) 6 | 7 | Released on **2023-12-26** 8 | 9 | #### ✨ Features 10 | 11 | - **misc**: Support defaultPluginSettings options on Gateway. 12 | 13 |
14 | 15 |
16 | Improvements and Fixes 17 | 18 | #### What's improved 19 | 20 | - **misc**: Support defaultPluginSettings options on Gateway ([fea957e](https://github.com/lobehub/chat-plugins-gateway/commit/fea957e)) 21 | 22 |
23 | 24 |
25 | 26 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 27 | 28 |
29 | 30 | ### [Version 1.8.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.1...v1.8.2) 31 | 32 | Released on **2023-12-15** 33 | 34 | #### 🐛 Bug Fixes 35 | 36 | - **misc**: Fix openapi requestBody. 37 | 38 |
39 | 40 |
41 | Improvements and Fixes 42 | 43 | #### What's fixed 44 | 45 | - **misc**: Fix openapi requestBody ([4385080](https://github.com/lobehub/chat-plugins-gateway/commit/4385080)) 46 | 47 |
48 | 49 |
50 | 51 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 52 | 53 |
54 | 55 | ### [Version 1.8.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.0...v1.8.1) 56 | 57 | Released on **2023-12-14** 58 | 59 | #### 🐛 Bug Fixes 60 | 61 | - **misc**: Handling unknown error. 62 | 63 |
64 | 65 |
66 | Improvements and Fixes 67 | 68 | #### What's fixed 69 | 70 | - **misc**: Handling unknown error ([e503d1f](https://github.com/lobehub/chat-plugins-gateway/commit/e503d1f)) 71 | 72 |
73 | 74 |
75 | 76 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 77 | 78 |
79 | 80 | ## [Version 1.8.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.7.2...v1.8.0) 81 | 82 | Released on **2023-12-13** 83 | 84 | #### ✨ Features 85 | 86 | - **misc**: Support node runtime. 87 | 88 |
89 | 90 |
91 | Improvements and Fixes 92 | 93 | #### What's improved 94 | 95 | - **misc**: Support node runtime, closes [#9](https://github.com/lobehub/chat-plugins-gateway/issues/9) ([28ef918](https://github.com/lobehub/chat-plugins-gateway/commit/28ef918)) 96 | 97 |
98 | 99 |
100 | 101 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 102 | 103 |
104 | 105 | ## [Version 1.8.0-beta.4](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.0-beta.3...v1.8.0-beta.4) 106 | 107 | Released on **2023-12-13** 108 | 109 | #### 🐛 Bug Fixes 110 | 111 | - **misc**: Close ajv strict mode. 112 | 113 |
114 | 115 |
116 | Improvements and Fixes 117 | 118 | #### What's fixed 119 | 120 | - **misc**: Close ajv strict mode ([a6eb5b6](https://github.com/lobehub/chat-plugins-gateway/commit/a6eb5b6)) 121 | 122 |
123 | 124 |
125 | 126 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 127 | 128 |
129 | 130 | ## [Version 1.8.0-beta.3](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.0-beta.2...v1.8.0-beta.3) 131 | 132 | Released on **2023-12-13** 133 | 134 | #### 🐛 Bug Fixes 135 | 136 | - **misc**: Fix ajv schema. 137 | 138 |
139 | 140 |
141 | Improvements and Fixes 142 | 143 | #### What's fixed 144 | 145 | - **misc**: Fix ajv schema ([40f22c5](https://github.com/lobehub/chat-plugins-gateway/commit/40f22c5)) 146 | 147 |
148 | 149 |
150 | 151 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 152 | 153 |
154 | 155 | ## [Version 1.8.0-beta.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.8.0-beta.1...v1.8.0-beta.2) 156 | 157 | Released on **2023-12-13** 158 | 159 | #### 🐛 Bug Fixes 160 | 161 | - **misc**: Fix request input. 162 | 163 |
164 | 165 |
166 | Improvements and Fixes 167 | 168 | #### What's fixed 169 | 170 | - **misc**: Fix request input ([1b896de](https://github.com/lobehub/chat-plugins-gateway/commit/1b896de)) 171 | 172 |
173 | 174 |
175 | 176 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 177 | 178 |
179 | 180 | ## [Version 1.8.0-beta.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.7.2...v1.8.0-beta.1) 181 | 182 | Released on **2023-12-13** 183 | 184 | #### ♻ Code Refactoring 185 | 186 | - **misc**: Refactor to the Gateway Class and add tests. 187 | 188 | #### ✨ Features 189 | 190 | - **misc**: Support node runtime. 191 | 192 |
193 | 194 |
195 | Improvements and Fixes 196 | 197 | #### Code refactoring 198 | 199 | - **misc**: Refactor to the Gateway Class and add tests ([e8c8cb5](https://github.com/lobehub/chat-plugins-gateway/commit/e8c8cb5)) 200 | 201 | #### What's improved 202 | 203 | - **misc**: Support node runtime ([a8e2933](https://github.com/lobehub/chat-plugins-gateway/commit/a8e2933)) 204 | 205 |
206 | 207 |
208 | 209 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 210 | 211 |
212 | 213 | ### [Version 1.7.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.7.1...v1.7.2) 214 | 215 | Released on **2023-12-12** 216 | 217 | #### 🐛 Bug Fixes 218 | 219 | - **misc**: Improve PluginGatewayError error response. 220 | 221 |
222 | 223 |
224 | Improvements and Fixes 225 | 226 | #### What's fixed 227 | 228 | - **misc**: Improve PluginGatewayError error response ([18b1c43](https://github.com/lobehub/chat-plugins-gateway/commit/18b1c43)) 229 | 230 |
231 | 232 |
233 | 234 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 235 | 236 |
237 | 238 | ### [Version 1.7.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.7.0...v1.7.1) 239 | 240 | Released on **2023-12-12** 241 | 242 | #### 🐛 Bug Fixes 243 | 244 | - **misc**: Fix error with openapi client init. 245 | 246 |
247 | 248 |
249 | Improvements and Fixes 250 | 251 | #### What's fixed 252 | 253 | - **misc**: Fix error with openapi client init ([b652cb5](https://github.com/lobehub/chat-plugins-gateway/commit/b652cb5)) 254 | 255 |
256 | 257 |
258 | 259 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 260 | 261 |
262 | 263 | ## [Version 1.7.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.6.0...v1.7.0) 264 | 265 | Released on **2023-12-11** 266 | 267 | #### ✨ Features 268 | 269 | - **misc**: Support basic auth for OpenAPI. 270 | 271 |
272 | 273 |
274 | Improvements and Fixes 275 | 276 | #### What's improved 277 | 278 | - **misc**: Support basic auth for OpenAPI ([d3fe874](https://github.com/lobehub/chat-plugins-gateway/commit/d3fe874)) 279 | 280 |
281 | 282 |
283 | 284 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 285 | 286 |
287 | 288 | ## [Version 1.6.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.5.3...v1.6.0) 289 | 290 | Released on **2023-12-11** 291 | 292 | #### ✨ Features 293 | 294 | - **misc**: Support OpenAPI request mode. 295 | 296 | #### 🐛 Bug Fixes 297 | 298 | - **misc**: Fix types. 299 | 300 |
301 | 302 |
303 | Improvements and Fixes 304 | 305 | #### What's improved 306 | 307 | - **misc**: Support OpenAPI request mode ([605d947](https://github.com/lobehub/chat-plugins-gateway/commit/605d947)) 308 | 309 | #### What's fixed 310 | 311 | - **misc**: Fix types ([8e2dde4](https://github.com/lobehub/chat-plugins-gateway/commit/8e2dde4)) 312 | 313 |
314 | 315 |
316 | 317 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 318 | 319 |
320 | 321 | ### [Version 1.5.3](https://github.com/lobehub/chat-plugins-gateway/compare/v1.5.2...v1.5.3) 322 | 323 | Released on **2023-09-12** 324 | 325 | #### 🐛 Bug Fixes 326 | 327 | - **misc**: Fix ci, Fix father version, Fix lint. 328 | 329 |
330 | 331 |
332 | Improvements and Fixes 333 | 334 | #### What's fixed 335 | 336 | - **misc**: Fix ci ([9099c61](https://github.com/lobehub/chat-plugins-gateway/commit/9099c61)) 337 | - **misc**: Fix father version ([9668073](https://github.com/lobehub/chat-plugins-gateway/commit/9668073)) 338 | - **misc**: Fix lint ([3e47791](https://github.com/lobehub/chat-plugins-gateway/commit/3e47791)) 339 | 340 |
341 | 342 |
343 | 344 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 345 | 346 |
347 | 348 | ### [Version 1.5.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.5.1...v1.5.2) 349 | 350 | Released on **2023-09-08** 351 | 352 | #### 💄 Styles 353 | 354 | - **misc**: Update lint. 355 | 356 |
357 | 358 |
359 | Improvements and Fixes 360 | 361 | #### Styles 362 | 363 | - **misc**: Update lint ([6385312](https://github.com/lobehub/chat-plugins-gateway/commit/6385312)) 364 | 365 |
366 | 367 |
368 | 369 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 370 | 371 |
372 | 373 | ### [Version 1.5.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.5.0...v1.5.1) 374 | 375 | Released on **2023-09-05** 376 | 377 | #### 🐛 Bug Fixes 378 | 379 | - **misc**: Fix getPluginSettingsFromRequest. 380 | 381 |
382 | 383 |
384 | Improvements and Fixes 385 | 386 | #### What's fixed 387 | 388 | - **misc**: Fix getPluginSettingsFromRequest ([aad3c7a](https://github.com/lobehub/chat-plugins-gateway/commit/aad3c7a)) 389 | 390 |
391 | 392 |
393 | 394 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 395 | 396 |
397 | 398 | ## [Version 1.5.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.4.3...v1.5.0) 399 | 400 | Released on **2023-08-29** 401 | 402 | #### ✨ Features 403 | 404 | - **misc**: 更新入参配置,解决跨域兼容性的问题. 405 | 406 |
407 | 408 |
409 | Improvements and Fixes 410 | 411 | #### What's improved 412 | 413 | - **misc**: 更新入参配置,解决跨域兼容性的问题 ([c02451c](https://github.com/lobehub/chat-plugins-gateway/commit/c02451c)) 414 | 415 |
416 | 417 |
418 | 419 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 420 | 421 |
422 | 423 | ### [Version 1.4.3](https://github.com/lobehub/chat-plugins-gateway/compare/v1.4.2...v1.4.3) 424 | 425 | Released on **2023-08-29** 426 | 427 | #### 🐛 Bug Fixes 428 | 429 | - **misc**: 移除跨域限制,解决插件复用的问题. 430 | 431 |
432 | 433 |
434 | Improvements and Fixes 435 | 436 | #### What's fixed 437 | 438 | - **misc**: 移除跨域限制,解决插件复用的问题 ([975b705](https://github.com/lobehub/chat-plugins-gateway/commit/975b705)) 439 | 440 |
441 | 442 |
443 | 444 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 445 | 446 |
447 | 448 | ### [Version 1.4.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.4.1...v1.4.2) 449 | 450 | Released on **2023-08-29** 451 | 452 | #### 🐛 Bug Fixes 453 | 454 | - **misc**: 移除跨域限制,解决插件复用的问题. 455 | 456 |
457 | 458 |
459 | Improvements and Fixes 460 | 461 | #### What's fixed 462 | 463 | - **misc**: 移除跨域限制,解决插件复用的问题 ([1de6837](https://github.com/lobehub/chat-plugins-gateway/commit/1de6837)) 464 | 465 |
466 | 467 |
468 | 469 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 470 | 471 |
472 | 473 | ### [Version 1.4.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.4.0...v1.4.1) 474 | 475 | Released on **2023-08-29** 476 | 477 | #### 🐛 Bug Fixes 478 | 479 | - **misc**: 内联默认的 索引 URL,解决外部使用问题. 480 | 481 |
482 | 483 |
484 | Improvements and Fixes 485 | 486 | #### What's fixed 487 | 488 | - **misc**: 内联默认的 索引 URL,解决外部使用问题 ([54b93d2](https://github.com/lobehub/chat-plugins-gateway/commit/54b93d2)) 489 | 490 |
491 | 492 |
493 | 494 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 495 | 496 |
497 | 498 | ## [Version 1.4.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.3.2...v1.4.0) 499 | 500 | Released on **2023-08-28** 501 | 502 | #### ✨ Features 503 | 504 | - **misc**: 支持入参包含 manifest. 505 | 506 |
507 | 508 |
509 | Improvements and Fixes 510 | 511 | #### What's improved 512 | 513 | - **misc**: 支持入参包含 manifest ([b209fba](https://github.com/lobehub/chat-plugins-gateway/commit/b209fba)) 514 | 515 |
516 | 517 |
518 | 519 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 520 | 521 |
522 | 523 | ### [Version 1.3.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.3.1...v1.3.2) 524 | 525 | Released on **2023-08-26** 526 | 527 | #### 🐛 Bug Fixes 528 | 529 | - **misc**: Change master to main. 530 | 531 |
532 | 533 |
534 | Improvements and Fixes 535 | 536 | #### What's fixed 537 | 538 | - **misc**: Change master to main ([134c77f](https://github.com/lobehub/chat-plugins-gateway/commit/134c77f)) 539 | 540 |
541 | 542 |
543 | 544 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 545 | 546 |
547 | 548 | ### [Version 1.3.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.3.0...v1.3.1) 549 | 550 | Released on **2023-08-26** 551 | 552 | #### 🐛 Bug Fixes 553 | 554 | - **misc**: Fix an error when plugin settings is undefined. 555 | 556 |
557 | 558 |
559 | Improvements and Fixes 560 | 561 | #### What's fixed 562 | 563 | - **misc**: Fix an error when plugin settings is undefined ([666e503](https://github.com/lobehub/chat-plugins-gateway/commit/666e503)) 564 | 565 |
566 | 567 |
568 | 569 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 570 | 571 |
572 | 573 | ## [Version 1.3.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.5...v1.3.0) 574 | 575 | Released on **2023-08-25** 576 | 577 | #### ✨ Features 578 | 579 | - **misc**: 补充插件网关的校验逻辑. 580 | 581 |
582 | 583 |
584 | Improvements and Fixes 585 | 586 | #### What's improved 587 | 588 | - **misc**: 补充插件网关的校验逻辑 ([c6f5286](https://github.com/lobehub/chat-plugins-gateway/commit/c6f5286)) 589 | 590 |
591 | 592 |
593 | 594 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 595 | 596 |
597 | 598 | ### [Version 1.2.5](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.4...v1.2.5) 599 | 600 | Released on **2023-08-25** 601 | 602 | #### 🐛 Bug Fixes 603 | 604 | - **misc**: 修正没有呈现索引 url 的问题. 605 | 606 |
607 | 608 |
609 | Improvements and Fixes 610 | 611 | #### What's fixed 612 | 613 | - **misc**: 修正没有呈现索引 url 的问题 ([75f94dd](https://github.com/lobehub/chat-plugins-gateway/commit/75f94dd)) 614 | 615 |
616 | 617 |
618 | 619 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 620 | 621 |
622 | 623 | ### [Version 1.2.4](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.3...v1.2.4) 624 | 625 | Released on **2023-08-25** 626 | 627 | #### 🐛 Bug Fixes 628 | 629 | - **misc**: 兼容 sdk 变更. 630 | 631 |
632 | 633 |
634 | Improvements and Fixes 635 | 636 | #### What's fixed 637 | 638 | - **misc**: 兼容 sdk 变更 ([7ff08c8](https://github.com/lobehub/chat-plugins-gateway/commit/7ff08c8)) 639 | 640 |
641 | 642 |
643 | 644 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 645 | 646 |
647 | 648 | ### [Version 1.2.3](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.2...v1.2.3) 649 | 650 | Released on **2023-08-24** 651 | 652 | #### ♻ Code Refactoring 653 | 654 | - **misc**: Refactor with new manifest schema. 655 | 656 |
657 | 658 |
659 | Improvements and Fixes 660 | 661 | #### Code refactoring 662 | 663 | - **misc**: Refactor with new manifest schema, closes [#5](https://github.com/lobehub/chat-plugins-gateway/issues/5) ([583f834](https://github.com/lobehub/chat-plugins-gateway/commit/583f834)) 664 | 665 |
666 | 667 |
668 | 669 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 670 | 671 |
672 | 673 | ### [Version 1.2.2](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.1...v1.2.2) 674 | 675 | Released on **2023-08-22** 676 | 677 | #### 🐛 Bug Fixes 678 | 679 | - **misc**: Fix missing types. 680 | 681 |
682 | 683 |
684 | Improvements and Fixes 685 | 686 | #### What's fixed 687 | 688 | - **misc**: Fix missing types ([4d8029d](https://github.com/lobehub/chat-plugins-gateway/commit/4d8029d)) 689 | 690 |
691 | 692 |
693 | 694 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 695 | 696 |
697 | 698 | ### [Version 1.2.1](https://github.com/lobehub/chat-plugins-gateway/compare/v1.2.0...v1.2.1) 699 | 700 | Released on **2023-08-22** 701 | 702 | #### 🐛 Bug Fixes 703 | 704 | - **misc**: Fix not release. 705 | 706 |
707 | 708 |
709 | Improvements and Fixes 710 | 711 | #### What's fixed 712 | 713 | - **misc**: Fix not release ([3a0d30f](https://github.com/lobehub/chat-plugins-gateway/commit/3a0d30f)) 714 | 715 |
716 | 717 |
718 | 719 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 720 | 721 |
722 | 723 | ## [Version 1.2.0](https://github.com/lobehub/chat-plugins-gateway/compare/v1.1.0...v1.2.0) 724 | 725 | Released on **2023-08-22** 726 | 727 | #### ✨ Features 728 | 729 | - **misc**: Add reusable gateway function. 730 | 731 | #### 🐛 Bug Fixes 732 | 733 | - **misc**: Fix config error, fix config error. 734 | 735 |
736 | 737 |
738 | Improvements and Fixes 739 | 740 | #### What's improved 741 | 742 | - **misc**: Add reusable gateway function ([eba16ab](https://github.com/lobehub/chat-plugins-gateway/commit/eba16ab)) 743 | 744 | #### What's fixed 745 | 746 | - **misc**: Fix config error ([96ed006](https://github.com/lobehub/chat-plugins-gateway/commit/96ed006)) 747 | - **misc**: Fix config error ([25fbd18](https://github.com/lobehub/chat-plugins-gateway/commit/25fbd18)) 748 | 749 |
750 | 751 |
752 | 753 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 754 | 755 |
756 | 757 | ## [Version 1.1.0](https://github.com/lobehub/chat-plugin-market/compare/v1.0.0...v1.1.0) 758 | 759 | Released on **2023-08-20** 760 | 761 | #### ✨ Features 762 | 763 | - **misc**: Custom market index url. 764 | 765 |
766 | 767 |
768 | Improvements and Fixes 769 | 770 | #### What's improved 771 | 772 | - **misc**: Custom market index url, closes [#3](https://github.com/lobehub/chat-plugin-market/issues/3) ([14cd4b2](https://github.com/lobehub/chat-plugin-market/commit/14cd4b2)) 773 | 774 |
775 | 776 |
777 | 778 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 779 | 780 |
781 | 782 | ## Version 1.0.0 783 | 784 | Released on **2023-08-19** 785 | 786 | #### ♻ Code Refactoring 787 | 788 | - **misc**: Remove searchEngine, remove webCrawler. 789 | 790 | #### ✨ Features 791 | 792 | - **misc**: Chat plugin gateway 1.0, prod 生产锁定 indexURL 版本,先使用最简单的方式迁移插件服务端,初步剥离天气插件,支持 V1 manifest 版本的方案. 793 | 794 | #### 🐛 Bug Fixes 795 | 796 | - **misc**: Fix console time error, fix import error, fix request error, 修正动态链路的命中逻辑,修正动态链路的命中逻辑. 797 | 798 |
799 | 800 |
801 | Improvements and Fixes 802 | 803 | #### Code refactoring 804 | 805 | - **misc**: Remove searchEngine ([d0fc888](https://github.com/lobehub/chat-plugin-market/commit/d0fc888)) 806 | - **misc**: Remove webCrawler ([91663ea](https://github.com/lobehub/chat-plugin-market/commit/91663ea)) 807 | 808 | #### What's improved 809 | 810 | - **misc**: Chat plugin gateway 1.0, closes [#2](https://github.com/lobehub/chat-plugin-market/issues/2) ([788a30a](https://github.com/lobehub/chat-plugin-market/commit/788a30a)) 811 | - **misc**: Prod 生产锁定 indexURL 版本 ([80cdafb](https://github.com/lobehub/chat-plugin-market/commit/80cdafb)) 812 | - **misc**: 先使用最简单的方式迁移插件服务端 ([eaf26bd](https://github.com/lobehub/chat-plugin-market/commit/eaf26bd)) 813 | - **misc**: 初步剥离天气插件,closes [#1](https://github.com/lobehub/chat-plugin-market/issues/1) ([ecf1c4c](https://github.com/lobehub/chat-plugin-market/commit/ecf1c4c)) 814 | - **misc**: 支持 V1 manifest 版本的方案 ([8f26a6c](https://github.com/lobehub/chat-plugin-market/commit/8f26a6c)) 815 | 816 | #### What's fixed 817 | 818 | - **misc**: Fix console time error ([1cd882c](https://github.com/lobehub/chat-plugin-market/commit/1cd882c)) 819 | - **misc**: Fix import error ([a9fa133](https://github.com/lobehub/chat-plugin-market/commit/a9fa133)) 820 | - **misc**: Fix request error ([d8bb922](https://github.com/lobehub/chat-plugin-market/commit/d8bb922)) 821 | - **misc**: 修正动态链路的命中逻辑 ([beb14eb](https://github.com/lobehub/chat-plugin-market/commit/beb14eb)) 822 | - **misc**: 修正动态链路的命中逻辑 ([525654f](https://github.com/lobehub/chat-plugin-market/commit/525654f)) 823 | 824 |
825 | 826 |
827 | 828 | [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) 829 | 830 |
831 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LobeHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |

LobeChat Plugins Gateway

10 | 11 | Plugin Gateway Service for Lobe Chat and Lobe Web 12 | 13 | [![][🤯-🧩-lobehub-shield]][🤯-🧩-lobehub-link] 14 | [![][npm-release-shield]][npm-release-link] 15 | [![][github-releasedate-shield]][github-releasedate-link] 16 | [![][github-action-test-shield]][github-action-test-link] 17 | [![][github-action-release-shield]][github-action-release-link]
18 | [![][github-contributors-shield]][github-contributors-link] 19 | [![][github-forks-shield]][github-forks-link] 20 | [![][github-stars-shield]][github-stars-link] 21 | [![][github-issues-shield]][github-issues-link] 22 | [![][github-license-shield]][github-license-link] 23 | 24 | **English** · [简体中文](./README.zh-CN.md) · [Changelog](./CHANGELOG.md) · [Report Bug][github-issues-link] · [Request Feature][github-issues-link] 25 | 26 | ![](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png) 27 | 28 |
29 | 30 |
31 | Table of contents 32 | 33 | #### TOC 34 | 35 | - [👋 Intro](#-intro) 36 | - [🤯 Usage](#-usage) 37 | - [Base URLs](#base-urls) 38 | - [POST Plugin Gateway](#post-plugin-gateway) 39 | - [🛳 Self Hosting](#-self-hosting) 40 | - [Deploy to Vercel](#deploy-to-vercel) 41 | - [📦 Plugin Ecosystem](#-plugin-ecosystem) 42 | - [⌨️ Local Development](#️-local-development) 43 | - [🤝 Contributing](#-contributing) 44 | - [🔗 Links](#-links) 45 | 46 | #### 47 | 48 |
49 | 50 | ## 👋 Intro 51 | 52 | LobeChat Plugins Gateway is a backend service that provides a gateway for LobeChat plugins. We use [vercel](https://vercel.com/) to deploy this service. The main API `POST /api/v1/runner` is deployed as an [Edge Function](https://vercel.com/docs/functions/edge-functions). 53 | 54 | The gateway service fetches lobe plugins index from the [LobeChat Plugins](https://github.com/lobehub/lobe-chat-plugins), if you want to add your plugin to the index, please [submit a PR](https://github.com/lobehub/lobe-chat-plugins/pulls) to the LobeChat Plugins repository. 55 | 56 |
57 | 58 | [![][back-to-top]](#readme-top) 59 | 60 |
61 | 62 | ## 🤯 Usage 63 | 64 | ### Base URLs 65 | 66 | | Environment | URL | 67 | | ----------- | ---------------------------------------------- | 68 | | `PROD` | | 69 | | `DEV` | | 70 | 71 | ### POST Plugin Gateway 72 | 73 | > **Note**\ 74 | > **POST** `/api/v1/runner`\ 75 | > Interface to communicate with the LobeChat plugin. This interface describes how to use the LobeChat plugin gateway API to send requests and get responses 76 | 77 | #### Body Request Parameters 78 | 79 | ```json 80 | { 81 | "arguments": "{\n \"city\": \"杭州\"\n}", 82 | "name": "realtimeWeather" 83 | } 84 | ``` 85 | 86 | #### Response 87 | 88 | ```json 89 | [ 90 | { 91 | "city": "杭州市", 92 | "adcode": "330100", 93 | "province": "浙江", 94 | "reporttime": "2023-08-17 23:32:22", 95 | "casts": [ 96 | { 97 | "date": "2023-08-17", 98 | "week": "4", 99 | "dayweather": "小雨", 100 | "nightweather": "小雨", 101 | "daytemp": "33", 102 | "nighttemp": "24", 103 | "daywind": "东", 104 | "nightwind": "东", 105 | "daypower": "≤3", 106 | "nightpower": "≤3", 107 | "daytemp_float": "33.0", 108 | "nighttemp_float": "24.0" 109 | }, 110 | { 111 | "date": "2023-08-18", 112 | "week": "5", 113 | "dayweather": "小雨", 114 | "nightweather": "小雨", 115 | "daytemp": "32", 116 | "nighttemp": "23", 117 | "daywind": "东北", 118 | "nightwind": "东北", 119 | "daypower": "4", 120 | "nightpower": "4", 121 | "daytemp_float": "32.0", 122 | "nighttemp_float": "23.0" 123 | }, 124 | { 125 | "date": "2023-08-19", 126 | "week": "6", 127 | "dayweather": "小雨", 128 | "nightweather": "雷阵雨", 129 | "daytemp": "32", 130 | "nighttemp": "24", 131 | "daywind": "东", 132 | "nightwind": "东", 133 | "daypower": "4", 134 | "nightpower": "4", 135 | "daytemp_float": "32.0", 136 | "nighttemp_float": "24.0" 137 | }, 138 | { 139 | "date": "2023-08-20", 140 | "week": "7", 141 | "dayweather": "雷阵雨", 142 | "nightweather": "多云", 143 | "daytemp": "33", 144 | "nighttemp": "25", 145 | "daywind": "东", 146 | "nightwind": "东", 147 | "daypower": "≤3", 148 | "nightpower": "≤3", 149 | "daytemp_float": "33.0", 150 | "nighttemp_float": "25.0" 151 | } 152 | ] 153 | } 154 | ] 155 | ``` 156 | 157 | See [API Document](https://apifox.com/apidoc/shared-c574e77f-4230-4727-9c05-c5c9988eed06) for more information. 158 | 159 |
160 | 161 | [![][back-to-top]](#readme-top) 162 | 163 |
164 | 165 | ## 🛳 Self Hosting 166 | 167 | If you want to deploy this service by yourself, you can follow the steps below. 168 | 169 | ### Deploy to Vercel 170 | 171 | Click button below to deploy your private plugins' gateway. 172 | 173 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flobehub%2Fchat-plugins-gateway&project-name=chat-plugins-gateway&repository-name=chat-plugins-gateway) 174 | 175 | If you want to make some customization, you can add environment variable: 176 | 177 | - `PLUGINS_INDEX_URL`: You can change the default plugins index url as your need. 178 | 179 |
180 | 181 | [![][back-to-top]](#readme-top) 182 | 183 |
184 | 185 | ## 📦 Plugin Ecosystem 186 | 187 | Plugins provide a means to extend the Function Calling capabilities of LobeChat. They can be used to introduce new function calls and even new ways to render message results. If you are interested in plugin development, please refer to our [📘 Plugin Development Guide](https://github.com/lobehub/lobe-chat/wiki/Plugin-Development) in the Wiki. 188 | 189 | - [lobe-chat-plugins][lobe-chat-plugins]: This is the plugin index for LobeChat. It accesses index.json from this repository to display a list of available plugins for LobeChat to the user. 190 | - [chat-plugin-template][chat-plugin-template]: This is the plugin template for LobeChat plugin development. 191 | - [@lobehub/chat-plugin-sdk][chat-plugin-sdk]: The LobeChat Plugin SDK assists you in creating exceptional chat plugins for Lobe Chat. 192 | - [@lobehub/chat-plugins-gateway][chat-plugins-gateway]: The LobeChat Plugins Gateway is a backend service that provides a gateway for LobeChat plugins. We deploy this service using Vercel. The primary API POST /api/v1/runner is deployed as an Edge Function. 193 | 194 |
195 | 196 | [![][back-to-top]](#readme-top) 197 | 198 |
199 | 200 | ## ⌨️ Local Development 201 | 202 | You can use Github Codespaces for online development: 203 | 204 | [![][github-codespace-shield]][github-codespace-link] 205 | 206 | Or clone it for local development: 207 | 208 | [![][bun-shield]][bun-link] 209 | 210 | ```bash 211 | $ git clone https://github.com/lobehub/chat-plugins-gateway.git 212 | $ cd chat-plugins-gateway 213 | $ bun install 214 | $ bun dev 215 | ``` 216 | 217 |
218 | 219 | [![][back-to-top]](#readme-top) 220 | 221 |
222 | 223 | ## 🤝 Contributing 224 | 225 | Contributions of all types are more than welcome, if you are interested in contributing code, feel free to check out our GitHub [Issues][github-issues-link] to get stuck in to show us what you’re made of. 226 | 227 | [![][pr-welcome-shield]][pr-welcome-link] 228 | 229 | [![][github-contrib-shield]][github-contrib-link] 230 | 231 |
232 | 233 | [![][back-to-top]](#readme-top) 234 | 235 |
236 | 237 | ## 🔗 Links 238 | 239 | - **[🤖 Lobe Chat](https://github.com/lobehub/lobe-chat)** - An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application. 240 | - **[🧩 / 🏪 Plugin Index](https://github.com/lobehub/lobe-chat-plugins)** - This is the plugin index for LobeChat. It accesses index.json from this repository to display a list of available plugins for Function Calling to the user. 241 | 242 |
243 | 244 | [![][back-to-top]](#readme-top) 245 | 246 |
247 | 248 | --- 249 | 250 | #### 📝 License 251 | 252 | Copyright © 2023 [LobeHub][profile-link].
253 | This project is [MIT](./LICENSE) licensed. 254 | 255 | 256 | 257 | [🤯-🧩-lobehub-link]: https://github.com/lobehub/lobe-chat-plugins 258 | [🤯-🧩-lobehub-shield]: https://img.shields.io/badge/%F0%9F%A4%AF%20%26%20%F0%9F%A7%A9%20LobeHub-Plugin-95f3d9?labelColor=black&style=flat-square 259 | [back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-black?style=flat-square 260 | [bun-link]: https://bun.sh 261 | [bun-shield]: https://img.shields.io/badge/-speedup%20with%20bun-black?logo=bun&style=for-the-badge 262 | [chat-plugin-sdk]: https://github.com/lobehub/chat-plugin-sdk 263 | [chat-plugin-template]: https://github.com/lobehub/chat-plugin- 264 | [chat-plugins-gateway]: https://github.com/lobehub/chat-plugins-gateway 265 | [github-action-release-link]: https://github.com/lobehub/chat-plugins-gateway/actions/workflows/release.yml 266 | [github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/lobehub/chat-plugins-gateway/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square 267 | [github-action-test-link]: https://github.com/lobehub/chat-plugins-gateway/actions/workflows/test.yml 268 | [github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/lobehub/chat-plugins-gateway/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square 269 | [github-codespace-link]: https://codespaces.new/lobehub/chat-plugins-gateway 270 | [github-codespace-shield]: https://github.com/codespaces/badge.svg 271 | [github-contrib-link]: https://github.com/lobehub/chat-plugins-gateway/graphs/contributors 272 | [github-contrib-shield]: https://contrib.rocks/image?repo=lobehub%2Fchat-plugins-gateway 273 | [github-contributors-link]: https://github.com/lobehub/chat-plugins-gateway/graphs/contributors 274 | [github-contributors-shield]: https://img.shields.io/github/contributors/lobehub/chat-plugins-gateway?color=c4f042&labelColor=black&style=flat-square 275 | [github-forks-link]: https://github.com/lobehub/chat-plugins-gateway/network/members 276 | [github-forks-shield]: https://img.shields.io/github/forks/lobehub/chat-plugins-gateway?color=8ae8ff&labelColor=black&style=flat-square 277 | [github-issues-link]: https://github.com/lobehub/chat-plugins-gateway/issues 278 | [github-issues-shield]: https://img.shields.io/github/issues/lobehub/chat-plugins-gateway?color=ff80eb&labelColor=black&style=flat-square 279 | [github-license-link]: https://github.com/lobehub/chat-plugins-gateway/blob/main/LICENSE 280 | [github-license-shield]: https://img.shields.io/github/license/lobehub/chat-plugins-gateway?color=white&labelColor=black&style=flat-square 281 | [github-releasedate-link]: https://github.com/lobehub/chat-plugins-gateway/releases 282 | [github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/chat-plugins-gateway?labelColor=black&style=flat-square 283 | [github-stars-link]: https://github.com/lobehub/chat-plugins-gateway/network/stargazers 284 | [github-stars-shield]: https://img.shields.io/github/stars/lobehub/chat-plugins-gateway?color=ffcb47&labelColor=black&style=flat-square 285 | [lobe-chat-plugins]: https://github.com/lobehub/lobe-chat-plugins 286 | [npm-release-link]: https://www.npmjs.com/package/@lobehub/chat-plugins-gateway 287 | [npm-release-shield]: https://img.shields.io/npm/v/@lobehub/chat-plugins-gateway?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square 288 | [pr-welcome-link]: https://github.com/lobehub/chat-plugins-gateway/pulls 289 | [pr-welcome-shield]: https://img.shields.io/badge/%F0%9F%A4%AF%20PR%20WELCOME-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge 290 | [profile-link]: https://github.com/lobehub 291 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |

LobeChat 插件网关

10 | 11 | LobeChat Plugin Gateway 是一个为 LobeChat 和 LobeHub 提供 Chat 插件网关的后端服务。 12 | 13 | [![][🤯-🧩-lobehub-shield]][🤯-🧩-lobehub-link] 14 | [![][npm-release-shield]][npm-release-link] 15 | [![][github-releasedate-shield]][github-releasedate-link] 16 | [![][github-action-test-shield]][github-action-test-link] 17 | [![][github-action-release-shield]][github-action-release-link]
18 | [![][github-contributors-shield]][github-contributors-link] 19 | [![][github-forks-shield]][github-forks-link] 20 | [![][github-stars-shield]][github-stars-link] 21 | [![][github-issues-shield]][github-issues-link] 22 | [![][github-license-shield]][github-license-link] 23 | 24 | [English](./README.md) · **简体中文** · [更新日志](./CHANGELOG.md) · [报告 Bug][github-issues-link] · [请求功能][github-issues-link] 25 | 26 |
27 | 28 |
29 | 目录 30 | 31 | #### TOC 32 | 33 | - [👋 简介](#-简介) 34 | - [🤯 使用方法](#-使用方法) 35 | - [基本 URL](#基本-url) 36 | - [POST 插件网关](#post-插件网关) 37 | - [🛳 自托管](#-自托管) 38 | - [部署到 Vercel](#部署到-vercel) 39 | - [📦 插件生态](#-插件生态) 40 | - [⌨️ Local Development](#️-local-development) 41 | - [🤝 Contributing](#-contributing) 42 | - [🔗 Links](#-links) 43 | 44 | #### 45 | 46 |
47 | 48 | ## 👋 简介 49 | 50 | LobeChat 插件网关是一个后端服务,为 LobeChat 插件提供网关。我们使用 [vercel](https://vercel.com/) 来部署此服务。主要 API `POST /api/v1/runner` 部署为[Edge Function](https://vercel.com/docs/functions/edge-functions)。 51 | 52 | 网关服务从 [LobeChat 插件](https://github.com/lobehub/lobe-chat-plugins) 获取 Lobe 插件索引,如果您想将您的插件添加到索引中,请在 LobeChat 插件仓库中[提交 PR](https://github.com/lobehub/lobe-chat-plugins/pulls)。 53 | 54 |
55 | 56 | [![][back-to-top]](#readme-top) 57 | 58 |
59 | 60 | ## 🤯 使用方法 61 | 62 | ### 基本 URL 63 | 64 | | 环境 | URL | 65 | | ------ | ---------------------------------------------- | 66 | | `PROD` | | 67 | | `DEV` | | 68 | 69 | ### POST 插件网关 70 | 71 | > **Note**\ 72 | > **POST** `/api/v1/runner`\ 73 | > 与 LobeChat 插件进行通信的接口。此接口描述了如何使用 LobeChat 插件网关 API 发送请求和获取响应。 74 | 75 | #### Body Request Parameters 请求体参数 76 | 77 | ```json 78 | { 79 | "arguments": "{\n \"city\": \"杭州\"\n}", 80 | "name": "realtimeWeather" 81 | } 82 | ``` 83 | 84 | #### Response 响应 85 | 86 | ```json 87 | [ 88 | { 89 | "city": "杭州市", 90 | "adcode": "330100", 91 | "province": "浙江", 92 | "reporttime": "2023-08-17 23:32:22", 93 | "casts": [ 94 | { 95 | "date": "2023-08-17", 96 | "week": "4", 97 | "dayweather": "小雨", 98 | "nightweather": "小雨", 99 | "daytemp": "33", 100 | "nighttemp": "24", 101 | "daywind": "东", 102 | "nightwind": "东", 103 | "daypower": "≤3", 104 | "nightpower": "≤3", 105 | "daytemp_float": "33.0", 106 | "nighttemp_float": "24.0" 107 | }, 108 | { 109 | "date": "2023-08-18", 110 | "week": "5", 111 | "dayweather": "小雨", 112 | "nightweather": "小雨", 113 | "daytemp": "32", 114 | "nighttemp": "23", 115 | "daywind": "东北", 116 | "nightwind": "东北", 117 | "daypower": "4", 118 | "nightpower": "4", 119 | "daytemp_float": "32.0", 120 | "nighttemp_float": "23.0" 121 | }, 122 | { 123 | "date": "2023-08-19", 124 | "week": "6", 125 | "dayweather": "小雨", 126 | "nightweather": "雷阵雨", 127 | "daytemp": "32", 128 | "nighttemp": "24", 129 | "daywind": "东", 130 | "nightwind": "东", 131 | "daypower": "4", 132 | "nightpower": "4", 133 | "daytemp_float": "32.0", 134 | "nighttemp_float": "24.0" 135 | }, 136 | { 137 | "date": "2023-08-20", 138 | "week": "7", 139 | "dayweather": "雷阵雨", 140 | "nightweather": "多云", 141 | "daytemp": "33", 142 | "nighttemp": "25", 143 | "daywind": "东", 144 | "nightwind": "东", 145 | "daypower": "≤3", 146 | "nightpower": "≤3", 147 | "daytemp_float": "33.0", 148 | "nighttemp_float": "25.0" 149 | } 150 | ] 151 | } 152 | ] 153 | ``` 154 | 155 | 更多信息请参见[API 文档](https://apifox.com/apidoc/shared-c574e77f-4230-4727-9c05-c5c9988eed06)。 156 | 157 |
158 | 159 | [![][back-to-top]](#readme-top) 160 | 161 |
162 | 163 | ## 🛳 自托管 164 | 165 | 如果您想自己部署此服务,可以按照以下步骤进行操作。 166 | 167 | ### 部署到 Vercel 168 | 169 | 点击下方按钮来部署您的私有插件网关。 170 | 171 | [![使用 Vercel 部署](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flobehub%2Fchat-plugins-gateway&project-name=chat-plugins-gateway&repository-name=chat-plugins-gateway) 172 | 173 | 如果您想进行一些自定义设置,可以在部署时添加环境变量(Environment Variable): 174 | 175 | - `PLUGINS_INDEX_URL`:你可以通过该变量指定插件市场的索引地址 176 | 177 |
178 | 179 | [![][back-to-top]](#readme-top) 180 | 181 |
182 | 183 | ## 📦 插件生态 184 | 185 | 插件提供了扩展 LobeChat Function Calling 能力的方法。可以用于引入新的 Function Calling,甚至是新的消息结果渲染方式。如果你对插件开发感兴趣,请在 Wiki 中查阅我们的 [📘 插件开发指引](https://github.com/lobehub/lobe-chat/wiki/Plugin-Development.zh-CN) 。 186 | 187 | - [lobe-chat-plugins][lobe-chat-plugins]:这是 LobeChat 的插件索引。它从该仓库的 index.json 中获取插件列表并显示给用户。 188 | - [chat-plugin-template][chat-plugin-template]: Chat Plugin 插件开发模版,你可以通过项目模版快速新建插件项目。 189 | - [@lobehub/chat-plugin-sdk][chat-plugin-sdk]:LobeChat 插件 SDK 可帮助您创建出色的 Lobe Chat 插件。 190 | - [@lobehub/chat-plugins-gateway][chat-plugins-gateway]:LobeChat 插件网关是一个后端服务,作为 LobeChat 插件的网关。我们使用 Vercel 部署此服务。主要的 API POST /api/v1/runner 被部署为 Edge Function。 191 | 192 |
193 | 194 | [![][back-to-top]](#readme-top) 195 | 196 |
197 | 198 | ## ⌨️ Local Development 199 | 200 | 可以使用 GitHub Codespaces 进行在线开发: 201 | 202 | [![][github-codespace-shield]][github-codespace-link] 203 | 204 | 或者使用以下命令进行本地开发: 205 | 206 | [![][bun-shield]][bun-link] 207 | 208 | ```bash 209 | $ git clone https://github.com/lobehub/chat-plugins-gateway.git 210 | $ cd chat-plugins-gateway 211 | $ bun install 212 | $ bun dev 213 | ``` 214 | 215 |
216 | 217 | [![][back-to-top]](#readme-top) 218 | 219 |
220 | 221 | ## 🤝 Contributing 222 | 223 | 我们非常欢迎各种形式的贡献。如果你对贡献代码感兴趣,可以查看我们的 GitHub [Issues][github-issues-link],大展身手,向我们展示你的奇思妙想。 224 | 225 | [![][pr-welcome-shield]][pr-welcome-link] 226 | 227 | [![][github-contrib-shield]][github-contrib-link] 228 | 229 |
230 | 231 | [![][back-to-top]](#readme-top) 232 | 233 |
234 | 235 | ## 🔗 Links 236 | 237 | - **[🤖 Lobe Chat](https://github.com/lobehub/lobe-chat)** - An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application. 238 | - **[🧩 / 🏪 Plugin Index](https://github.com/lobehub/lobe-chat-plugins)** - This is the plugin index for LobeChat. It accesses index.json from this repository to display a list of available plugins for Function Calling to the user. 239 | 240 |
241 | 242 | [![][back-to-top]](#readme-top) 243 | 244 |
245 | 246 | --- 247 | 248 | #### 📝 License 249 | 250 | Copyright © 2023 [LobeHub][profile-link].
251 | This project is [MIT](./LICENSE) licensed. 252 | 253 | 254 | 255 | [🤯-🧩-lobehub-link]: https://github.com/lobehub/lobe-chat-plugins 256 | [🤯-🧩-lobehub-shield]: https://img.shields.io/badge/%F0%9F%A4%AF%20%26%20%F0%9F%A7%A9%20LobeHub-Plugin-95f3d9?labelColor=black&style=flat-square 257 | [back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-black?style=flat-square 258 | [bun-link]: https://bun.sh 259 | [bun-shield]: https://img.shields.io/badge/-speedup%20with%20bun-black?logo=bun&style=for-the-badge 260 | [chat-plugin-sdk]: https://github.com/lobehub/chat-plugin-sdk 261 | [chat-plugin-template]: https://github.com/lobehub/chat-plugin- 262 | [chat-plugins-gateway]: https://github.com/lobehub/chat-plugins-gateway 263 | [github-action-release-link]: https://github.com/lobehub/chat-plugins-gateway/actions/workflows/release.yml 264 | [github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/lobehub/chat-plugins-gateway/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square 265 | [github-action-test-link]: https://github.com/lobehub/chat-plugins-gateway/actions/workflows/test.yml 266 | [github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/lobehub/chat-plugins-gateway/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square 267 | [github-codespace-link]: https://codespaces.new/lobehub/chat-plugins-gateway 268 | [github-codespace-shield]: https://github.com/codespaces/badge.svg 269 | [github-contrib-link]: https://github.com/lobehub/chat-plugins-gateway/graphs/contributors 270 | [github-contrib-shield]: https://contrib.rocks/image?repo=lobehub%2Fchat-plugins-gateway 271 | [github-contributors-link]: https://github.com/lobehub/chat-plugins-gateway/graphs/contributors 272 | [github-contributors-shield]: https://img.shields.io/github/contributors/lobehub/chat-plugins-gateway?color=c4f042&labelColor=black&style=flat-square 273 | [github-forks-link]: https://github.com/lobehub/chat-plugins-gateway/network/members 274 | [github-forks-shield]: https://img.shields.io/github/forks/lobehub/chat-plugins-gateway?color=8ae8ff&labelColor=black&style=flat-square 275 | [github-issues-link]: https://github.com/lobehub/chat-plugins-gateway/issues 276 | [github-issues-shield]: https://img.shields.io/github/issues/lobehub/chat-plugins-gateway?color=ff80eb&labelColor=black&style=flat-square 277 | [github-license-link]: https://github.com/lobehub/chat-plugins-gateway/blob/main/LICENSE 278 | [github-license-shield]: https://img.shields.io/github/license/lobehub/chat-plugins-gateway?color=white&labelColor=black&style=flat-square 279 | [github-releasedate-link]: https://github.com/lobehub/chat-plugins-gateway/releases 280 | [github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/chat-plugins-gateway?labelColor=black&style=flat-square 281 | [github-stars-link]: https://github.com/lobehub/chat-plugins-gateway/network/stargazers 282 | [github-stars-shield]: https://img.shields.io/github/stars/lobehub/chat-plugins-gateway?color=ffcb47&labelColor=black&style=flat-square 283 | [lobe-chat-plugins]: https://github.com/lobehub/lobe-chat-plugins 284 | [npm-release-link]: https://www.npmjs.com/package/@lobehub/chat-plugins-gateway 285 | [npm-release-shield]: https://img.shields.io/npm/v/@lobehub/chat-plugins-gateway?color=369eff&labelColor=black&logo=npm&logoColor=white&style=flat-square 286 | [pr-welcome-link]: https://github.com/lobehub/chat-plugins-gateway/pulls 287 | [pr-welcome-shield]: https://img.shields.io/badge/%F0%9F%A4%AF%20PR%20WELCOME-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge 288 | [profile-link]: https://github.com/lobehub 289 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from '@vercel/node'; 2 | 3 | export default async (req: VercelRequest, response: VercelResponse) => { 4 | response.json({ 5 | status: 'ok', 6 | }); 7 | return 'hello'; 8 | }; 9 | -------------------------------------------------------------------------------- /api/v1/runner.ts: -------------------------------------------------------------------------------- 1 | import { createGatewayOnNodeRuntime } from '../../src'; 2 | 3 | export default createGatewayOnNodeRuntime(); 4 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LobeChat Plugins v1.0.0 3 | language_tabs: 4 | - shell: Shell 5 | - http: HTTP 6 | - javascript: JavaScript 7 | - ruby: Ruby 8 | - python: Python 9 | - php: PHP 10 | - java: Java 11 | - go: Go 12 | toc_footers: [] 13 | includes: [] 14 | search: true 15 | code_clipboard: true 16 | highlight_theme: darkula 17 | headingLevel: 2 18 | generator: '@tarslib/widdershins v4.0.17' 19 | --- 20 | 21 | # LobeChat Plugins 22 | 23 | > v1.0.0 24 | 25 | Base URLs: 26 | 27 | - Prod URL: 28 | 29 | # Default 30 | 31 | ## POST 插件网关 32 | 33 | POST /api/v1/runner 34 | 35 | 与 LobeChat 插件进行通信的接口。本接口描述了如何使用 LobeChat 插件网关 API 来发送请求和获取响应。 36 | 37 | > Body 请求参数 38 | 39 | ```json 40 | { 41 | "arguments": "{\n \"city\": \"杭州\"\n}", 42 | "name": "realtimeWeather" 43 | } 44 | ``` 45 | 46 | ### 请求参数 47 | 48 | | 名称 | 位置 | 类型 | 必选 | 说明 | 49 | | ----------- | ---- | ------ | ---- | ---- | 50 | | body | body | object | 否 | none | 51 | | » name | body | string | 是 | none | 52 | | » arguments | body | string | 是 | none | 53 | | » indexUrl | body | string | 否 | none | 54 | 55 | > 返回示例 56 | 57 | > 成功 58 | 59 | > 请求入参校验失败 60 | 61 | ```json 62 | { 63 | "body": { 64 | "issues": [ 65 | { 66 | "code": "invalid_type", 67 | "expected": "string", 68 | "received": "undefined", 69 | "path": ["name"], 70 | "message": "Required" 71 | } 72 | ], 73 | "name": "ZodError" 74 | }, 75 | "errorType": 400 76 | } 77 | ``` 78 | 79 | ```json 80 | { 81 | "body": { 82 | "error": [ 83 | { 84 | "path": ["city"], 85 | "property": "instance.city", 86 | "message": "is not of a type(s) string", 87 | "instance": 123, 88 | "name": "type", 89 | "argument": ["string"], 90 | "stack": "instance.city is not of a type(s) string" 91 | } 92 | ], 93 | "manifest": { 94 | "version": "1", 95 | "name": "realtimeWeather", 96 | "schema": { 97 | "description": "获取当前天气情况", 98 | "name": "realtimeWeather", 99 | "parameters": { 100 | "properties": { 101 | "city": { 102 | "description": "城市名称", 103 | "type": "string" 104 | } 105 | }, 106 | "required": ["city"], 107 | "type": "object" 108 | } 109 | }, 110 | "server": { 111 | "url": "https://realtime-weather.chat-plugin.lobehub.com/api/v1" 112 | } 113 | }, 114 | "message": "[plugin] args is invalid with plugin manifest schema" 115 | }, 116 | "errorType": 400 117 | } 118 | ``` 119 | 120 | > 插件信息不存在 121 | 122 | ```json 123 | { 124 | "body": { 125 | "message": "[gateway] plugin is not found", 126 | "name": "abcccccc" 127 | }, 128 | "errorType": "pluginMetaNotFound" 129 | } 130 | ``` 131 | 132 | ```json 133 | { 134 | "body": { 135 | "manifestUrl": "https://web-crawler.chat-plugin.lobehub.com/manifest.json", 136 | "message": "[plugin] plugin manifest not found" 137 | }, 138 | "errorType": "pluginManifestNotFound" 139 | } 140 | ``` 141 | 142 | > 插件元数据错误 143 | 144 | ```json 145 | { 146 | "body": { 147 | "error": { 148 | "issues": [ 149 | { 150 | "code": "invalid_type", 151 | "expected": "string", 152 | "received": "undefined", 153 | "path": ["manifest"], 154 | "message": "Required" 155 | } 156 | ], 157 | "name": "ZodError" 158 | }, 159 | "message": "[plugin] plugin meta is invalid", 160 | "pluginMeta": { 161 | "createAt": "2023-08-12", 162 | "homepage": "https://github.com/lobehub/chat-plugin-real-time-weather", 163 | "meta": { 164 | "avatar": "☂️", 165 | "tags": ["weather", "realtime"] 166 | }, 167 | "name": "realtimeWeather", 168 | "schemaVersion": "v1" 169 | } 170 | }, 171 | "errorType": "pluginMetaInvalid" 172 | } 173 | ``` 174 | 175 | > 491 Response 176 | 177 | ```json 178 | {} 179 | ``` 180 | 181 | > 插件市场索引错误 182 | 183 | ```json 184 | { 185 | "body": { 186 | "error": { 187 | "issues": [ 188 | { 189 | "code": "invalid_type", 190 | "expected": "array", 191 | "received": "undefined", 192 | "path": ["plugins"], 193 | "message": "Required" 194 | }, 195 | { 196 | "code": "invalid_type", 197 | "expected": "number", 198 | "received": "undefined", 199 | "path": ["version"], 200 | "message": "Required" 201 | } 202 | ], 203 | "name": "ZodError" 204 | }, 205 | "indexUrl": "https://registry.npmmirror.com", 206 | "marketIndex": { 207 | "last_package": "@c2pkg/storage-upload", 208 | "last_package_version": "@c2pkg/storage-upload@3.10.0", 209 | "doc_count": 3561547, 210 | "doc_version_count": 34943697, 211 | "download": { 212 | "today": 25587449, 213 | "yesterday": 128550602, 214 | "samedayLastweek": 35493324, 215 | "thisweek": 776453391, 216 | "thismonth": 2190677962, 217 | "thisyear": 27479998339, 218 | "lastweek": 792798408, 219 | "lastmonth": 4224663889, 220 | "lastyear": 35082329272 221 | }, 222 | "update_seq": 63479086, 223 | "sync_model": "all", 224 | "sync_changes_steam": { 225 | "since": "35627379", 226 | "registryId": "6306496ddd636c97816f37d4", 227 | "taskWorker": "npmmirror-x86-20220823002:3275935", 228 | "task_count": 1597861518, 229 | "last_package": "gridcreator", 230 | "last_package_created": "2023-08-09T14:38:06.068Z" 231 | }, 232 | "sync_binary": true, 233 | "instance_start_time": "2023-08-17T14:05:53.874Z", 234 | "node_version": "v16.20.1", 235 | "app_version": "3.41.0", 236 | "engine": "mysql", 237 | "source_registry": "https://r.cnpmjs.org", 238 | "changes_stream_registry": "https://r.cnpmjs.org", 239 | "cache_time": "2023-08-19T07:05:14.181Z", 240 | "upstream_registries": [ 241 | { 242 | "since": "35627379", 243 | "registryId": "6306496ddd636c97816f37d4", 244 | "taskWorker": "npmmirror-x86-20220823002:3275935", 245 | "task_count": 1597861518, 246 | "last_package": "gridcreator", 247 | "last_package_created": "2023-08-09T14:38:06.068Z", 248 | "source_registry": "https://r.cnpmjs.org", 249 | "changes_stream_url": "https://r.cnpmjs.org/_changes", 250 | "registry_name": "default" 251 | } 252 | ] 253 | }, 254 | "message": "[gateway] plugin market index is invalid" 255 | }, 256 | "errorType": "pluginMarketIndexInvalid" 257 | } 258 | ``` 259 | 260 | ```json 261 | { 262 | "body": { 263 | "indexUrl": "https://baidu.com", 264 | "message": "[gateway] plugin market index not found" 265 | }, 266 | "errorType": "pluginMarketIndexNotFound" 267 | } 268 | ``` 269 | 270 | ### 返回结果 271 | 272 | | 状态码 | 状态码含义 | 说明 | 数据模型 | 273 | | ------ | ---------------------------------------------------------------- | -------------------- | -------- | 274 | | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | 成功 | Inline | 275 | | 400 | [Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1) | 请求入参校验失败 | Inline | 276 | | 404 | [Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4) | 插件信息不存在 | Inline | 277 | | 490 | Unknown | 插件元数据错误 | Inline | 278 | | 491 | Unknown | 插件描述文件校验失败 | Inline | 279 | | 590 | Unknown | 插件市场索引错误 | Inline | 280 | 281 | ### 返回数据结构 282 | 283 | 状态码 **490** 284 | 285 | | 名称 | 类型 | 必选 | 约束 | 中文名 | 说明 | 286 | | ------------- | ------ | ---- | ---- | ------ | ---- | 287 | | » body | object | true | none | | none | 288 | | »» error | object | true | none | | none | 289 | | »» message | string | true | none | | none | 290 | | »» pluginMeta | object | true | none | | none | 291 | | » errorType | string | true | none | | none | 292 | 293 | 状态码 **590** 294 | 295 | | 名称 | 类型 | 必选 | 约束 | 中文名 | 说明 | 296 | | -------------- | ------ | ----- | ---- | ------ | ---- | 297 | | » body | object | true | none | | none | 298 | | »» error | object | false | none | | none | 299 | | »» indexUrl | string | true | none | | none | 300 | | »» marketIndex | object | false | none | | none | 301 | | »» message | string | true | none | | none | 302 | | » errorType | string | true | none | | none | 303 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lobehub/chat-plugins-gateway", 3 | "version": "1.9.0", 4 | "description": "Lobe Chat Plugin Market", 5 | "repository": "https://github.com/lobehub/chat-plugins-gateway.git", 6 | "author": "LobeHub ", 7 | "sideEffects": false, 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "father build", 15 | "build:watch": "father dev", 16 | "ci": "npm run lint && npm run type-check && npm run doctor", 17 | "doctor": "father doctor", 18 | "lint": "eslint \"{src,api,docs}/**/*.{js,jsx,ts,tsx}\" --fix", 19 | "lint:md": "remark . --quiet --frail --output", 20 | "prepare": "husky install", 21 | "prepublishOnly": "npm run build", 22 | "prettier": "prettier -c --write \"**/**\"", 23 | "release": "semantic-release", 24 | "start": "vercel dev", 25 | "test": "vitest", 26 | "test:coverage": "vitest --coverage", 27 | "test:update": "vitest -u", 28 | "type-check": "tsc --noEmit" 29 | }, 30 | "lint-staged": { 31 | "*.md": [ 32 | "remark --quiet --output --", 33 | "prettier --write --no-error-on-unmatched-pattern" 34 | ], 35 | "*.json": [ 36 | "prettier --write --no-error-on-unmatched-pattern" 37 | ], 38 | "*.{js,jsx}": [ 39 | "prettier --write", 40 | "eslint --fix" 41 | ], 42 | "*.{ts,tsx}": [ 43 | "prettier --parser=typescript --write", 44 | "eslint --fix" 45 | ] 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not ie <= 10" 51 | ], 52 | "dependencies": { 53 | "@babel/runtime": "^7", 54 | "@cfworker/json-schema": "^1", 55 | "@lobehub/chat-plugin-sdk": "^1", 56 | "ajv": "^8", 57 | "swagger-client": "^3", 58 | "zod": "^3" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "^17", 62 | "@lobehub/lint": "latest", 63 | "@vercel/node": "^2", 64 | "@vitest/coverage-v8": "0.34.6", 65 | "commitlint": "^17", 66 | "cross-env": "^7", 67 | "eslint": "^8", 68 | "father": "4.3.1", 69 | "husky": "^8", 70 | "lint-staged": "^13", 71 | "prettier": "^3", 72 | "remark": "^14", 73 | "remark-cli": "^11", 74 | "semantic-release": "^21", 75 | "typescript": "^5", 76 | "vercel": "^32.7.1", 77 | "vitest": "0.34.6" 78 | }, 79 | "publishConfig": { 80 | "access": "public", 81 | "registry": "https://registry.npmjs.org" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lobe Chat Plugin Market 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/cors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Multi purpose CORS lib. 3 | * Note: Based on the `cors` package in npm but using only 4 | * web APIs. Feel free to use it in your own projects. 5 | */ 6 | 7 | export type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[]; 8 | 9 | export type OriginFn = ( 10 | origin: string | undefined, 11 | req: Request, 12 | ) => StaticOrigin | Promise; 13 | 14 | export interface CorsOptions { 15 | allowedHeaders?: string | string[]; 16 | credentials?: boolean; 17 | exposedHeaders?: string | string[]; 18 | maxAge?: number; 19 | methods?: string | string[]; 20 | optionsSuccessStatus?: number; 21 | origin?: StaticOrigin | OriginFn; 22 | preflightContinue?: boolean; 23 | } 24 | 25 | const defaultOptions: CorsOptions = { 26 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 27 | optionsSuccessStatus: 204, 28 | origin: '*', 29 | preflightContinue: false, 30 | }; 31 | 32 | function isOriginAllowed(origin: string, allowed: StaticOrigin): boolean { 33 | return Array.isArray(allowed) 34 | ? allowed.some((o) => isOriginAllowed(origin, o)) 35 | : typeof allowed === 'string' 36 | ? origin === allowed 37 | : allowed instanceof RegExp 38 | ? allowed.test(origin) 39 | : !!allowed; 40 | } 41 | 42 | function getOriginHeaders(reqOrigin: string | undefined, origin: StaticOrigin) { 43 | const headers = new Headers(); 44 | 45 | if (origin === '*') { 46 | // Allow any origin 47 | headers.set('Access-Control-Allow-Origin', '*'); 48 | } else if (typeof origin === 'string') { 49 | // Fixed origin 50 | headers.set('Access-Control-Allow-Origin', origin); 51 | headers.append('Vary', 'Origin'); 52 | } else { 53 | const allowed = isOriginAllowed(reqOrigin ?? '', origin); 54 | 55 | if (allowed && reqOrigin) { 56 | headers.set('Access-Control-Allow-Origin', reqOrigin); 57 | } 58 | headers.append('Vary', 'Origin'); 59 | } 60 | 61 | return headers; 62 | } 63 | 64 | // originHeadersFromReq 65 | 66 | async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginFn) { 67 | const reqOrigin = req.headers.get('Origin') || undefined; 68 | const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin; 69 | 70 | if (!value) return; 71 | return getOriginHeaders(reqOrigin, value); 72 | } 73 | 74 | function getAllowedHeaders(req: Request, allowed?: string | string[]) { 75 | const headers = new Headers(); 76 | 77 | if (!allowed) { 78 | // eslint-disable-next-line no-param-reassign 79 | allowed = req.headers.get('Access-Control-Request-Headers')!; 80 | headers.append('Vary', 'Access-Control-Request-Headers'); 81 | } else if (Array.isArray(allowed)) { 82 | // If the allowed headers is an array, turn it into a string 83 | // eslint-disable-next-line no-param-reassign 84 | allowed = allowed.join(','); 85 | } 86 | if (allowed) { 87 | headers.set('Access-Control-Allow-Headers', allowed); 88 | } 89 | 90 | return headers; 91 | } 92 | 93 | export default async function cors(req: Request, res: Response, options?: CorsOptions) { 94 | const opts = { ...defaultOptions, ...options }; 95 | const { headers } = res; 96 | const originHeaders = await originHeadersFromReq(req, opts.origin ?? false); 97 | const mergeHeaders = (v: string, k: string) => { 98 | if (k === 'Vary') headers.append(k, v); 99 | else headers.set(k, v); 100 | }; 101 | 102 | // If there's no origin we won't touch the response 103 | if (!originHeaders) return res; 104 | 105 | originHeaders.forEach((element, index) => { 106 | mergeHeaders(element, index); 107 | }); 108 | 109 | if (opts.credentials) { 110 | headers.set('Access-Control-Allow-Credentials', 'true'); 111 | } 112 | 113 | const exposed = Array.isArray(opts.exposedHeaders) 114 | ? opts.exposedHeaders.join(',') 115 | : opts.exposedHeaders; 116 | 117 | if (exposed) { 118 | headers.set('Access-Control-Expose-Headers', exposed); 119 | } 120 | 121 | // Handle the preflight request 122 | if (req.method === 'OPTIONS') { 123 | if (opts.methods) { 124 | const methods = Array.isArray(opts.methods) ? opts.methods.join(',') : opts.methods; 125 | 126 | headers.set('Access-Control-Allow-Methods', methods); 127 | } 128 | 129 | getAllowedHeaders(req, opts.allowedHeaders).forEach((element, index) => { 130 | mergeHeaders(element, index); 131 | }); 132 | 133 | if (typeof opts.maxAge === 'number') { 134 | headers.set('Access-Control-Max-Age', String(opts.maxAge)); 135 | } 136 | 137 | if (opts.preflightContinue) return res; 138 | 139 | headers.set('Content-Length', '0'); 140 | return new Response(null, { headers, status: opts.optionsSuccessStatus }); 141 | } 142 | 143 | // If we got here, it's a normal request 144 | return res; 145 | } 146 | 147 | export function initCors(options?: CorsOptions) { 148 | return (req: Request, res: Response) => cors(req, res, options); 149 | } 150 | -------------------------------------------------------------------------------- /src/edge.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PluginErrorType, 3 | PluginRequestPayload, 4 | createErrorResponse, 5 | getPluginSettingsFromRequest, 6 | } from '@lobehub/chat-plugin-sdk'; 7 | 8 | import cors, { CorsOptions } from './cors'; 9 | import { Gateway, GatewayErrorResponse, GatewayOptions } from './gateway'; 10 | 11 | export interface EdgeRuntimeGatewayOptions extends GatewayOptions { 12 | cors?: CorsOptions; 13 | } 14 | 15 | /** 16 | * create Gateway Edge Function with plugins index url 17 | * @param options {EdgeRuntimeGatewayOptions} 18 | */ 19 | export const createGatewayOnEdgeRuntime = (options: EdgeRuntimeGatewayOptions = {}) => { 20 | const gateway = new Gateway(options); 21 | 22 | const handler = async (req: Request): Promise => { 23 | // ========== 1. 校验请求方法 ========== // 24 | if (req.method !== 'POST') 25 | return createErrorResponse(PluginErrorType.MethodNotAllowed, { 26 | message: '[gateway] only allow POST method', 27 | }); 28 | 29 | const requestPayload = (await req.json()) as PluginRequestPayload; 30 | const settings = getPluginSettingsFromRequest(req); 31 | 32 | try { 33 | const res = await gateway.execute(requestPayload, settings); 34 | return new Response(res.data); 35 | } catch (error) { 36 | const { errorType, body } = error as GatewayErrorResponse; 37 | 38 | if (!errorType) { 39 | const err = error as Error; 40 | console.error(err.stack); 41 | return createErrorResponse(PluginErrorType.PluginGatewayError, { 42 | stack: err.stack, 43 | }); 44 | } 45 | return createErrorResponse(errorType, body); 46 | } 47 | }; 48 | 49 | return async (req: Request) => cors(req, await handler(req), options.cors); 50 | }; 51 | -------------------------------------------------------------------------------- /src/gateway.ts: -------------------------------------------------------------------------------- 1 | // reason to use cfworker json schema: 2 | // https://github.com/vercel/next.js/discussions/47063#discussioncomment-5303951 3 | import { Schema } from '@cfworker/json-schema'; 4 | import { 5 | IPluginErrorType, 6 | LobeChatPluginApi, 7 | LobeChatPluginManifest, 8 | LobeChatPluginsMarketIndex, 9 | PluginErrorType, 10 | PluginRequestPayload, 11 | createHeadersWithPluginSettings, 12 | marketIndexSchema, 13 | pluginManifestSchema, 14 | pluginMetaSchema, 15 | pluginRequestPayloadSchema, 16 | } from '@lobehub/chat-plugin-sdk'; 17 | import { OPENAPI_REQUEST_BODY_KEY } from '@lobehub/chat-plugin-sdk/openapi'; 18 | // @ts-ignore 19 | import SwaggerClient from 'swagger-client'; 20 | 21 | export const DEFAULT_PLUGINS_INDEX_URL = 'https://chat-plugins.lobehub.com'; 22 | 23 | type IValidator = (schema: Schema, value: any) => { errors?: any; valid: boolean }; 24 | 25 | export interface GatewayOptions { 26 | Validator?: IValidator; 27 | defaultPluginSettings?: Record>; 28 | /** 29 | * @default https://chat-plugins.lobehub.com 30 | */ 31 | pluginsIndexUrl?: string; 32 | } 33 | 34 | export interface GatewaySuccessResponse { 35 | data: string; 36 | success: true; 37 | } 38 | export interface GatewayErrorResponse { 39 | body: string | object; 40 | errorType: IPluginErrorType; 41 | success: false; 42 | } 43 | 44 | export class Gateway { 45 | private pluginIndexUrl = DEFAULT_PLUGINS_INDEX_URL; 46 | private _validator: IValidator | undefined; 47 | private defaultPluginSettings: Record> = {}; 48 | 49 | constructor(options?: GatewayOptions) { 50 | if (options?.pluginsIndexUrl) { 51 | this.pluginIndexUrl = options.pluginsIndexUrl; 52 | } 53 | 54 | if (options?.defaultPluginSettings) { 55 | this.defaultPluginSettings = options.defaultPluginSettings; 56 | } 57 | 58 | if (options?.Validator) { 59 | this._validator = options.Validator; 60 | } 61 | } 62 | 63 | private createSuccessResponse = (data: string) => { 64 | return { data, success: true } as const; 65 | }; 66 | 67 | private createErrorResponse = (errorType: IPluginErrorType | string, body?: string | object) => { 68 | throw { body, errorType, success: false }; 69 | }; 70 | 71 | private async validate(schema: Schema, value: any) { 72 | if (this._validator) return this._validator(schema, value); 73 | 74 | // reason to use cfworker json schema: 75 | // https://github.com/vercel/next.js/discussions/47063#discussioncomment-5303951 76 | const { Validator } = await import('@cfworker/json-schema'); 77 | const v = new Validator(schema); 78 | const validator = v.validate(value); 79 | if (!validator.valid) return { errors: validator.errors, valid: false }; 80 | return { valid: true }; 81 | } 82 | 83 | execute = async ( 84 | payload: PluginRequestPayload, 85 | settings?: any, 86 | ): Promise => { 87 | // ========== 2. 校验请求入参基础格式 ========== // 88 | const payloadParseResult = pluginRequestPayloadSchema.safeParse(payload); 89 | if (!payloadParseResult.success) 90 | return this.createErrorResponse(PluginErrorType.BadRequest, payloadParseResult.error); 91 | 92 | const { identifier, arguments: args, indexUrl, apiName } = payload; 93 | 94 | let manifest = payload.manifest as LobeChatPluginManifest | undefined; 95 | console.info(`[${identifier}] - ${apiName} `); 96 | 97 | // 入参中如果没有 manifest,则从插件市场索引中获取 98 | if (!manifest) { 99 | const marketIndexUrl = indexUrl ?? this.pluginIndexUrl; 100 | // ========== 3. 获取插件市场索引 ========== // 101 | 102 | let marketIndex: LobeChatPluginsMarketIndex | undefined; 103 | try { 104 | const indexRes = await fetch(marketIndexUrl); 105 | marketIndex = await indexRes.json(); 106 | } catch (error) { 107 | console.error(error); 108 | marketIndex = undefined; 109 | } 110 | 111 | // 插件市场索引不存在 112 | if (!marketIndex) 113 | return this.createErrorResponse(PluginErrorType.PluginMarketIndexNotFound, { 114 | indexUrl: marketIndexUrl, 115 | message: '[gateway] plugin market index not found', 116 | }); 117 | 118 | // 插件市场索引解析失败 119 | const indexParseResult = marketIndexSchema.safeParse(marketIndex); 120 | 121 | if (!indexParseResult.success) 122 | return this.createErrorResponse(PluginErrorType.PluginMarketIndexInvalid, { 123 | error: indexParseResult.error, 124 | indexUrl: marketIndexUrl, 125 | marketIndex, 126 | message: '[gateway] plugin market index is invalid', 127 | }); 128 | 129 | console.info( 130 | `[marketIndex V${marketIndex.schemaVersion}] total ${marketIndex.plugins.length} plugins`, 131 | ); 132 | 133 | // ========== 4. 校验插件 meta 完备性 ========== // 134 | 135 | const pluginMeta = marketIndex.plugins.find((i) => i.identifier === identifier); 136 | 137 | // 一个不规范的插件示例 138 | // const pluginMeta = { 139 | // createAt: '2023-08-12', 140 | // homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather', 141 | // manifest: 'https://registry.npmmirror.com/@lobehub/lobe-chat-plugins/latest/files', 142 | // meta: { 143 | // avatar: '☂️', 144 | // tags: ['weather', 'realtime'], 145 | // }, 146 | // name: 'realtimeWeather', 147 | // schemaVersion: 'v1', 148 | // }; 149 | 150 | // 校验插件是否存在 151 | if (!pluginMeta) 152 | return this.createErrorResponse(PluginErrorType.PluginMetaNotFound, { 153 | identifier, 154 | message: `[gateway] plugin '${identifier}' is not found,please check the plugin list in ${marketIndexUrl}, or create an issue to [lobe-chat-plugins](https://github.com/lobehub/lobe-chat-plugins/issues)`, 155 | }); 156 | 157 | const metaParseResult = pluginMetaSchema.safeParse(pluginMeta); 158 | 159 | if (!metaParseResult.success) 160 | return this.createErrorResponse(PluginErrorType.PluginMetaInvalid, { 161 | error: metaParseResult.error, 162 | message: '[plugin] plugin meta is invalid', 163 | pluginMeta, 164 | }); 165 | // ========== 5. 校验插件 manifest 完备性 ========== // 166 | 167 | // 获取插件的 manifest 168 | try { 169 | const pluginRes = await fetch(pluginMeta.manifest); 170 | manifest = (await pluginRes.json()) as LobeChatPluginManifest; 171 | } catch (error) { 172 | console.error(error); 173 | manifest = undefined; 174 | } 175 | 176 | if (!manifest) 177 | return this.createErrorResponse(PluginErrorType.PluginManifestNotFound, { 178 | manifestUrl: pluginMeta.manifest, 179 | message: '[plugin] plugin manifest not found', 180 | }); 181 | } 182 | 183 | const manifestParseResult = pluginManifestSchema.safeParse(manifest); 184 | 185 | if (!manifestParseResult.success) 186 | return this.createErrorResponse(PluginErrorType.PluginManifestInvalid, { 187 | error: manifestParseResult.error, 188 | manifest: manifest, 189 | message: '[plugin] plugin manifest is invalid', 190 | }); 191 | 192 | // console.log(`[${identifier}] plugin manifest:`, manifest); 193 | 194 | const defaultSettings = this.defaultPluginSettings[identifier] || {}; 195 | const finalSettings = { ...defaultSettings, ...settings }; 196 | // ========== 6. 校验是否按照 manifest 包含了 settings 配置 ========== // 197 | 198 | if (manifest.settings) { 199 | const { valid, errors } = await this.validate(manifest.settings as any, finalSettings); 200 | 201 | if (!valid) 202 | return this.createErrorResponse(PluginErrorType.PluginSettingsInvalid, { 203 | error: errors, 204 | message: '[plugin] your settings is invalid with plugin manifest setting schema', 205 | settings, 206 | }); 207 | } 208 | 209 | // ========== 7. 校验请求入参与 manifest 要求一致性 ========== // 210 | const api = manifest.api.find((i) => i.name === apiName); 211 | 212 | if (!api) 213 | return this.createErrorResponse(PluginErrorType.PluginApiNotFound, { 214 | manifest, 215 | message: '[plugin] api not found', 216 | request: { 217 | apiName, 218 | identifier, 219 | }, 220 | }); 221 | 222 | if (args) { 223 | const params = JSON.parse(args!); 224 | const { valid, errors } = await this.validate(api.parameters as any, params); 225 | 226 | if (!valid) 227 | return this.createErrorResponse(PluginErrorType.PluginApiParamsError, { 228 | api, 229 | error: errors, 230 | message: '[plugin] args is invalid with plugin manifest api schema', 231 | request: params, 232 | }); 233 | } 234 | 235 | // ========== 8. 兼容 OpenAPI 请求模式 ========== // 236 | if (manifest.openapi) { 237 | return await this.callOpenAPI(payload, finalSettings, manifest); 238 | } 239 | 240 | return await this.callApi(api, args, finalSettings); 241 | }; 242 | 243 | private async callApi( 244 | api: LobeChatPluginApi, 245 | args: string | undefined, 246 | settings: any, 247 | ): Promise { 248 | if (!api.url) 249 | return this.createErrorResponse(PluginErrorType.PluginApiParamsError, { 250 | api, 251 | message: '[plugin] missing api url', 252 | }); 253 | 254 | const response = await fetch(api.url, { 255 | body: args, 256 | headers: createHeadersWithPluginSettings(settings), 257 | method: 'POST', 258 | }); 259 | 260 | // ========== 9. 发送请求 ========== // 261 | 262 | // 不正常的错误,直接返回请求 263 | if (!response.ok) 264 | return this.createErrorResponse(response.status as IPluginErrorType, await response.text()); 265 | 266 | const data = await response.text(); 267 | 268 | // console.log(`[${identifier}]`, args, `result:`, JSON.stringify(data).slice(0, 1000)); 269 | 270 | return this.createSuccessResponse(data); 271 | } 272 | 273 | private async callOpenAPI( 274 | payload: PluginRequestPayload, 275 | settings: any = {}, 276 | manifest: LobeChatPluginManifest, 277 | ): Promise { 278 | const { arguments: args, apiName } = payload; 279 | 280 | // @ts-ignore 281 | // const { default: SwaggerClient } = await import('swagger-client'); 282 | 283 | const authorizations = {} as { 284 | [key: string]: any; 285 | basicAuth?: any; 286 | oauth2?: { 287 | accessToken: string; 288 | clientId: string; 289 | clientSecret: string; 290 | }; 291 | }; 292 | 293 | // 根据 settings 中的每个属性来构建 authorizations 对象 294 | for (const [key, value] of Object.entries(settings)) { 295 | // 处理 API Key 和 Bearer Token 296 | authorizations[key] = value as string; 297 | 298 | // TODO: Basic Auth and OAuth2 299 | // if (key.endsWith('_username') && key.endsWith('_password')) { 300 | // // 处理 HTTP Basic Authentication 301 | // const username = settings[key]; 302 | // const password = settings[key.replace('_username', '_password')]; 303 | // authorizations.basicAuth = new SwaggerClient.PasswordAuthorization(username, password); 304 | // console.log(authorizations.basicAuth); 305 | // } else if ( 306 | // key.endsWith('_clientId') && 307 | // key.endsWith('_clientSecret') && 308 | // key.endsWith('_accessToken') 309 | // ) { 310 | // // 处理 OAuth2 311 | // const clientId = settings[key]; 312 | // const clientSecret = settings[key.replace('_clientId', '_clientSecret')]; 313 | // const accessToken = settings[key.replace('_clientId', '_accessToken')]; 314 | // authorizations.oauth2 = { accessToken, clientId, clientSecret }; 315 | // } 316 | } 317 | 318 | let client; 319 | try { 320 | client = await SwaggerClient({ authorizations, url: manifest.openapi }); 321 | } catch (error) { 322 | return this.createErrorResponse(PluginErrorType.PluginOpenApiInitError, { 323 | error: error, 324 | message: '[plugin] openapi client init error', 325 | openapi: manifest.openapi, 326 | }); 327 | } 328 | 329 | const requestParams = JSON.parse(args || '{}'); 330 | const { [OPENAPI_REQUEST_BODY_KEY]: requestBody, ...parameters } = requestParams; 331 | 332 | try { 333 | const res = await client.execute({ operationId: apiName, parameters, requestBody }); 334 | 335 | return this.createSuccessResponse(res.text); 336 | } catch (error) { 337 | // 如果没有 status,说明没有发送请求,可能是 openapi 相关调用实现的问题 338 | if (!(error as any).status) { 339 | console.error(error); 340 | 341 | return this.createErrorResponse(PluginErrorType.PluginGatewayError, { 342 | apiName, 343 | authorizations, 344 | error: (error as Error).message, 345 | message: 346 | '[plugin] there are problem with sending openapi request, please contact with LobeHub Team', 347 | openapi: manifest.openapi, 348 | parameters, 349 | requestBody, 350 | }); 351 | } 352 | 353 | // 如果是 401 则说明是鉴权问题 354 | if ((error as Response).status === 401) 355 | return this.createErrorResponse(PluginErrorType.PluginSettingsInvalid); 356 | 357 | return this.createErrorResponse(PluginErrorType.PluginServerError, { error }); 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createGatewayOnEdgeRuntime, 3 | /** 4 | * please use `createGatewayOnEdgeRuntime` instead 5 | * @deprecated 6 | */ 7 | createGatewayOnEdgeRuntime as createLobeChatPluginGateway, 8 | } from './edge'; 9 | export * from './gateway'; 10 | export * from './node'; 11 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PluginErrorType, 3 | PluginRequestPayload, 4 | getPluginErrorStatus, 5 | getPluginSettingsFromHeaders, 6 | } from '@lobehub/chat-plugin-sdk'; 7 | import { VercelRequest, VercelResponse } from '@vercel/node'; 8 | import Ajv from 'ajv'; 9 | 10 | import { Gateway, GatewayErrorResponse, GatewayOptions } from './gateway'; 11 | 12 | export type NodeRuntimeGatewayOptions = Pick; 13 | 14 | export const createGatewayOnNodeRuntime = (options: NodeRuntimeGatewayOptions = {}) => { 15 | const gateway = new Gateway({ 16 | ...options, 17 | Validator: (schema, value) => { 18 | const ajv = new Ajv({ strict: false }); 19 | const validate = ajv.compile(schema); 20 | 21 | const valid = validate(value); 22 | return { 23 | errors: validate.errors, 24 | valid, 25 | }; 26 | }, 27 | }); 28 | 29 | return async (req: VercelRequest, res: VercelResponse) => { 30 | // ========== 1. 校验请求方法 ========== // 31 | if (req.method !== 'POST') { 32 | res.status(PluginErrorType.MethodNotAllowed).send({ 33 | message: '[gateway] only allow POST method', 34 | }); 35 | 36 | return; 37 | } 38 | 39 | let requestPayload = req.body as PluginRequestPayload | string; 40 | if (typeof requestPayload === 'string') { 41 | requestPayload = JSON.parse(requestPayload) as PluginRequestPayload; 42 | } 43 | 44 | const settings = getPluginSettingsFromHeaders(req.headers as any); 45 | 46 | try { 47 | const { data } = await gateway.execute(requestPayload, settings); 48 | 49 | res.send(data); 50 | } catch (error) { 51 | const { errorType, body } = error as GatewayErrorResponse; 52 | 53 | res 54 | .status(getPluginErrorStatus(errorType)) 55 | .send( 56 | errorType 57 | ? { body, errorType } 58 | : { body: error, errorType: PluginErrorType.PluginGatewayError }, 59 | ); 60 | } 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /tests/__snapshots__/edge.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginApiNotFound error when apiName is not found in manifest 1`] = ` 4 | { 5 | "body": { 6 | "manifest": { 7 | "api": [ 8 | { 9 | "description": "", 10 | "name": "test-api", 11 | "parameters": { 12 | "properties": {}, 13 | "type": "object", 14 | }, 15 | "url": "https://test-api-url.com", 16 | }, 17 | ], 18 | "identifier": "abc", 19 | "meta": {}, 20 | }, 21 | "message": "[plugin] api not found", 22 | "request": { 23 | "apiName": "unknown-api", 24 | "identifier": "test-plugin", 25 | }, 26 | }, 27 | "errorType": "PluginApiNotFound", 28 | } 29 | `; 30 | 31 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginApiParamsError when api parameters are invalid 1`] = ` 32 | { 33 | "body": { 34 | "api": { 35 | "description": "", 36 | "name": "test-api", 37 | "parameters": { 38 | "properties": { 39 | "a": { 40 | "type": "string", 41 | }, 42 | }, 43 | "required": [ 44 | "a", 45 | ], 46 | "type": "object", 47 | }, 48 | "url": "https://test-api-url.com", 49 | }, 50 | "error": [ 51 | { 52 | "error": "Instance does not have required property \\"a\\".", 53 | "instanceLocation": "#", 54 | "keyword": "required", 55 | "keywordLocation": "#/required", 56 | }, 57 | ], 58 | "message": "[plugin] args is invalid with plugin manifest api schema", 59 | "request": { 60 | "invalid": "params", 61 | }, 62 | }, 63 | "errorType": "PluginApiParamsError", 64 | } 65 | `; 66 | 67 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginApiParamsError when api url is missing 1`] = ` 68 | { 69 | "body": { 70 | "api": { 71 | "description": "", 72 | "name": "test-api", 73 | "parameters": { 74 | "properties": {}, 75 | "type": "object", 76 | }, 77 | }, 78 | "message": "[plugin] missing api url", 79 | }, 80 | "errorType": "PluginApiParamsError", 81 | } 82 | `; 83 | 84 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginManifestInvalid error when plugin manifest is invalid 1`] = ` 85 | { 86 | "body": { 87 | "error": { 88 | "issues": [ 89 | { 90 | "code": "invalid_type", 91 | "expected": "array", 92 | "message": "Required", 93 | "path": [ 94 | "api", 95 | ], 96 | "received": "undefined", 97 | }, 98 | { 99 | "code": "invalid_type", 100 | "expected": "string", 101 | "message": "Required", 102 | "path": [ 103 | "identifier", 104 | ], 105 | "received": "undefined", 106 | }, 107 | { 108 | "code": "invalid_type", 109 | "expected": "object", 110 | "message": "Required", 111 | "path": [ 112 | "meta", 113 | ], 114 | "received": "undefined", 115 | }, 116 | ], 117 | "name": "ZodError", 118 | }, 119 | "manifest": { 120 | "invalid": "manifest", 121 | }, 122 | "message": "[plugin] plugin manifest is invalid", 123 | }, 124 | "errorType": "PluginManifestInvalid", 125 | } 126 | `; 127 | 128 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginManifestNotFound error when plugin manifest is unreachable 1`] = ` 129 | { 130 | "body": { 131 | "manifestUrl": "https://test-plugin-url.com/manifest.json", 132 | "message": "[plugin] plugin manifest not found", 133 | }, 134 | "errorType": "PluginManifestNotFound", 135 | } 136 | `; 137 | 138 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginMarketIndexInvalid error when market index is invalid 1`] = ` 139 | { 140 | "body": { 141 | "error": { 142 | "issues": [ 143 | { 144 | "code": "invalid_type", 145 | "expected": "array", 146 | "message": "Required", 147 | "path": [ 148 | "plugins", 149 | ], 150 | "received": "undefined", 151 | }, 152 | { 153 | "code": "invalid_type", 154 | "expected": "number", 155 | "message": "Required", 156 | "path": [ 157 | "schemaVersion", 158 | ], 159 | "received": "undefined", 160 | }, 161 | ], 162 | "name": "ZodError", 163 | }, 164 | "indexUrl": "https://test-market-index-url.com", 165 | "marketIndex": { 166 | "invalid": "index", 167 | }, 168 | "message": "[gateway] plugin market index is invalid", 169 | }, 170 | "errorType": "PluginMarketIndexInvalid", 171 | } 172 | `; 173 | 174 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginMarketIndexNotFound error when market index is unreachable 1`] = ` 175 | { 176 | "body": { 177 | "indexUrl": "https://chat-plugins.lobehub.com", 178 | "message": "[gateway] plugin market index not found", 179 | }, 180 | "errorType": "PluginMarketIndexNotFound", 181 | } 182 | `; 183 | 184 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginMetaInvalid error when plugin meta is invalid 1`] = ` 185 | { 186 | "body": { 187 | "error": { 188 | "issues": [ 189 | { 190 | "code": "invalid_type", 191 | "expected": "string", 192 | "message": "Required", 193 | "path": [ 194 | "author", 195 | ], 196 | "received": "undefined", 197 | }, 198 | { 199 | "code": "invalid_type", 200 | "expected": "string", 201 | "message": "Required", 202 | "path": [ 203 | "createdAt", 204 | ], 205 | "received": "undefined", 206 | }, 207 | { 208 | "code": "invalid_type", 209 | "expected": "string", 210 | "message": "Required", 211 | "path": [ 212 | "homepage", 213 | ], 214 | "received": "undefined", 215 | }, 216 | { 217 | "code": "invalid_type", 218 | "expected": "string", 219 | "message": "Required", 220 | "path": [ 221 | "manifest", 222 | ], 223 | "received": "undefined", 224 | }, 225 | { 226 | "code": "invalid_type", 227 | "expected": "object", 228 | "message": "Required", 229 | "path": [ 230 | "meta", 231 | ], 232 | "received": "undefined", 233 | }, 234 | { 235 | "code": "invalid_type", 236 | "expected": "number", 237 | "message": "Required", 238 | "path": [ 239 | "schemaVersion", 240 | ], 241 | "received": "undefined", 242 | }, 243 | ], 244 | "name": "ZodError", 245 | }, 246 | "message": "[plugin] plugin meta is invalid", 247 | "pluginMeta": { 248 | "identifier": "test-plugin", 249 | "invalidMeta": true, 250 | }, 251 | }, 252 | "errorType": "PluginMetaInvalid", 253 | } 254 | `; 255 | 256 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginSettingsInvalid error when provided empty settings 1`] = ` 257 | { 258 | "body": { 259 | "error": [ 260 | { 261 | "error": "Instance does not have required property \\"abc\\".", 262 | "instanceLocation": "#", 263 | "keyword": "required", 264 | "keywordLocation": "#/required", 265 | }, 266 | ], 267 | "message": "[plugin] your settings is invalid with plugin manifest setting schema", 268 | }, 269 | "errorType": "PluginSettingsInvalid", 270 | } 271 | `; 272 | 273 | exports[`createGatewayOnEdgeRuntime > Error > should return PluginSettingsInvalid error when provided settings are invalid 1`] = ` 274 | { 275 | "body": { 276 | "error": [ 277 | { 278 | "error": "Instance does not have required property \\"abc\\".", 279 | "instanceLocation": "#", 280 | "keyword": "required", 281 | "keywordLocation": "#/required", 282 | }, 283 | ], 284 | "message": "[plugin] your settings is invalid with plugin manifest setting schema", 285 | "settings": { 286 | "invalid": "settings", 287 | }, 288 | }, 289 | "errorType": "PluginSettingsInvalid", 290 | } 291 | `; 292 | 293 | exports[`createGatewayOnEdgeRuntime > OpenAPI Auth > should return PluginGatewayError when not return status 1`] = ` 294 | { 295 | "body": { 296 | "apiName": "test-api", 297 | "authorizations": {}, 298 | "message": "[plugin] there are problem with sending openapi request, please contact with LobeHub Team", 299 | "openapi": "https://test-openapi-url.com", 300 | "parameters": {}, 301 | }, 302 | "errorType": "PluginGatewayError", 303 | } 304 | `; 305 | 306 | exports[`createGatewayOnEdgeRuntime > OpenAPI Auth > should return PluginServerError when OpenAPI request returns non-200 status 1`] = ` 307 | { 308 | "body": { 309 | "error": { 310 | "status": 400, 311 | "text": "Bad Request", 312 | }, 313 | }, 314 | "errorType": "PluginServerError", 315 | } 316 | `; 317 | 318 | exports[`createGatewayOnEdgeRuntime > OpenAPI Auth > should return PluginSettingsInvalid when OpenAPI request fails with 401 1`] = ` 319 | { 320 | "errorType": "PluginSettingsInvalid", 321 | } 322 | `; 323 | 324 | exports[`createGatewayOnEdgeRuntime > OpenAPI Error > should return PluginOpenApiInitError error when openapi client init fails 1`] = ` 325 | { 326 | "body": { 327 | "error": {}, 328 | "message": "[plugin] openapi client init error", 329 | "openapi": "https://test-openapi-url.com", 330 | }, 331 | "errorType": "PluginOpenApiInitError", 332 | } 333 | `; 334 | 335 | exports[`createGatewayOnEdgeRuntime > OpenAPI Error > should return error response when OpenAPI request fails 1`] = ` 336 | { 337 | "body": { 338 | "error": { 339 | "status": 500, 340 | "text": "Internal Server Error", 341 | }, 342 | }, 343 | "errorType": "PluginServerError", 344 | } 345 | `; 346 | -------------------------------------------------------------------------------- /tests/edge.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LobeChatPluginManifest, 3 | PluginErrorType, 4 | PluginRequestPayload, 5 | createHeadersWithPluginSettings, 6 | } from '@lobehub/chat-plugin-sdk'; 7 | import { LOBE_PLUGIN_SETTINGS } from '@lobehub/chat-plugin-sdk/lib/request'; 8 | import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway'; 9 | // @ts-ignore 10 | import SwaggerClient from 'swagger-client'; 11 | import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; 12 | 13 | vi.stubGlobal('fetch', vi.fn()); 14 | 15 | vi.mock('swagger-client', () => ({ 16 | default: vi.fn(), 17 | })); 18 | 19 | // 模拟响应数据 20 | const mockMarketIndex = { 21 | plugins: [ 22 | { 23 | identifier: 'test-plugin', 24 | manifest: 'https://test-plugin-url.com/manifest.json', 25 | }, 26 | ], 27 | schemaVersion: 1, 28 | }; 29 | 30 | const mockManifest = { 31 | api: [ 32 | { 33 | description: '', 34 | name: 'test-api', 35 | parameters: { properties: {}, type: 'object' }, 36 | url: 'https://test-api-url.com', 37 | }, 38 | ], 39 | identifier: 'abc', 40 | meta: {}, 41 | } as LobeChatPluginManifest; 42 | 43 | const mockManifestWithOpenAPI = { 44 | ...mockManifest, 45 | openapi: 'https://test-openapi-url.com', 46 | }; 47 | 48 | const mockPluginRequestPayload: PluginRequestPayload = { 49 | apiName: 'test-api', 50 | arguments: '{}', 51 | identifier: 'test-plugin', 52 | indexUrl: 'https://test-market-index-url.com', 53 | manifest: mockManifest, 54 | }; 55 | 56 | const mockRequest: Request = new Request('https://test-url.com', { 57 | body: JSON.stringify(mockPluginRequestPayload), 58 | method: 'POST', 59 | }); 60 | 61 | let gateway: ReturnType; 62 | 63 | beforeEach(() => { 64 | // Reset the mocked fetch before each test 65 | vi.resetAllMocks(); 66 | gateway = createGatewayOnEdgeRuntime(); 67 | }); 68 | 69 | describe('createGatewayOnEdgeRuntime', () => { 70 | it('should execute successfully when provided with correct payload and settings', async () => { 71 | (fetch as Mock).mockImplementation(async (url) => { 72 | if (url === 'https://test-market-index-url.com') 73 | return { 74 | json: async () => mockMarketIndex, 75 | ok: true, 76 | }; 77 | 78 | return new Response(JSON.stringify({ success: true }), { status: 200 }); 79 | }); 80 | 81 | const response = await gateway(mockRequest); 82 | const data = await response.json(); 83 | expect(response.status).toBe(200); 84 | expect(data).toEqual({ success: true }); 85 | }); 86 | 87 | it('should return correct response when OpenAPI request is successful', async () => { 88 | const mockSettings = { 89 | apiKey: 'mock-api-key', 90 | }; 91 | 92 | vi.mocked(SwaggerClient).mockResolvedValue({ 93 | execute: vi.fn().mockResolvedValue({ 94 | status: 200, 95 | text: JSON.stringify({ success: true }), 96 | }), 97 | }); 98 | 99 | const mockRequest: Request = new Request('https://test-url.com', { 100 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 101 | headers: createHeadersWithPluginSettings(mockSettings), 102 | method: 'POST', 103 | }); 104 | 105 | const response = await gateway(mockRequest); 106 | const data = await response.json(); 107 | expect(response.status).toBe(200); 108 | expect(data).toEqual({ success: true }); 109 | }); 110 | 111 | describe('with defaultPluginSettings', () => { 112 | it('should execute successfully when provided with defaultPluginSettings payload', async () => { 113 | (fetch as Mock).mockImplementation(async (url, { headers }) => { 114 | if (url === 'https://test-market-index-url.com') 115 | return { 116 | json: async () => mockMarketIndex, 117 | ok: true, 118 | }; 119 | 120 | return new Response(JSON.stringify({ headers }), { status: 200 }); 121 | }); 122 | 123 | const config = { abc: '123' }; 124 | 125 | gateway = createGatewayOnEdgeRuntime({ defaultPluginSettings: { 'test-plugin': config } }); 126 | 127 | const request: Request = new Request('https://test-url.com', { 128 | body: JSON.stringify(mockPluginRequestPayload), 129 | method: 'POST', 130 | }); 131 | 132 | const response = await gateway(request); 133 | const data = await response.json(); 134 | expect(response.status).toBe(200); 135 | expect(data).toEqual({ 136 | headers: { [LOBE_PLUGIN_SETTINGS]: JSON.stringify(config) }, 137 | }); 138 | }); 139 | }); 140 | 141 | describe('Error', () => { 142 | it('only allow post method', async () => { 143 | const invalidPayload = { ...mockPluginRequestPayload, identifier: null }; // Invalid payload 144 | const mockRequest: Request = new Request('https://test-url.com', { 145 | body: JSON.stringify(invalidPayload), 146 | method: 'PUT', 147 | }); 148 | const response = await gateway(mockRequest); 149 | expect(response.status).toBe(PluginErrorType.MethodNotAllowed); 150 | }); 151 | it('should return BadRequest error when payload is invalid', async () => { 152 | const invalidPayload = { ...mockPluginRequestPayload, identifier: null }; // Invalid payload 153 | const mockRequest: Request = new Request('https://test-url.com', { 154 | body: JSON.stringify(invalidPayload), 155 | method: 'POST', 156 | }); 157 | const response = await gateway(mockRequest); 158 | expect(response.status).toBe(PluginErrorType.BadRequest); 159 | }); 160 | 161 | it('should return PluginMarketIndexNotFound error when market index is unreachable', async () => { 162 | (fetch as Mock).mockRejectedValue(new Error('Network error')); 163 | const mockRequest: Request = new Request('https://test-url.com', { 164 | body: JSON.stringify({ 165 | ...mockPluginRequestPayload, 166 | indexUrl: undefined, 167 | manifest: undefined, 168 | }), 169 | method: 'POST', 170 | }); 171 | const response = await gateway(mockRequest); 172 | expect(response.status).toBe(590); 173 | expect(await response.json()).toMatchSnapshot(); 174 | }); 175 | 176 | it('should return PluginMarketIndexInvalid error when market index is invalid', async () => { 177 | (fetch as Mock).mockResolvedValueOnce({ 178 | json: async () => ({ invalid: 'index' }), 179 | ok: true, // Invalid market index 180 | }); 181 | const mockRequest: Request = new Request('https://test-url.com', { 182 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: undefined }), 183 | method: 'POST', 184 | }); 185 | const response = await gateway(mockRequest); 186 | expect(response.status).toBe(490); 187 | expect(await response.json()).toMatchSnapshot(); 188 | }); 189 | 190 | it('should return PluginMetaNotFound error when plugin meta does not exist', async () => { 191 | (fetch as Mock).mockResolvedValueOnce({ 192 | json: async () => mockMarketIndex, 193 | ok: true, 194 | }); 195 | const invalidPayload = { 196 | ...mockPluginRequestPayload, 197 | identifier: 'unknown-plugin', 198 | manifest: undefined, 199 | }; 200 | const mockRequest: Request = new Request('https://test-url.com', { 201 | body: JSON.stringify(invalidPayload), 202 | method: 'POST', 203 | }); 204 | 205 | const response = await gateway(mockRequest); 206 | 207 | expect(response.status).toEqual(404); 208 | expect(await response.json()).toEqual({ 209 | body: { 210 | identifier: 'unknown-plugin', 211 | message: 212 | "[gateway] plugin 'unknown-plugin' is not found,please check the plugin list in https://test-market-index-url.com, or create an issue to [lobe-chat-plugins](https://github.com/lobehub/lobe-chat-plugins/issues)", 213 | }, 214 | errorType: 'PluginMetaNotFound', 215 | }); 216 | }); 217 | 218 | it('should return PluginMetaInvalid error when plugin meta is invalid', async () => { 219 | (fetch as Mock).mockResolvedValueOnce({ 220 | json: async () => ({ 221 | ...mockMarketIndex, 222 | plugins: [{ identifier: 'test-plugin', invalidMeta: true }], 223 | }), 224 | ok: true, 225 | }); 226 | const mockRequest: Request = new Request('https://test-url.com', { 227 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: undefined }), 228 | method: 'POST', 229 | }); 230 | const response = await gateway(mockRequest); 231 | expect(response.status).toBe(490); 232 | expect(await response.json()).toMatchSnapshot(); 233 | }); 234 | 235 | it('should return PluginManifestNotFound error when plugin manifest is unreachable', async () => { 236 | (fetch as Mock).mockImplementation(async (url) => { 237 | if (url === mockPluginRequestPayload.indexUrl) 238 | return { 239 | json: async () => ({ 240 | ...mockMarketIndex, 241 | plugins: [ 242 | { 243 | author: 'test-plugin', 244 | createdAt: '2023-08-12', 245 | homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather', 246 | identifier: 'test-plugin', 247 | manifest: 'https://test-plugin-url.com/manifest.json', 248 | meta: { 249 | avatar: '☂️', 250 | tags: ['weather', 'realtime'], 251 | title: 'realtimeWeather', 252 | }, 253 | schemaVersion: 1, 254 | }, 255 | ], 256 | }), 257 | ok: true, 258 | }; 259 | 260 | if (url === 'https://test-plugin-url.com/manifest.json') 261 | return { 262 | json: async () => { 263 | throw new Error('Network error'); 264 | }, 265 | ok: true, 266 | }; 267 | 268 | throw new Error('Network error'); 269 | }); 270 | 271 | const mockRequest: Request = new Request('https://test-url.com', { 272 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: undefined }), 273 | method: 'POST', 274 | }); 275 | const response = await gateway(mockRequest); 276 | 277 | expect(response.status).toBe(404); 278 | expect(await response.json()).toMatchSnapshot(); 279 | }); 280 | 281 | it('should return PluginManifestInvalid error when plugin manifest is invalid', async () => { 282 | (fetch as Mock).mockImplementation(async (url) => { 283 | if (url === mockPluginRequestPayload.indexUrl) 284 | return { 285 | json: async () => ({ 286 | ...mockMarketIndex, 287 | plugins: [ 288 | { 289 | author: 'test-plugin', 290 | createdAt: '2023-08-12', 291 | homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather', 292 | identifier: 'test-plugin', 293 | manifest: 'https://test-plugin-url.com/manifest.json', 294 | meta: { 295 | avatar: '☂️', 296 | tags: ['weather', 'realtime'], 297 | title: 'realtimeWeather', 298 | }, 299 | schemaVersion: 1, 300 | }, 301 | ], 302 | }), 303 | ok: true, 304 | }; 305 | 306 | if (url === 'https://test-plugin-url.com/manifest.json') 307 | return { 308 | json: async () => ({ invalid: 'manifest' }), 309 | ok: true, 310 | }; 311 | 312 | throw new Error('Network error'); 313 | }); 314 | 315 | const mockRequest: Request = new Request('https://test-url.com', { 316 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: undefined }), 317 | method: 'POST', 318 | }); 319 | const response = await gateway(mockRequest); 320 | expect(response.status).toBe(490); 321 | expect(await response.json()).toMatchSnapshot(); 322 | }); 323 | 324 | it('should return PluginSettingsInvalid error when provided settings are invalid', async () => { 325 | const settings = { invalid: 'settings' }; 326 | 327 | const payload = { 328 | ...mockPluginRequestPayload, 329 | manifest: { 330 | ...mockManifest, 331 | settings: { properties: { abc: { type: 'string' } }, required: ['abc'], type: 'object' }, 332 | } as LobeChatPluginManifest, 333 | }; 334 | const mockRequest: Request = new Request('https://test-url.com', { 335 | body: JSON.stringify(payload), 336 | headers: createHeadersWithPluginSettings(settings), 337 | method: 'POST', 338 | }); 339 | 340 | const response = await gateway(mockRequest); 341 | expect(response.status).toBe(422); 342 | expect(await response.json()).toMatchSnapshot(); 343 | }); 344 | 345 | it('should return PluginSettingsInvalid error when provided empty settings', async () => { 346 | const payload = { 347 | ...mockPluginRequestPayload, 348 | manifest: { 349 | ...mockManifest, 350 | settings: { properties: { abc: { type: 'string' } }, required: ['abc'], type: 'object' }, 351 | } as LobeChatPluginManifest, 352 | }; 353 | 354 | const mockRequest: Request = new Request('https://test-url.com', { 355 | body: JSON.stringify(payload), 356 | method: 'POST', 357 | }); 358 | const response = await gateway(mockRequest); 359 | expect(response.status).toBe(422); 360 | expect(await response.json()).toMatchSnapshot(); 361 | }); 362 | 363 | it('should return PluginApiNotFound error when apiName is not found in manifest', async () => { 364 | const payload = { ...mockPluginRequestPayload, apiName: 'unknown-api' }; 365 | const mockRequest: Request = new Request('https://test-url.com', { 366 | body: JSON.stringify(payload), 367 | method: 'POST', 368 | }); 369 | const response = await gateway(mockRequest); 370 | expect(response.status).toBe(404); 371 | expect(await response.json()).toMatchSnapshot(); 372 | }); 373 | 374 | it('should return PluginApiParamsError when api parameters are invalid', async () => { 375 | const payload: PluginRequestPayload = { 376 | ...mockPluginRequestPayload, 377 | // Invalid parameters 378 | arguments: '{"invalid": "params"}', 379 | manifest: { 380 | ...mockManifest, 381 | api: [ 382 | { 383 | description: '', 384 | name: 'test-api', 385 | parameters: { 386 | properties: { a: { type: 'string' } }, 387 | required: ['a'], 388 | type: 'object', 389 | }, 390 | url: 'https://test-api-url.com', 391 | }, 392 | ], 393 | } as LobeChatPluginManifest, 394 | }; 395 | const mockRequest: Request = new Request('https://test-url.com', { 396 | body: JSON.stringify(payload), 397 | method: 'POST', 398 | }); 399 | const response = await gateway(mockRequest); 400 | expect(response.status).toBe(422); 401 | expect(await response.json()).toMatchSnapshot(); 402 | }); 403 | 404 | it('should return PluginApiParamsError when api url is missing', async () => { 405 | const manifestWithoutUrl = { 406 | ...mockManifest, 407 | api: [{ ...mockManifest.api[0], url: undefined }], 408 | }; 409 | const mockRequest: Request = new Request('https://test-url.com', { 410 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: manifestWithoutUrl }), 411 | method: 'POST', 412 | }); 413 | 414 | const response = await gateway(mockRequest); 415 | expect(response.status).toBe(422); 416 | expect(await response.json()).toMatchSnapshot(); 417 | }); 418 | 419 | it('should return correct response when API request is successful', async () => { 420 | (fetch as Mock).mockResolvedValueOnce( 421 | new Response(JSON.stringify({ success: true }), { status: 200 }), 422 | ); 423 | 424 | const mockRequest: Request = new Request('https://test-url.com', { 425 | body: JSON.stringify(mockPluginRequestPayload), 426 | method: 'POST', 427 | }); 428 | 429 | const response = await gateway(mockRequest); 430 | const data = await response.json(); 431 | expect(response.status).toBe(200); 432 | expect(data).toEqual({ success: true }); 433 | }); 434 | 435 | it('should return error response when API request fails', async () => { 436 | (fetch as Mock).mockResolvedValueOnce(new Response('Internal Server Error', { status: 500 })); 437 | 438 | const mockRequest: Request = new Request('https://test-url.com', { 439 | body: JSON.stringify(mockPluginRequestPayload), 440 | method: 'POST', 441 | }); 442 | 443 | const response = await gateway(mockRequest); 444 | expect(response.status).toBe(500); 445 | expect(await response.text()).toBe( 446 | JSON.stringify({ body: 'Internal Server Error', errorType: 500 }), 447 | ); 448 | }); 449 | }); 450 | 451 | describe('OpenAPI Error', () => { 452 | const mockSettings = { 453 | apiKey: 'mock-api-key', 454 | }; 455 | 456 | it('should return PluginOpenApiInitError error when openapi client init fails', async () => { 457 | vi.mocked(SwaggerClient).mockImplementationOnce(() => { 458 | throw new Error('Initialization failed'); 459 | }); 460 | 461 | const mockRequest: Request = new Request('https://test-url.com', { 462 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 463 | headers: createHeadersWithPluginSettings(mockSettings), 464 | method: 'POST', 465 | }); 466 | 467 | const response = await gateway(mockRequest); 468 | expect(response.status).toBe(500); 469 | expect(await response.json()).toMatchSnapshot(); 470 | }); 471 | 472 | it('should return error response when OpenAPI request fails', async () => { 473 | vi.mocked(SwaggerClient).mockResolvedValue({ 474 | execute: vi.fn().mockRejectedValueOnce({ 475 | status: 500, 476 | text: 'Internal Server Error', 477 | }), 478 | }); 479 | 480 | const mockRequest: Request = new Request('https://test-url.com', { 481 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 482 | headers: createHeadersWithPluginSettings(mockSettings), 483 | method: 'POST', 484 | }); 485 | 486 | const response = await gateway(mockRequest); 487 | expect(response.status).toBe(500); 488 | expect(await response.json()).toMatchSnapshot(); 489 | }); 490 | }); 491 | 492 | describe('OpenAPI Auth', () => { 493 | it.skip('should handle authorization correctly for basicAuth', async () => { 494 | const settingsWithBasicAuth = { 495 | basic_auth_password: 'testpass', 496 | basic_auth_username: 'testuser', 497 | }; 498 | const mockSwaggerExecute = vi.fn().mockResolvedValue({ 499 | status: 200, 500 | text: JSON.stringify({ success: true }), 501 | }); 502 | vi.mocked(SwaggerClient).mockResolvedValue({ 503 | execute: mockSwaggerExecute, 504 | }); 505 | const PasswordAuthorization = vi 506 | .fn() 507 | .mockImplementation((u: string, p: string) => [u, p].join('-')); 508 | 509 | SwaggerClient['PasswordAuthorization'] = PasswordAuthorization; 510 | 511 | const mockRequest: Request = new Request('https://test-url.com', { 512 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 513 | headers: createHeadersWithPluginSettings(settingsWithBasicAuth), 514 | method: 'POST', 515 | }); 516 | 517 | await gateway(mockRequest); 518 | 519 | expect(SwaggerClient).toHaveBeenCalledWith( 520 | expect.objectContaining({ 521 | authorizations: expect.objectContaining({ 522 | basicAuth: expect.anything(), 523 | }), 524 | url: 'https://test-openapi-url.com', 525 | }), 526 | ); 527 | expect(mockSwaggerExecute).toHaveBeenCalled(); 528 | }); 529 | 530 | it.skip('should handle authorization correctly for OAuth2', async () => { 531 | const settingsWithOAuth2 = { 532 | oauth2_accessToken: 'testtoken', 533 | oauth2_clientId: 'testclient', 534 | oauth2_clientSecret: 'testsecret', 535 | }; 536 | const mockSwaggerExecute = vi.fn().mockResolvedValue({ 537 | status: 200, 538 | text: JSON.stringify({ success: true }), 539 | }); 540 | vi.mocked(SwaggerClient).mockResolvedValue({ 541 | execute: mockSwaggerExecute, 542 | }); 543 | 544 | const mockRequest: Request = new Request('https://test-url.com', { 545 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 546 | headers: createHeadersWithPluginSettings(settingsWithOAuth2), 547 | method: 'POST', 548 | }); 549 | 550 | await gateway(mockRequest); 551 | 552 | expect(SwaggerClient).toHaveBeenCalledWith({ 553 | authorizations: expect.objectContaining({ 554 | oauth2: expect.objectContaining({ 555 | accessToken: 'testtoken', 556 | clientId: 'testclient', 557 | clientSecret: 'testsecret', 558 | }), 559 | }), 560 | url: 'https://test-openapi-url.com', 561 | }); 562 | expect(mockSwaggerExecute).toHaveBeenCalled(); 563 | }); 564 | 565 | it('should return PluginServerError when OpenAPI request returns non-200 status', async () => { 566 | const mockSwaggerExecute = vi.fn().mockRejectedValue({ 567 | status: 400, 568 | text: 'Bad Request', 569 | }); 570 | vi.mocked(SwaggerClient).mockResolvedValue({ 571 | execute: mockSwaggerExecute, 572 | }); 573 | 574 | const mockRequest: Request = new Request('https://test-url.com', { 575 | body: JSON.stringify({ 576 | ...mockPluginRequestPayload, 577 | manifest: mockManifestWithOpenAPI, 578 | }), 579 | headers: createHeadersWithPluginSettings({}), 580 | method: 'POST', 581 | }); 582 | 583 | const response = await gateway(mockRequest); 584 | 585 | expect(response.status).toBe(500); 586 | expect(await response.json()).toMatchSnapshot(); 587 | }); 588 | 589 | it('should return PluginGatewayError when not return status', async () => { 590 | const mockSwaggerExecute = vi.fn().mockRejectedValue({ 591 | text: 'Bad Request', 592 | }); 593 | vi.mocked(SwaggerClient).mockResolvedValue({ 594 | execute: mockSwaggerExecute, 595 | }); 596 | const mockRequest: Request = new Request('https://test-url.com', { 597 | body: JSON.stringify({ 598 | ...mockPluginRequestPayload, 599 | arguments: undefined, 600 | manifest: mockManifestWithOpenAPI, 601 | }), 602 | headers: createHeadersWithPluginSettings({}), 603 | method: 'POST', 604 | }); 605 | 606 | const response = await gateway(mockRequest); 607 | 608 | expect(response.status).toBe(500); 609 | expect(await response.json()).toMatchSnapshot(); 610 | }); 611 | 612 | it('should return PluginSettingsInvalid when OpenAPI request fails with 401', async () => { 613 | const mockSwaggerExecute = vi.fn().mockRejectedValue({ 614 | status: 401, 615 | text: 'Unauthorized', 616 | }); 617 | vi.mocked(SwaggerClient).mockResolvedValue({ 618 | execute: mockSwaggerExecute, 619 | }); 620 | 621 | const mockRequest: Request = new Request('https://test-url.com', { 622 | body: JSON.stringify({ ...mockPluginRequestPayload, manifest: mockManifestWithOpenAPI }), 623 | headers: createHeadersWithPluginSettings({}), 624 | method: 'POST', 625 | }); 626 | const response = await gateway(mockRequest); 627 | expect(response.status).toBe(422); 628 | expect(await response.json()).toMatchSnapshot(); 629 | }); 630 | }); 631 | }); 632 | -------------------------------------------------------------------------------- /tests/gateway.test.ts: -------------------------------------------------------------------------------- 1 | import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk'; 2 | import { Gateway } from '@lobehub/chat-plugins-gateway'; 3 | import Ajv from 'ajv'; 4 | // @ts-ignore 5 | import SwaggerClient from 'swagger-client'; 6 | import { describe, expect, it, vi } from 'vitest'; 7 | 8 | vi.mock('swagger-client', () => ({ 9 | default: vi.fn(), 10 | })); 11 | 12 | describe('Gateway', () => { 13 | it('should init with pluginIndexUrl', () => { 14 | const gateway = new Gateway({ pluginsIndexUrl: 'https://test-market-index-url.com' }); 15 | 16 | expect(gateway['pluginIndexUrl']).toBe('https://test-market-index-url.com'); 17 | }); 18 | 19 | describe('using AJV Validator', () => { 20 | const gateway = new Gateway({ 21 | Validator: (schema, value) => { 22 | const ajv = new Ajv({ strict: false }); 23 | const validate = ajv.compile(schema); 24 | 25 | const valid = validate(value); 26 | return { 27 | errors: validate.errors, 28 | valid, 29 | }; 30 | }, 31 | }); 32 | 33 | it('run with ajv validator', async () => { 34 | const mockResult = JSON.stringify({ success: true }); 35 | vi.mocked(SwaggerClient).mockResolvedValue({ 36 | execute: vi.fn().mockResolvedValue({ 37 | status: 200, 38 | text: mockResult, 39 | }), 40 | }); 41 | 42 | const payload = { 43 | apiName: 'getSupportedVendors', 44 | arguments: '{}', 45 | identifier: 'mock-credit-card', 46 | manifest: { 47 | $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json', 48 | api: [ 49 | { 50 | description: 'It is API to get list of supported vendors', 51 | name: 'getSupportedVendors', 52 | parameters: { 53 | properties: {}, 54 | type: 'object', 55 | }, 56 | }, 57 | ], 58 | author: 'arvinxx', 59 | createdAt: '2023-12-11', 60 | identifier: 'mock-credit-card', 61 | meta: { 62 | avatar: '💳', 63 | description: 'Credit Card Number Generator', 64 | tags: ['credit-card', 'mockup', 'generator'], 65 | title: 'Credit Card Generator', 66 | }, 67 | openapi: 68 | 'https://lobe-plugin-mock-credit-card-arvinxx.vercel.app/credit-card-number-generator.json', 69 | settings: { 70 | properties: { 71 | apiKeyAuth: { 72 | description: 'apiKeyAuth API Key', 73 | format: 'password', 74 | title: 'X-OpenAPIHub-Key', 75 | type: 'string', 76 | }, 77 | }, 78 | required: ['apiKeyAuth'], 79 | type: 'object', 80 | }, 81 | version: '1', 82 | }, 83 | type: 'default', 84 | } as PluginRequestPayload; 85 | 86 | const res = await gateway.execute(payload, { apiKeyAuth: 'abc' }); 87 | 88 | expect(res.success).toBeTruthy(); 89 | expect(res.data).toEqual(mockResult); 90 | }); 91 | 92 | it('run with ajv with $$refs', async () => { 93 | const mockResult = JSON.stringify({ success: true }); 94 | vi.mocked(SwaggerClient).mockResolvedValue({ 95 | execute: vi.fn().mockResolvedValue({ 96 | status: 200, 97 | text: mockResult, 98 | }), 99 | }); 100 | 101 | const payload = { 102 | apiName: 'getRandomCardNumber', 103 | arguments: '{"vendor":"visa"}', 104 | identifier: 'mock-credit-card', 105 | manifest: { 106 | $schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json', 107 | api: [ 108 | { 109 | description: 'It is API to generate random credit card numbers', 110 | name: 'getRandomCardNumber', 111 | parameters: { 112 | properties: { 113 | vendor: { 114 | $$ref: 'https://chat-dev.lobehub.com/chat#/components/schemas/Vendor', 115 | enum: [ 116 | 'visa', 117 | 'master-card', 118 | 'diners-club', 119 | 'american-express', 120 | 'discover', 121 | 'jcb', 122 | ], 123 | type: 'string', 124 | }, 125 | }, 126 | required: ['vendor'], 127 | type: 'object', 128 | }, 129 | }, 130 | { 131 | description: 'It is API to get list of supported vendors', 132 | name: 'getSupportedVendors', 133 | parameters: { properties: {}, type: 'object' }, 134 | }, 135 | ], 136 | author: 'arvinxx', 137 | createdAt: '2023-12-11', 138 | identifier: 'mock-credit-card', 139 | meta: { 140 | avatar: '💳', 141 | description: 'Credit Card Number Generator', 142 | tags: ['credit-card', 'mockup', 'generator'], 143 | title: 'Credit Card Generator', 144 | }, 145 | openapi: 146 | 'https://lobe-plugin-mock-credit-card-arvinxx.vercel.app/credit-card-number-generator.json', 147 | settings: { 148 | properties: { 149 | apiKeyAuth: { 150 | description: 'apiKeyAuth API Key', 151 | format: 'password', 152 | title: 'X-OpenAPIHub-Key', 153 | type: 'string', 154 | }, 155 | }, 156 | required: ['apiKeyAuth'], 157 | type: 'object', 158 | }, 159 | version: '1', 160 | }, 161 | type: 'default', 162 | } as PluginRequestPayload; 163 | 164 | const res = await gateway.execute(payload, { apiKeyAuth: 'abc' }); 165 | 166 | expect(res.success).toBeTruthy(); 167 | expect(res.data).toEqual(mockResult); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES5", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "skipDefaultLibCheck": true, 9 | "baseUrl": ".", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "paths": { 19 | "@lobehub/chat-plugins-gateway": ["src"] 20 | } 21 | }, 22 | "include": ["types", "api", "src", "*.ts", "tests"] 23 | } 24 | -------------------------------------------------------------------------------- /types/pluginItem.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionFunctions } from './schema'; 2 | 3 | /** 4 | * 插件项 5 | * @template Result - 结果类型,默认为 any 6 | * @template RunnerParams - 运行参数类型,默认为 any 7 | */ 8 | export interface PluginItem { 9 | /** 10 | * 头像 11 | */ 12 | avatar: string; 13 | /** 14 | * 名称 15 | */ 16 | name: string; 17 | /** 18 | * 运行器 19 | * @param params - 运行参数 20 | * @returns 运行结果的 Promise 21 | */ 22 | runner: PluginRunner; 23 | /** 24 | * 聊天完成函数的模式 25 | */ 26 | schema: ChatCompletionFunctions; 27 | } 28 | 29 | /** 30 | * 插件运行器 31 | * @template Params - 参数类型,默认为 object 32 | * @template Result - 结果类型,默认为 any 33 | * @param params - 运行参数 34 | * @returns 运行结果的 Promise 35 | */ 36 | export type PluginRunner = (params: Params) => Promise; 37 | -------------------------------------------------------------------------------- /types/plugins.ts: -------------------------------------------------------------------------------- 1 | export interface OpenAIPluginPayload { 2 | arguments: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /types/schema.ts: -------------------------------------------------------------------------------- 1 | export interface ChatCompletionFunctions { 2 | /** 3 | * The description of what the function does. 4 | * @type {string} 5 | * @memberof ChatCompletionFunctions 6 | */ 7 | description?: string; 8 | /** 9 | * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. 10 | * @type {string} 11 | * @memberof ChatCompletionFunctions 12 | */ 13 | name: string; 14 | /** 15 | * The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. 16 | * @type {{ [key: string]: any }} 17 | * @memberof ChatCompletionFunctions 18 | */ 19 | parameters?: { 20 | [key: string]: any; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | alias: { 6 | '@': 'src', 7 | '@lobehub/chat-plugins-gateway': 'src', 8 | }, 9 | coverage: { 10 | provider: 'v8', 11 | reporter: ['text', 'json', 'lcov', 'text-summary'], 12 | }, 13 | environment: 'node', 14 | globals: true, 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------