Compare commits

...

38 Commits

Author SHA1 Message Date
Junyan Qin
696162ee52 chore: release v4.0.3 2025-05-14 22:05:03 +08:00
Junyan Qin
533f993e3a fix: bad Dockerfile CMD 2025-05-14 22:04:08 +08:00
Junyan Qin
738b0af5fb chore: release v4.0.2 2025-05-14 21:35:21 +08:00
Junyan Qin
5d9bac5e7b doc: remove gewechat 2025-05-14 21:32:05 +08:00
Junyan Qin (Chin)
f376c9703a feat: add supports for open router (#1422) 2025-05-14 21:28:33 +08:00
fdc310
20a62fcf69 feat: add wechatpad for personal wechat
* 更新了wechatpad接口,以及适配器

* 更新了wechatpad接口,以及适配器

* 修复一些细节问题,比如at回复,以及启动登录和启动ws长连接的线程同步

* importutil中修复了在wi上启动替换斜杠问题,login中加上了一个login,暂时没啥用。wechatpad中做出了一些细节修改

* 更新了wechatpad接口,以及适配器

* 怎加了处理图片链接转换为image_base64发送

* feat(wechatpad): 调整日志+bugfix

* feat(wechatpad): fix typo

* 修正了发送语音api参数错误,添加了发送链接处理为base64数据(好像只有一部分链接可以)

* 修复了部分手抽的typo错误

* chore: remove manager.py

---------

Co-authored-by: shinelin <shinelinxx@gmail.com>
Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com>
2025-05-14 21:18:08 +08:00
devin-ai-integration[bot]
248d4beed1 fix: add super().__init__() call to EchoTextHandler to initialize logger attribute (#1421)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-14 20:52:27 +08:00
Junyan Qin
0e52aff363 chore: remove requirements.txt 2025-05-14 19:37:06 +08:00
Junyan Qin (Chin)
4ed854d7b8 ci: update Dockerfile (#1420)
* ci: update Dockerfile

* ci: update Dockerfile

* ci: no `--locked`
2025-05-14 19:29:44 +08:00
Junyan Qin
c6ff33c6ab chore: add google ai deps 2025-05-14 19:14:12 +08:00
简律纯
6c10cb7dca feat: support package manager(uv) (#1414)
* chore: set Python version to 3.10

* feat: add pyproject.toml for project configuration and dependencies

* style: streamline bot retrieval and update logic in PipelineService

* feat: update dependencies and configuration for ruff and pip

* chore: remove ruff configuration file

* style: change quote style from single to double in ruff configuration

* style: unify string quote style to double quotes across multiple files

* chore: update .gitignore to include .venv and uv.lock

* chore: remove unused configuration files and clean up project structure

* chore: revert quote-style to `single`

* chore: set default python version to 3.12

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-05-14 19:09:52 +08:00
Junyan Qin
130495f519 perf: missing translation in zh-Hans 2025-05-14 17:02:40 +08:00
Junyan Qin
219d328342 perf: completion some english translation 2025-05-14 17:00:03 +08:00
Junyan Qin
c835555a59 chore: change zh_CN to zh_Hans 2025-05-14 16:44:48 +08:00
Junyan Qin
6652b57a0d doc: README 2025-05-14 16:08:34 +08:00
Junyan Qin
bf51afedf6 perf: async bug in llm form 2025-05-14 15:37:58 +08:00
Junyan Qin
39f9400de7 fix: modelscope no usable 2025-05-14 15:35:37 +08:00
devin-ai-integration[bot]
ac1d39580b feat: add Google Gemini API support (#1418)
* feat: add Google Gemini API support

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: remove unused imports

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: add google-genai dependency

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: update Gemini API implementation to use correct API methods

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* refactor: improve Gemini API implementation based on official documentation

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: remove unsupported timeout parameter from Gemini API implementation

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: correct Gemini API implementation based on official documentation

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: update geminichatcmpl

* deps: add google-generativeai

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-14 15:05:04 +08:00
Junyan Qin
9362b34858 doc: readme images 2025-05-14 12:34:49 +08:00
Junyan Qin
c6f6c715bd doc: add demo images 2025-05-14 12:33:59 +08:00
Junyan Qin
6a8106d9ac doc: remove usage badge in README 2025-05-14 12:22:45 +08:00
Junyan Qin (Chin)
5abbcb62a2 Fix/system info 404 (#1413)
* fix: system info 404

* fix: lint error
2025-05-13 23:14:06 +08:00
devin-ai-integration[bot]
2bf94539bd Add i18n support with language selector on login page (#1410)
* feat: add i18n support with language selector on login page

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: complete i18n implementation for all webui components

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: complete all hardcoded text

* feat: dynamic label i18n

* fix: lint errors

* fix: lint errors

* delete sh fils

* fix: edit model dialog title

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 22:39:19 +08:00
Junyan Qin (Chin)
91cd8cf380 chore: release v4.0.1 (#1409) 2025-05-13 19:37:47 +08:00
Guanchao Wang
c3de3fa275 fix: wrong status when creating a WecomCS bot (#1408) 2025-05-13 19:33:32 +08:00
devin-ai-integration[bot]
039752419b Add User Card and Logout Button to Sidebar (#1405)
* feat: add user card and logout button to sidebar

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: add test code to set dummy values in localStorage

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* style: fix formatting issues in HomeSidebar.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* style: fix whitespace in HomeSidebar.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* perf: styles of logout button

* fix: lint errors

* fix: lint errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 19:33:04 +08:00
Junyan Qin (Chin)
18c708da58 fix: windows path bug in importutil (#1404) 2025-05-13 16:52:16 +08:00
Junyan Qin (Chin)
8c08b8ee8a perf: no delay for model selector hover card (#1402) 2025-05-13 16:15:35 +08:00
Junyan Qin (Chin)
015be6008d fix: bugs in requesters (#1401) 2025-05-13 16:09:23 +08:00
Junyan Qin
da86384e58 doc(README): add Ask DeepWiki badge 2025-05-13 14:36:29 +08:00
devin-ai-integration[bot]
86ff6f5eb6 feat: plugin reordering (#1398)
* Add @dnd-kit/core and @dnd-kit/sortable dependencies for plugin sorting

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Add PluginSortDialog component with drag-and-drop functionality

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Integrate sorting button and dialog into PluginInstalledComponent

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Update HttpClient to use local backend URL for development

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix reorderPlugins method to use PUT and correct request format

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Update hover-card component using shadcn CLI

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix formatting issues in plugin sorting components

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* refactor: move plugin sorting button and dialog to page component

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* refactor: move PluginSortDialog component to plugins directory

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* chore: remove old PluginSortDialog component file

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: api bug

* perf: desciption in plugin sorting dialog

* fix: lint errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 14:10:18 +08:00
devin-ai-integration[bot]
ae6979151f Fix #1391: Update bot's pipeline name when pipeline is renamed (#1397)
* Fix #1391: Update bot's pipeline name when pipeline is renamed

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Refactor: Use bot_service to update bot pipeline names

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 12:44:00 +08:00
devin-ai-integration[bot]
fd1b5d494e Add hover card to LLM model selector (#1393)
* Add hover card to LLM model selector to display detailed model information

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix formatting issues to resolve lint errors

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix remaining formatting issue in DynamicFormItemComponent.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* perf(model preview): hover card style

* fix: wrong base url

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 12:25:32 +08:00
Junyan Qin (Chin)
cd68760c75 Fix/runner bugs (#1388)
* fix: bugs in runners

* fix: model entity of exists conversation is None after changed runner
2025-05-12 18:21:08 +08:00
Junyan Qin (Chin)
13d36412dd fix: llm model wrongly required when runner is not local-agent (#1386) 2025-05-12 14:16:17 +08:00
Junyan Qin (Chin)
f2e1ae432c fix: deadlinks in README (#1385) 2025-05-12 09:50:53 +08:00
Junyan Qin (Chin)
0f30f1dcbd doc: fix deadlinks of doc in README (#1384) 2025-05-12 09:48:05 +08:00
Junyan Qin (Chin)
d070737ef7 ci: fix bad frontend build output path (#1383) 2025-05-12 09:28:30 +08:00
120 changed files with 3886 additions and 766 deletions

View File

@@ -16,5 +16,5 @@
- [ ] 相关 issues 链接了吗?
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗?
- [ ] 依赖requirements.txt 和 core/bootutils/deps.py 了吗
- [ ] 依赖pyproject.toml 和 core/bootutils/deps.py 了吗
- [ ] 文档编写了吗?

View File

@@ -46,7 +46,7 @@ jobs:
npm run build
- name: Package Output
run: |
cp -r /tmp/langbot_build_web/web/dist ./web
cp -r /tmp/langbot_build_web/web/out ./web
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ botpy.log*
/venv
test.py
/web_ui
.venv/
uv.lock

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

View File

@@ -1,42 +0,0 @@
line-length = 120
[lint]
ignore = [
"E712", # Comparison to true should be 'if cond is true:' or 'if cond:' (E712)
"F402", # Import `loader` from line 8 shadowed by loop variable
"F403", # * used, unable to detect undefined names
"F405", # may be undefined, or defined from star imports
"E741", # Ambiguous variable name: `l`
"E722", # bare-except
"E721", # type-comparison
"F821", # undefined-all
"FURB113", # repeated-append
"FURB152", # math-constant
"UP007", # non-pep604-annotation
"UP032", # f-string
"UP045", # non-pep604-annotation-optional
"B005", # strip-with-multi-characters
"B006", # mutable-argument-default
"B007", # unused-loop-control-variable
"B026", # star-arg-unpacking-after-keyword-arg
"B903", # class-as-data-structure
"B904", # raise-without-from-inside-except
"B905", # zip-without-explicit-strict
"N806", # non-lowercase-variable-in-function
"N815", # mixed-case-variable-in-class-scope
"PT011", # pytest-raises-too-broad
"SIM102", # collapsible-if
"SIM103", # needless-bool
"SIM105", # suppressible-exception
"SIM107", # return-in-try-except-finally
"SIM108", # if-else-block-instead-of-if-exp
"SIM113", # enumerate-for-loop
"SIM117", # multiple-with-statements
"SIM210", # if-expr-with-true-false
]
[format]
# 5. Use single quotes in `ruff format`.
quote-style = "single"

View File

@@ -16,7 +16,8 @@ COPY --from=node /app/web/out ./web/out
RUN apt update \
&& apt install gcc -y \
&& python -m pip install -r requirements.txt \
&& python -m pip install --no-cache-dir uv \
&& uv sync \
&& touch /.dockerenv
CMD [ "python", "main.py" ]
CMD [ "uv", "run", "main.py" ]

View File

@@ -9,10 +9,8 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.html">功能介绍</a>
<a href="https://docs.langbot.app/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/usage/faq.html">常见问题</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">插件介绍</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<div align="center">
@@ -21,15 +19,14 @@
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
</div>
@@ -38,23 +35,27 @@
## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
## 📦 开始使用
> [!IMPORTANT]
>
> 在您开始任何方式部署之前,请务必阅读[新手指引](https://docs.langbot.app/insight/guide.html)。
#### Docker Compose 部署
适合熟悉 Docker 的用户,查看文档[Docker 部署](https://docs.langbot.app/deploy/langbot/docker.html)。
```bash
git clone https://github.com/RockChinQ/LangBot
cd LangBot
docker compose up -d
```
访问 http://localhost:5300 即可开始使用。
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
#### 宝塔面板部署
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/deploy/langbot/one-click/bt.html)使用。
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 云部署
@@ -68,10 +69,18 @@
#### 手动部署
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/deploy/langbot/manual.html)。
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
## 📸 效果展示
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
@@ -88,7 +97,7 @@
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 企微对外客服 | ✅ | |
| 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 |
| 个人微信 | ✅ | |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
@@ -111,6 +120,7 @@
| [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |

View File

@@ -8,10 +8,8 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">Home</a>
<a href="https://docs.langbot.app/insight/intro.html">Features</a>
<a href="https://docs.langbot.app/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/usage/faq.html">FAQ</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">Plugin</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
<div align="center">
@@ -22,11 +20,11 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
</div>
@@ -35,31 +33,33 @@
## ✨ Features
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has dozens of [plugins](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web UI: Support management LangBot instance through the browser, for details, see [documentation](https://docs.langbot.app/webui/intro.html)
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
- 😻 [New] Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
## 📦 Getting Started
> [!IMPORTANT]
>
> - Before you start deploying in any way, please read the [New User Guide](https://docs.langbot.app/insight/guide.html).
> - All documentation is in Chinese, we will provide i18n version in the near future.
> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot).
#### Docker Compose Deployment
Suitable for users familiar with Docker, see the [Docker Deployment](https://docs.langbot.app/deploy/langbot/docker.html) documentation.
```bash
git clone https://github.com/RockChinQ/LangBot
cd LangBot
docker compose up -d
```
Visit http://localhost:5300 to start using it.
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
#### One-click Deployment on BTPanel
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/deploy/langbot/one-click/bt.html) to use it.
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
#### Zeabur Cloud Deployment
Community contributed Zeabur template.
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railway Cloud Deployment
@@ -67,10 +67,18 @@ Community contributed Zeabur template.
#### Other Deployment Methods
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/deploy/langbot/manual.html) documentation.
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
## 📸 Demo
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
@@ -87,7 +95,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access |
| Personal WeChat | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| Discord | ✅ | |
@@ -110,6 +118,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |

View File

@@ -8,10 +8,8 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">ホーム</a>
<a href="https://docs.langbot.app/insight/intro.html">機能</a>
<a href="https://docs.langbot.app/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/usage/faq.html">FAQ</a>
<a href="https://docs.langbot.app/plugin/plugin-intro.html">プラグイン</a>
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a>
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
<div align="center">
@@ -21,11 +19,11 @@
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
</div>
@@ -34,31 +32,33 @@
## ✨ 機能
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。
- 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
## 📦 始め方
> [!IMPORTANT]
>
> - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。
> - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。
> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot)。
#### Docker Compose デプロイ
Dockerに慣れているユーザーに適しています。[Dockerデプロイ](https://docs.langbot.app/deploy/langbot/docker.html)のドキュメントを参照してください。
```bash
git clone https://github.com/RockChinQ/LangBot
cd LangBot
docker compose up -d
```
http://localhost:5300 にアクセスして使用を開始します。
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
#### BTPanelでのワンクリックデプロイ
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/deploy/langbot/one-click/bt.html)を使用して使用できます。
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
#### Zeaburクラウドデプロイ
コミュニティが提供するZeaburテンプレート。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railwayクラウドデプロイ
@@ -66,10 +66,18 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
#### その他のデプロイ方法
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/deploy/langbot/manual.html)のドキュメントを参照してください。
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
## 📸 デモ
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
<img alt="返信効果(インターネットプラグイン付き)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUIデモ: https://demo.langbot.dev/
@@ -86,7 +94,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| QQ公式API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| 個人WeChat | ✅ | [Gewechat](https://github.com/Devo919/Gewechat)を使用して接続 |
| 個人WeChat | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| Discord | ✅ | |
@@ -108,6 +116,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |

View File

@@ -4,7 +4,7 @@ metadata:
name: builtin-components
label:
en_US: Builtin Components
zh_CN: 内置组件
zh_Hans: 内置组件
spec:
components:
ComponentTemplate:

View File

@@ -5,6 +5,7 @@ from dingtalk_stream import AckMessage
class EchoTextHandler(dingtalk_stream.ChatbotHandler):
def __init__(self, client):
super().__init__() # Call parent class initializer to set up logger
self.msg_id = ''
self.incoming_message = None
self.client = client # 用于更新 DingTalkClient 中的 incoming_message

201
libs/wechatpad_api/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,38 @@
# wechatpad-python
## 此项目时准备对接wechatpadpro 的pythonsdk
## 未完工接口
* 关于好友的接口
* 关于群管理的接口
* 关于下载的接口
* 关于用户的部分接口
* 关于消息的部分接口
* 关于支付的
* 关于朋友圈的
* 关于标签的
* 关于收藏的
* 暂时只写了一部分接口
## 已完工接口
1. 获取普通token
2. 登录二维码(只是返回数据,暂时还未打印二维码)
3. 获取登录状态
4. 唤醒登录
5. 退出登录
6. 获取用户信息
7. 获取用户二维码
8. 上传用户头像
9. 获取设备信息
10. 发送文本消息
11. 发送图片消息
12. 发送语音消息
13. 发送app消息
14. 发送emoji消息
15. 发送名片消息
16. 撤回消息

View File

@@ -0,0 +1 @@
from .client import WeChatPadClient

View File

View File

@@ -0,0 +1,14 @@
from libs.wechatpad_api.util.http_util import async_request, post_json
class ChatRoomApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
def get_chatroom_member_detail(self, chatroom_name):
params = {
"ChatRoomName": chatroom_name
}
url = self.base_url + '/group/GetChatroomMemberDetail'
return post_json(url, token=self.token, data=params)

View File

@@ -0,0 +1,39 @@
from libs.wechatpad_api.util.http_util import async_request, post_json
import httpx
import base64
class DownloadApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
def send_download(self, aeskey, file_type, file_url):
json_data = {
"AesKey": aeskey,
"FileType": file_type,
"FileURL": file_url
}
url = self.base_url + "/message/SendCdnDownload"
return post_json(url, token=self.token, data=json_data)
def get_msg_voice(self,buf_id, length, new_msgid):
json_data = {
"Bufid": buf_id,
"Length": length,
"NewMsgId": new_msgid,
"ToUserName": ""
}
url = self.base_url + "/message/GetMsgVoice"
return post_json(url, token=self.token, data=json_data)
async def download_url_to_base64(self, download_url):
async with httpx.AsyncClient() as client:
response = await client.get(download_url)
if response.status_code == 200:
file_bytes = response.content
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str
else:
raise Exception('获取文件失败')

View File

@@ -0,0 +1,11 @@
from libs.wechatpad_api.util.http_util import post_json,async_request
from typing import List, Dict, Any, Optional
class FriendApi:
"""联系人API类处理所有与联系人相关的操作"""
def __init__(self, base_url: str, token: str):
self.base_url = base_url
self.token = token

View File

@@ -0,0 +1,102 @@
from libs.wechatpad_api.util.http_util import async_request,post_json,get_json
class LoginApi:
def __init__(self, base_url: str, token: str = None, admin_key: str = None):
'''
Args:
base_url: 原始路径
token: token
admin_key: 管理员key
'''
self.base_url = base_url
self.token = token
# self.admin_key = admin_key
def get_token(self, admin_key, day: int=365):
# 获取普通token
url = f"{self.base_url}/admin/GenAuthKey1"
json_data = {
"Count": 1,
"Days": day
}
return post_json(base_url=url, token=admin_key, data=json_data)
def get_login_qr(self, Proxy: str = ""):
'''
Args:
Proxy:异地使用时代理
Returns:json数据
'''
"""
{
"Code": 200,
"Data": {
"Key": "3141312",
"QrCodeUrl": "https://1231x/g6bMlv2dX8zwNbqE6-Zs",
"Txt": "建议返回data=之后内容自定义生成二维码",
"baseResp": {
"ret": 0,
"errMsg": {}
}
},
"Text": ""
}
"""
#获取登录二维码
url = f"{self.base_url}/login/GetLoginQrCodeNew"
check = False
if Proxy != "":
check = True
json_data = {
"Check": check,
"Proxy": Proxy
}
return post_json(base_url=url, token=self.token, data=json_data)
def get_login_status(self):
# 获取登录状态
url = f'{self.base_url}/login/GetLoginStatus'
return get_json(base_url=url, token=self.token)
def logout(self):
# 退出登录
url = f'{self.base_url}/login/LogOut'
return post_json(base_url=url, token=self.token)
def wake_up_login(self, Proxy: str = ""):
# 唤醒登录
url = f'{self.base_url}/login/WakeUpLogin'
check = False
if Proxy != "":
check = True
json_data = {
"Check": check,
"Proxy": ""
}
return post_json(base_url=url, token=self.token, data=json_data)
def login(self,admin_key):
login_status = self.get_login_status()
if login_status["Code"] == 300 and login_status["Text"] == "你已退出微信":
print("token已经失效重新获取")
token_data = self.get_token(admin_key)
self.token = token_data["Data"][0]

View File

@@ -0,0 +1,123 @@
from libs.wechatpad_api.util.http_util import async_request, post_json
class MessageApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
def post_text(self, to_wxid, content, ats: list= []):
'''
Args:
app_id: 微信id
to_wxid: 发送方的微信id
content: 内容
ats: at
Returns:
'''
url = self.base_url + "/message/SendTextMessage"
"""发送文字消息"""
json_data = {
"MsgItem": [
{
"AtWxIDList": ats,
"ImageContent": "",
"MsgType": 0,
"TextContent": content,
"ToUserName": to_wxid
}
]
}
return post_json(base_url=url, token=self.token, data=json_data)
def post_image(self, to_wxid, img_url, ats: list= []):
"""发送图片消息"""
# 这里好像可以尝试发送多个暂时未测试
json_data = {
"MsgItem": [
{
"AtWxIDList": ats,
"ImageContent": img_url,
"MsgType": 0,
"TextContent": '',
"ToUserName": to_wxid
}
]
}
url = self.base_url + "/message/SendImageMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration):
"""发送语音消息"""
json_data = {
"ToUserName": to_wxid,
"VoiceData": voice_data,
"VoiceFormat": voice_forma,
"VoiceSecond": voice_duration
}
url = self.base_url + "/message/SendVoice"
return post_json(base_url=url, token=self.token, data=json_data)
def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag):
"""发送名片消息"""
param = {
"CardAlias": alias,
"CardFlag": flag,
"CardNickName": nick_name,
"CardWxId": name_card_wxid,
"ToUserName": to_wxid
}
url = f"{self.base_url}/message/ShareCardMessage"
return post_json(base_url=url, token=self.token, data=param)
def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0):
"""发送emoji消息"""
json_data = {
"EmojiList": [
{
"EmojiMd5": emoji_md5,
"EmojiSize": emoji_size,
"ToUserName": to_wxid
}
]
}
url = f"{self.base_url}/message/SendEmojiMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def post_app_msg(self, to_wxid,xml_data, contenttype:int=0):
"""发送appmsg消息"""
json_data = {
"AppList": [
{
"ContentType": contenttype,
"ContentXML": xml_data,
"ToUserName": to_wxid
}
]
}
url = f"{self.base_url}/message/SendAppMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
"""撤回消息"""
param = {
"ClientMsgId": msg_id,
"CreateTime": create_time,
"NewMsgId": new_msg_id,
"ToUserName": to_wxid
}
url = f"{self.base_url}/message/RevokeMsg"
return post_json(base_url=url, token=self.token, data=param)

View File

@@ -0,0 +1,37 @@
from libs.wechatpad_api.util.http_util import post_json, async_request, get_json
class UserApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
def get_profile(self):
"""获取个人资料"""
url = f'{self.base_url}/user/GetProfile'
return get_json(base_url=url, token=self.token)
def get_qr_code(self, recover:bool=True, style:int=8):
"""获取自己的二维码"""
param = {
"Recover": recover,
"Style": style
}
url = f'{self.base_url}/user/GetMyQRCode'
return post_json(base_url=url, token=self.token, data=param)
def get_safety_info(self):
"""获取设备记录"""
url = f'{self.base_url}/equipment/GetSafetyInfo'
return post_json(base_url=url, token=self.token)
async def update_head_img(self, head_img_base64):
"""修改头像"""
param = {
"Base64": head_img_base64
}
url = f'{self.base_url}/user/UploadHeadImage'
return await async_request(base_url=url, token_key=self.token, json=param)

View File

@@ -0,0 +1,97 @@
from libs.wechatpad_api.api.login import LoginApi
from libs.wechatpad_api.api.friend import FriendApi
from libs.wechatpad_api.api.message import MessageApi
from libs.wechatpad_api.api.user import UserApi
from libs.wechatpad_api.api.downloadpai import DownloadApi
from libs.wechatpad_api.api.chatroom import ChatRoomApi
class WeChatPadClient:
def __init__(self,base_url, token):
self._login_api = LoginApi(base_url, token)
self._friend_api = FriendApi(base_url, token)
self._message_api = MessageApi(base_url, token)
self._user_api = UserApi(base_url, token)
self._download_api = DownloadApi(base_url, token)
self._chatroom_api = ChatRoomApi(base_url, token)
def get_token(self,admin_key, day: int):
'''获取token'''
return self._login_api.get_token(admin_key, day)
def get_login_qr(self, Proxy:str=""):
"""登录二维码"""
return self._login_api.get_login_qr(Proxy=Proxy)
def awaken_login(self, Proxy:str=""):
'''唤醒登录'''
return self._login_api.wake_up_login(Proxy=Proxy)
def log_out(self):
"""退出登录"""
return self._login_api.logout()
def get_login_status(self):
"""获取登录状态"""
return self._login_api.get_login_status()
def send_text_message(self, to_wxid, message, ats: list=[]):
"""发送文本消息"""
return self._message_api.post_text(to_wxid, message, ats)
def send_image_message(self, to_wxid, img_url, ats: list=[]):
"""发送图片消息"""
return self._message_api.post_image(to_wxid, img_url, ats)
def send_voice_message(self, to_wxid, voice_data, voice_forma, voice_duration):
"""发送音频消息"""
return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration)
def send_app_message(self, to_wxid, app_message, type):
"""发送app消息"""
return self._message_api.post_app_msg(to_wxid, app_message, type)
def send_emoji_message(self, to_wxid, emoji_md5, emoji_size):
"""发送emoji消息"""
return self._message_api.post_emoji(to_wxid,emoji_md5,emoji_size)
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
"""撤回消息"""
return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time)
def get_profile(self):
"""获取用户信息"""
return self._user_api.get_profile()
def get_qr_code(self, recover:bool=True, style:int=8):
"""获取用户二维码"""
return self._user_api.get_qr_code(recover=recover, style=style)
def get_safety_info(self):
"""获取设备信息"""
return self._user_api.get_safety_info()
def update_head_img(self, head_img_base64):
"""上传用户头像"""
return self._user_api.update_head_img(head_img_base64)
def cdn_download(self, aeskey, file_type, file_url):
"""cdn下载"""
return self._download_api.send_download( aeskey, file_type, file_url)
def get_msg_voice(self,buf_id, length, msgid):
"""下载语音"""
return self._download_api.get_msg_voice(buf_id, length, msgid)
async def download_base64(self,url):
return await self._download_api.download_url_to_base64(download_url=url)
def get_chatroom_member_detail(self, chatroom_name):
"""查看群成员详情"""
return self._chatroom_api.get_chatroom_member_detail(chatroom_name)

View File

View File

@@ -0,0 +1,92 @@
import requests
def post_json(base_url, token, data=None):
headers = {
'Content-Type': 'application/json'
}
url = base_url + f'?key={token}'
try:
response = requests.post(url, json=data, headers=headers, timeout=60)
response.raise_for_status()
result = response.json()
if result:
return result
else:
raise RuntimeError(response.text)
except Exception as e:
print(f"http请求失败, url={url}, exception={e}")
raise RuntimeError(str(e))
def get_json(base_url, token):
headers = {
'Content-Type': 'application/json'
}
url = base_url + f'?key={token}'
try:
response = requests.get(url, headers=headers, timeout=60)
response.raise_for_status()
result = response.json()
if result:
return result
else:
raise RuntimeError(response.text)
except Exception as e:
print(f"http请求失败, url={url}, exception={e}")
raise RuntimeError(str(e))
import aiohttp
import asyncio
async def async_request(
base_url: str,
token_key: str,
method: str = 'POST',
params: dict = None,
# headers: dict = None,
data: dict = None,
json: dict = None
):
"""
通用异步请求函数
:param base_url: 请求URL
:param token_key: 请求token
:param method: HTTP方法 (GET, POST, PUT, DELETE等)
:param params: URL查询参数
# :param headers: 请求头
:param data: 表单数据
:param json: JSON数据
:return: 响应文本
"""
headers = {
'Content-Type': 'application/json'
}
url = f"{base_url}?key={token_key}"
async with aiohttp.ClientSession() as session:
async with session.request(
method=method,
url=url,
params=params,
headers=headers,
data=data,
json=json
) as response:
response.raise_for_status() # 如果状态码不是200抛出异常
result = await response.json()
# print(result)
return result
# if result.get('Code') == 200:
#
# return await result
# else:
# raise RuntimeError("请求失败",response.text)

View File

@@ -0,0 +1,31 @@
import qrcode
def print_green(text):
print(f"\033[32m{text}\033[0m")
def print_yellow(text):
print(f"\033[33m{text}\033[0m")
def print_red(text):
print(f"\033[31m{text}\033[0m")
def make_and_print_qr(url):
"""生成并打印二维码
Args:
url: 需要生成二维码的URL字符串
Returns:
None
功能:
1. 在终端打印二维码的ASCII图形
2. 同时提供在线二维码生成链接作为备选
"""
print_green("请扫描下方二维码登录")
qr = qrcode.QRCode()
qr.add_data(url)
qr.make()
qr.print_ascii(invert=True)
print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}")

View File

@@ -22,7 +22,6 @@ class WecomCSClient:
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
self.access_token = ''
self.app = Quart(__name__)
self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
@@ -198,17 +197,21 @@ class WecomCSClient:
msg_signature = request.args.get('msg_signature')
timestamp = request.args.get('timestamp')
nonce = request.args.get('nonce')
try:
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
except Exception as e:
raise Exception(f'初始化失败,错误码: {e}')
if request.method == 'GET':
echostr = request.args.get('echostr')
ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
elif request.method == 'POST':
encrypt_msg = await request.data
ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
raise Exception(f'消息解密失败,错误码: {ret}')

View File

@@ -94,10 +94,22 @@ class PipelineService:
.values(**pipeline_data)
)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
pipeline = await self.get_pipeline(pipeline_uuid)
if 'name' in pipeline_data:
from ....entity.persistence import bot as persistence_bot
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid)
)
bots = result.all()
for bot in bots:
bot_data = {'use_pipeline_name': pipeline_data['name']}
await self.ap.bot_service.update_bot(bot.uuid, bot_data)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
async def delete_pipeline(self, pipeline_uuid: str) -> None:

View File

@@ -133,7 +133,7 @@ class Conversation(pydantic.BaseModel):
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
use_llm_model: requester.RuntimeLLMModel
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]

View File

@@ -15,7 +15,7 @@ class I18nString(pydantic.BaseModel):
en_US: str
"""英文"""
zh_CN: typing.Optional[str] = None
zh_Hans: typing.Optional[str] = None
"""中文"""
ja_JP: typing.Optional[str] = None
@@ -26,8 +26,8 @@ class I18nString(pydantic.BaseModel):
dic = {}
if self.en_US is not None:
dic['en_US'] = self.en_US
if self.zh_CN is not None:
dic['zh_CN'] = self.zh_CN
if self.zh_Hans is not None:
dic['zh_Hans'] = self.zh_Hans
if self.ja_JP is not None:
dic['ja_JP'] = self.ja_JP
return dic

View File

@@ -30,19 +30,33 @@ class PreProcessor(stage.PipelineStage):
stage_inst_name: str,
) -> entities.StageProcessResult:
"""处理"""
selected_runner = query.pipeline_config['ai']['runner']['runner']
session = await self.ap.sess_mgr.get_session(query)
conversation = await self.ap.sess_mgr.get_conversation(
query, session, query.pipeline_config['ai']['local-agent']['prompt']
# 非 local-agent 时llm_model 为 None
llm_model = (
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
if selected_runner == 'local-agent'
else None
)
conversation = await self.ap.sess_mgr.get_conversation(
query,
session,
query.pipeline_config['ai']['local-agent']['prompt'],
)
conversation.use_llm_model = llm_model
# 设置query
query.session = session
query.prompt = conversation.prompt.copy()
query.messages = conversation.messages.copy()
query.use_llm_model = conversation.use_llm_model
query.use_llm_model = llm_model
if selected_runner == 'local-agent':
query.use_funcs = (
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None
)
@@ -50,16 +64,14 @@ class PreProcessor(stage.PipelineStage):
query.variables = {
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
'conversation_id': conversation.uuid,
'msg_create_time': int(query.message_event.time)
if query.message_event.time
else int(datetime.datetime.now().timestamp()),
'msg_create_time': (
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
),
}
# Check if this model supports vision, if not, remove all images
# TODO this checking should be performed in runner, and in this stage, the image should be reserved
if query.pipeline_config['ai']['runner'][
'runner'
] == 'local-agent' and not query.use_llm_model.model_entity.abilities.__contains__('vision'):
if selected_runner == 'local-agent' and not query.use_llm_model.model_entity.abilities.__contains__('vision'):
for msg in query.messages:
if isinstance(msg.content, list):
for me in msg.content:
@@ -75,9 +87,9 @@ class PreProcessor(stage.PipelineStage):
content_list.append(llm_entities.ContentElement.from_text(me.text))
plain_text += me.text
elif isinstance(me, platform_message.Image):
if query.pipeline_config['ai']['runner'][
'runner'
] != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__('vision'):
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
'vision'
):
if me.base64 is not None:
content_list.append(llm_entities.ContentElement.from_image_base64(me.base64))

View File

@@ -4,7 +4,7 @@ metadata:
name: MessagePlatformAdapter
label:
en_US: Message Platform Adapter
zh_CN: 消息平台适配器模板类
zh_Hans: 消息平台适配器模板类
spec:
type:
- python

View File

@@ -4,40 +4,40 @@ metadata:
name: aiocqhttp
label:
en_US: OneBot v11
zh_CN: OneBot v11
zh_Hans: OneBot v11
description:
en_US: OneBot v11 Adapter
zh_CN: OneBot v11 适配器,请查看文档了解使用方式
zh_Hans: OneBot v11 适配器,请查看文档了解使用方式
icon: onebot.png
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 主机
zh_Hans: 主机
description:
en_US: The host that OneBot v11 listens on for reverse WebSocket connections. Unless you know what you're doing, use 0.0.0.0
zh_CN: OneBot v11 监听的反向 WS 主机,除非你知道自己在做什么,否则请写 0.0.0.0
zh_Hans: OneBot v11 监听的反向 WS 主机,除非你知道自己在做什么,否则请写 0.0.0.0
type: string
required: true
default: 0.0.0.0
- name: port
label:
en_US: Port
zh_CN: 端口
zh_Hans: 端口
description:
en_US: Port
zh_CN: 监听的端口
zh_Hans: 监听的端口
type: integer
required: true
default: 2280
- name: access-token
label:
en_US: Access Token
zh_CN: 访问令牌
zh_Hans: 访问令牌
description:
en_US: Custom connection token for the protocol endpoint. If the protocol endpoint is not set, don't fill it
zh_CN: 自定义的与协议端的连接令牌,若协议端未设置,则不填
zh_Hans: 自定义的与协议端的连接令牌,若协议端未设置,则不填
type: string
required: false
default: ""

View File

@@ -4,45 +4,45 @@ metadata:
name: dingtalk
label:
en_US: DingTalk
zh_CN: 钉钉
zh_Hans: 钉钉
description:
en_US: DingTalk Adapter
zh_CN: 钉钉适配器,请查看文档了解使用方式
zh_Hans: 钉钉适配器,请查看文档了解使用方式
icon: dingtalk.svg
spec:
config:
- name: client_id
label:
en_US: Client ID
zh_CN: 客户端ID
zh_Hans: 客户端ID
type: string
required: true
default: ""
- name: client_secret
label:
en_US: Client Secret
zh_CN: 客户端密钥
zh_Hans: 客户端密钥
type: string
required: true
default: ""
- name: robot_code
label:
en_US: Robot Code
zh_CN: 机器人代码
zh_Hans: 机器人代码
type: string
required: true
default: ""
- name: robot_name
label:
en_US: Robot Name
zh_CN: 机器人名称
zh_Hans: 机器人名称
type: string
required: true
default: ""
- name: markdown_card
label:
en_US: Markdown Card
zh_CN: 是否使用 Markdown 卡片
zh_Hans: 是否使用 Markdown 卡片
type: boolean
required: false
default: true

View File

@@ -4,24 +4,24 @@ metadata:
name: discord
label:
en_US: Discord
zh_CN: Discord
zh_Hans: Discord
description:
en_US: Discord Adapter
zh_CN: Discord 适配器,请查看文档了解使用方式
zh_Hans: Discord 适配器,请查看文档了解使用方式
icon: discord.svg
spec:
config:
- name: client_id
label:
en_US: Client ID
zh_CN: 客户端ID
zh_Hans: 客户端ID
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""

View File

@@ -4,52 +4,52 @@ metadata:
name: gewechat
label:
en_US: GeWeChat
zh_CN: GeWeChat个人微信
zh_Hans: GeWeChat个人微信
description:
en_US: GeWeChat Adapter
zh_CN: GeWeChat 适配器,请查看文档了解使用方式
zh_Hans: GeWeChat 适配器,请查看文档了解使用方式
icon: gewechat.png
spec:
config:
- name: gewechat_url
label:
en_US: GeWeChat URL
zh_CN: GeWeChat URL
zh_Hans: GeWeChat URL
type: string
required: true
default: ""
- name: gewechat_file_url
label:
en_US: GeWeChat file download URL
zh_CN: GeWeChat 文件下载URL
zh_Hans: GeWeChat 文件下载URL
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 端口
zh_Hans: 端口
type: integer
required: true
default: 2286
- name: callback_url
label:
en_US: Callback URL
zh_CN: 回调URL
zh_Hans: 回调URL
type: string
required: true
default: ""
- name: app_id
label:
en_US: App ID
zh_CN: 应用ID
zh_Hans: 应用ID
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""

View File

@@ -417,7 +417,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
lark_message = await self.message_converter.yiri2target(message, self.api_client)
final_content = {
'zh_cn': {
'zh_Hans': {
'title': '',
'content': lark_message,
},

View File

@@ -4,61 +4,61 @@ metadata:
name: lark
label:
en_US: Lark
zh_CN: 飞书
zh_Hans: 飞书
description:
en_US: Lark Adapter
zh_CN: 飞书适配器,请查看文档了解使用方式
zh_Hans: 飞书适配器,请查看文档了解使用方式
icon: lark.svg
spec:
config:
- name: app_id
label:
en_US: App ID
zh_CN: 应用ID
zh_Hans: 应用ID
type: string
required: true
default: ""
- name: app_secret
label:
en_US: App Secret
zh_CN: 应用密钥
zh_Hans: 应用密钥
type: string
required: true
default: ""
- name: bot_name
label:
en_US: Bot Name
zh_CN: 机器人名称
zh_Hans: 机器人名称
type: string
required: true
default: ""
- name: enable-webhook
label:
en_US: Enable Webhook Mode
zh_CN: 启用Webhook模式
zh_Hans: 启用Webhook模式
description:
en_US: If enabled, the bot will use webhook mode to receive messages. Otherwise, it will use WS long connection mode
zh_CN: 如果启用,机器人将使用 Webhook 模式接收消息。否则,将使用 WS 长连接模式
zh_Hans: 如果启用,机器人将使用 Webhook 模式接收消息。否则,将使用 WS 长连接模式
type: boolean
required: true
default: false
- name: port
label:
en_US: Webhook Port
zh_CN: Webhook端口
zh_Hans: Webhook端口
description:
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
zh_CN: 仅在启用 Webhook 模式时有效,请填写 Webhook 端口
zh_Hans: 仅在启用 Webhook 模式时有效,请填写 Webhook 端口
type: integer
required: true
default: 2285
- name: encrypt-key
label:
en_US: Encrypt Key
zh_CN: 加密密钥
zh_Hans: 加密密钥
description:
en_US: Only valid when webhook mode is enabled, please fill in the encrypt key
zh_CN: 仅在启用 Webhook 模式时有效,请填写加密密钥
zh_Hans: 仅在启用 Webhook 模式时有效,请填写加密密钥
type: string
required: true
default: ""

View File

@@ -4,38 +4,38 @@ metadata:
name: nakuru
label:
en_US: Nakuru
zh_CN: Nakuru
zh_Hans: Nakuru
description:
en_US: Nakuru Adapter
zh_CN: Nakuru 适配器(go-cqhttp),请查看文档了解使用方式
zh_Hans: Nakuru 适配器(go-cqhttp),请查看文档了解使用方式
icon: nakuru.png
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 主机
zh_Hans: 主机
type: string
required: true
default: "127.0.0.1"
- name: http_port
label:
en_US: HTTP Port
zh_CN: HTTP端口
zh_Hans: HTTP端口
type: integer
required: true
default: 5700
- name: ws_port
label:
en_US: WebSocket Port
zh_CN: WebSocket端口
zh_Hans: WebSocket端口
type: integer
required: true
default: 8080
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""

View File

@@ -4,66 +4,66 @@ metadata:
name: officialaccount
label:
en_US: Official Account
zh_CN: 微信公众号
zh_Hans: 微信公众号
description:
en_US: Official Account Adapter
zh_CN: 微信公众号适配器,请查看文档了解使用方式
zh_Hans: 微信公众号适配器,请查看文档了解使用方式
icon: officialaccount.png
spec:
config:
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""
- name: EncodingAESKey
label:
en_US: EncodingAESKey
zh_CN: 消息加解密密钥
zh_Hans: 消息加解密密钥
type: string
required: true
default: ""
- name: AppID
label:
en_US: App ID
zh_CN: 应用ID
zh_Hans: 应用ID
type: string
required: true
default: ""
- name: AppSecret
label:
en_US: App Secret
zh_CN: 应用密钥
zh_Hans: 应用密钥
type: string
required: true
default: ""
- name: Mode
label:
en_US: Mode
zh_CN: 接入模式
zh_Hans: 接入模式
type: string
required: true
default: "drop"
- name: LoadingMessage
label:
en_US: Loading Message
zh_CN: 加载消息
zh_Hans: 加载消息
type: string
required: true
default: "AI正在思考中请发送任意内容获取回复。"
- name: host
label:
en_US: Host
zh_CN: 监听主机
zh_Hans: 监听主机
type: string
required: true
default: 0.0.0.0
- name: port
label:
en_US: Port
zh_CN: 监听端口
zh_Hans: 监听端口
type: integer
required: true
default: 2287

View File

@@ -4,31 +4,31 @@ metadata:
name: qq-botpy
label:
en_US: QQBotPy
zh_CN: QQBotPy
zh_Hans: QQBotPy
description:
en_US: QQ Official API (WebSocket)
zh_CN: QQ 官方 API (WebSocket),请查看文档了解使用方式
zh_Hans: QQ 官方 API (WebSocket),请查看文档了解使用方式
icon: qqbotpy.svg
spec:
config:
- name: appid
label:
en_US: App ID
zh_CN: 应用ID
zh_Hans: 应用ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
zh_Hans: 密钥
type: string
required: true
default: ""
- name: intents
label:
en_US: Intents
zh_CN: 权限
zh_Hans: 权限
type: array
required: true
default: []

View File

@@ -4,38 +4,38 @@ metadata:
name: qqofficial
label:
en_US: QQ Official API
zh_CN: QQ 官方 API
zh_Hans: QQ 官方 API
description:
en_US: QQ Official API (Webhook)
zh_CN: QQ 官方 API (Webhook),请查看文档了解使用方式
zh_Hans: QQ 官方 API (Webhook),请查看文档了解使用方式
icon: qqofficial.svg
spec:
config:
- name: appid
label:
en_US: App ID
zh_CN: 应用ID
zh_Hans: 应用ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
zh_Hans: 密钥
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 监听端口
zh_Hans: 监听端口
type: integer
required: true
default: 2284
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""

View File

@@ -4,31 +4,31 @@ metadata:
name: slack
label:
en_US: Slack
zh_CN: Slack
zh_Hans: Slack
description:
en_US: Slack Adapter
zh_CN: Slack 适配器,请查看文档了解使用方式
zh_Hans: Slack 适配器,请查看文档了解使用方式
icon: slack.png
spec:
config:
- name: bot_token
label:
en_US: Bot Token
zh_CN: 机器人令牌
zh_Hans: 机器人令牌
type: string
required: true
default: ""
- name: signing_secret
label:
en_US: signing_secret
zh_CN: 密钥
zh_Hans: 密钥
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 监听端口
zh_Hans: 监听端口
type: int
required: true
default: 2288

View File

@@ -4,24 +4,24 @@ metadata:
name: telegram
label:
en_US: Telegram
zh_CN: 电报
zh_Hans: 电报
description:
en_US: Telegram Adapter
zh_CN: 电报适配器,请查看文档了解使用方式
zh_Hans: 电报适配器,请查看文档了解使用方式
icon: telegram.svg
spec:
config:
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""
- name: markdown_card
label:
en_US: Markdown Card
zh_CN: 是否使用 Markdown 卡片
zh_Hans: 是否使用 Markdown 卡片
type: boolean
required: false
default: true

View File

@@ -0,0 +1,773 @@
import requests
import websockets
import websocket
import json
import time
import httpx
from libs.wechatpad_api.client import WeChatPadClient
import typing
import asyncio
import traceback
import time
import re
import base64
import uuid
import json
import os
import copy
import datetime
import threading
import quart
import aiohttp
from .. import adapter
from ...pipeline.longtext.strategies import forward
from ...core import app
from ..types import message as platform_message
from ..types import events as platform_events
from ..types import entities as platform_entities
from ...utils import image
import xml.etree.ElementTree as ET
from typing import Optional, List, Tuple
from functools import partial
import logging
class WeChatPadMessageConverter(adapter.MessageConverter):
def __init__(self, config: dict):
self.config = config
self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"])
self.logger = logging.getLogger("WeChatPadMessageConverter")
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain
) -> list[dict]:
content_list = []
current_file_path = os.path.abspath(__file__)
for component in message_chain:
if isinstance(component, platform_message.At):
content_list.append({"type": "at", "target": component.target})
elif isinstance(component, platform_message.Plain):
content_list.append({"type": "text", "content": component.text})
elif isinstance(component, platform_message.Image):
if component.url:
async with httpx.AsyncClient() as client:
response = await client.get(component.url)
if response.status_code == 200:
file_bytes = response.content
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
else:
raise Exception('获取文件失败')
# pass
content_list.append({"type": "image", "image": base64_str})
elif component.base64:
content_list.append({"type": "image", "image": component.base64})
elif isinstance(component, platform_message.WeChatEmoji):
content_list.append(
{'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size})
elif isinstance(component, platform_message.Voice):
content_list.append({"type": "voice", "data": component.url, "duration": component.length, "forma": 0})
elif isinstance(component, platform_message.WeChatAppMsg):
content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg})
elif isinstance(component, platform_message.Forward):
for node in component.node_list:
if node.message_chain:
content_list.extend(await WeChatPadMessageConverter.yiri2target(node.message_chain))
return content_list
async def target2yiri(
self,
message: dict,
bot_account_id: str
) -> platform_message.MessageChain:
"""外部消息转平台消息"""
# 数据预处理
message_list = []
ats_bot = False # 是否被@
content = message["content"]["str"]
content_no_preifx = content # 群消息则去掉前缀
is_group_message = self._is_group_message(message)
if is_group_message:
ats_bot = self._ats_bot(message, bot_account_id)
if "@所有人" in content:
message_list.append(platform_message.AtAll())
elif ats_bot:
message_list.append(platform_message.At(target=bot_account_id))
content_no_preifx, _ = self._extract_content_and_sender(content)
msg_type = message["msg_type"]
# 映射消息类型到处理器方法
handler_map = {
1: self._handler_text,
3: self._handler_image,
34: self._handler_voice,
49: self._handler_compound, # 复合类型
}
# 分派处理
handler = handler_map.get(msg_type, self._handler_default)
handler_result = await handler(
message=message, # 原始的message
content_no_preifx=content_no_preifx, # 处理后的content
)
if handler_result and len(handler_result) > 0:
message_list.extend(handler_result)
return platform_message.MessageChain(message_list)
async def _handler_text(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理文本消息 (msg_type=1)"""
if message and self._is_group_message(message):
pattern = r'@\S{1,20}'
content_no_preifx = re.sub(pattern, '', content_no_preifx)
return platform_message.MessageChain([platform_message.Plain(content_no_preifx)])
async def _handler_image(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理图像消息 (msg_type=3)"""
try:
image_xml = content_no_preifx
if not image_xml:
return platform_message.MessageChain([platform_message.Unknown("[图片内容为空]")])
root = ET.fromstring(image_xml)
# 提取img标签的属性
img_tag = root.find('img')
if img_tag is not None:
aeskey = img_tag.get('aeskey')
cdnthumburl = img_tag.get('cdnthumburl')
# cdnmidimgurl = img_tag.get('cdnmidimgurl')
image_data = self.bot.cdn_download(aeskey=aeskey, file_type=1, file_url=cdnthumburl)
if image_data["Data"]['FileData'] == '':
image_data = self.bot.cdn_download(aeskey=aeskey, file_type=2, file_url=cdnthumburl)
base64_str = image_data["Data"]['FileData']
# self.logger.info(f"data:image/png;base64,{base64_str}")
elements = [
platform_message.Image(base64=f"data:image/png;base64,{base64_str}"),
# platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发
]
return platform_message.MessageChain(elements)
except Exception as e:
self.logger.error(f"处理图片失败: {str(e)}")
return platform_message.MessageChain([platform_message.Unknown("[图片处理失败]")])
async def _handler_voice(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理语音消息 (msg_type=34)"""
message_List = []
try:
# 从消息中提取语音数据(需根据实际数据结构调整字段名)
# audio_base64 = message["img_buf"]["buffer"]
voice_xml = content_no_preifx
new_msg_id = message['new_msg_id']
root = ET.fromstring(voice_xml)
# 提取voicemsg标签的属性
voicemsg = root.find('voicemsg')
if voicemsg is not None:
bufid = voicemsg.get('bufid')
length = voicemsg.get('voicelength')
voice_data = self.bot.get_msg_voice(buf_id=str(bufid), length=int(length), msgid=str(new_msg_id))
audio_base64 = voice_data["Data"]['Base64']
# 验证语音数据有效性
if not audio_base64:
message_List.append(platform_message.Unknown(text="[语音内容为空]"))
return platform_message.MessageChain(message_List)
# 转换为平台支持的语音格式(如 Silk 格式)
voice_element = platform_message.Voice(
base64=f"data:audio/silk;base64,{audio_base64}"
)
message_List.append(voice_element)
except KeyError as e:
self.logger.error(f"语音数据字段缺失: {str(e)}")
message_List.append(platform_message.Unknown(text="[语音数据解析失败]"))
except Exception as e:
self.logger.error(f"处理语音消息异常: {str(e)}")
message_List.append(platform_message.Unknown(text="[语音处理失败]"))
return platform_message.MessageChain(message_List)
async def _handler_compound(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理复合消息 (msg_type=49),根据子类型分派"""
try:
xml_data = ET.fromstring(content_no_preifx)
appmsg_data = xml_data.find('.//appmsg')
if appmsg_data:
data_type = appmsg_data.findtext('.//type', "")
# 二次分派处理器
sub_handler_map = {
'57': self._handler_compound_quote,
'5': self._handler_compound_link,
'6': self._handler_compound_file,
'33': self._handler_compound_mini_program,
'36': self._handler_compound_mini_program,
'2000': partial(self._handler_compound_unsupported, text="[转账消息]"),
'2001': partial(self._handler_compound_unsupported, text="[红包消息]"),
'51': partial(self._handler_compound_unsupported, text="[视频号消息]"),
}
handler = sub_handler_map.get(data_type, self._handler_compound_unsupported)
return await handler(
message=message, # 原始msg
xml_data=xml_data, # xml数据
)
else:
return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)])
except Exception as e:
self.logger.error(f"解析复合消息失败: {str(e)}")
return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)])
async def _handler_compound_quote(
self,
message: Optional[dict],
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理引用消息 (data_type=57)"""
message_list = []
# self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode'))
appmsg_data = xml_data.find('.//appmsg')
quote_data = "" # 引用原文
quote_id = None # 引用消息的原发送者
tousername = None # 接收方: 所属微信的wxid
user_data = "" # 用户消息
sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member
# 引用消息转发
if appmsg_data:
user_data = appmsg_data.findtext('.//title') or ""
quote_data = appmsg_data.find('.//refermsg').findtext('.//content')
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr')
message_list.append(
platform_message.WeChatAppMsg(
app_msg=ET.tostring(appmsg_data, encoding='unicode'))
)
if message:
tousername = message['to_user_name']["str"]
if quote_data:
quote_data_message_list = platform_message.MessageChain()
# 文本消息
try:
if "<msg>" not in quote_data:
quote_data_message_list.append(platform_message.Plain(quote_data))
else:
# 引用消息展开
quote_data_xml = ET.fromstring(quote_data)
if quote_data_xml.find("img"):
quote_data_message_list.extend(await self._handler_image(None, quote_data))
elif quote_data_xml.find("voicemsg"):
quote_data_message_list.extend(await self._handler_voice(None, quote_data))
elif quote_data_xml.find("videomsg"):
quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理
else:
# appmsg
quote_data_message_list.extend(await self._handler_compound(None, quote_data))
except Exception as e:
self.logger.error(f"处理引用消息异常 expcetion:{e}")
quote_data_message_list.append(platform_message.Plain(quote_data))
message_list.append(
platform_message.Quote(
sender_id=sender_id,
origin=quote_data_message_list,
)
)
if len(user_data) > 0:
pattern = r'@\S{1,20}'
user_data = re.sub(pattern, '', user_data)
message_list.append(platform_message.Plain(user_data))
return platform_message.MessageChain(message_list)
async def _handler_compound_file(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理文件消息 (data_type=6)"""
xml_data_str = ET.tostring(xml_data, encoding='unicode')
return platform_message.MessageChain([
platform_message.WeChatForwardFile(xml_data=xml_data_str)
])
async def _handler_compound_link(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理链接消息(如公众号文章、外部网页)"""
message_list = []
try:
# 解析 XML 中的链接参数
appmsg = xml_data.find('.//appmsg')
if appmsg is None:
return platform_message.MessageChain()
message_list.append(
platform_message.WeChatLink(
link_title=appmsg.findtext('title', ''),
link_desc=appmsg.findtext('des', ''),
link_url=appmsg.findtext('url', ''),
link_thumb_url=appmsg.findtext("thumburl", '') # 这个字段拿不到
)
)
# 还没有发链接的接口, 暂时还需要自己构造appmsg, 先用WeChatAppMsg。
message_list.append(
platform_message.WeChatAppMsg(
app_msg=ET.tostring(appmsg, encoding='unicode')
)
)
except Exception as e:
self.logger.error(f"解析链接消息失败: {str(e)}")
return platform_message.MessageChain(message_list)
async def _handler_compound_mini_program(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理小程序消息(如小程序卡片、服务通知)"""
xml_data_str = ET.tostring(xml_data, encoding='unicode')
return platform_message.MessageChain([
platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)
])
async def _handler_default(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理未知消息类型"""
if message:
msg_type = message["msg_type"]
else:
msg_type = ""
return platform_message.MessageChain([
platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]")
])
def _handler_compound_unsupported(
self,
message: dict,
xml_data: str,
text: Optional[str] = None
) -> platform_message.MessageChain:
"""处理未支持复合消息类型(msg_type=49)子类型"""
if not text:
text = f"[xml_data={xml_data}]"
content_list = []
content_list.append(
platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}"))
return platform_message.MessageChain(content_list)
# 返回是否被艾特
def _ats_bot(self, message: dict, bot_account_id: str) -> bool:
ats_bot = False
try:
to_user_name = message['to_user_name']['str'] # 接收方: 所属微信的wxid
raw_content = message["content"]["str"] # 原始消息内容
content_no_prefix, _ = self._extract_content_and_sender(raw_content)
# 直接艾特机器人这个有bug当被引用的消息里面有@bot,会套娃
# ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix)
# 文本类@bot
push_content = message.get('push_content', '')
ats_bot = ats_bot or ('在群聊中@了你' in push_content)
# 引用别人时@bot
msg_source = message.get('msg_source', '') or ''
if len(msg_source) > 0:
msg_source_data = ET.fromstring(msg_source)
at_user_list = msg_source_data.findtext("atuserlist") or ""
ats_bot = ats_bot or (to_user_name in at_user_list)
# 引用bot
if message.get('msg_type', 0) == 49:
xml_data = ET.fromstring(content_no_prefix)
appmsg_data = xml_data.find('.//appmsg')
tousername = message['to_user_name']['str']
if appmsg_data: # 接收方: 所属微信的wxid
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
ats_bot = ats_bot or (quote_id == tousername)
except Exception as e:
self.logger.error(f"_ats_bot got except: {e}")
finally:
return ats_bot
# 提取一下content前面的sender_id, 和去掉前缀的内容
def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]:
try:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
# add: 有些用户的wxid不是上述格式。换成user_name:
regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:")
line_split = raw_content.split("\n")
if len(line_split) > 0 and regex.match(line_split[0]):
raw_content = "\n".join(line_split[1:])
sender_id = line_split[0].strip(":")
return raw_content, sender_id
except Exception as e:
self.logger.error(f"_extract_content_and_sender got except: {e}")
finally:
return raw_content, None
# 是否是群消息
def _is_group_message(self, message: dict) -> bool:
from_user_name = message['from_user_name']['str']
return from_user_name.endswith("@chatroom")
class WeChatPadEventConverter(adapter.EventConverter):
def __init__(self, config: dict):
self.config = config
self.message_converter = WeChatPadMessageConverter(config)
self.logger = logging.getLogger("WeChatPadEventConverter")
@staticmethod
async def yiri2target(
event: platform_events.MessageEvent
) -> dict:
pass
async def target2yiri(
self,
event: dict,
bot_account_id: str
) -> platform_events.MessageEvent:
# 排除公众号以及微信团队消息
if event['from_user_name']['str'].startswith('gh_') \
or event['from_user_name']['str']=='weixin'\
or event['from_user_name']['str'] == "newsapp"\
or event['from_user_name']['str'] == self.config["wxid"]:
return None
message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id)
if not message_chain:
return None
if '@chatroom' in event['from_user_name']['str']:
# 找出开头的 wxid_ 字符串,以:结尾
sender_wxid = event['content']['str'].split(":")[0]
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=sender_wxid,
member_name=event['from_user_name']['str'],
permission=platform_entities.Permission.Member,
group=platform_entities.Group(
id=event['from_user_name']['str'],
name=event['from_user_name']['str'],
permission=platform_entities.Permission.Member,
),
special_title="",
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0,
),
message_chain=message_chain,
time=event["create_time"],
source_platform_object=event,
)
else:
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event['from_user_name']['str'],
nickname=event['from_user_name']['str'],
remark='',
),
message_chain=message_chain,
time=event["create_time"],
source_platform_object=event,
)
class WeChatPadAdapter(adapter.MessagePlatformAdapter):
name: str = "WeChatPad" # 定义适配器名称
bot: WeChatPadClient
quart_app: quart.Quart
bot_account_id: str
config: dict
ap: app.Application
message_converter: WeChatPadMessageConverter
event_converter: WeChatPadEventConverter
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application):
self.config = config
self.ap = ap
self.quart_app = quart.Quart(__name__)
self.message_converter = WeChatPadMessageConverter(config)
self.event_converter = WeChatPadEventConverter(config)
async def ws_message(self, data):
"""处理接收到的消息"""
# self.ap.logger.debug(f"Gewechat callback event: {data}")
# print(data)
try:
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
except Exception as e:
traceback.print_exc()
if event.__class__ in self.listeners:
await self.listeners[event.__class__](event, self)
return 'ok'
async def _handle_message(
self,
message: platform_message.MessageChain,
target_id: str
):
"""统一消息处理核心逻辑"""
content_list = await self.message_converter.yiri2target(message)
# print(content_list)
at_targets = [item["target"] for item in content_list if item["type"] == "at"]
# print(at_targets)
# 处理@逻辑
at_targets = at_targets or []
member_info = []
if at_targets:
member_info = self.bot.get_chatroom_member_detail(
target_id,
)["Data"]["member_data"]["chatroom_member_list"]
# 处理消息组件
for msg in content_list:
# 文本消息处理@
if msg['type'] == 'text' and at_targets:
at_nick_name_list = []
for member in member_info:
if member["user_name"] in at_targets:
at_nick_name_list.append(f'@{member["nick_name"]}')
msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}'
# 统一消息派发
handler_map = {
'text': lambda msg: self.bot.send_text_message(
to_wxid=target_id,
message=msg['content'],
ats=at_targets
),
'image': lambda msg: self.bot.send_image_message(
to_wxid=target_id,
img_url=msg["image"],
ats = at_targets
),
'WeChatEmoji': lambda msg: self.bot.send_emoji_message(
to_wxid=target_id,
emoji_md5=msg['emoji_md5'],
emoji_size=msg['emoji_size']
),
'voice': lambda msg: self.bot.send_voice_message(
to_wxid=target_id,
voice_data=msg['data'],
voice_duration=msg["duration"],
voice_forma=msg["forma"],
),
'WeChatAppMsg': lambda msg: self.bot.send_app_message(
to_wxid=target_id,
app_message=msg['app_msg'],
type=0,
),
'at': lambda msg: None
}
if handler := handler_map.get(msg['type']):
handler(msg)
# self.ap.logger.warning(f"未处理的消息类型: {ret}")
else:
self.ap.logger.warning(f"未处理的消息类型: {msg['type']}")
continue
async def send_message(
self,
target_type: str,
target_id: str,
message: platform_message.MessageChain
):
"""主动发送消息"""
return await self._handle_message(message, target_id)
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False
):
"""回复消息"""
if message_source.source_platform_object:
target_id = message_source.source_platform_object['from_user_name']['str']
return await self._handle_message(message, target_id)
async def is_muted(self, group_id: int) -> bool:
pass
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None]
):
self.listeners[event_type] = callback
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None]
):
pass
async def run_async(self):
if not self.config["admin_key"] and not self.config["token"]:
raise RuntimeError("无wechatpad管理密匙请填入配置文件后重启")
else:
if self.config["token"]:
self.bot = WeChatPadClient(
self.config['wechatpad_url'],
self.config["token"]
)
data = self.bot.get_login_status()
self.ap.logger.info(data)
if data["Code"] == 300 and data["Text"] == "你已退出微信":
response = requests.post(
f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}",
json={"Count": 1, "Days": 365}
)
if response.status_code != 200:
raise Exception(f"获取token失败: {response.text}")
self.config["token"] = response.json()["Data"][0]
elif not self.config["token"]:
response = requests.post(
f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}",
json={"Count": 1, "Days": 365}
)
if response.status_code != 200:
raise Exception(f"获取token失败: {response.text}")
self.config["token"] = response.json()["Data"][0]
self.bot = WeChatPadClient(
self.config['wechatpad_url'],
self.config["token"]
)
self.ap.logger.info(self.config["token"])
thread_1 = threading.Event()
def wechat_login_process():
# 不登录这些先注释掉避免登陆态尝试拉qrcode。
# login_data =self.bot.get_login_qr()
# url = login_data['Data']["QrCodeUrl"]
# self.ap.logger.info(login_data)
profile =self.bot.get_profile()
self.ap.logger.info(profile)
self.bot_account_id = profile["Data"]["userInfo"]["nickName"]["str"]
self.config["wxid"] = profile["Data"]["userInfo"]["userName"]["str"]
thread_1.set()
# asyncio.create_task(wechat_login_process)
threading.Thread(target=wechat_login_process).start()
def connect_websocket_sync() -> None:
thread_1.wait()
uri = f"{self.config['wechatpad_ws']}/GetSyncMsg?key={self.config['token']}"
self.ap.logger.info(f"Connecting to WebSocket: {uri}")
def on_message(ws, message):
try:
data = json.loads(message)
self.ap.logger.debug(f"Received message: {data}")
# 这里需要确保ws_message是同步的或者使用asyncio.run调用异步方法
asyncio.run(self.ws_message(data))
except json.JSONDecodeError:
self.ap.logger.error(f"Non-JSON message: {message[:100]}...")
def on_error(ws, error):
self.ap.logger.error(f"WebSocket error: {str(error)[:200]}")
def on_close(ws, close_status_code, close_msg):
self.ap.logger.info("WebSocket closed, reconnecting...")
time.sleep(5)
connect_websocket_sync() # 自动重连
def on_open(ws):
self.ap.logger.info("WebSocket connected successfully!")
ws = websocket.WebSocketApp(
uri,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
ws.run_forever(
ping_interval=60,
ping_timeout=20
)
# 直接调用同步版本(会阻塞)
# connect_websocket_sync()
# 这行代码会在WebSocket连接断开后才会执行
# self.ap.logger.info("WebSocket client thread started")
thread = threading.Thread(
target=connect_websocket_sync,
name="WebSocketClientThread",
daemon=True
)
thread.start()
self.ap.logger.info("WebSocket client thread started")
async def kill(self) -> bool:
pass

View File

@@ -0,0 +1,51 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: wechatpad
label:
en_US: WeChatPad
zh_CN: WeChatPad个人微信ipad
description:
en_US: WeChatPad Adapter
zh_CN: WeChatPad 适配器
spec:
config:
- name: wechatpad_url
label:
en_US: WeChatPad ERL
zh_CN: WeChatPad URL
type: string
required: true
default: ""
- name: wechatpad_ws
label:
en_US: WeChatPad_Ws
zh_CN: WeChatPad_Ws
type: string
required: true
default: ""
- name: admin_key
label:
en_US: Admin_Key
zh_CN: 管理员密匙
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
type: string
required: true
default: ""
- name: wxid
label:
en_US: wxid
zh_CN: wxid
type: string
required: true
default: ""
execution:
python:
path: ./wechatpad.py
attr: WeChatPadAdapter

View File

@@ -4,62 +4,62 @@ metadata:
name: wecom
label:
en_US: WeCom
zh_CN: 企业微信
zh_Hans: 企业微信
description:
en_US: WeCom Adapter
zh_CN: 企业微信适配器,请查看文档了解使用方式
zh_Hans: 企业微信适配器,请查看文档了解使用方式
icon: wecom.png
spec:
config:
- name: host
label:
en_US: Host
zh_CN: 监听主机
zh_Hans: 监听主机
description:
en_US: Webhook host, unless you know what you're doing, please write 0.0.0.0
zh_CN: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0
zh_Hans: Webhook 监听主机,除非你知道自己在做什么,否则请写 0.0.0.0
type: string
required: true
default: "0.0.0.0"
- name: port
label:
en_US: Port
zh_CN: 监听端口
zh_Hans: 监听端口
type: integer
required: true
default: 2290
- name: corpid
label:
en_US: Corpid
zh_CN: 企业ID
zh_Hans: 企业ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
zh_Hans: 密钥
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""
- name: EncodingAESKey
label:
en_US: EncodingAESKey
zh_CN: 消息加解密密钥
zh_Hans: 消息加解密密钥
type: string
required: true
default: ""
- name: contacts_secret
label:
en_US: Contacts Secret
zh_CN: 通讯录密钥
zh_Hans: 通讯录密钥
type: string
required: true
default: ""

View File

@@ -4,45 +4,45 @@ metadata:
name: wecomcs
label:
en_US: WeComCustomerService
zh_CN: 企业微信客服
zh_Hans: 企业微信客服
description:
en_US: WeComCSAdapter
zh_CN: 企业微信客服适配器
zh_Hans: 企业微信客服适配器
icon: wecom.png
spec:
config:
- name: port
label:
en_US: Port
zh_CN: 监听端口
zh_Hans: 监听端口
type: int
required: true
default: 2289
- name: corpid
label:
en_US: Corpid
zh_CN: 企业ID
zh_Hans: 企业ID
type: string
required: true
default: ""
- name: secret
label:
en_US: Secret
zh_CN: 密钥
zh_Hans: 密钥
type: string
required: true
default: ""
- name: token
label:
en_US: Token
zh_CN: 令牌
zh_Hans: 令牌
type: string
required: true
default: ""
- name: EncodingAESKey
label:
en_US: EncodingAESKey
zh_CN: 消息加解密密钥
zh_Hans: 消息加解密密钥
type: string
required: true
default: ""

View File

@@ -39,8 +39,8 @@ class PluginLoader(loader.PluginLoader):
self.ap.logger.debug(f'注册插件 {name} {version} by {author}')
container = context.RuntimeContainer(
plugin_name=name,
plugin_label=discover_engine.I18nString(en_US=name, zh_CN=name),
plugin_description=discover_engine.I18nString(en_US=description, zh_CN=description),
plugin_label=discover_engine.I18nString(en_US=name, zh_Hans=name),
plugin_description=discover_engine.I18nString(en_US=description, zh_Hans=description),
plugin_version=version,
plugin_author=author,
plugin_repository='',

View File

@@ -79,7 +79,7 @@ class PluginManager:
await self.load_plugin_settings(self.plugin_containers)
# 按优先级倒序
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}')
@@ -295,7 +295,7 @@ class PluginManager:
plugin.priority = plugin_priority
break
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
for plugin in self.plugin_containers:
await self.dump_plugin_container_setting(plugin)

View File

@@ -99,7 +99,7 @@ class ModelManager:
for model in self.model_list:
if model.name == name:
return model
raise ValueError(f'无法确定模型 {name} 的信息,请在元数据中配置')
raise ValueError(f'无法确定模型 {name} 的信息')
async def get_model_by_uuid(self, uuid: str) -> entities.LLMModelInfo:
"""通过uuid获取模型"""

View File

@@ -4,7 +4,7 @@ metadata:
name: LLMAPIRequester
label:
en_US: LLM API Requester
zh_CN: LLM API 请求器
zh_Hans: LLM API 请求器
spec:
type:
- python

View File

@@ -4,21 +4,21 @@ metadata:
name: anthropic-messages
label:
en_US: Anthropic
zh_CN: Anthropic
zh_Hans: Anthropic
icon: anthropic.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.anthropic.com/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: bailian-chat-completions
label:
en_US: Aliyun Bailian
zh_CN: 阿里云百炼
zh_Hans: 阿里云百炼
icon: bailian.png
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://dashscope.aliyuncs.com/compatible-mode/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: openai-chat-completions
label:
en_US: OpenAI
zh_CN: OpenAI
zh_Hans: OpenAI
icon: openai.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.openai.com/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -27,7 +27,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -47,7 +47,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = messages
# 发送请求
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
if resp is None:
raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常')

View File

@@ -4,21 +4,21 @@ metadata:
name: deepseek-chat-completions
label:
en_US: DeepSeek
zh_CN: 深度求索
zh_Hans: 深度求索
icon: deepseek.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.deepseek.com"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><defs><linearGradient id="lobe-icons-gemini-fill" x1="0%" x2="68.73%" y1="100%" y2="30.395%"><stop offset="0%" stop-color="#1C7DFF"></stop><stop offset="52.021%" stop-color="#1C69FF"></stop><stop offset="100%" stop-color="#F0DCD6"></stop></linearGradient></defs><path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12" fill="url(#lobe-icons-gemini-fill)" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
import typing
import google.genai
from google.genai import types
from .. import errors, requester
from ....core import entities as core_entities
from ... import entities as llm_entities
from ...tools import entities as tools_entities
class GeminiChatCompletions(requester.LLMAPIRequester):
"""Google Gemini API 请求器"""
default_config: dict[str, typing.Any] = {
'base_url': 'https://generativelanguage.googleapis.com',
'timeout': 120,
}
async def initialize(self):
"""初始化 Gemini API 客户端"""
pass
async def invoke_llm(
self,
query: core_entities.Query,
model: requester.RuntimeLLMModel,
messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None,
extra_args: dict[str, typing.Any] = {},
) -> llm_entities.Message:
"""调用 Gemini API 生成回复"""
try:
self.client = google.genai.Client(
api_key=model.token_mgr.get_token(),
http_options=types.HttpOptions(api_version='v1alpha'),
)
contents = []
system_content = None
for message in messages:
role = message.role
parts = []
if isinstance(message.content, str):
parts.append(types.Part.from_text(text=message.content))
elif isinstance(message.content, list):
for content in message.content:
if content.type == 'text':
parts.append(types.Part.from_text(text=content.text))
# elif content.type == 'image_url':
# parts.append(types.Part.from_image_url(url=content.image_url))
if role == 'system':
system_content = parts
else:
content = types.Content(role=role, parts=parts)
contents.append(content)
response = self.client.models.generate_content(
model=model.model_entity.name,
contents=contents,
config=types.GenerateContentConfig(
system_instruction=system_content,
**extra_args,
),
)
return llm_entities.Message(
role='assistant',
content=response.candidates[0].content.parts[0].text,
)
except Exception as e:
error_message = str(e).lower()
if 'invalid api key' in error_message:
raise errors.RequesterError(f'无效的 API 密钥: {str(e)}')
elif 'not found' in error_message:
raise errors.RequesterError(f'请求路径错误或模型无效: {str(e)}')
elif any(keyword in error_message for keyword in ['rate limit', 'quota', 'permission denied']):
raise errors.RequesterError(f'请求过于频繁或余额不足: {str(e)}')
elif 'timeout' in error_message:
raise errors.RequesterError(f'请求超时: {str(e)}')
else:
raise errors.RequesterError(f'Gemini API 请求错误: {str(e)}')

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: gemini-chat-completions
label:
en_US: Google Gemini
zh_Hans: Google Gemini
icon: gemini.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://generativelanguage.googleapis.com"
- name: timeout
label:
en_US: Timeout
zh_Hans: 超时时间
type: integer
required: true
default: 120
execution:
python:
path: ./geminichatcmpl.py
attr: GeminiChatCompletions

View File

@@ -28,7 +28,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -44,7 +44,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = req_messages
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
message = await self._make_msg(resp)

View File

@@ -4,21 +4,21 @@ metadata:
name: gitee-ai-chat-completions
label:
en_US: Gitee AI
zh_CN: Gitee AI
zh_Hans: Gitee AI
icon: giteeai.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://ai.gitee.com/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: lmstudio-chat-completions
label:
en_US: LM Studio
zh_CN: LM Studio
zh_Hans: LM Studio
icon: lmstudio.webp
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "http://127.0.0.1:1234/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -0,0 +1 @@
<svg id="_层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 274.37 172.76"><defs><style>.cls-2{fill:#36cfd1}.cls-3{fill:#624aff}</style></defs><g id="_层_1-2"><path class="cls-3" d="M24.78 73.55h25.65V99.2H24.78zm99.14 25.66h25.65v25.65h-25.65zm76.95 25.65h-25.65v22.19h47.84V99.21h-22.19v25.65z"/><path class="cls-2" d="M149.57 73.55h25.65V99.2h-25.65zM24.78 47.9h25.65v25.65H24.78z"/><path class="cls-3" d="M223.06 73.55h25.65V99.2h-25.65z"/><path class="cls-2" d="M223.06 47.9h25.65v25.65h-25.65z"/><path class="cls-3" d="M175.22 25.71V47.9h25.65v25.65h22.19V25.71h-47.84z"/><path class="cls-2" d="M98.27 73.55h25.65V99.2H98.27z"/><path class="cls-3" d="M72.62 47.9h25.65V25.71H50.43v47.84h22.19V47.9zm0 51.31H50.43v47.84h47.84v-22.19H72.62V99.21z"/><path style="fill:none" d="M0 0h274.37v172.76H0z"/></g></svg>

After

Width:  |  Height:  |  Size: 820 B

View File

@@ -9,7 +9,7 @@ import openai.types.chat.chat_completion_message_tool_call as chat_completion_me
import httpx
from .. import entities, errors, requester
from ....core import entities as core_entities, app
from ....core import entities as core_entities
from ... import entities as llm_entities
from ...tools import entities as tools_entities
@@ -19,17 +19,15 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['modelscope-chat-completions']
default_config: dict[str, typing.Any] = {
'base_url': 'https://api-inference.modelscope.cn/v1',
'timeout': 120,
}
async def initialize(self):
self.client = openai.AsyncClient(
api_key='',
base_url=self.requester_cfg['base-url'],
base_url=self.requester_cfg['base_url'],
timeout=self.requester_cfg['timeout'],
http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']),
)
@@ -37,6 +35,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
async def _req(
self,
args: dict,
extra_body: dict = {},
) -> chat_completion.ChatCompletion:
args['stream'] = True
@@ -46,7 +45,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
tool_calls = []
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args)
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args, extra_body=extra_body)
async for chunk in resp_gen:
# print(chunk)
@@ -107,7 +106,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
if chunk
else None
)
return await self.client.chat.completions.create(**args)
async def _make_msg(
self,
@@ -127,13 +125,14 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
self,
query: core_entities.Query,
req_messages: list[dict],
use_model: entities.LLMModelInfo,
use_model: requester.RuntimeLLMModel,
use_funcs: list[tools_entities.LLMFunction] = None,
extra_args: dict[str, typing.Any] = {},
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args['model'] = use_model.name if use_model.model_name is None else use_model.model_name
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
@@ -156,19 +155,20 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
args['messages'] = messages
# 发送请求
resp = await self._req(args)
resp = await self._req(args, extra_body=extra_args)
# 处理请求结果
message = await self._make_msg(resp)
return message
async def call(
async def invoke_llm(
self,
query: core_entities.Query,
model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None,
extra_args: dict[str, typing.Any] = {},
) -> llm_entities.Message:
req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
for m in messages:
@@ -182,7 +182,9 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
req_messages.append(msg_dict)
try:
return await self._closure(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs)
return await self._closure(
query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args
)
except asyncio.TimeoutError:
raise errors.RequesterError('请求超时')
except openai.BadRequestError as e:

View File

@@ -4,27 +4,28 @@ metadata:
name: modelscope-chat-completions
label:
en_US: ModelScope
zh_CN: 魔搭社区
zh_Hans: 魔搭社区
icon: modelscope.svg
spec:
config:
- name: base-url
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api-inference.modelscope.cn/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
zh_Hans: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: int
required: true
default: 120

View File

@@ -28,7 +28,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -51,7 +51,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = messages
# 发送请求
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
# 处理请求结果
message = await self._make_msg(resp)

View File

@@ -4,21 +4,21 @@ metadata:
name: moonshot-chat-completions
label:
en_US: Moonshot
zh_CN: 月之暗面
zh_Hans: 月之暗面
icon: moonshot.png
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.moonshot.com/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: ollama-chat
label:
en_US: Ollama
zh_CN: Ollama
zh_Hans: Ollama
icon: ollama.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "http://127.0.0.1:11434"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -0,0 +1,10 @@
<svg width="25" height="21" viewBox="0 0 25 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.05858 10.1738C1.76158 10.1738 4.47988 9.56715 5.88589 8.77041C7.2919 7.97367 7.2919 7.97367 10.1977 5.91152C13.8766 3.30069 16.4779 4.17486 20.7428 4.17486" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4182 7.63145L11.3787 7.65951C8.50565 9.69845 8.42504 9.75566 6.92566 10.6053C5.98567 11.138 4.74704 11.5436 3.75151 11.8089C2.80313 12.0615 1.71203 12.2829 1.05858 12.2829V8.06483C1.05075 8.06483 1.05422 8.06445 1.06984 8.06276C1.11491 8.05788 1.26116 8.04203 1.52896 7.9926C1.84599 7.9341 2.24205 7.84582 2.6657 7.73296C3.55657 7.49564 4.3801 7.1996 4.84612 6.93552C4.88175 6.91533 4.91635 6.89573 4.95001 6.87666C6.15007 6.19693 6.15657 6.19325 8.97708 4.1916C12.5199 1.67735 15.5815 1.83587 18.5849 1.99138C19.3056 2.0287 20.0229 2.06584 20.7428 2.06584V6.28388C19.6102 6.28388 18.6583 6.24193 17.8263 6.20527C15.1245 6.08621 13.685 6.02278 11.4182 7.63145Z" fill="black"/>
<path d="M24.8671 4.20087L17.6613 8.36117V0.0405881L24.8671 4.20087Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6378 0L24.9139 4.20087L17.6378 8.40176V0ZM17.6847 0.0811762V8.32058L24.8202 4.20087L17.6847 0.0811762Z" fill="black"/>
<path d="M0.917975 10.1764C1.62098 10.1764 4.33927 10.7831 5.74529 11.5799C7.1513 12.3766 7.1513 12.3766 10.0571 14.4388C13.736 17.0496 16.3373 16.1754 20.6022 16.1754" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.929234 12.2875C0.913615 12.2858 0.910145 12.2854 0.917975 12.2854V8.06741C1.57142 8.06741 2.66253 8.28878 3.61091 8.54142C4.60644 8.80663 5.84507 9.21231 6.78506 9.74497C8.28444 10.5946 8.36505 10.6518 11.2381 12.6908L11.2776 12.7188C13.5444 14.3275 14.9839 14.2641 17.6857 14.145C18.5177 14.1083 19.4696 14.0664 20.6022 14.0664V18.2844C19.8823 18.2844 19.165 18.3216 18.4443 18.3589C15.4409 18.5144 12.3793 18.6729 8.83648 16.1587C6.01597 14.157 6.00947 14.1533 4.80941 13.4736C4.77575 13.4545 4.74115 13.4349 4.70551 13.4148C4.2395 13.1507 3.41597 12.8546 2.5251 12.6173C2.10145 12.5045 1.70538 12.4162 1.38836 12.3577C1.12056 12.3083 0.974309 12.2924 0.929234 12.2875Z" fill="black"/>
<path d="M24.7265 16.1494L17.5207 11.9892V20.3097L24.7265 16.1494Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4972 11.9486L24.7733 16.1494L17.4972 20.3503V11.9486ZM17.5441 12.0297V20.2691L24.6796 16.1494L17.5441 12.0297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
import typing
import openai
from . import modelscopechatcmpl
class OpenRouterChatCompletions(modelscopechatcmpl.ModelScopeChatCompletions):
"""OpenRouter ChatCompletion API 请求器"""
client: openai.AsyncClient
default_config: dict[str, typing.Any] = {
'base_url': 'https://openrouter.ai/api/v1',
'timeout': 120,
}

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: openrouter-chat-completions
label:
en_US: OpenRouter
zh_Hans: OpenRouter
icon: openrouter.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://openrouter.ai/api/v1"
- name: timeout
label:
en_US: Timeout
zh_Hans: 超时时间
type: integer
required: true
default: 120
execution:
python:
path: ./openrouterchatcmpl.py
attr: OpenRouterChatCompletions

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
import openai
import typing
from . import chatcmpl
from ....core import app
class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions):
@@ -11,9 +11,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions):
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['ppio-chat-completions']
default_config: dict[str, typing.Any] = {
'base_url': 'https://api.ppinfra.com/v3/openai',
'timeout': 120,
}

View File

@@ -4,27 +4,27 @@ metadata:
name: ppio-chat-completions
label:
en_US: ppio
zh_CN: 派欧云
zh_Hans: 派欧云
spec:
config:
- name: base-url
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.ppinfra.com/v3/openai"
- name: args
label:
en_US: Args
zh_CN: 附加参数
zh_Hans: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: int
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: siliconflow-chat-completions
label:
en_US: SiliconFlow
zh_CN: 硅基流动
zh_Hans: 硅基流动
icon: siliconflow.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.siliconflow.cn/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: volcark-chat-completions
label:
en_US: Volc Engine Ark
zh_CN: 火山方舟
zh_Hans: 火山方舟
icon: volcark.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://ark.cn-beijing.volces.com/api/v3"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: xai-chat-completions
label:
en_US: xAI
zh_CN: xAI
zh_Hans: xAI
icon: xai.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://api.x.ai/v1"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -4,21 +4,21 @@ metadata:
name: zhipuai-chat-completions
label:
en_US: ZhipuAI
zh_CN: 智谱 AI
zh_Hans: 智谱 AI
icon: zhipuai.svg
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
default: "https://open.bigmodel.cn/api/paas/v4"
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
zh_Hans: 超时时间
type: integer
required: true
default: 120

View File

@@ -121,7 +121,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
conversation_id=cov_id,
files=files,
timeout=self.pipeline_config['ai']['dify-service-api']['timeout'],
timeout=120,
):
self.ap.logger.debug('dify-chat-chunk: ' + str(chunk))
@@ -184,7 +184,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
response_mode='streaming',
conversation_id=cov_id,
files=files,
timeout=self.pipeline_config['ai']['dify-service-api']['timeout'],
timeout=120,
):
self.ap.logger.debug('dify-agent-chunk: ' + str(chunk))
@@ -276,7 +276,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
inputs=inputs,
user=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
files=files,
timeout=self.pipeline_config['ai']['dify-service-api']['timeout'],
timeout=120,
):
self.ap.logger.debug('dify-workflow-chunk: ' + str(chunk))
if chunk['event'] in ignored_events:

View File

@@ -62,9 +62,6 @@ class SessionManager:
conversation = core_entities.Conversation(
prompt=prompt,
messages=[],
use_llm_model=await self.ap.model_mgr.get_model_by_uuid(
query.pipeline_config['ai']['local-agent']['model']
),
use_funcs=await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True,
),

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.0.0'
semantic_version = 'v4.0.3'
required_database_version = 1
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""

View File

@@ -32,6 +32,7 @@ def import_dir(path: str):
rel_path = full_path.replace(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '')
rel_path = rel_path[1:]
rel_path = rel_path.replace('/', '.')[:-3]
rel_path = rel_path.replace("\\",".")
importlib.import_module(rel_path)

183
pyproject.toml Normal file
View File

@@ -0,0 +1,183 @@
[project]
name = "langbot"
version = "4.0.3"
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
readme = "README.md"
requires-python = ">=3.10.1"
dependencies = [
"aiocqhttp>=1.4.4",
"aiofiles>=24.1.0",
"aiohttp>=3.11.18",
"aioshutil>=1.5",
"aiosqlite>=0.21.0",
"anthropic>=0.51.0",
"argon2-cffi>=23.1.0",
"async-lru>=2.0.5",
"certifi>=2025.4.26",
"colorlog~=6.6.0",
"cryptography>=44.0.3",
"dashscope>=1.23.2",
"dingtalk-stream>=0.24.0",
"discord-py>=2.5.2",
"gewechat-client>=0.1.5",
"lark-oapi>=1.4.15",
"mcp>=1.8.1",
"nakuru-project-idk>=0.0.2.1",
"ollama>=0.4.8",
"openai>1.0.0",
"pillow>=11.2.1",
"psutil>=7.0.0",
"pycryptodome>=3.22.0",
"pydantic>2.0",
"pyjwt>=2.10.1",
"python-telegram-bot>=22.0",
"pyyaml>=6.0.2",
"qq-botpy-rc>=1.2.1.6",
"quart>=0.20.0",
"quart-cors>=0.8.0",
"requests>=2.32.3",
"slack-sdk>=3.35.0",
"sqlalchemy[asyncio]>=2.0.40",
"sqlmodel>=0.0.24",
"telegramify-markdown>=0.5.1",
"tiktoken>=0.9.0",
"urllib3>=2.4.0",
"websockets>=15.0.1",
"python-socks>=2.7.1", # dingtalk missing dependency
"taskgroup==0.0.0a4", # graingert/taskgroup#20
"pip>=25.1.1", # pkg.core.bootutils.deps
"google-genai>=1.15.0",
"google-generativeai>=0.8.5",
"ruff>=0.11.9",
"pre-commit>=4.2.0",
]
keywords = [
"bot",
"agent",
"telegram",
"plugins",
"openai",
"instant-messaging",
"wechat",
"qq",
"dify",
"llm",
"chatgpt",
"deepseek",
"onebot",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: AsyncIO",
"Framework :: Robot Framework",
"Framework :: Robot Framework :: Library",
"License :: OSI Approved :: AGPL-3 License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
]
[project.urls]
Homepage = "https://langbot.app"
Documentation = "https://docs.langbot.app"
Repository = "https://github.com/RockChinQ/langbot"
[dependency-groups]
dev = [
"pre-commit>=4.2.0",
"ruff>=0.11.9",
]
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
line-length = 120
indent-width = 4
# Assume Python 3.12
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = [
"E712", # Comparison to true should be 'if cond is true:' or 'if cond:' (E712)
"F402", # Import `loader` from line 8 shadowed by loop variable
"F403", # * used, unable to detect undefined names
"F405", # may be undefined, or defined from star imports
"E741", # Ambiguous variable name: `l`
"E722", # bare-except
"E721", # type-comparison
"F821", # undefined-all
"FURB113", # repeated-append
"FURB152", # math-constant
"UP007", # non-pep604-annotation
"UP032", # f-string
"UP045", # non-pep604-annotation-optional
"B005", # strip-with-multi-characters
"B006", # mutable-argument-default
"B007", # unused-loop-control-variable
"B026", # star-arg-unpacking-after-keyword-arg
"B903", # class-as-data-structure
"B904", # raise-without-from-inside-except
"B905", # zip-without-explicit-strict
"N806", # non-lowercase-variable-in-function
"N815", # mixed-case-variable-in-class-scope
"PT011", # pytest-raises-too-broad
"SIM102", # collapsible-if
"SIM103", # needless-bool
"SIM105", # suppressible-exception
"SIM107", # return-in-try-except-finally
"SIM108", # if-else-block-instead-of-if-exp
"SIM113", # enumerate-for-loop
"SIM117", # multiple-with-statements
"SIM210", # if-expr-with-true-false
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "single"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

View File

@@ -1,45 +0,0 @@
# direct
requests
openai>1.0.0
anthropic
colorlog~=6.6.0
aiocqhttp
qq-botpy-rc
nakuru-project-idk
Pillow
tiktoken
PyYaml
aiohttp
pydantic>2.0
websockets
urllib3
psutil
async-lru
ollama
quart
sqlalchemy[asyncio]
aiosqlite
quart-cors
aiofiles
aioshutil
argon2-cffi
pyjwt
pycryptodome
lark-oapi
discord.py
cryptography
gewechat-client
dingtalk_stream
dashscope
python-telegram-bot
certifi
mcp
sqlmodel
slack_sdk
telegramify-markdown
# indirect
taskgroup==0.0.0a4
ruff
pre-commit
python-socks

View File

@@ -1,87 +1,87 @@
name: ai
label:
en_US: AI Feature
zh_CN: AI 能力
zh_Hans: AI 能力
stages:
- name: runner
label:
en_US: Runner
zh_CN: 运行方式
zh_Hans: 运行方式
description:
en_US: Strategy to call AI to process messages
zh_CN: 调用 AI 处理消息的方式
zh_Hans: 调用 AI 处理消息的方式
config:
- name: runner
label:
en_US: Runner
zh_CN: 运行器
zh_Hans: 运行器
type: select
required: true
default: local-agent
options:
- name: local-agent
label:
en_US: Embedded Agent
zh_CN: 内置 Agent
en_US: Local Agent
zh_Hans: 内置 Agent
- name: dify-service-api
label:
en_US: Dify Service API
zh_CN: Dify 服务 API
zh_Hans: Dify 服务 API
- name: dashscope-app-api
label:
en_US: Aliyun Dashscope App API
zh_CN: 阿里云百炼平台 API
zh_Hans: 阿里云百炼平台 API
- name: local-agent
label:
en_US: Embedded Agent
zh_CN: 内置 Agent
en_US: Local Agent
zh_Hans: 内置 Agent
description:
en_US: Configure the embedded agent of the pipeline
zh_CN: 配置内置 Agent
zh_Hans: 配置内置 Agent
config:
- name: model
label:
en_US: Model
zh_CN: 模型
zh_Hans: 模型
type: llm-model-selector
required: true
- name: max-round
label:
en_US: Max Round
zh_CN: 最大回合数
zh_Hans: 最大回合数
description:
en_US: The maximum number of previous messages that the agent can remember
zh_CN: 最大前文消息回合数
zh_Hans: 最大前文消息回合数
type: integer
required: true
default: 10
- name: prompt
label:
en_US: Prompt
zh_CN: 提示词
zh_Hans: 提示词
description:
en_US: The prompt of the agent
zh_CN: 除非您了解消息结构,否则请只使用 system 单提示词
zh_Hans: 除非您了解消息结构,否则请只使用 system 单提示词
type: prompt-editor
required: true
- name: dify-service-api
label:
en_US: Dify Service API
zh_CN: Dify 服务 API
zh_Hans: Dify 服务 API
description:
en_US: Configure the Dify service API of the pipeline
zh_CN: 配置 Dify 服务 API
zh_Hans: 配置 Dify 服务 API
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
zh_Hans: 基础 URL
type: string
required: true
- name: app-type
label:
en_US: App Type
zh_CN: 应用类型
zh_Hans: 应用类型
type: select
required: true
default: chat
@@ -89,25 +89,25 @@ stages:
- name: chat
label:
en_US: Chat
zh_CN: 聊天包括Chatflow
zh_Hans: 聊天包括Chatflow
- name: agent
label:
en_US: Agent
zh_CN: Agent
zh_Hans: Agent
- name: workflow
label:
en_US: Workflow
zh_CN: 工作流
zh_Hans: 工作流
- name: api-key
label:
en_US: API Key
zh_CN: API 密钥
zh_Hans: API 密钥
type: string
required: true
- name: thinking-convert
label:
en_US: CoT Convert
zh_CN: 思维链转换策略
zh_Hans: 思维链转换策略
type: select
required: true
default: plain
@@ -115,27 +115,27 @@ stages:
- name: plain
label:
en_US: Convert to <think>...</think>
zh_CN: 转换成 <think>...</think>
zh_Hans: 转换成 <think>...</think>
- name: original
label:
en_US: Original
zh_CN: 原始
zh_Hans: 原始
- name: remove
label:
en_US: Remove
zh_CN: 移除
zh_Hans: 移除
- name: dashscope-app-api
label:
en_US: Aliyun Dashscope App API
zh_CN: 阿里云百炼平台 API
zh_Hans: 阿里云百炼平台 API
description:
en_US: Configure the Aliyun Dashscope App API of the pipeline
zh_CN: 配置阿里云百炼平台 API
zh_Hans: 配置阿里云百炼平台 API
config:
- name: app-type
label:
en_US: App Type
zh_CN: 应用类型
zh_Hans: 应用类型
type: select
required: true
default: agent
@@ -143,30 +143,30 @@ stages:
- name: agent
label:
en_US: Agent
zh_CN: Agent
zh_Hans: Agent
- name: workflow
label:
en_US: Workflow
zh_CN: 工作流
zh_Hans: 工作流
- name: api-key
label:
en_US: API Key
zh_CN: API 密钥
zh_Hans: API 密钥
type: string
required: true
- name: app-id
label:
en_US: App ID
zh_CN: 应用 ID
zh_Hans: 应用 ID
type: string
required: true
- name: references_quote
label:
en_US: References Quote
zh_CN: 引用文本
zh_Hans: 引用文本
description:
en_US: The text prompt when the references are included
zh_CN: 包含引用资料时的文本提示
zh_Hans: 包含引用资料时的文本提示
type: string
required: false
default: '参考资料来自:'

View File

@@ -1,30 +1,30 @@
name: output
label:
en_US: Output Processing
zh_CN: 输出处理
zh_Hans: 输出处理
stages:
- name: long-text-processing
label:
en_US: Long Text Processing
zh_CN: 长文本处理
zh_Hans: 长文本处理
config:
- name: threshold
label:
en_US: Threshold
zh_CN: 阈值
zh_Hans: 阈值
description:
en_US: The threshold of the long text
zh_CN: 超过此长度的文本将被处理
zh_Hans: 超过此长度的文本将被处理
type: integer
required: true
default: 1000
- name: strategy
label:
en_US: Strategy
zh_CN: 策略
zh_Hans: 策略
description:
en_US: The strategy of the long text
zh_CN: 长文本的处理策略
zh_Hans: 长文本的处理策略
type: select
required: true
default: forward
@@ -32,76 +32,76 @@ stages:
- name: forward
label:
en_US: Forward Message Component
zh_CN: 转换为转发消息组件(部分平台不支持)
zh_Hans: 转换为转发消息组件(部分平台不支持)
- name: image
label:
en_US: Convert to Image
zh_CN: 转换为图片
zh_Hans: 转换为图片
- name: font-path
label:
en_US: Font Path
zh_CN: 字体路径
zh_Hans: 字体路径
description:
en_US: The path of the font to be used when converting to image
zh_CN: 选用转换为图片时,所使用的字体路径
zh_Hans: 选用转换为图片时,所使用的字体路径
type: string
required: false
default: ''
- name: force-delay
label:
en_US: Force Delay
zh_CN: 强制延迟
zh_Hans: 强制延迟
description:
en_US: Force the output to be delayed for a while
zh_CN: 强制延迟一段时间后再回复给用户
zh_Hans: 强制延迟一段时间后再回复给用户
config:
- name: min
label:
en_US: Min Seconds
zh_CN: 最小秒数
zh_Hans: 最小秒数
type: integer
required: true
default: 0
- name: max
label:
en_US: Max Seconds
zh_CN: 最大秒数
zh_Hans: 最大秒数
type: integer
required: true
default: 0
- name: misc
label:
en_US: Misc
zh_CN: 杂项
zh_Hans: 杂项
config:
- name: hide-exception
label:
en_US: Hide Exception
zh_CN: 不输出异常信息给用户
zh_Hans: 不输出异常信息给用户
type: boolean
required: true
default: true
- name: at-sender
label:
en_US: At Sender
zh_CN: 在群聊回复中@发送者
zh_Hans: 在群聊回复中@发送者
type: boolean
required: true
default: true
- name: quote-origin
label:
en_US: Quote Origin Message
zh_CN: 引用原消息
zh_Hans: 引用原消息
type: boolean
required: true
default: false
- name: track-function-calls
label:
en_US: Track Function Calls
zh_CN: 跟踪函数调用
zh_Hans: 跟踪函数调用
description:
en_US: If enabled, the function calls will be tracked and output to the user
zh_CN: 启用后Agent 每次调用工具时都会输出一个提示给用户
zh_Hans: 启用后Agent 每次调用工具时都会输出一个提示给用户
type: boolean
required: true
default: false

View File

@@ -1,17 +1,17 @@
name: safety
label:
en_US: Safety Control
zh_CN: 安全控制
zh_Hans: 安全控制
stages:
- name: content-filter
label:
en_US: Content Filter
zh_CN: 内容过滤
zh_Hans: 内容过滤
config:
- name: scope
label:
en_US: Scope
zh_CN: 检查范围
zh_Hans: 检查范围
type: select
required: true
default: all
@@ -19,48 +19,48 @@ stages:
- name: all
label:
en_US: All
zh_CN: 全部
zh_Hans: 全部
- name: income-msg
label:
en_US: Income Message
zh_CN: 传入消息(用户消息)
zh_Hans: 传入消息(用户消息)
- name: output-msg
label:
en_US: Output Message
zh_CN: 传出消息(机器人消息)
zh_Hans: 传出消息(机器人消息)
- name: check-sensitive-words
label:
en_US: Check Sensitive Words
zh_CN: 检查敏感词
zh_Hans: 检查敏感词
description:
en_US: Sensitive words can be configured in data/metadata/sensitive-words.json
zh_CN: 敏感词内容可以在 data/metadata/sensitive-words.json 中配置
zh_Hans: 敏感词内容可以在 data/metadata/sensitive-words.json 中配置
type: boolean
required: true
default: false
- name: rate-limit
label:
en_US: Rate Limit
zh_CN: 速率限制
zh_Hans: 速率限制
config:
- name: window-length
label:
en_US: Window Length
zh_CN: 窗口长度(秒)
zh_Hans: 窗口长度(秒)
type: integer
required: true
default: 60
- name: limitation
label:
en_US: Limitation
zh_CN: 限制次数
zh_Hans: 限制次数
type: integer
required: true
default: 60
- name: strategy
label:
en_US: Strategy
zh_CN: 策略
zh_Hans: 策略
type: select
required: true
default: drop
@@ -68,8 +68,8 @@ stages:
- name: drop
label:
en_US: Drop
zh_CN: 丢弃
zh_Hans: 丢弃
- name: wait
label:
en_US: Wait
zh_CN: 等待
zh_Hans: 等待

View File

@@ -1,68 +1,68 @@
name: trigger
label:
en_US: Trigger
zh_CN: 触发条件
zh_Hans: 触发条件
stages:
- name: group-respond-rules
label:
en_US: Group Respond Rule
zh_CN: 群响应规则
zh_Hans: 群响应规则
description:
en_US: The respond rule of the messages in the groups
zh_CN: 群内消息的响应规则
zh_Hans: 群内消息的响应规则
config:
- name: at
label:
en_US: At
zh_CN: '@'
zh_Hans: '@'
description:
en_US: Whether to trigger when the message mentions the bot
zh_CN: 是否在消息@机器人时触发
zh_Hans: 是否在消息@机器人时触发
type: boolean
required: true
default: false
- name: prefix
label:
en_US: Prefix
zh_CN: 前缀
zh_Hans: 前缀
description:
en_US: Messages with these prefixes will be responded (the prefixes will be removed automatically when sending to AI)
zh_CN: 具有这些前缀的消息将被响应(发送给 AI 时会自动去除对应前缀)
zh_Hans: 具有这些前缀的消息将被响应(发送给 AI 时会自动去除对应前缀)
type: array[string]
required: true
default: []
- name: regexp
label:
en_US: Regexp
zh_CN: 正则表达式
zh_Hans: 正则表达式
description:
en_US: Messages with these regular expressions will be responded
zh_CN: 符合这些正则表达式的消息将被响应
zh_Hans: 符合这些正则表达式的消息将被响应
type: array[string]
required: true
default: []
- name: random
label:
en_US: Random
zh_CN: 随机
zh_Hans: 随机
description:
en_US: The probability of the random response, range from 0.0 to 1.0
zh_CN: 随机响应概率,范围为 0.0-1.0,对应 0% 到 100%
zh_Hans: 随机响应概率,范围为 0.0-1.0,对应 0% 到 100%
type: float
required: false
default: 0
- name: access-control
label:
en_US: Access Control
zh_CN: 访问控制
zh_Hans: 访问控制
config:
- name: mode
label:
en_US: Mode
zh_CN: 模式
zh_Hans: 模式
description:
en_US: The mode of the access control
zh_CN: 访问控制模式
zh_Hans: 访问控制模式
type: select
required: true
default: blacklist
@@ -70,50 +70,50 @@ stages:
- name: blacklist
label:
en_US: Blacklist
zh_CN: 黑名单
zh_Hans: 黑名单
- name: whitelist
label:
en_US: Whitelist
zh_CN: 白名单
zh_Hans: 白名单
- name: blacklist
label:
en_US: Blacklist
zh_CN: 黑名单
zh_Hans: 黑名单
type: array[string]
required: true
default: []
- name: whitelist
label:
en_US: Whitelist
zh_CN: 白名单
zh_Hans: 白名单
type: array[string]
required: true
default: []
- name: ignore-rules
label:
en_US: Ignore Rules
zh_CN: 消息忽略规则
zh_Hans: 消息忽略规则
description:
en_US: Ignore rules that apply to both group and private messages
zh_CN: 对群聊、私聊消息均适用的忽略规则(优先级高于群响应规则)
zh_Hans: 对群聊、私聊消息均适用的忽略规则(优先级高于群响应规则)
config:
- name: prefix
label:
en_US: Prefix
zh_CN: 前缀
zh_Hans: 前缀
description:
en_US: Messages with these prefixes will be ignored
zh_CN: 包含这些前缀的消息将被忽略
zh_Hans: 包含这些前缀的消息将被忽略
type: array[string]
required: true
default: []
- name: regexp
label:
en_US: Regexp
zh_CN: 正则表达式
zh_Hans: 正则表达式
description:
en_US: Messages with these regular expressions will be ignored
zh_CN: 符合这些正则表达式的消息将被忽略
zh_Hans: 符合这些正则表达式的消息将被忽略
type: array[string]
required: true
default: []

193
web/package-lock.json generated
View File

@@ -8,9 +8,12 @@
"name": "web",
"version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
@@ -22,6 +25,8 @@
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.507.0",
"next": "15.2.4",
@@ -30,6 +35,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
@@ -46,7 +52,6 @@
"eslint-config-next": "15.2.4",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"tw-animate-css": "^1.2.9",
@@ -66,6 +71,68 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -1115,6 +1182,37 @@
}
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.13.tgz",
"integrity": "sha512-Wtjvx0d/6Bgd/jAYS1mW6IPSUQ25y0hkUSOS1z5/4+U8+DJPwKroqJlM/AlVFl3LywGoruiPmcvB9Aks9mSOQw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.9",
"@radix-ui/react-popper": "1.2.6",
"@radix-ui/react-portal": "1.1.8",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -4289,6 +4387,15 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz",
@@ -4298,19 +4405,44 @@
"node": ">=16.17.0"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"bin": {
"husky": "bin.js"
"node_modules/i18next": {
"version": "25.1.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.1.2.tgz",
"integrity": "sha512-SP63m8LzdjkrAjruH7SCI3ndPSgjt4/wX7ouUUOzCW/eY+HzlIo19IQSfYA9X3qRiRP1SYtaTsg/Oz/PGsfD8w==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
"engines": {
"node": ">=18"
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
@@ -5962,6 +6094,32 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-i18next": {
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6998,7 +7156,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7113,6 +7271,15 @@
"uuidjs": "dist/cli.js"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -16,9 +16,12 @@
]
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
@@ -30,6 +33,8 @@
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.507.0",
"next": "15.2.4",
@@ -38,6 +43,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",

View File

@@ -18,6 +18,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import {
Dialog,
@@ -46,11 +47,15 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { i18nObj } from '@/i18n/I18nProvider';
const formSchema = z.object({
name: z.string().min(1, { message: '机器人名称不能为空' }),
description: z.string().min(1, { message: '机器人描述不能为空' }),
adapter: z.string().min(1, { message: '适配器不能为空' }),
const getFormSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, { message: t('bots.botNameRequired') }),
description: z
.string()
.min(1, { message: t('bots.botDescriptionRequired') }),
adapter: z.string().min(1, { message: t('bots.adapterRequired') }),
adapter_config: z.record(z.string(), z.any()),
enable: z.boolean().optional(),
use_pipeline_uuid: z.string().optional(),
@@ -64,16 +69,19 @@ export default function BotForm({
onNewBotCreated,
}: {
initBotId?: string;
onFormSubmit: (value: z.infer<typeof formSchema>) => void;
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
onFormCancel: () => void;
onBotDeleted: () => void;
onNewBotCreated: (botId: string) => void;
}) {
const { t } = useTranslation();
const formSchema = getFormSchema(t);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
description: '一个机器人',
description: t('bots.defaultDescription'),
adapter: '',
adapter_config: {},
enable: true,
@@ -129,7 +137,7 @@ export default function BotForm({
// dynamicForm.setFieldsValue(val.adapter_config);
})
.catch((err) => {
toast.error('获取机器人配置失败:' + err.message);
toast.error(t('bots.getBotConfigError') + err.message);
});
} else {
form.reset();
@@ -156,7 +164,7 @@ export default function BotForm({
setAdapterNameList(
adaptersRes.adapters.map((item) => {
return {
label: item.label.zh_CN,
label: i18nObj(item.label),
value: item.name,
};
}),
@@ -177,7 +185,7 @@ export default function BotForm({
setAdapterDescriptionList(
adaptersRes.adapters.reduce(
(acc, item) => {
acc[item.name] = item.description.zh_CN;
acc[item.name] = i18nObj(item.description);
return acc;
},
{} as Record<string, string>,
@@ -266,10 +274,10 @@ export default function BotForm({
.then((res) => {
console.log('update bot success', res);
onFormSubmit(form.getValues());
toast.success('保存成功');
toast.success(t('bots.saveSuccess'));
})
.catch((err) => {
toast.error('保存失败:' + err.message);
toast.error(t('bots.saveError') + err.message);
})
.finally(() => {
setIsLoading(false);
@@ -289,7 +297,7 @@ export default function BotForm({
.createBot(newBot)
.then((res) => {
console.log('create bot success', res);
toast.success('创建成功 请启用或修改绑定流水线');
toast.success(t('bots.createSuccess'));
initBotId = res.uuid;
setBotFormValues();
@@ -297,7 +305,7 @@ export default function BotForm({
onNewBotCreated(res.uuid);
})
.catch((err) => {
toast.error('创建失败:' + err.message);
toast.error(t('bots.createError') + err.message);
})
.finally(() => {
setIsLoading(false);
@@ -315,10 +323,10 @@ export default function BotForm({
.deleteBot(initBotId)
.then(() => {
onBotDeleted();
toast.success('删除成功');
toast.success(t('bots.deleteSuccess'));
})
.catch((err) => {
toast.error('删除失败:' + err.message);
toast.error(t('bots.deleteError') + err.message);
});
}
}
@@ -331,9 +339,9 @@ export default function BotForm({
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
</DialogHeader>
<DialogDescription></DialogDescription>
<DialogDescription>{t('bots.deleteConfirmation')}</DialogDescription>
<DialogFooter>
<Button
variant="outline"
@@ -348,7 +356,7 @@ export default function BotForm({
setShowDeleteConfirmModal(false);
}}
>
{t('common.confirmDelete')}
</Button>
</DialogFooter>
</DialogContent>
@@ -368,7 +376,7 @@ export default function BotForm({
name="enable"
render={({ field }) => (
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
<FormLabel></FormLabel>
<FormLabel>{t('common.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
@@ -384,11 +392,13 @@ export default function BotForm({
name="use_pipeline_uuid"
render={({ field }) => (
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
<FormLabel>线</FormLabel>
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} {...field}>
<SelectTrigger>
<SelectValue placeholder="选择流水线" />
<SelectValue
placeholder={t('bots.selectPipeline')}
/>
</SelectTrigger>
<SelectContent className="fixed z-[1000]">
<SelectGroup>
@@ -413,7 +423,8 @@ export default function BotForm({
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-red-500">*</span>
{t('bots.botName')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -428,7 +439,8 @@ export default function BotForm({
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-red-500">*</span>
{t('bots.botDescription')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -444,7 +456,8 @@ export default function BotForm({
render={({ field }) => (
<FormItem>
<FormLabel>
/<span className="text-red-500">*</span>
{t('bots.platformAdapter')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<div className="relative">
@@ -456,7 +469,7 @@ export default function BotForm({
value={field.value}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择适配器" />
<SelectValue placeholder={t('bots.selectAdapter')} />
</SelectTrigger>
<SelectContent className="fixed z-[1000]">
<SelectGroup>
@@ -499,7 +512,9 @@ export default function BotForm({
{showDynamicForm && dynamicFormConfigList.length > 0 && (
<div className="space-y-4">
<div className="text-lg font-medium"></div>
<div className="text-lg font-medium">
{t('bots.adapterConfig')}
</div>
<DynamicFormComponent
itemConfigList={dynamicFormConfigList}
initialValues={form.watch('adapter_config')}
@@ -518,7 +533,7 @@ export default function BotForm({
type="submit"
onClick={form.handleSubmit(onDynamicFormSubmit)}
>
{t('common.submit')}
</Button>
)}
{initBotId && (
@@ -528,13 +543,13 @@ export default function BotForm({
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
<Button
type="button"
onClick={form.handleSubmit(onDynamicFormSubmit)}
>
{t('common.save')}
</Button>
</>
)}
@@ -543,7 +558,7 @@ export default function BotForm({
variant="outline"
onClick={() => onFormCancel()}
>
{t('common.cancel')}
</Button>
</div>
</div>

View File

@@ -15,7 +15,11 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { i18nObj } from '@/i18n/I18nProvider';
export default function BotConfigPage() {
const { t } = useTranslation();
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [botList, setBotList] = useState<BotCardVO[]>([]);
const [isEditForm, setIsEditForm] = useState(false);
@@ -29,7 +33,7 @@ export default function BotConfigPage() {
const adapterListResp = await httpClient.getAdapters();
const adapterList = adapterListResp.adapters.map((adapter: Adapter) => {
return {
label: adapter.label.zh_CN,
label: i18nObj(adapter.label),
value: adapter.name,
};
});
@@ -53,7 +57,7 @@ export default function BotConfigPage() {
})
.catch((err) => {
console.error('get bot list error', err);
toast.error('获取机器人列表失败:' + err.message);
toast.error(t('bots.getBotListError') + err.message);
})
.finally(() => {
// setIsLoading(false);
@@ -78,7 +82,7 @@ export default function BotConfigPage() {
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
<DialogHeader className="px-6 pt-6 pb-4">
<DialogTitle>
{isEditForm ? '编辑机器人' : '创建机器人'}
{isEditForm ? t('bots.editBot') : t('bots.createBot')}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6">

View File

@@ -12,6 +12,7 @@ import {
} from '@/components/ui/form';
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
import { useEffect } from 'react';
import { i18nObj } from '@/i18n/I18nProvider';
export default function DynamicFormComponent({
itemConfigList,
@@ -141,7 +142,7 @@ export default function DynamicFormComponent({
render={({ field }) => (
<FormItem>
<FormLabel>
{config.label.zh_CN}{' '}
{i18nObj(config.label)}{' '}
{config.required && <span className="text-red-500">*</span>}
</FormLabel>
<FormControl>
@@ -149,7 +150,7 @@ export default function DynamicFormComponent({
</FormControl>
{config.description && (
<p className="text-sm text-muted-foreground">
{config.description.zh_CN}
{i18nObj(config.description)}
</p>
)}
<FormMessage />

View File

@@ -18,6 +18,13 @@ import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LLMModel } from '@/app/infra/entities/api';
import { toast } from 'sonner';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { useTranslation } from 'react-i18next';
import { i18nObj } from '@/i18n/I18nProvider';
export default function DynamicFormItemComponent({
config,
@@ -28,6 +35,7 @@ export default function DynamicFormItemComponent({
field: ControllerRenderProps<any, any>;
}) {
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
const { t } = useTranslation();
useEffect(() => {
if (config.type === DynamicFormItemType.LLM_MODEL_SELECTOR) {
@@ -101,7 +109,7 @@ export default function DynamicFormItemComponent({
field.onChange([...field.value, '']);
}}
>
{t('common.add')}
</Button>
</div>
);
@@ -110,13 +118,13 @@ export default function DynamicFormItemComponent({
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="请选择" />
<SelectValue placeholder={t('common.select')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{config.options?.map((option) => (
<SelectItem key={option.name} value={option.name}>
{option.label.zh_CN}
{i18nObj(option.label)}
</SelectItem>
))}
</SelectGroup>
@@ -128,14 +136,113 @@ export default function DynamicFormItemComponent({
return (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="请选择模型" />
<SelectValue placeholder={t('models.selectModel')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{llmModels.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
{model.name}
</SelectItem>
<HoverCard key={model.uuid} openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<SelectItem value={model.uuid}>{model.name}</SelectItem>
</HoverCardTrigger>
<HoverCardContent
className="w-80 data-[state=open]:animate-none data-[state=closed]:animate-none"
align="end"
side="right"
sideOffset={10}
>
<div className="space-y-2">
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
model.requester,
)}
alt="icon"
className="w-8 h-8 rounded-full"
/>
<h4 className="font-medium">{model.name}</h4>
</div>
<p className="text-sm text-muted-foreground">
{model.description}
</p>
{model.requester_config && (
<div className="flex items-center gap-1 text-xs">
<svg
className="w-4 h-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13.0607 8.11097L14.4749 9.52518C17.2086 12.2589 17.2086 16.691 14.4749 19.4247L14.1214 19.7782C11.3877 22.5119 6.95555 22.5119 4.22188 19.7782C1.48821 17.0446 1.48821 12.6124 4.22188 9.87874L5.6361 11.293C3.68348 13.2456 3.68348 16.4114 5.6361 18.364C7.58872 20.3166 10.7545 20.3166 12.7072 18.364L13.0607 18.0105C15.0133 16.0578 15.0133 12.892 13.0607 10.9394L11.6465 9.52518L13.0607 8.11097ZM19.7782 14.1214L18.364 12.7072C20.3166 10.7545 20.3166 7.58872 18.364 5.6361C16.4114 3.68348 13.2456 3.68348 11.293 5.6361L10.9394 5.98965C8.98678 7.94227 8.98678 11.1081 10.9394 13.0607L12.3536 14.4749L10.9394 15.8891L9.52518 14.4749C6.79151 11.7413 6.79151 7.30911 9.52518 4.57544L9.87874 4.22188C12.6124 1.48821 17.0446 1.48821 19.7782 4.22188C22.5119 6.95555 22.5119 11.3877 19.7782 14.1214Z"></path>
</svg>
<span className="font-semibold">Base URL</span>
{model.requester_config.base_url}
</div>
)}
{model.abilities && model.abilities.length > 0 && (
<div className="flex flex-wrap gap-1">
{model.abilities.map((ability) => (
<div
key={ability}
className="flex items-center gap-1 px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-600"
>
{ability === 'vision' && (
<svg
className="w-3 h-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 11.4872 7.07719 10.9925 7.22057 10.5268C7.61175 11.3954 8.48527 12 9.5 12C10.8807 12 12 10.8807 12 9.5C12 8.48527 11.3954 7.61175 10.5269 7.21995C10.9925 7.07719 11.4872 7 12 7Z"></path>
</svg>
)}
{ability === 'func_call' && (
<svg
className="w-3 h-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
</svg>
)}
<span>
{ability === 'vision'
? t('models.visionAbility')
: ability === 'func_call'
? t('models.functionCallAbility')
: ability}
</span>
</div>
))}
</div>
)}
{model.extra_args &&
Object.keys(model.extra_args).length > 0 && (
<div className="text-xs">
<div className="font-semibold mb-1">
{t('models.extraParameters')}
</div>
<div className="space-y-1">
{Object.entries(
model.extra_args as Record<string, unknown>,
).map(([key, value]) => (
<div
key={key}
className="flex items-center gap-1"
>
<span className="text-gray-500">{key}</span>
<span className="break-all">
{JSON.stringify(value)}
</span>
</div>
))}
</div>
</div>
)}
</div>
</HoverCardContent>
</HoverCard>
))}
</SelectGroup>
</SelectContent>
@@ -219,7 +326,7 @@ export default function DynamicFormItemComponent({
field.onChange([...field.value, { role: 'user', content: '' }]);
}}
>
{t('common.addRound')}
</Button>
</div>
);

View File

@@ -9,7 +9,7 @@ export const testDynamicConfigList: IDynamicFormItemSchema[] = [
default: '',
id: '111',
label: {
zh_CN: '测试字段string',
zh_Hans: '测试字段string',
en_US: 'eng test',
},
name: 'string_test',
@@ -20,7 +20,7 @@ export const testDynamicConfigList: IDynamicFormItemSchema[] = [
default: '',
id: '222',
label: {
zh_CN: '测试字段int',
zh_Hans: '测试字段int',
en_US: 'int eng test',
},
name: 'int_test',
@@ -31,7 +31,7 @@ export const testDynamicConfigList: IDynamicFormItemSchema[] = [
default: '',
id: '333',
label: {
zh_CN: '测试字段boolean',
zh_Hans: '测试字段boolean',
en_US: 'boolean eng test',
},
name: 'boolean_test',

View File

@@ -8,6 +8,7 @@
align-items: flex-start;
justify-content: space-between;
padding-block: 1rem;
padding-left: 0.4rem;
user-select: none;
/* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */
}
@@ -54,13 +55,18 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
gap: 0.8rem;
}
.sidebarItemsContainer {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.sidebarChildContainer {
width: 9rem;
height: 3rem;
margin: 0.8rem 0;
padding-left: 1.6rem;
font-size: 1rem;
border-radius: 12px;
@@ -70,6 +76,7 @@
justify-content: flex-start;
cursor: pointer;
gap: 0.5rem;
/* background-color: aqua; */
}
.sidebarSelected {
@@ -90,11 +97,12 @@
.sidebarBottomContainer {
width: 100%;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: auto;
padding-bottom: 1rem;
}
.sidebarBottomChildContainer {

View File

@@ -9,7 +9,8 @@ import {
import { useRouter, usePathname } from 'next/navigation';
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { httpClient } from '@/app/infra/http/HttpClient';
import { systemInfo } from '@/app/infra/http/HttpClient';
import { useTranslation } from 'react-i18next';
// TODO 侧边导航栏要加动画
export default function HomeSidebar({
@@ -27,10 +28,15 @@ export default function HomeSidebar({
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
const { t } = useTranslation();
useEffect(() => {
console.log('HomeSidebar挂载完成');
initSelect();
return () => console.log('HomeSidebar卸载');
if (!localStorage.getItem('token')) {
localStorage.setItem('token', 'test-token');
localStorage.setItem('userEmail', 'test@example.com');
}
return () => console.log('sidebar.unmounted');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -77,6 +83,12 @@ export default function HomeSidebar({
}
}
function handleLogout() {
localStorage.removeItem('token');
localStorage.removeItem('userEmail');
window.location.href = '/login';
}
return (
<div className={`${styles.sidebarContainer}`}>
<div className={`${styles.sidebarTopContainer}`}>
@@ -92,12 +104,12 @@ export default function HomeSidebar({
<div className={`${styles.langbotTextContainer}`}>
<div className={`${styles.langbotText}`}>LangBot</div>
<div className={`${styles.langbotVersion}`}>
{httpClient.systemInfo?.version}
{systemInfo?.version}
</div>
</div>
</div>
{/* 菜单列表,后期可升级成配置驱动 */}
<div>
<div className={styles.sidebarItemsContainer}>
{sidebarConfigList.map((config) => {
return (
<div
@@ -123,20 +135,6 @@ export default function HomeSidebar({
</div>
<div className={`${styles.sidebarBottomContainer}`}>
{/* <SidebarChild
onClick={() => {}}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 11.9998C2 11.1353 2.1097 10.2964 2.31595 9.49631C3.40622 9.55283 4.48848 9.01015 5.0718 7.99982C5.65467 6.99025 5.58406 5.78271 4.99121 4.86701C6.18354 3.69529 7.66832 2.82022 9.32603 2.36133C9.8222 3.33385 10.8333 3.99982 12 3.99982C13.1667 3.99982 14.1778 3.33385 14.674 2.36133C16.3317 2.82022 17.8165 3.69529 19.0088 4.86701C18.4159 5.78271 18.3453 6.99025 18.9282 7.99982C19.5115 9.01015 20.5938 9.55283 21.6841 9.49631C21.8903 10.2964 22 11.1353 22 11.9998C22 12.8643 21.8903 13.7032 21.6841 14.5033C20.5938 14.4468 19.5115 14.9895 18.9282 15.9998C18.3453 17.0094 18.4159 18.2169 19.0088 19.1326C17.8165 20.3043 16.3317 21.1794 14.674 21.6383C14.1778 20.6658 13.1667 19.9998 12 19.9998C10.8333 19.9998 9.8222 20.6658 9.32603 21.6383C7.66832 21.1794 6.18354 20.3043 4.99121 19.1326C5.58406 18.2169 5.65467 17.0094 5.0718 15.9998C4.48848 14.9895 3.40622 14.4468 2.31595 14.5033C2.1097 13.7032 2 12.8643 2 11.9998ZM6.80385 14.9998C7.43395 16.0912 7.61458 17.3459 7.36818 18.5236C7.77597 18.8138 8.21005 19.0652 8.66489 19.2741C9.56176 18.4712 10.7392 17.9998 12 17.9998C13.2608 17.9998 14.4382 18.4712 15.3351 19.2741C15.7899 19.0652 16.224 18.8138 16.6318 18.5236C16.3854 17.3459 16.566 16.0912 17.1962 14.9998C17.8262 13.9085 18.8225 13.1248 19.9655 12.7493C19.9884 12.5015 20 12.2516 20 11.9998C20 11.7481 19.9884 11.4981 19.9655 11.2504C18.8225 10.8749 17.8262 10.0912 17.1962 8.99982C16.566 7.90845 16.3854 6.65378 16.6318 5.47605C16.224 5.18588 15.7899 4.93447 15.3351 4.72552C14.4382 5.52844 13.2608 5.99982 12 5.99982C10.7392 5.99982 9.56176 5.52844 8.66489 4.72552C8.21005 4.93447 7.77597 5.18588 7.36818 5.47605C7.61458 6.65378 7.43395 7.90845 6.80385 8.99982C6.17376 10.0912 5.17754 10.8749 4.03451 11.2504C4.01157 11.4981 4 11.7481 4 11.9998C4 12.2516 4.01157 12.5015 4.03451 12.7493C5.17754 13.1248 6.17376 13.9085 6.80385 14.9998ZM12 14.9998C10.3431 14.9998 9 13.6567 9 11.9998C9 10.343 10.3431 8.99982 12 8.99982C13.6569 8.99982 15 10.343 15 11.9998C15 13.6567 13.6569 14.9998 12 14.9998ZM12 12.9998C12.5523 12.9998 13 12.5521 13 11.9998C13 11.4475 12.5523 10.9998 12 10.9998C11.4477 10.9998 11 11.4475 11 11.9998C11 12.5521 11.4477 12.9998 12 12.9998Z"></path>
</svg>
}
name="系统设置"
/> */}
<SidebarChild
onClick={() => {
// open docs.langbot.app
@@ -152,7 +150,23 @@ export default function HomeSidebar({
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z"></path>
</svg>
}
name="帮助文档"
name={t('common.helpDocs')}
/>
<SidebarChild
onClick={() => {
handleLogout();
}}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
</svg>
}
name={t('common.logout')}
/>
</div>
</div>

View File

@@ -1,10 +1,15 @@
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
import styles from './HomeSidebar.module.css';
import i18n from '@/i18n';
const t = (key: string) => {
return i18n.t(key);
};
export const sidebarConfigList = [
new SidebarChildVO({
id: 'bots',
name: '机器人',
name: t('bots.title'),
icon: (
<svg
className={`${styles.sidebarChildIcon}`}
@@ -16,12 +21,12 @@ export const sidebarConfigList = [
</svg>
),
route: '/home/bots',
description: '创建和管理机器人,这是 LangBot 与各个平台连接的入口',
description: t('bots.description'),
helpLink: 'https://docs.langbot.app/zh/deploy/platforms/readme.html',
}),
new SidebarChildVO({
id: 'models',
name: '模型配置',
name: t('models.title'),
icon: (
<svg
className={`${styles.sidebarChildIcon}`}
@@ -33,12 +38,12 @@ export const sidebarConfigList = [
</svg>
),
route: '/home/models',
description: '配置和管理可在流水线中使用的模型',
description: t('models.description'),
helpLink: 'https://docs.langbot.app/zh/deploy/models/readme.html',
}),
new SidebarChildVO({
id: 'pipelines',
name: '流水线',
name: t('pipelines.title'),
icon: (
<svg
className={`${styles.sidebarChildIcon}`}
@@ -50,12 +55,12 @@ export const sidebarConfigList = [
</svg>
),
route: '/home/pipelines',
description: '流水线定义了对消息事件的处理流程,用于绑定到机器人',
description: t('pipelines.description'),
helpLink: 'https://docs.langbot.app/zh/deploy/pipelines/readme.html',
}),
new SidebarChildVO({
id: 'plugins',
name: '插件管理',
name: t('plugins.title'),
icon: (
<svg
className={`${styles.sidebarChildIcon}`}
@@ -67,7 +72,7 @@ export const sidebarConfigList = [
</svg>
),
route: '/home/plugins',
description: '安装和配置用于扩展 LangBot 功能的插件',
description: t('plugins.description'),
helpLink: 'https://docs.langbot.app/zh/plugin/plugin-intro.html',
}),
];

View File

@@ -1,7 +1,9 @@
import styles from './LLMCard.module.css';
import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
import { useTranslation } from 'react-i18next';
function checkAbilityBadges(abilities: string[]) {
function AbilityBadges(abilities: string[]) {
const { t } = useTranslation();
const abilityBadges = {
vision: (
<div key="vision" className={`${styles.abilityBadge}`}>
@@ -13,7 +15,9 @@ function checkAbilityBadges(abilities: string[]) {
>
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 11.4872 7.07719 10.9925 7.22057 10.5268C7.61175 11.3954 8.48527 12 9.5 12C10.8807 12 12 10.8807 12 9.5C12 8.48527 11.3954 7.61175 10.5269 7.21995C10.9925 7.07719 11.4872 7 12 7Z"></path>
</svg>
<span className={`${styles.abilityLabel}`}></span>
<span className={`${styles.abilityLabel}`}>
{t('models.visionAbility')}
</span>
</div>
),
func_call: (
@@ -26,7 +30,9 @@ function checkAbilityBadges(abilities: string[]) {
>
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
</svg>
<span className={`${styles.abilityLabel}`}></span>
<span className={`${styles.abilityLabel}`}>
{t('models.functionCallAbility')}
</span>
</div>
),
};
@@ -83,7 +89,7 @@ export default function LLMCard({ cardVO }: { cardVO: LLMCardVO }) {
</div>
{/* 能力 */}
<div className={`${styles.abilitiesContainer}`}>
{checkAbilityBadges(cardVO.abilities)}
{AbilityBadges(cardVO.abilities)}
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import { UUID } from 'uuidjs';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import {
Dialog,
@@ -38,9 +39,12 @@ import {
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
const extraArgSchema = z
import { i18nObj } from '@/i18n/I18nProvider';
const getExtraArgSchema = (t: (key: string) => string) =>
z
.object({
key: z.string().min(1, { message: '键名不能为空' }),
key: z.string().min(1, { message: t('models.keyNameRequired') }),
type: z.enum(['string', 'number', 'boolean']),
value: z.string(),
})
@@ -48,7 +52,7 @@ const extraArgSchema = z
if (data.type === 'number' && isNaN(Number(data.value))) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '必须是有效的数字',
message: t('models.mustBeValidNumber'),
path: ['value'],
});
}
@@ -59,19 +63,22 @@ const extraArgSchema = z
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '必须是 true 或 false',
message: t('models.mustBeTrueOrFalse'),
path: ['value'],
});
}
});
const formSchema = z.object({
name: z.string().min(1, { message: '模型名称不能为空' }),
model_provider: z.string().min(1, { message: '模型供应商不能为空' }),
url: z.string().min(1, { message: '请求URL不能为空' }),
api_key: z.string().min(1, { message: 'API Key不能为空' }),
const getFormSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, { message: t('models.modelNameRequired') }),
model_provider: z
.string()
.min(1, { message: t('models.modelProviderRequired') }),
url: z.string().min(1, { message: t('models.requestURLRequired') }),
api_key: z.string().min(1, { message: t('models.apiKeyRequired') }),
abilities: z.array(z.string()),
extra_args: z.array(extraArgSchema).optional(),
extra_args: z.array(getExtraArgSchema(t)).optional(),
});
export default function LLMForm({
@@ -87,6 +94,9 @@ export default function LLMForm({
onFormCancel: () => void;
onLLMDeleted: () => void;
}) {
const { t } = useTranslation();
const formSchema = getFormSchema(t);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -106,11 +116,11 @@ export default function LLMForm({
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const abilityOptions: { label: string; value: string }[] = [
{
label: '视觉能力',
label: t('models.visionAbility'),
value: 'vision',
},
{
label: '函数调用',
label: t('models.functionCallAbility'),
value: 'func_call',
},
];
@@ -122,14 +132,17 @@ export default function LLMForm({
>([]);
useEffect(() => {
initLLMModelFormComponent();
initLLMModelFormComponent().then(() => {
if (editMode && initLLMId) {
getLLMConfig(initLLMId).then((val) => {
form.setValue('name', val.name);
form.setValue('model_provider', val.model_provider);
form.setValue('url', val.url);
form.setValue('api_key', val.api_key);
form.setValue('abilities', val.abilities as ('vision' | 'func_call')[]);
form.setValue(
'abilities',
val.abilities as ('vision' | 'func_call')[],
);
// 转换extra_args为新格式
if (val.extra_args) {
const args = val.extra_args.map((arg) => {
@@ -154,6 +167,7 @@ export default function LLMForm({
form.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
});
}, []);
const addExtraArg = () => {
@@ -185,7 +199,7 @@ export default function LLMForm({
setRequesterNameList(
requesterNameList.requesters.map((item) => {
return {
label: item.label.zh_CN,
label: i18nObj(item.label),
value: item.name,
};
}),
@@ -223,7 +237,8 @@ export default function LLMForm({
function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {};
value.extra_args?.forEach((arg) => {
value.extra_args?.forEach(
(arg: { key: string; type: string; value: string }) => {
if (arg.type === 'number') {
extraArgsObj[arg.key] = Number(arg.value);
} else if (arg.type === 'boolean') {
@@ -231,7 +246,8 @@ export default function LLMForm({
} else {
extraArgsObj[arg.key] = arg.value;
}
});
},
);
const llmModel: LLMModel = {
uuid: editMode ? initLLMId || '' : UUID.generate(),
@@ -262,9 +278,9 @@ export default function LLMForm({
try {
await httpClient.createProviderLLMModel(llmModel);
onFormSubmit();
toast.success('创建成功');
toast.success(t('models.createSuccess'));
} catch (err) {
toast.error('创建失败:' + (err as Error).message);
toast.error(t('models.createError') + (err as Error).message);
}
}
@@ -272,9 +288,9 @@ export default function LLMForm({
try {
await httpClient.updateProviderLLMModel(initLLMId || '', llmModel);
onFormSubmit();
toast.success('保存成功');
toast.success(t('models.saveSuccess'));
} catch (err) {
toast.error('保存失败:' + (err as Error).message);
toast.error(t('models.saveError') + (err as Error).message);
}
}
@@ -284,10 +300,10 @@ export default function LLMForm({
.deleteProviderLLMModel(initLLMId)
.then(() => {
onLLMDeleted();
toast.success('删除成功');
toast.success(t('models.deleteSuccess'));
})
.catch((err) => {
toast.error('删除失败:' + err.message);
toast.error(t('models.deleteError') + err.message);
});
}
}
@@ -300,15 +316,17 @@ export default function LLMForm({
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
</DialogHeader>
<DialogDescription></DialogDescription>
<DialogDescription>
{t('models.deleteConfirmation')}
</DialogDescription>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDeleteConfirmModal(false)}
>
{t('common.cancel')}
</Button>
<Button
variant="destructive"
@@ -317,7 +335,7 @@ export default function LLMForm({
setShowDeleteConfirmModal(false);
}}
>
{t('common.confirmDelete')}
</Button>
</DialogFooter>
</DialogContent>
@@ -335,14 +353,15 @@ export default function LLMForm({
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-red-500">*</span>
{t('models.modelName')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{t('models.modelProviderDescription')}
</FormDescription>
</FormItem>
)}
@@ -354,7 +373,8 @@ export default function LLMForm({
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-red-500">*</span>
{t('models.modelProvider')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Select
@@ -370,7 +390,9 @@ export default function LLMForm({
value={field.value}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择模型供应商" />
<SelectValue
placeholder={t('models.selectModelProvider')}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -394,7 +416,8 @@ export default function LLMForm({
render={({ field }) => (
<FormItem>
<FormLabel>
URL<span className="text-red-500">*</span>
{t('models.requestURL')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -409,7 +432,8 @@ export default function LLMForm({
render={({ field }) => (
<FormItem>
<FormLabel>
API Key<span className="text-red-500">*</span>
{t('models.apiKey')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -423,9 +447,11 @@ export default function LLMForm({
name="abilities"
render={() => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('models.abilities')}</FormLabel>
<div className="mb-0">
<FormDescription></FormDescription>
<FormDescription>
{t('models.selectModelAbilities')}
</FormDescription>
</div>
{abilityOptions.map((item) => (
<FormField
@@ -452,7 +478,8 @@ export default function LLMForm({
])
: field.onChange(
field.value?.filter(
(value) => value !== item.value,
(value: string) =>
value !== item.value,
),
);
}}
@@ -472,12 +499,12 @@ export default function LLMForm({
/>
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('models.extraParameters')}</FormLabel>
<div className="space-y-2">
{extraArgs.map((arg, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder="键名"
placeholder={t('models.keyName')}
value={arg.key}
onChange={(e) =>
updateExtraArg(index, 'key', e.target.value)
@@ -490,16 +517,22 @@ export default function LLMForm({
}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="类型" />
<SelectValue placeholder={t('models.type')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="string">
{t('models.string')}
</SelectItem>
<SelectItem value="number">
{t('models.number')}
</SelectItem>
<SelectItem value="boolean">
{t('models.boolean')}
</SelectItem>
</SelectContent>
</Select>
<Input
placeholder="值"
placeholder={t('models.value')}
value={arg.value}
onChange={(e) =>
updateExtraArg(index, 'value', e.target.value)
@@ -522,11 +555,11 @@ export default function LLMForm({
</div>
))}
<Button type="button" variant="outline" onClick={addExtraArg}>
{t('models.addParameter')}
</Button>
</div>
<FormDescription>
max_tokens, temperature, top_p
{t('models.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -538,18 +571,20 @@ export default function LLMForm({
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
)}
<Button type="submit">{editMode ? '保存' : '提交'}</Button>
<Button type="submit">
{editMode ? t('common.save') : t('common.submit')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => onFormCancel()}
>
{t('common.cancel')}
</Button>
</DialogFooter>
</form>

View File

@@ -15,8 +15,11 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { i18nObj } from '@/i18n/I18nProvider';
export default function LLMConfigPage() {
const { t } = useTranslation();
const [cardList, setCardList] = useState<LLMCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [isEditForm, setIsEditForm] = useState(false);
@@ -30,7 +33,7 @@ export default function LLMConfigPage() {
const requesterNameListResp = await httpClient.getProviderRequesters();
const requesterNameList = requesterNameListResp.requesters.map((item) => {
return {
label: item.label.zh_CN,
label: i18nObj(item.label),
value: item.name,
};
});
@@ -56,7 +59,7 @@ export default function LLMConfigPage() {
})
.catch((err) => {
console.error('get LLM model list error', err);
toast.error('获取模型列表失败:' + err.message);
toast.error(t('models.getModelListError') + err.message);
});
}
@@ -77,7 +80,9 @@ export default function LLMConfigPage() {
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="w-[700px] p-6">
<DialogHeader>
<DialogTitle>{isEditForm ? '预览模型' : '创建模型'}</DialogTitle>
<DialogTitle>
{isEditForm ? t('models.editModel') : t('models.createModel')}
</DialogTitle>
</DialogHeader>
<LLMForm
editMode={isEditForm}

Some files were not shown because too many files have changed in this diff Show More