mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 19:37:36 +08:00
Compare commits
38 Commits
v4.0.0-bet
...
v4.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
696162ee52 | ||
|
|
533f993e3a | ||
|
|
738b0af5fb | ||
|
|
5d9bac5e7b | ||
|
|
f376c9703a | ||
|
|
20a62fcf69 | ||
|
|
248d4beed1 | ||
|
|
0e52aff363 | ||
|
|
4ed854d7b8 | ||
|
|
c6ff33c6ab | ||
|
|
6c10cb7dca | ||
|
|
130495f519 | ||
|
|
219d328342 | ||
|
|
c835555a59 | ||
|
|
6652b57a0d | ||
|
|
bf51afedf6 | ||
|
|
39f9400de7 | ||
|
|
ac1d39580b | ||
|
|
9362b34858 | ||
|
|
c6f6c715bd | ||
|
|
6a8106d9ac | ||
|
|
5abbcb62a2 | ||
|
|
2bf94539bd | ||
|
|
91cd8cf380 | ||
|
|
c3de3fa275 | ||
|
|
039752419b | ||
|
|
18c708da58 | ||
|
|
8c08b8ee8a | ||
|
|
015be6008d | ||
|
|
da86384e58 | ||
|
|
86ff6f5eb6 | ||
|
|
ae6979151f | ||
|
|
fd1b5d494e | ||
|
|
cd68760c75 | ||
|
|
13d36412dd | ||
|
|
f2e1ae432c | ||
|
|
0f30f1dcbd | ||
|
|
d070737ef7 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -16,5 +16,5 @@
|
||||
|
||||
- [ ] 相关 issues 链接了吗?
|
||||
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗?
|
||||
- [ ] 依赖写到 requirements.txt 和 core/bootutils/deps.py 了吗
|
||||
- [ ] 依赖加到 pyproject.toml 和 core/bootutils/deps.py 了吗
|
||||
- [ ] 文档编写了吗?
|
||||
@@ -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:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,4 +40,6 @@ botpy.log*
|
||||
/libs/wecom_api/test.py
|
||||
/venv
|
||||
test.py
|
||||
/web_ui
|
||||
/web_ui
|
||||
.venv/
|
||||
uv.lock
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
42
.ruff.toml
42
.ruff.toml
@@ -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"
|
||||
@@ -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" ]
|
||||
46
README.md
46
README.md
@@ -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/>
|
||||
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||

|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](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/) | ✅ | 本地大模型运行平台 |
|
||||
|
||||
49
README_EN.md
49
README_EN.md
@@ -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 @@
|
||||
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||
)
|
||||
<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.
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
[](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) |
|
||||
|
||||
49
README_JP.md
49
README_JP.md
@@ -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/>
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||
)
|
||||
<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テンプレート。
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
[](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実行プラットフォーム |
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: builtin-components
|
||||
label:
|
||||
en_US: Builtin Components
|
||||
zh_CN: 内置组件
|
||||
zh_Hans: 内置组件
|
||||
spec:
|
||||
components:
|
||||
ComponentTemplate:
|
||||
|
||||
@@ -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
201
libs/wechatpad_api/LICENSE
Normal 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.
|
||||
38
libs/wechatpad_api/README.md
Normal file
38
libs/wechatpad_api/README.md
Normal 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. 撤回消息
|
||||
1
libs/wechatpad_api/__init__.py
Normal file
1
libs/wechatpad_api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .client import WeChatPadClient
|
||||
0
libs/wechatpad_api/api/__init__.py
Normal file
0
libs/wechatpad_api/api/__init__.py
Normal file
14
libs/wechatpad_api/api/chatroom.py
Normal file
14
libs/wechatpad_api/api/chatroom.py
Normal 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)
|
||||
39
libs/wechatpad_api/api/downloadpai.py
Normal file
39
libs/wechatpad_api/api/downloadpai.py
Normal 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('获取文件失败')
|
||||
11
libs/wechatpad_api/api/friend.py
Normal file
11
libs/wechatpad_api/api/friend.py
Normal 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
|
||||
|
||||
102
libs/wechatpad_api/api/login.py
Normal file
102
libs/wechatpad_api/api/login.py
Normal 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]
|
||||
|
||||
|
||||
|
||||
123
libs/wechatpad_api/api/message.py
Normal file
123
libs/wechatpad_api/api/message.py
Normal 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)
|
||||
37
libs/wechatpad_api/api/user.py
Normal file
37
libs/wechatpad_api/api/user.py
Normal 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)
|
||||
97
libs/wechatpad_api/client.py
Normal file
97
libs/wechatpad_api/client.py
Normal 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)
|
||||
|
||||
|
||||
0
libs/wechatpad_api/util/__init__.py
Normal file
0
libs/wechatpad_api/util/__init__.py
Normal file
92
libs/wechatpad_api/util/http_util.py
Normal file
92
libs/wechatpad_api/util/http_util.py
Normal 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)
|
||||
|
||||
31
libs/wechatpad_api/util/terminal_printer.py
Normal file
31
libs/wechatpad_api/util/terminal_printer.py
Normal 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}")
|
||||
|
||||
@@ -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}')
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,36 +30,48 @@ 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
|
||||
|
||||
query.use_funcs = (
|
||||
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None
|
||||
)
|
||||
if selected_runner == 'local-agent':
|
||||
query.use_funcs = (
|
||||
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: MessagePlatformAdapter
|
||||
label:
|
||||
en_US: Message Platform Adapter
|
||||
zh_CN: 消息平台适配器模板类
|
||||
zh_Hans: 消息平台适配器模板类
|
||||
spec:
|
||||
type:
|
||||
- python
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
773
pkg/platform/sources/wechatpad.py
Normal file
773
pkg/platform/sources/wechatpad.py
Normal 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
|
||||
51
pkg/platform/sources/wechatpad.yaml
Normal file
51
pkg/platform/sources/wechatpad.yaml
Normal 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
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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='',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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获取模型"""
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: LLMAPIRequester
|
||||
label:
|
||||
en_US: LLM API Requester
|
||||
zh_CN: LLM API 请求器
|
||||
zh_Hans: LLM API 请求器
|
||||
spec:
|
||||
type:
|
||||
- python
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('接口返回为空,请确定模型提供商服务是否正常')
|
||||
|
||||
@@ -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
|
||||
|
||||
1
pkg/provider/modelmgr/requesters/gemini.svg
Normal file
1
pkg/provider/modelmgr/requesters/gemini.svg
Normal 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 |
87
pkg/provider/modelmgr/requesters/geminichatcmpl.py
Normal file
87
pkg/provider/modelmgr/requesters/geminichatcmpl.py
Normal 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)}')
|
||||
28
pkg/provider/modelmgr/requesters/geminichatcmpl.yaml
Normal file
28
pkg/provider/modelmgr/requesters/geminichatcmpl.yaml
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
pkg/provider/modelmgr/requesters/modelscope.svg
Normal file
1
pkg/provider/modelmgr/requesters/modelscope.svg
Normal 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 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
10
pkg/provider/modelmgr/requesters/openrouter.svg
Normal file
10
pkg/provider/modelmgr/requesters/openrouter.svg
Normal 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 |
17
pkg/provider/modelmgr/requesters/openrouterchatcmpl.py
Normal file
17
pkg/provider/modelmgr/requesters/openrouterchatcmpl.py
Normal 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,
|
||||
}
|
||||
28
pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml
Normal file
28
pkg/provider/modelmgr/requesters/openrouterchatcmpl.yaml
Normal 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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
semantic_version = 'v4.0.0'
|
||||
semantic_version = 'v4.0.3'
|
||||
|
||||
required_database_version = 1
|
||||
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
||||
|
||||
@@ -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
183
pyproject.toml
Normal 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"
|
||||
@@ -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
|
||||
@@ -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: '参考资料来自:'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 等待
|
||||
@@ -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
193
web/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,15 +47,19 @@ 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: '适配器不能为空' }),
|
||||
adapter_config: z.record(z.string(), z.any()),
|
||||
enable: z.boolean().optional(),
|
||||
use_pipeline_uuid: z.string().optional(),
|
||||
});
|
||||
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(),
|
||||
});
|
||||
|
||||
export default function BotForm({
|
||||
initBotId,
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,41 +39,47 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { toast } from 'sonner';
|
||||
const extraArgSchema = z
|
||||
.object({
|
||||
key: z.string().min(1, { message: '键名不能为空' }),
|
||||
type: z.enum(['string', 'number', 'boolean']),
|
||||
value: z.string(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'number' && isNaN(Number(data.value))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '必须是有效的数字',
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.type === 'boolean' &&
|
||||
data.value !== 'true' &&
|
||||
data.value !== 'false'
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '必须是 true 或 false',
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
});
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
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不能为空' }),
|
||||
abilities: z.array(z.string()),
|
||||
extra_args: z.array(extraArgSchema).optional(),
|
||||
});
|
||||
const getExtraArgSchema = (t: (key: string) => string) =>
|
||||
z
|
||||
.object({
|
||||
key: z.string().min(1, { message: t('models.keyNameRequired') }),
|
||||
type: z.enum(['string', 'number', 'boolean']),
|
||||
value: z.string(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'number' && isNaN(Number(data.value))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('models.mustBeValidNumber'),
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.type === 'boolean' &&
|
||||
data.value !== 'true' &&
|
||||
data.value !== 'false'
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('models.mustBeTrueOrFalse'),
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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(getExtraArgSchema(t)).optional(),
|
||||
});
|
||||
|
||||
export default function LLMForm({
|
||||
editMode,
|
||||
@@ -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,38 +132,42 @@ export default function LLMForm({
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
initLLMModelFormComponent();
|
||||
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')[]);
|
||||
// 转换extra_args为新格式
|
||||
if (val.extra_args) {
|
||||
const args = val.extra_args.map((arg) => {
|
||||
const [key, value] = arg.split(':');
|
||||
let type: 'string' | 'number' | 'boolean' = 'string';
|
||||
if (!isNaN(Number(value))) {
|
||||
type = 'number';
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
type = 'boolean';
|
||||
}
|
||||
return {
|
||||
key,
|
||||
type,
|
||||
value,
|
||||
};
|
||||
});
|
||||
setExtraArgs(args);
|
||||
form.setValue('extra_args', args);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
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')[],
|
||||
);
|
||||
// 转换extra_args为新格式
|
||||
if (val.extra_args) {
|
||||
const args = val.extra_args.map((arg) => {
|
||||
const [key, value] = arg.split(':');
|
||||
let type: 'string' | 'number' | 'boolean' = 'string';
|
||||
if (!isNaN(Number(value))) {
|
||||
type = 'number';
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
type = 'boolean';
|
||||
}
|
||||
return {
|
||||
key,
|
||||
type,
|
||||
value,
|
||||
};
|
||||
});
|
||||
setExtraArgs(args);
|
||||
form.setValue('extra_args', args);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
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,15 +237,17 @@ export default function LLMForm({
|
||||
|
||||
function handleFormSubmit(value: z.infer<typeof formSchema>) {
|
||||
const extraArgsObj: Record<string, string | number | boolean> = {};
|
||||
value.extra_args?.forEach((arg) => {
|
||||
if (arg.type === 'number') {
|
||||
extraArgsObj[arg.key] = Number(arg.value);
|
||||
} else if (arg.type === 'boolean') {
|
||||
extraArgsObj[arg.key] = arg.value === 'true';
|
||||
} else {
|
||||
extraArgsObj[arg.key] = arg.value;
|
||||
}
|
||||
});
|
||||
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') {
|
||||
extraArgsObj[arg.key] = arg.value === 'true';
|
||||
} 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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user