Compare commits

..

190 Commits

Author SHA1 Message Date
Junyan Qin (Chin)
13e29a9966 chore: release v3.4.13.1 (#1299) 2025-04-14 20:19:18 +08:00
Guanchao Wang
601b0a8964 fix(moonshot): tool_call_id not found error (#1040) (#1298) 2025-04-14 20:17:11 +08:00
Guanchao Wang
7c2ceb0aca fix: add reasoning content for deepseek-reasoner (#1296) 2025-04-14 15:05:53 +08:00
Guanchao Wang
42fabd5133 fix: delete print function in lark (#1295) 2025-04-14 14:37:34 +08:00
Guanchao Wang
210a8856e2 fix: telegram markdown & supergroup bugs (#1293) 2025-04-13 18:48:38 +08:00
Guanchao Wang
c531cb11af fix: bailian api streaming mode can't be established 2025-04-13 17:47:05 +08:00
Junyan Qin (Chin)
07e073f526 chore: perf issue template (#1289) 2025-04-11 17:52:04 +08:00
Junyan Qin (Chin)
c5457374a8 chore: release v3.4.13 (#1284) 2025-04-09 21:58:23 +08:00
Junyan Qin (Chin)
5198349591 Merge pull request #1275 from yrk111222/master
Add ModelScope Support
2025-04-03 21:00:03 +08:00
Junyan Qin
8a4967525a fix(modelscope): bad base-url in migration 2025-04-03 20:52:01 +08:00
Junyan Qin
30b068c6e2 doc: reorder modelscope in README 2025-04-03 20:44:41 +08:00
Junyan Qin
ea3fff59ac chore: remove verbose models from llm-models.json 2025-04-03 20:40:36 +08:00
yrk
b09ce8296f Add ModelScope Support 2025-04-03 16:55:14 +08:00
Junyan Qin (Chin)
f9d07779a9 fix: slack is incorrectly enabled as default (#1274) 2025-04-03 14:17:21 +08:00
Junyan Qin (Chin)
51634c1caf chore: release v3.4.12.1 (#1271) 2025-04-02 15:23:38 +08:00
Guanchao Wang
0e00da6617 Merge pull request #1270 from RockChinQ/fix/telegram-markdown
fix: markdown and image problems in tg
2025-04-02 12:33:15 +08:00
Junyan Qin (Chin)
5ee6baeaaa Merge pull request #1268 from RockChinQ/version/3.4.12
chore: release v3.4.12
2025-04-01 21:15:46 +08:00
Junyan Qin
f11a036c60 chore: release v3.4.12 2025-04-01 21:13:41 +08:00
Junyan Qin (Chin)
0ac02ff4ce Merge pull request #1267 from RockChinQ/chore/default-prompt
chore: provide default prompt
2025-04-01 20:43:33 +08:00
Junyan Qin
99cc50b5cb chore: provide default prompt 2025-04-01 20:42:23 +08:00
Junyan Qin (Chin)
1d8fb02989 Merge pull request #1218 from fdc310/master
新增了微信发送小程序、转发小程序,发送emoji表情以及发送链接
2025-04-01 20:38:32 +08:00
Junyan Qin
122cb1188c style: standardized component names 2025-04-01 20:37:39 +08:00
Junyan Qin (Chin)
ca36ade288 Merge pull request #1266 from RockChinQ/chore/slack-schema
chore: add slack config schema
2025-04-01 20:04:08 +08:00
Junyan Qin
0877046db7 chore: add slack config schema 2025-04-01 20:03:42 +08:00
Junyan Qin (Chin)
ce9615a00e Merge pull request #1265 from RockChinQ/feat/markdowncard
add support for markdown card in dingtalk & tg
2025-04-01 20:01:44 +08:00
Junyan Qin
dbe5a41395 chore: schema for markdown config 2025-04-01 20:01:20 +08:00
Junyan Qin
4a4ca54c6e feat: migration for markdown config 2025-04-01 19:59:45 +08:00
wangcham
47acb63feb add support for markdown card in dingtalk & tg 2025-04-01 07:11:48 -04:00
Junyan Qin (Chin)
038c5d41e2 Merge pull request #1258 from RockChinQ/feat/slack
feat: add slack adapter
2025-04-01 15:33:22 +08:00
Junyan Qin
011a795895 doc(README): add slack 2025-04-01 15:32:48 +08:00
wangcham
873a0339d8 feat: add support for sending active message in slack 2025-04-01 03:03:48 -04:00
wangcham
715da548c8 fix: put the link and content together 2025-04-01 02:37:25 -04:00
Junyan Qin (Chin)
5378c6ba35 chore: provides TZ=Asia/Shanghai in docker-compose.yaml as default (#1259) 2025-03-31 14:00:08 +08:00
Guanchao Wang
8799f86ea4 Update pkg/platform/sources/slack.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-31 13:48:37 +08:00
wangcham
686be4acbc fix: eliminate host config 2025-03-31 01:10:45 -04:00
wangcham
5744eca37a fix: bot user id in slack 2025-03-30 23:06:03 -04:00
wangcham
70f8ddb1ba fix: delete useless image function in slack 2025-03-30 22:56:51 -04:00
wangcham
be1328cee9 feat: add support for slack 2025-03-30 22:24:53 -04:00
wangcham
c0dbf6fd13 feat:add support for slack 2025-03-30 12:53:48 -04:00
Junyan Qin (Chin)
ffe9c3e0f8 chore: release v3.4.11.2 (#1257) 2025-03-31 00:02:54 +08:00
Junyan Qin (Chin)
e20b79b0ed perf(chatcmpl): remove space from base-url (#1256) 2025-03-30 23:59:55 +08:00
Junyan Qin (Chin)
e04d46db2c perf(claude): ensure system message removed (#867) (#1255) 2025-03-30 23:51:53 +08:00
Junyan Qin (Chin)
7341435127 perf(chatcmpl): use extra_body to pass args (#1254) 2025-03-30 23:43:45 +08:00
Junyan Qin (Chin)
8b56f94667 perf: add debugging msg for webhook style adapters (#1253) 2025-03-30 23:23:31 +08:00
Junyan Qin (Chin)
f5e98d4ebb fix(gewe): should not block main launching process (#1163) (#1252) 2025-03-30 23:14:56 +08:00
Junyan Qin (Chin)
23a0dba470 feat(dify): throw error event (#1251) 2025-03-30 23:04:46 +08:00
fdc310
512371cc25 Merge branch 'RockChinQ:master' into master 2025-03-30 22:55:55 +08:00
Dong_master
cd4a06b692 修改因为手误的参数名错误以及类名规范化 2025-03-29 01:18:30 +08:00
Junyan Qin (Chin)
629ebae0e9 chore: release v3.4.11.1 (#1244) 2025-03-28 23:48:09 +08:00
Junyan Qin (Chin)
394d4b3c1b fix: static_file sent with wrong mimetype (#1243) 2025-03-28 23:46:24 +08:00
Dong_master
432440d6bf 新增reply发送消息及文件 2025-03-27 00:01:05 +08:00
Junyan Qin (Chin)
a0fd152d19 doc(README): add 3.13 in python version badge (#1232) 2025-03-24 15:43:46 +08:00
Junyan Qin (Chin)
1a62e08bab chore: update gitignore (#1231) 2025-03-24 15:40:51 +08:00
Junyan Qin (Chin)
edbc59c117 perf: use source_platform_object to pass source event in aiocqhttp (#1230) 2025-03-24 14:03:44 +08:00
WangCham
cfdd0f8cb2 fix: Format the code in a standardized way (#1222) 2025-03-21 14:04:13 +08:00
Junyan Qin (Chin)
808f30675d doc(README): contributors (#1223) 2025-03-21 10:47:32 +08:00
Junyan Qin (Chin)
46072abb41 doc(README): add planning platforms comments (#1219) 2025-03-19 23:23:56 +08:00
fdc310
71ffbb9eb5 Merge branch 'RockChinQ:master' into master 2025-03-19 23:13:58 +08:00
Junyan Qin (Chin)
27bbb2297a Merge pull request #1217 from RockChinQ/version/3.4.11
chore: bump version 3.4.11
2025-03-19 22:50:50 +08:00
Junyan Qin
0d235aaef8 chore: bump version 3.4.11 2025-03-19 22:50:15 +08:00
Dong_master
e22c804deb 新增发送emoji表情?(好像没啥用)和发送链接功能 2025-03-19 22:47:10 +08:00
Dong_master
c136e790ef 新增小程序发送,小程序转发更名为ForwardMiniPrograms 2025-03-19 21:56:13 +08:00
Dong_master
3697afd9d6 新增小程序发送,小程序转发更名为ForwardMiniPrograms 2025-03-19 21:55:36 +08:00
Dong_master
c597c6482a 新增小程序转发 2025-03-19 20:46:56 +08:00
Junyan Qin (Chin)
dda8c637d8 Merge pull request #1216 from RockChinQ/doc/mcp-readme
doc(README): add mcp comments
2025-03-19 13:10:30 +08:00
Junyan Qin
e6d7aaa440 doc(README): add mcp comments 2025-03-19 13:10:01 +08:00
Junyan Qin (Chin)
028458b33c Merge pull request #1210 from fdc310/master
处理at全员的atall,以及修复回复公众号和微信团队的问题
2025-03-19 13:03:07 +08:00
Junyan Qin (Chin)
9c7d8099cb Merge pull request #1215 from RockChinQ/feat/mcp-tools
feat: add supports for loading mcp server as LLM tools provider
2025-03-19 13:01:09 +08:00
Junyan Qin
5640dc332d feat(mcp): available for provider reloading 2025-03-19 12:41:04 +08:00
Junyan Qin
40275c3ef1 feat: add supports for loading mcp server as LLM tools provider 2025-03-19 12:08:47 +08:00
Junyan Qin (Chin)
ebe0b2f335 Merge pull request #1214 from RockChinQ/feat/tool-loaders
feat: tool loader abstraction
2025-03-19 09:37:10 +08:00
Junyan Qin
97603e8441 feat: tool loader abstraction 2025-03-19 09:36:03 +08:00
Dong_master
72cd444861 处理at全员的atall,以及修复回复公众号和微信团队的问题 2025-03-18 23:14:55 +08:00
Dong_master
955b859f2c 处理at全员的atall,以及修复回复公众号和微信团队的问题 2025-03-18 23:14:33 +08:00
Junyan Qin
dea5cc9c0c stash 2025-03-18 21:59:53 +08:00
Junyan Qin (Chin)
d13ab1703e Merge pull request #1209 from wangcham/feat/wxoa-notice-msg
feat: add support for loading message in wxoa
2025-03-18 20:39:22 +08:00
Junyan Qin
61ab6a009b chore: migration for wxoa loading message 2025-03-18 20:38:33 +08:00
wangcham
a9ae36d362 feat: add support for loading message in wxoa 2025-03-18 06:58:35 -04:00
Junyan Qin (Chin)
f518395ce5 Merge pull request #1204 from fdc310/master
增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。
2025-03-18 12:04:09 +08:00
Junyan Qin (Chin)
20b17fe378 Merge pull request #1203 from IGCrystal/master
fix: Fix SSL certificate verification error during GitHub plugin installation
2025-03-17 23:54:25 +08:00
Junyan Qin
572182180c deps: add certifi 2025-03-17 23:53:29 +08:00
Dong_master
de261099aa 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:33:44 +08:00
Dong_master
50f0122955 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:31:13 +08:00
Dong_master
fe9eff923e 增加引用消息回复(暂时只是引用是机器人消息时候构建at并传入消息),增加小程序,转账,红包,视频号等消息的判断。 2025-03-17 23:23:21 +08:00
冰苷晶
dd36278032 fix: add certifi to requirement 2025-03-17 23:16:51 +08:00
冰苷晶
a079821976 fix: fix SSL certificateverification error during GitHub plugin installation.
- Create a custom SSL context using certifi for proper HTTPS certificate verification, meow - Add the ssl parameter to aiohttp requests to prevent download failure due to missing root certificates, meow - Improve error messages and enhance the overall plugin installation process, meow!
2025-03-17 23:12:23 +08:00
Junyan Qin
fa233e0a24 fix(variables): user_message_text not provided 2025-03-17 22:04:00 +08:00
Junyan Qin (Chin)
22306cb4ea Merge pull request #1199 from fdc310/master
Fixed bot sending messages to others without PushContent field causing error in judgment, and fixed bot sending messages to reply to themselves
2025-03-16 22:12:59 +08:00
Dong_master
f2d45a3668 将bot账号手动发送消息拦截提前到转换层 2025-03-16 21:43:07 +08:00
Dong_master
db91ff12f7 修复bot自己给别人发消息时没有PushContent字段导致判断错误,并修复bot自己发消息自己回复问题 2025-03-16 19:10:07 +08:00
Dong_master
eb841fb73e 修复bot自己给别人发消息时没有PushContent字段导致判断错误,并修复bot自己发消息自己回复问题 2025-03-16 19:08:56 +08:00
Junyan Qin
bd0438df76 chore: release v3.4.10.4 2025-03-16 17:05:10 +08:00
Junyan Qin (Chin)
9ca1fc59ef Merge pull request #1198 from fdc310/master
当机器人群名称改名后群聊单独at机器人时候替换@信息为空
2025-03-16 16:24:52 +08:00
Dong_master
84a80a5ec8 当是单独群聊at机器人时候替换@信息为空 2025-03-15 22:21:21 +08:00
Dong_master
4b2e248646 当是单独群聊at机器人时候替换@信息为空 2025-03-15 22:13:15 +08:00
Dong_master
b90e45590a 当微信群有at消息时删除第一个at已达到能激活管理员模式 2025-03-15 01:15:56 +08:00
Junyan Qin (Chin)
ff93d563a8 Merge pull request #1194 from fdc310/master
'增加了主动发送at信息'
2025-03-14 22:41:41 +08:00
Junyan Qin (Chin)
53228498ed Merge pull request #1191 from wangcham/master
fix: eliminate critical message queue blockage in wxoa
2025-03-14 22:29:03 +08:00
Dong_master
8ece82e43a '增加了主动发送at信息' 2025-03-14 02:33:52 +08:00
wangcham
8b4684675e fix: eliminate critical message queue blockage in wxoa 2025-03-13 10:44:09 -04:00
Junyan Qin (Chin)
8cca12fff2 Merge pull request #1190 from wangcham/master
feat: add support for longer response in wxoa
2025-03-13 17:30:19 +08:00
Junyan Qin
a74111612e chore: config for wxoa mode 2025-03-13 17:29:05 +08:00
wangcham
c7799a65c4 fix: update config in wxoa 2025-03-13 05:15:03 -04:00
wangcham
aabb01c50f feat: add support for longer response in wxoa 2025-03-12 23:39:43 -04:00
Junyan Qin
95e2ada965 fix(schema): adapt str type session id 2025-03-12 21:32:19 +08:00
Junyan Qin (Chin)
3fe7d53c76 Merge pull request #1188 from RockChinQ/feat/query-variables
feat: add query variables
2025-03-12 21:21:50 +08:00
Junyan Qin
e8634bb1ab feat(variables): add api for plugin 2025-03-12 20:57:42 +08:00
Junyan Qin
dbe46b5770 feat: add query variables 2025-03-12 19:13:04 +08:00
Junyan Qin (Chin)
6d9fba30b1 Merge pull request #1187 from wangcham/master
feat: add support for sending active messages in wecom
2025-03-12 16:49:47 +08:00
wangcham
6a866bf871 feat: add support for sending active messages in wecom 2025-03-12 04:03:02 -04:00
Junyan Qin (Chin)
3c961e4652 Merge pull request #1184 from wangcham/master
feat: add support for sending active messages in dingtalk
2025-03-11 16:42:29 +08:00
wangcham
7abd999420 feat: add support for sending active messages in dingtalk 2025-03-11 04:27:17 -04:00
Junyan Qin
fca8fbb135 perf: no longer add slash as ignored prefix as default 2025-03-11 12:06:37 +08:00
Junyan Qin
c67caf18df chore: release v3.4.10.3 2025-03-10 22:58:23 +08:00
Junyan Qin (Chin)
fe956fe4a5 Merge pull request #1181 from wangcham/master
fix: wrong group id in qqofficial web hook
2025-03-10 19:38:00 +08:00
wangcham
0e52f679a2 fix: wrong group id in webhook qq 2025-03-10 06:56:15 -04:00
Junyan Qin
b9500283ec fix(gewechat): remove necessary await in send_message 2025-03-10 15:16:10 +08:00
Junyan Qin (Chin)
441b69b528 Merge pull request #1174 from Muffeter/master 2025-03-07 18:37:18 +08:00
sonmu
898bcdc96b typo: fix README 2025-03-07 18:33:27 +08:00
Junyan Qin
02bc1fc45e chore: release v3.4.10.2 2025-03-05 21:12:52 +08:00
Junyan Qin (Chin)
5585981dc3 Merge pull request #1165 from fdc310/master
修复因为gewe将字段修改回原版而导致的我的判断逻辑问题
2025-03-05 21:12:21 +08:00
Dong_master
a4777f194b 修复因为gewe将字段修改回原版而导致的我的判断逻辑问题 2025-03-05 19:56:28 +08:00
Junyan Qin
41aeda8dc0 chore: release v3.4.10.1 2025-03-05 17:16:05 +08:00
Junyan Qin (Chin)
2ed522667e Merge pull request #1162 from fdc310/master
'修复了gewe更新回调参数data和typename字段改变造成的不回复的问题'
2025-03-05 17:14:27 +08:00
Dong_master
1932444666 '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 16:48:46 +08:00
Dong_master
b49b7e963d '修复了gewe更新回调参数data和typename字段改变造成的不回复的问题' 2025-03-05 00:54:39 +08:00
Junyan Qin
435c11ff27 doc(README): add more model in README 2025-03-03 21:26:39 +08:00
Junyan Qin
2e93600437 feat: update llm-models.json template 2025-03-03 21:02:48 +08:00
Junyan Qin (Chin)
faecb70d0f Merge pull request #1154 from Yi-Lyu/master
将微信消息时间戳传递给 dify,便于 dify 通过消息时间戳来做业务逻辑。
2025-03-02 20:21:08 +08:00
Junyan Qin
92e1ac5c3a feat: add supports for passing time to dify workflow 2025-03-02 20:18:33 +08:00
Junyan Qin
8963a2117b feat: add field time in MessageEvent 2025-03-02 20:16:34 +08:00
Ethan
aa300258ab feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:45:10 +08:00
Ethan
48841daff5 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:30:07 +08:00
Ethan
8878f1ed87 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:20:10 +08:00
Ethan
f6205d79c0 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:18:26 +08:00
Ethan
d6d5dac6b3 feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:53 +08:00
Ethan
05b979e68a feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 19:10:09 +08:00
Ethan
9f7d9e4c0d feat: enhance user message preprocessing to include message creation timestamp 2025-03-02 18:49:32 +08:00
Junyan Qin
98a9fed726 chore: release v3.4.10 2025-03-02 18:08:59 +08:00
Junyan Qin
720a218259 perf: simplify platform entities 2025-03-02 17:59:13 +08:00
Junyan Qin (Chin)
60c0adc6f9 Merge pull request #1152 from RockChinQ/feat/dingtalk-audio
feat(dingtalk): add supports for audio receiving
2025-03-02 17:38:19 +08:00
Junyan Qin
bc8c346e68 fix(dingtalk): group and person id not distinguished 2025-03-02 17:35:06 +08:00
Junyan Qin
a198b6da0b feat(dingtalk): add supports for audio receiving 2025-03-02 17:03:19 +08:00
Junyan Qin (Chin)
0f3dc35df4 Merge pull request #1150 from Tigrex-Dai/master
添加针对Anthropic新模型的thinking开关
2025-03-02 15:39:58 +08:00
Junyan Qin
7b6e6b046a perf(claude): simplify the thinking resp processing 2025-03-02 15:24:08 +08:00
Tigrex Dai
9e503191d6 Update anthropicmsgs.py 2025-03-01 17:27:01 +01:00
Tigrex Dai
1fd23a0d8d Merge branch 'RockChinQ:master' into master 2025-03-01 16:53:23 +01:00
Junyan Qin
3811700a78 chore: update llm-models.json template 2025-03-01 21:33:47 +08:00
Junyan Qin
8762ba3d9c feat(anthropic): add supports for tool use #763 2025-03-01 20:34:22 +08:00
Junyan Qin
c42b5aab5a feat: update components.yaml 2025-03-01 11:45:15 +08:00
Junyan Qin (Chin)
d724899ec0 Merge pull request #1148 from RockChinQ/feat/requester-manifests
feat: add manifests for all requesters
2025-03-01 00:12:55 +08:00
Junyan Qin
81aacdd76e refactor: lookup requester from discover engine 2025-03-01 00:12:23 +08:00
Junyan Qin
0aa072b4e8 feat: add manifests for all requesters 2025-02-28 22:47:34 +08:00
Tigrex Dai
6335e9dd8b Update anthropicmsgs.py 2025-02-28 13:02:06 +01:00
Tigrex Dai
a785289ac9 Update entities.py 2025-02-28 13:00:44 +01:00
Junyan Qin (Chin)
f8bace040c Merge pull request #1142 from fdc310/master
个人微信中主动发送图片的修改,但是只能发送链接
2025-02-28 11:33:43 +08:00
Dong_master
d62d597695 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:31:59 +08:00
Dong_master
d938129884 '删除先' 2025-02-28 01:30:55 +08:00
Dong_master
327f448321 Resolved merge conflict in gewechat.py 2025-02-28 01:22:15 +08:00
Dong_master
19af3740c1 '个人微信中主动发送图片的修改,但是只能发送链接' 2025-02-28 01:17:25 +08:00
Junyan Qin
11b1110eed chore: release v3.4.9.5 2025-02-27 17:04:54 +08:00
Junyan Qin (Chin)
682b897e21 Merge pull request #1130 from fdc310/master
'个人微信中主动发送信息send_message的修改'
2025-02-26 15:54:02 +08:00
Junyan Qin
998ad7623c perf(gewechat): simplify 2025-02-26 15:53:26 +08:00
Junyan Qin (Chin)
4f1db33abc Merge pull request #1131 from shockno1/master
Update gewechat.py 添加gewe微信接口中voice语音的处理
2025-02-26 15:38:56 +08:00
shockno1
ca6cb60bdd Update gewechat.py 添加gewe微信接口中voice语音的处理
添加gewe微信接口中voice语音的处理
2025-02-26 12:45:28 +08:00
Dong_master
133e48a5a9 '个人微信中主动发送信息send_message的修改' 2025-02-26 02:54:42 +08:00
Junyan Qin
d659d01b1e chore: release v3.4.9.4 2025-02-25 17:03:00 +08:00
Junyan Qin
34f73fd84b fix: typo 2025-02-25 17:02:36 +08:00
Junyan Qin (Chin)
54b87ff79d Merge pull request #1128 from wang149876/master
让llm重载可以直接获取本地最新的llm_models.json里面的内容
2025-02-25 16:54:53 +08:00
wang149876
6c2843e7c1 精简为直接复制给llm_models_meta 2025-02-25 16:52:00 +08:00
Junyan Qin (Chin)
6761a31982 Merge pull request #1127 from Yi-Lyu/master
围绕 Gewechat 修改,1)支持聊天记录的消息; 2)图片消息改为图片常规尺寸图片放弃原来的缩略图
2025-02-25 16:15:17 +08:00
Junyan Qin
9401a79b2b feat: update file download url 2025-02-25 16:12:45 +08:00
wang149876
7a4905d943 让llm重载可以直接获取本地最新的llm_models.json里面的内容 2025-02-25 12:56:00 +08:00
Ethan
4db1d2b3a3 fix: comment out debug print statement in gewechat callback 2025-02-25 11:53:23 +08:00
Ethan
2ffe2967d6 feat: add download image port configuration and improve image retrieval process 2025-02-25 11:32:35 +08:00
Ethan
0875c0f266 Merge branch 'RockChinQ:master' into master 2025-02-25 08:48:01 +08:00
Junyan Qin
68c7de5199 chore: release v3.4.9.3 2025-02-24 23:01:10 +08:00
Junyan Qin
4dfb8597ae fix: #1124 2025-02-24 23:00:19 +08:00
Ethan
e21a27ff23 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:36:03 +08:00
Ethan
91ad7944de 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:18:35 +08:00
Ethan
c86602ebaf 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:17:15 +08:00
Ethan
f75ac292db 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:11:27 +08:00
Ethan
2742c249bf 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:09:11 +08:00
Ethan
36f04849ab Merge remote-tracking branch 'origin/master'
# Conflicts:
#	pkg/platform/sources/gewechat.py
2025-02-24 20:03:18 +08:00
Ethan
a60c896e89 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 20:02:49 +08:00
Ethan
c442320c7f 增加微信聊天中图片获取能力,较之前的微信图片仅提供缩略图的情况,改善为获取微信聊天中实际图片大小,方便后续 ocr 或者 llm vision 识别聊天图片内容。 2025-02-24 19:53:43 +08:00
Ethan
6aeae7e9f5 解决运行报错(base LangBot v3.4.9.2):
[02-24 05:46:37.616] manager.py (169) - [ERROR] : 平台适配器运行出错: 'GeWeChatAdapter' object has no attribute 'name'
2025-02-24 18:53:29 +08:00
Ethan
cae79aac48 添加微信消息类型 49(聊天记录)的支持,支持处理聊天记录类型的微信消息。
微信聊天记录是 xml 数据格式,本质上也是字符串,可以按照字符串Plain类型来处理。
2025-02-24 18:09:02 +08:00
Junyan Qin
0623f4009a chore: release v3.4.9.2 2025-02-24 15:01:00 +08:00
Junyan Qin
06adeb72c4 fix: components.yaml encoding error on windows 2025-02-24 15:00:17 +08:00
93 changed files with 2958 additions and 537 deletions

View File

@@ -3,22 +3,6 @@ description: 报错或漏洞请使用这个模板创建,不使用此模板创
title: "[Bug]: "
labels: ["bug?"]
body:
- type: dropdown
attributes:
label: 消息平台适配器
description: "接入的消息平台类型"
options:
- 其他(或暂未使用)
- Nakurugo-cqhttp
- aiocqhttp使用 OneBot 协议接入的)
- qq-botpyQQ官方API WebSocket
- qqofficialQQ官方API Webhook
- lark飞书
- wecom企业微信
- gewechat个人微信
- discord
validations:
required: true
- type: input
attributes:
label: 运行环境

3
.gitignore vendored
View File

@@ -19,7 +19,7 @@ cookies.json
data/labels/announcement_saved.json
cmdpriv.json
tips.py
.venv
venv*
bin/
.vscode
test_*
@@ -39,3 +39,4 @@ botpy.log*
/libs/wecom_api/test.py
/venv
/jp-tyo-churros-05.rockchin.top
test.py

View File

@@ -9,7 +9,7 @@
<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://docs.langbot.app">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.htmll">功能介绍</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>
@@ -26,7 +26,7 @@
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89)
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
@@ -39,7 +39,7 @@
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/)目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
## 📦 开始使用
@@ -93,11 +93,13 @@
| 钉钉 | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 正在开发中
### 大模型
### 大模型能力
| 模型 | 状态 | 备注 |
| --- | --- | --- |
@@ -114,12 +116,34 @@
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
### TTS
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## 😘 社区贡献
LangBot 离不开以下贡献者和社区内所有人的贡献,我们欢迎任何形式的贡献和反馈。
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
以及 LangBot 核心团队成员:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -8,7 +8,7 @@
<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://docs.langbot.app">Home</a>
<a href="https://docs.langbot.app/insight/intro.htmll">Features</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>
@@ -24,7 +24,7 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
<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)
@@ -36,7 +36,7 @@
- 💬 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; Rich ecology, currently has dozens of [plugins](https://docs.langbot.app/plugin/plugin-intro.html)
- 🧩 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)
## 📦 Getting Started
@@ -90,6 +90,8 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: In development
@@ -111,14 +113,20 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
## 🤝 Community Contribution
Thanks to the following contributors and everyone in the community for their contributions.
Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
And the core team members of LangBot:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -8,7 +8,7 @@
<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://docs.langbot.app">ホーム</a>
<a href="https://docs.langbot.app/insight/intro.htmll">機能</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>
@@ -23,7 +23,7 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 | 3.11 | 3.12-blue.svg" alt="python">
<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)
@@ -35,7 +35,7 @@
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/)豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。
- 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。
## 📦 始め方
@@ -89,6 +89,8 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 開発中
@@ -110,14 +112,20 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
## 🤝 コミュニティ貢献
以下の貢献者とコミュニティの皆さんの貢献に感謝します。
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
LangBot の核心チームメンバー:
- [RockChinQ](https://github.com/RockChinQ)
- [the-lazy-me](https://github.com/the-lazy-me)
- [wangcham](https://github.com/wangcham)
- [KaedeSAMA](https://github.com/KaedeSAMA)

View File

@@ -10,6 +10,10 @@ spec:
ComponentTemplate:
fromFiles:
- pkg/platform/adapter.yaml
- pkg/provider/modelmgr/requester.yaml
MessagePlatformAdapter:
fromDirs:
- path: pkg/platform/sources/
LLMAPIRequester:
fromDirs:
- path: pkg/provider/modelmgr/requesters/

View File

@@ -8,6 +8,8 @@ services:
- ./data:/app/data
- ./plugins:/app/plugins
restart: on-failure
environment:
- TZ=Asia/Shanghai
ports:
- 5300:5300 # 供 WebUI 使用
- 2280-2290:2280-2290 # 供消息平台适配器方向连接

View File

@@ -1,4 +1,5 @@
import asyncio
import json
import dingtalk_stream
from dingtalk_stream import AckMessage
@@ -17,11 +18,12 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
await self.client.update_incoming_message(incoming_message)
return AckMessage.STATUS_OK, 'OK'
async def get_incoming_message(self):
"""异步等待消息的到来"""
while self.incoming_message is None:
await asyncio.sleep(0.1) # 异步等待,避免阻塞
return self.incoming_message
async def get_dingtalk_client(client_id, client_secret):

View File

@@ -1,4 +1,5 @@
import base64
import json
import time
from typing import Callable
import dingtalk_stream
@@ -9,7 +10,7 @@ import traceback
class DingTalkClient:
def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str):
def __init__(self, client_id: str, client_secret: str,robot_name:str,robot_code:str,markdown_card:bool):
"""初始化 WebSocket 连接并自动启动"""
self.credential = dingtalk_stream.Credential(client_id, client_secret)
self.client = dingtalk_stream.DingTalkStreamClient(self.credential)
@@ -25,6 +26,7 @@ class DingTalkClient:
self.robot_name = robot_name
self.robot_code = robot_code
self.access_token_expiry_time = ''
self.markdown_card = markdown_card
@@ -92,8 +94,31 @@ class DingTalkClient:
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str
else:
raise Exception("获取图片失败")
raise Exception("获取文件失败")
async def get_audio_url(self,download_code:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
params = {
"downloadCode":download_code,
"robotCode":self.robot_code
}
headers ={
"x-acs-dingtalk-access-token": self.access_token
}
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=params)
if response.status_code == 200:
result = response.json()
download_url = result.get("downloadUrl")
if download_url:
return await self.download_url_to_base64(download_url)
else:
raise Exception("获取音频失败")
else:
raise Exception(f"Error: {response.status_code}, {response.text}")
async def update_incoming_message(self, message):
"""异步更新 DingTalkClient 中的 incoming_message"""
message_data = await self.get_message(message)
@@ -104,7 +129,10 @@ class DingTalkClient:
async def send_message(self,content:str,incoming_message):
self.EchoTextHandler.reply_text(content,incoming_message)
if self.markdown_card:
self.EchoTextHandler.reply_markdown(title=self.robot_name+'的回答',text=content,incoming_message=incoming_message)
else:
self.EchoTextHandler.reply_text(content,incoming_message)
async def get_incoming_message(self):
@@ -133,6 +161,8 @@ class DingTalkClient:
async def get_message(self,incoming_message:dingtalk_stream.chatbot.ChatbotMessage):
try:
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
message_data = {
"IncomingMessage":incoming_message,
}
@@ -160,14 +190,65 @@ class DingTalkClient:
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
message_data['Type'] = 'image'
elif incoming_message.message_type == 'audio':
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
# 删掉开头的@消息
if message_data["Content"].startswith("@"+self.robot_name):
message_data["Content"][len("@"+self.robot_name):]
message_data['Type'] = 'audio'
copy_message_data = message_data.copy()
del copy_message_data['IncomingMessage']
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
except Exception:
traceback.print_exc()
return message_data
async def send_proactive_message_to_one(self,target_id:str,content:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'
headers ={
"x-acs-dingtalk-access-token":self.access_token,
"Content-Type":"application/json",
}
data ={
"robotCode":self.robot_code,
"userIds":[target_id],
"msgKey": "sampleText",
"msgParam": json.dumps({"content":content}),
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url,headers=headers,json=data)
except Exception:
traceback.print_exc()
async def send_proactive_message_to_group(self,target_id:str,content:str):
if not await self.check_access_token():
await self.get_access_token()
url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'
headers ={
"x-acs-dingtalk-access-token":self.access_token,
"Content-Type":"application/json",
}
data ={
"robotCode":self.robot_code,
"openConversationId":target_id,
"msgKey": "sampleText",
"msgParam": json.dumps({"content":content}),
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url,headers=headers,json=data)
except Exception:
traceback.print_exc()
async def start(self):
"""启动 WebSocket 连接,监听消息"""

View File

@@ -1,4 +1,5 @@
from typing import Dict, Any, Optional
import dingtalk_stream
class DingTalkEvent(dict):
@staticmethod
@@ -15,7 +16,7 @@ class DingTalkEvent(dict):
return self.get("Content","")
@property
def incoming_message(self):
def incoming_message(self) -> Optional["dingtalk_stream.chatbot.ChatbotMessage"]:
return self.get("IncomingMessage")
@property
@@ -25,6 +26,10 @@ class DingTalkEvent(dict):
@property
def picture(self):
return self.get("Picture","")
@property
def audio(self):
return self.get("Audio","")
@property
def conversation(self):
@@ -61,4 +66,4 @@ class DingTalkEvent(dict):
Returns:
str: 字符串表示。
"""
return f"<WecomEvent {super().__repr__()}>"
return f"<DingTalkEvent {super().__repr__()}>"

View File

@@ -1,4 +1,5 @@
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
from collections import deque
import time
import traceback
from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
@@ -12,6 +13,7 @@ import httpx
import asyncio
import time
import xml.etree.ElementTree as ET
from pkg.platform.sources import officialaccount as oa
@@ -25,6 +27,7 @@ xml_template = """
</xml>
"""
class OAClient():
def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str):
@@ -41,6 +44,7 @@ class OAClient():
}
self.access_token_expiry_time = None
self.msg_id_map = {}
self.generated_content = {}
async def handle_callback_request(self):
@@ -83,12 +87,10 @@ class OAClient():
from_user = root.find("FromUserName").text # 发送者
to_user = root.find("ToUserName").text # 机器人
from pkg.platform.sources import officialaccount
timeout = 4.80
interval = 0.1
while True:
content = officialaccount.generated_content.pop(message_data["MsgId"], None)
content = self.generated_content.pop(message_data["MsgId"], None)
if content:
response_xml = xml_template.format(
to_user=from_user,
@@ -168,6 +170,168 @@ class OAClient():
for handler in self._message_handlers[msg_type]:
await handler(event)
async def set_message(self,msg_id:int,content:str):
self.generated_content[msg_id] = content
class OAClientForLongerResponse():
def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str,LoadingMessage:str):
self.token = token
self.aes = EncodingAESKey
self.appid = AppID
self.appsecret = Appsecret
self.base_url = 'https://api.weixin.qq.com'
self.access_token = ''
self.app = Quart(__name__)
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
self._message_handlers = {
"example":[],
}
self.access_token_expiry_time = None
self.loading_message = LoadingMessage
self.msg_queue = {}
self.user_msg_queue = {}
async def handle_callback_request(self):
try:
start_time = time.time()
signature = request.args.get("signature", "")
timestamp = request.args.get("timestamp", "")
nonce = request.args.get("nonce", "")
echostr = request.args.get("echostr", "")
msg_signature = request.args.get("msg_signature", "")
if msg_signature is None:
raise Exception("msg_signature不在请求体中")
if request.method == 'GET':
check_str = "".join(sorted([self.token, timestamp, nonce]))
check_signature = hashlib.sha1(check_str.encode("utf-8")).hexdigest()
return echostr if check_signature == signature else "拒绝请求"
elif request.method == "POST":
encryt_msg = await request.data
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
xml_msg = xml_msg.decode('utf-8')
if ret != 0:
raise Exception("消息解密失败")
# 解析 XML
root = ET.fromstring(xml_msg)
from_user = root.find("FromUserName").text
to_user = root.find("ToUserName").text
if self.msg_queue.get(from_user) and self.msg_queue[from_user][0]["content"]:
queue_top = self.msg_queue[from_user].pop(0)
queue_content = queue_top["content"]
# 弹出用户消息
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user]:
self.user_msg_queue[from_user].pop(0)
response_xml = xml_template.format(
to_user=from_user,
from_user=to_user,
create_time=int(time.time()),
content=queue_content
)
return response_xml
else:
response_xml = xml_template.format(
to_user=from_user,
from_user=to_user,
create_time=int(time.time()),
content=self.loading_message
)
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user][0]["content"]:
return response_xml
else:
message_data = await self.get_message(xml_msg)
if message_data:
event = OAEvent.from_payload(message_data)
if event:
self.user_msg_queue.setdefault(from_user,[]).append(
{
"content":event.message,
}
)
await self._handle_message(event)
return response_xml
except Exception as e:
traceback.print_exc()
async def get_message(self, xml_msg: str):
root = ET.fromstring(xml_msg)
message_data = {
"ToUserName": root.find("ToUserName").text,
"FromUserName": root.find("FromUserName").text,
"CreateTime": int(root.find("CreateTime").text),
"MsgType": root.find("MsgType").text,
"Content": root.find("Content").text if root.find("Content") is not None else None,
"MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None,
}
return message_data
async def run_task(self, host: str, port: int, *args, **kwargs):
"""
启动 Quart 应用。
"""
await self.app.run_task(host=host, port=port, *args, **kwargs)
def on_message(self, msg_type: str):
"""
注册消息类型处理器。
"""
def decorator(func: Callable[[OAEvent], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def _handle_message(self, event: OAEvent):
"""
处理消息事件。
"""
msg_type = event.type
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
async def set_message(self,from_user:int,message_id:int,content:str):
if from_user not in self.msg_queue:
self.msg_queue[from_user] = []
self.msg_queue[from_user].append(
{
"msg_id":message_id,
"content":content,
}
)

View File

111
libs/slack_api/api.py Normal file
View File

@@ -0,0 +1,111 @@
import json
from quart import Quart, jsonify,request
from slack_sdk.web.async_client import AsyncWebClient
from .slackevent import SlackEvent
from typing import Callable, Dict, Any
from pkg.platform.types import events as platform_events, message as platform_message
class SlackClient():
def __init__(self,bot_token:str,signing_secret:str):
self.bot_token = bot_token
self.signing_secret = signing_secret
self.app = Quart(__name__)
self.client = AsyncWebClient(self.bot_token)
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
self._message_handlers = {
"example":[],
}
self.bot_user_id = None # 避免机器人回复自己的消息
async def handle_callback_request(self):
try:
body = await request.get_data()
data = json.loads(body)
if 'type' in data:
if data['type'] == 'url_verification':
return data['challenge']
bot_user_id = data.get("event",{}).get("bot_id","")
if self.bot_user_id and bot_user_id == self.bot_user_id:
return jsonify({'status': 'ok'})
# 处理私信
if data and data.get("event", {}).get("channel_type") in ["im"]:
event = SlackEvent.from_payload(data)
await self._handle_message(event)
return jsonify({'status': 'ok'})
#处理群聊
if data.get("event",{}).get("type") == 'app_mention':
data.setdefault("event", {})["channel_type"] = "channel"
event = SlackEvent.from_payload(data)
await self._handle_message(event)
return jsonify({'status':'ok'})
return jsonify({'status': 'ok'})
except Exception as e:
raise(e)
async def _handle_message(self, event: SlackEvent):
"""
处理消息事件。
"""
msg_type = event.type
if msg_type in self._message_handlers:
for handler in self._message_handlers[msg_type]:
await handler(event)
def on_message(self, msg_type: str):
"""注册消息类型处理器"""
def decorator(func: Callable[[platform_events.Event], None]):
if msg_type not in self._message_handlers:
self._message_handlers[msg_type] = []
self._message_handlers[msg_type].append(func)
return func
return decorator
async def send_message_to_channel(self,text:str,channel_id:str):
try:
response = await self.client.chat_postMessage(
channel=channel_id,
text=text
)
if self.bot_user_id is None and response.get("ok"):
self.bot_user_id = response["message"]["bot_id"]
return
except Exception as e:
raise e
async def send_message_to_one(self,text:str,user_id:str):
try:
response = await self.client.chat_postMessage(
channel = '@'+user_id,
text= text
)
if self.bot_user_id is None and response.get("ok"):
self.bot_user_id = response["message"]["bot_id"]
return
except Exception as e:
raise e
async def run_task(self, host: str, port: int, *args, **kwargs):
"""
启动 Quart 应用。
"""
await self.app.run_task(host=host, port=port, *args, **kwargs)

View File

@@ -0,0 +1,91 @@
from typing import Dict, Any, Optional
class SlackEvent(dict):
@staticmethod
def from_payload(payload: Dict[str, Any]) -> Optional["SlackEvent"]:
try:
event = SlackEvent(payload)
return event
except KeyError:
return None
@property
def text(self) -> str:
if self.get("event", {}).get("channel_type") == "im":
blocks = self.get("event", {}).get("blocks", [])
if not blocks:
return ""
elements = blocks[0].get("elements", [])
if not elements:
return ""
elements = elements[0].get("elements", [])
text = ""
for el in elements:
if el.get("type") == "text":
text += el.get("text", "")
elif el.get("type") == "link":
text += el.get("url", "")
return text
if self.get("event",{}).get("channel_type") == 'channel':
message_text = ""
for block in self.get("event", {}).get("blocks", []):
if block.get("type") == "rich_text":
for element in block.get("elements", []):
if element.get("type") == "rich_text_section":
parts = []
for el in element.get("elements", []):
if el.get("type") == "text":
parts.append(el["text"])
elif el.get("type") == "link":
parts.append(el["url"])
message_text = "".join(parts)
return message_text
@property
def user_id(self) -> Optional[str]:
return self.get("event", {}).get("user","")
@property
def channel_id(self) -> Optional[str]:
return self.get("event", {}).get("channel","")
@property
def type(self) -> str:
""" message对应私聊app_mention对应频道at """
return self.get("event", {}).get("channel_type", "")
@property
def message_id(self) -> str:
return self.get("event_id","")
@property
def pic_url(self) -> str:
"""提取 Slack 事件中的图片 URL"""
files = self.get("event", {}).get("files", [])
if files:
return files[0].get("url_private", "")
return None
@property
def sender_name(self) -> str:
return self.get("event", {}).get("user","")
def __getattr__(self, key: str) -> Optional[Any]:
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
self[key] = value
def __repr__(self) -> str:
return f"<SlackEvent {super().__repr__()}>"

View File

@@ -66,7 +66,7 @@ class WecomClient():
else:
raise Exception("未获取用户")
async def send_to_all(self,content:str):
async def send_to_all(self,content:str,agent_id:int):
if not self.check_access_token_for_contacts():
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
@@ -77,7 +77,7 @@ class WecomClient():
params = {
"touser" : user_ids_string,
"msgtype" : "text",
"agentid" : 1000002,
"agentid" : agent_id,
"text" : {
"content" : content,
},

View File

@@ -66,8 +66,42 @@ class HTTPController:
@self.quart_app.route("/")
async def index():
return await quart.send_from_directory(frontend_path, "index.html")
return await quart.send_from_directory(
frontend_path,
"index.html",
mimetype="text/html"
)
@self.quart_app.route("/<path:path>")
async def static_file(path: str):
return await quart.send_from_directory(frontend_path, path)
mimetype = None
if path.endswith(".html"):
mimetype = "text/html"
elif path.endswith(".js"):
mimetype = "application/javascript"
elif path.endswith(".css"):
mimetype = "text/css"
elif path.endswith(".png"):
mimetype = "image/png"
elif path.endswith(".jpg"):
mimetype = "image/jpeg"
elif path.endswith(".jpeg"):
mimetype = "image/jpeg"
elif path.endswith(".gif"):
mimetype = "image/gif"
elif path.endswith(".svg"):
mimetype = "image/svg+xml"
elif path.endswith(".ico"):
mimetype = "image/x-icon"
elif path.endswith(".json"):
mimetype = "application/json"
elif path.endswith(".txt"):
mimetype = "text/plain"
return await quart.send_from_directory(
frontend_path,
path,
mimetype=mimetype
)

View File

@@ -16,7 +16,6 @@ class FuncOperator(operator.CommandOperator):
all_functions = await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True,
plugin_status=plugin_context.RuntimeContainerStatus.INITIALIZED,
)
for func in all_functions:

View File

@@ -29,6 +29,7 @@ from ..discover import engine as discover_engine
from ..utils import logcache, ip
from . import taskmgr
from . import entities as core_entities
from .bootutils import config
class Application:
@@ -203,6 +204,10 @@ class Application:
case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info("执行热重载 scope="+scope)
await self.tool_mgr.shutdown()
latest_llm_model_config = await config.load_json_config("data/metadata/llm-models.json", "templates/metadata/llm-models.json")
self.llm_models_meta = latest_llm_model_config
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
await llm_model_mgr_inst.initialize()
self.model_mgr = llm_model_mgr_inst

View File

@@ -33,6 +33,9 @@ required_deps = {
"dingtalk_stream": "dingtalk_stream",
"dashscope": "dashscope",
"telegram": "python-telegram-bot",
"certifi": "certifi",
"mcp": "mcp",
"telegramify_markdown":"telegramify-markdown",
}

View File

@@ -72,6 +72,9 @@ class Query(pydantic.BaseModel):
user_message: typing.Optional[llm_entities.Message] = None
"""此次请求的用户消息对象,由前置处理器阶段设置"""
variables: typing.Optional[dict[str, typing.Any]] = None
"""变量由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。"""
use_model: typing.Optional[entities.LLMModelInfo] = None
"""使用的模型,由前置处理器阶段设置"""
@@ -86,10 +89,31 @@ class Query(pydantic.BaseModel):
# ======= 内部保留 =======
current_stage: "pkg.pipeline.stagemgr.StageInstContainer" = None
"""当前所处阶段"""
class Config:
arbitrary_types_allowed = True
# ========== 插件可调用的 API请求 API ==========
def set_variable(self, key: str, value: typing.Any):
"""设置变量"""
if self.variables is None:
self.variables = {}
self.variables[key] = value
def get_variable(self, key: str) -> typing.Any:
"""获取变量"""
if self.variables is None:
return None
return self.variables.get(key)
def get_variables(self) -> dict[str, typing.Any]:
"""获取所有变量"""
if self.variables is None:
return {}
return self.variables
class Conversation(pydantic.BaseModel):
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation但只有一个当前使用的 Conversation"""

View File

@@ -23,6 +23,7 @@ class GewechatConfigMigration(migration.Migration):
"adapter": "gewechat",
"enable": False,
"gewechat_url": "http://your-gewechat-server:2531",
"gewechat_file_url": "http://your-gewechat-server:2532",
"port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "",

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from urllib.parse import urlparse
from .. import migration
@migration.migration_class("gewechat-file-url-config", 34)
class GewechatFileUrlConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
if 'gewechat_file_url' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'gewechat':
if 'gewechat_file_url' not in adapter:
parsed_url = urlparse(adapter['gewechat_url'])
adapter['gewechat_file_url'] = f"{parsed_url.scheme}://{parsed_url.hostname}:2532"
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wxoa-mode", 35)
class WxoaModeMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'Mode' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'Mode' not in adapter:
adapter['Mode'] = 'drop'
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("wxoa-loading-message", 36)
class WxoaLoadingMessageMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'LoadingMessage' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] == 'officialaccount':
if 'LoadingMessage' not in adapter:
adapter['LoadingMessage'] = 'AI正在思考中请发送任意内容获取回复。'
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("mcp-config", 37)
class MCPConfigMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
return 'mcp' not in self.ap.provider_cfg.data
async def run(self):
"""执行迁移"""
self.ap.provider_cfg.data['mcp'] = {
"servers": []
}
await self.ap.provider_cfg.dump_config()

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("tg-dingtalk-markdown", 38)
class TgDingtalkMarkdownMigration(migration.Migration):
"""迁移"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] in ['dingtalk','telegram']:
if 'markdown_card' not in adapter:
return True
return False
async def run(self):
"""执行迁移"""
for adapter in self.ap.platform_cfg.data['platform-adapters']:
if adapter['adapter'] in ['dingtalk','telegram']:
if 'markdown_card' not in adapter:
adapter['markdown_card'] = False
await self.ap.platform_cfg.dump_config()

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("modelscope-config-completion", 4)
class ModelScopeConfigCompletionMigration(migration.Migration):
"""OpenAI配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester'] \
or 'modelscope' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移
"""
if 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] = {
'base-url': 'https://api-inference.modelscope.cn/v1',
'args': {},
'timeout': 120,
}
if 'modelscope' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['modelscope'] = []
await self.ap.provider_cfg.dump_config()

View File

@@ -11,7 +11,9 @@ from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_
from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config
from ..migrations import m033_dify_thinking_config
from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message
from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion
@stage.stage_class("MigrationStage")
class MigrationStage(stage.BootingStage):

View File

@@ -139,7 +139,7 @@ class ComponentDiscoveryEngine:
def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component:
"""加载组件清单"""
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8') as f:
manifest = yaml.safe_load(f)
comp = Component(
owner=owner,
@@ -187,6 +187,9 @@ class ComponentDiscoveryEngine:
if name == 'ComponentTemplate':
continue
components[name] = self.load_blueprint_comp_group(component, owner)
self.ap.logger.debug(f'Components: {components}')
return blueprint_manifest, components

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import datetime
from .. import stage, entities, stagemgr
from ...core import entities as core_entities
@@ -34,7 +35,7 @@ class PreProcessor(stage.PipelineStage):
conversation = await self.ap.sess_mgr.get_conversation(session)
# 从会话取出消息和情景预设到query
# 设置query
query.session = session
query.prompt = conversation.prompt.copy()
query.messages = conversation.messages.copy()
@@ -43,6 +44,11 @@ class PreProcessor(stage.PipelineStage):
query.use_funcs = conversation.use_funcs if query.use_model.tool_call_supported 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()),
}
# 检查vision是否启用没启用就删除所有图片
if not self.ap.provider_cfg.data['enable-vision'] or (self.ap.provider_cfg.data['runner'] == 'local-agent' and not query.use_model.vision_supported):
@@ -54,11 +60,14 @@ class PreProcessor(stage.PipelineStage):
content_list = []
plain_text = ""
for me in query.message_chain:
if isinstance(me, platform_message.Plain):
content_list.append(
llm_entities.ContentElement.from_text(me.text)
)
plain_text += me.text
elif isinstance(me, platform_message.Image):
if self.ap.provider_cfg.data['enable-vision'] and (self.ap.provider_cfg.data['runner'] != 'local-agent' or query.use_model.vision_supported):
if me.base64 is not None:
@@ -66,6 +75,8 @@ class PreProcessor(stage.PipelineStage):
llm_entities.ContentElement.from_image_base64(me.base64)
)
query.variables['user_message_text'] = plain_text
query.user_message = llm_entities.Message(
role='user',
content=content_list

View File

@@ -110,7 +110,7 @@ class PlatformManager:
if len(self.adapters) == 0:
self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。')
async def write_back_config(self, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
def write_back_config(self, adapter_name: str, adapter_inst: msadapter.MessagePlatformAdapter, config: dict):
index = -2
for i, adapter in enumerate(self.adapters):
@@ -132,12 +132,12 @@ class PlatformManager:
break
new_cfg = {
'adapter': adapter_inst.name,
'adapter': adapter_name,
'enable': True,
**config
}
self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg
await self.ap.platform_cfg.dump_config()
self.ap.platform_cfg.dump_config_sync()
async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter):
@@ -170,7 +170,8 @@ class PlatformManager:
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
tasks.append(exception_wrapper(adapter))
for task in tasks:
self.ap.task_mgr.create_task(
task,

View File

@@ -57,7 +57,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif msg.path:
arg = msg.path
msg_list.append(aiocqhttp.MessageSegment.record(msg.path))
elif type(msg) is forward.Forward:
elif type(msg) is platform_message.Forward:
for node in msg.node_list:
msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0])
@@ -101,69 +101,8 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(event: platform_events.Event, bot_account_id: int):
msg, msg_id, msg_time = await AiocqhttpMessageConverter.yiri2target(event.message_chain)
if type(event) is platform_events.GroupMessage:
role = "member"
if event.sender.permission == "ADMINISTRATOR":
role = "admin"
elif event.sender.permission == "OWNER":
role = "owner"
payload = {
"post_type": "message",
"message_type": "group",
"time": int(msg_time.timestamp()),
"self_id": bot_account_id,
"sub_type": "normal",
"anonymous": None,
"font": 0,
"message": str(msg),
"raw_message": str(msg),
"sender": {
"age": 0,
"area": "",
"card": "",
"level": "",
"nickname": event.sender.member_name,
"role": role,
"sex": "unknown",
"title": "",
"user_id": event.sender.id,
},
"user_id": event.sender.id,
"message_id": msg_id,
"group_id": event.group.id,
"message_seq": 0,
}
return aiocqhttp.Event.from_payload(payload)
elif type(event) is platform_events.FriendMessage:
payload = {
"post_type": "message",
"message_type": "private",
"time": int(msg_time.timestamp()),
"self_id": bot_account_id,
"sub_type": "friend",
"target_id": bot_account_id,
"message": str(msg),
"raw_message": str(msg),
"font": 0,
"sender": {
"age": 0,
"nickname": event.sender.nickname,
"sex": "unknown",
"user_id": event.sender.id,
},
"message_id": msg_id,
"user_id": event.sender.id,
}
return aiocqhttp.Event.from_payload(payload)
async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int):
return event.source_platform_object
@staticmethod
async def target2yiri(event: aiocqhttp.Event):
@@ -196,6 +135,7 @@ class AiocqhttpEventConverter(adapter.EventConverter):
),
message_chain=yiri_chain,
time=event.time,
source_platform_object=event
)
return converted_event
elif event.message_type == "private":
@@ -207,6 +147,7 @@ class AiocqhttpEventConverter(adapter.EventConverter):
),
message_chain=yiri_chain,
time=event.time,
source_platform_object=event
)

View File

@@ -28,16 +28,23 @@ class DingTalkMessageConverter(adapter.MessageConverter):
return msg.text
@staticmethod
async def target2yiri(event:DingTalkEvent):
async def target2yiri(event:DingTalkEvent, bot_name:str):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id = '0',time=datetime.datetime.now())
platform_message.Source(id = event.incoming_message.message_id,time=datetime.datetime.now())
)
for atUser in event.incoming_message.at_users:
if atUser.dingtalk_id == event.incoming_message.chatbot_user_id:
yiri_msg_list.append(platform_message.At(target=bot_name))
if event.content:
yiri_msg_list.append(platform_message.Plain(text=event.content))
text_content = event.content.replace("@"+bot_name, '')
yiri_msg_list.append(platform_message.Plain(text=text_content))
if event.picture:
yiri_msg_list.append(platform_message.Image(base64=event.picture))
if event.audio:
yiri_msg_list.append(platform_message.Voice(base64=event.audio))
chain = platform_message.MessageChain(yiri_msg_list)
@@ -54,33 +61,33 @@ class DingTalkEventConverter(adapter.EventConverter):
@staticmethod
async def target2yiri(
event:DingTalkEvent
event:DingTalkEvent,
bot_name:str
):
message_chain = await DingTalkMessageConverter.target2yiri(event)
message_chain = await DingTalkMessageConverter.target2yiri(event, bot_name)
if event.conversation == 'FriendMessage':
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id= 0,
nickname ='nickname',
id=event.incoming_message.sender_id,
nickname = event.incoming_message.sender_nick,
remark=""
),
message_chain = message_chain,
time = datetime.datetime.now(),
time = event.incoming_message.create_at,
source_platform_object=event,
)
elif event.conversation == 'GroupMessage':
message_chain.insert(0, platform_message.At(target="justbot"))
sender = platform_entities.GroupMember(
id = 111,
member_name="name",
id = event.incoming_message.sender_id,
member_name=event.incoming_message.sender_nick,
permission= 'MEMBER',
group = platform_entities.Group(
id = 111,
name = 'MEMBER',
id = event.incoming_message.conversation_id,
name = event.incoming_message.conversation_title,
permission=platform_entities.Permission.Member
),
special_title='',
@@ -88,7 +95,7 @@ class DingTalkEventConverter(adapter.EventConverter):
last_speak_timestamp=0,
mute_time_remaining=0
)
time = datetime.datetime.now(),
time = event.incoming_message.create_at
return platform_events.GroupMessage(
sender =sender,
message_chain = message_chain,
@@ -117,12 +124,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("钉钉缺少相关配置项,请查看文档或联系管理员")
self.bot_account_id = self.config["robot_name"]
self.bot = DingTalkClient(
client_id=config["client_id"],
client_secret=config["client_secret"],
robot_name = config["robot_name"],
robot_code=config["robot_code"]
robot_code=config["robot_code"],
markdown_card=config["markdown_card"]
)
async def reply_message(
@@ -143,7 +153,11 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
content = await DingTalkMessageConverter.yiri2target(message)
if target_type == 'person':
await self.bot.send_proactive_message_to_one(target_id,content)
if target_type == 'group':
await self.bot.send_proactive_message_to_group(target_id,content)
def register_listener(
self,
@@ -153,10 +167,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
],
):
async def on_message(event: DingTalkEvent):
self.bot_account_id = 'justbot'
try:
return await callback(
await self.event_converter.target2yiri(event), self
await self.event_converter.target2yiri(event, self.config["robot_name"]), self
)
except:
traceback.print_exc()
@@ -167,7 +180,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
self.bot.on_message("GroupMessage")(on_message)
async def run_async(self):
await self.bot.start()
async def kill(self) -> bool:

View File

@@ -25,10 +25,14 @@ 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
class GewechatMessageConverter(adapter.MessageConverter):
def __init__(self, config: dict):
self.config = config
@staticmethod
async def yiri2target(
message_chain: platform_message.MessageChain
@@ -40,59 +44,209 @@ class GewechatMessageConverter(adapter.MessageConverter):
elif isinstance(component, platform_message.Plain):
content_list.append({"type": "text", "content": component.text})
elif isinstance(component, platform_message.Image):
# content_list.append({"type": "image", "image_id": component.image_id})
pass
if not component.url:
pass
content_list.append({"type": "image", "image": component.url})
elif isinstance(component, platform_message.WeChatMiniPrograms):
content_list.append({"type": 'WeChatMiniPrograms', 'mini_app_id': component.mini_app_id, 'display_name': component.display_name,
'page_path': component.page_path, 'cover_img_url': component.image_url, 'title': component.title,
'user_name': component.user_name})
elif isinstance(component, platform_message.WeChatForwardMiniPrograms):
content_list.append({"type": 'WeChatForwardMiniPrograms', 'xml_data': component.xml_data, 'image_url': component.image_url})
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.WeChatLink):
content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc,
'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url})
elif isinstance(component, platform_message.Voice):
content_list.append({"type": "voice", "url": component.url, "length": component.length})
elif isinstance(component, platform_message.Forward):
for node in component.node_list:
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain))
return content_list
@staticmethod
async def target2yiri(
self,
message: dict,
bot_account_id: str
) -> platform_message.MessageChain:
if message["Data"]["MsgType"] == 1:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
regex = re.compile(r"^wxid_.*:")
# print(message)
line_split = message["Data"]["Content"]["string"].split("\n")
if len(line_split) > 0 and regex.match(line_split[0]):
message["Data"]["Content"]["string"] = "\n".join(line_split[1:])
at_string = f'@{bot_account_id}'
# 正则表达式模式,匹配'@'后跟任意数量的非空白字符
pattern = r'@\S+'
at_string = f"@{bot_account_id}"
content_list = []
if at_string in message["Data"]["Content"]["string"]:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, "", 1)))
content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, '', 1)))
# 更优雅的替换改名后@机器人仅仅限于单独AT的情况
elif "PushContent" in message['Data'] and '在群聊中@了你' in message["Data"]["PushContent"]:
if '@所有人' in message["Data"]["Content"]["string"]: # at全员时候传入atll不当作at自己
content_list.append(platform_message.AtAll())
else:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(re.sub(pattern, '', message["Data"]["Content"]["string"])))
else:
content_list = [platform_message.Plain(message["Data"]["Content"]["string"])]
return platform_message.MessageChain(content_list)
elif message["Data"]["MsgType"] == 3:
image_base64 = message["Data"]["ImgBuf"]["buffer"]
image_xml = message["Data"]["Content"]["string"]
if not image_xml:
return platform_message.MessageChain([
platform_message.Plain(text="[图片内容为空]")
])
try:
base64_str, image_format = await image.get_gewechat_image_base64(
gewechat_url=self.config["gewechat_url"],
gewechat_file_url=self.config["gewechat_file_url"],
app_id=self.config["app_id"],
xml_content=image_xml,
token=self.config["token"],
image_type=2,
)
return platform_message.MessageChain([
platform_message.Image(
base64=f"data:image/{image_format};base64,{base64_str}"
)
])
except Exception as e:
print(f"处理图片消息失败: {str(e)}")
return platform_message.MessageChain([
platform_message.Plain(text=f"[图片处理失败]")
])
elif message["Data"]["MsgType"] == 34:
audio_base64 = message["Data"]["ImgBuf"]["buffer"]
return platform_message.MessageChain(
[platform_message.Image(base64=f"data:image/jpeg;base64,{image_base64}")]
[platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")]
)
elif message["Data"]["MsgType"] == 49:
# 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递
try:
content = message["Data"]["Content"]["string"]
# 有三种可能的消息结构weid开头私聊直接<?xml>和直接<msg>
if content.startswith('wxid'):
xml_list = content.split('\n')[2:]
xml_data = '\n'.join(xml_list)
elif content.startswith('<?xml'):
xml_list = content.split('\n')[1:]
xml_data = '\n'.join(xml_list)
else:
xml_data = content
content_data = ET.fromstring(xml_data)
# print(xml_data)
# 拿到细分消息类型按照gewe接口中描述
'''
小程序33/36
引用消息57
转账消息2000
红包消息2001
视频号消息51
'''
appmsg_data = content_data.find('.//appmsg')
data_type = appmsg_data.find('.//type').text
if data_type == '57':
user_data = appmsg_data.find('.//title').text # 拿到用户消息
quote_data = appmsg_data.find('.//refermsg').find('.//content').text # 引用原文
sender_id = appmsg_data.find('.//refermsg').find('.//chatusr').text # 引用用户id
from_name = message['Data']['FromUserName']['string']
message_list =[]
if message['Wxid'] == sender_id and from_name.endswith('@chatroom'): # 因为引用机制暂时无法响应用户所以当引用用户是机器人是构建一个at激活机器人
message_list.append(platform_message.At(target=bot_account_id))
message_list.append(platform_message.Quote(
sender_id=sender_id,
origin=platform_message.MessageChain(
[platform_message.Plain(quote_data)]
)))
message_list.append(platform_message.Plain(user_data))
return platform_message.MessageChain(message_list)
elif data_type == '51':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[视频号消息]')]
)
# print(content_data)
elif data_type == '2000':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[转账消息]')]
)
elif data_type == '2001':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[红包消息]')]
)
elif data_type == '5':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[公众号消息]')]
)
elif data_type == '33' or data_type == '36':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[小程序消息]')]
)
# print(data_type.text)
else:
try:
content_bytes = content.encode('utf-8')
decoded_content = base64.b64decode(content_bytes)
return platform_message.MessageChain(
[platform_message.Unknown(content=decoded_content)]
)
except Exception as e:
return platform_message.MessageChain(
[platform_message.Plain(text=content)]
)
except Exception as e:
print(f"Error processing type 49 message: {str(e)}")
return platform_message.MessageChain(
[platform_message.Plain(text="[无法解析的消息]")]
)
class GewechatEventConverter(adapter.EventConverter):
def __init__(self, config: dict):
self.config = config
self.message_converter = GewechatMessageConverter(config)
@staticmethod
async def yiri2target(
event: platform_events.MessageEvent
) -> dict:
pass
@staticmethod
async def target2yiri(
self,
event: dict,
bot_account_id: str
) -> platform_events.MessageEvent:
message_chain = await GewechatMessageConverter.target2yiri(copy.deepcopy(event), bot_account_id)
# print(event)
# 排除自己发消息回调回答问题
if event['Wxid'] == event['Data']['FromUserName']['string']:
return None
# 排除公众号以及微信团队消息
if event['Data']['FromUserName']['string'].startswith('gh_')\
or event['Data']['FromUserName']['string'].startswith('weixin'):
return None
message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id)
if not message_chain:
return None
@@ -120,7 +274,7 @@ class GewechatEventConverter(adapter.EventConverter):
time=event["Data"]["CreateTime"],
source_platform_object=event,
)
elif 'wxid_' in event["Data"]["FromUserName"]["string"]:
else:
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event["Data"]["FromUserName"]["string"],
@@ -134,7 +288,9 @@ class GewechatEventConverter(adapter.EventConverter):
class GeWeChatAdapter(adapter.MessagePlatformAdapter):
name: str = "gewechat" # 定义适配器名称
bot: gewechat_client.GewechatClient
quart_app: quart.Quart
@@ -144,8 +300,8 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
ap: app.Application
message_converter: GewechatMessageConverter = GewechatMessageConverter()
event_converter: GewechatEventConverter = GewechatEventConverter()
message_converter: GewechatMessageConverter
event_converter: GewechatEventConverter
listeners: typing.Dict[
typing.Type[platform_events.Event],
@@ -158,10 +314,23 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
self.quart_app = quart.Quart(__name__)
self.message_converter = GewechatMessageConverter(config)
self.event_converter = GewechatEventConverter(config)
@self.quart_app.route('/gewechat/callback', methods=['POST'])
async def gewechat_callback():
data = await quart.request.json
# print(json.dumps(data, indent=4, ensure_ascii=False))
self.ap.logger.debug(
f"Gewechat callback event: {data}"
)
if 'data' in data:
data['Data'] = data['data']
if 'type_name' in data:
data['TypeName'] = data['type_name']
# print(json.dumps(data, indent=4, ensure_ascii=False))
if 'testMsg' in data:
return 'ok'
@@ -183,7 +352,44 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
target_id: str,
message: platform_message.MessageChain
):
pass
geweap_msg = await self.message_converter.yiri2target(message)
# 此处加上群消息at处理
ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
for msg in geweap_msg:
# at主动发送消息
if msg['type'] == 'text':
if ats:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
target_id,
ats[::-1]
)["data"]
for member in member_info:
msg['content'] = f'@{member["nickName"]} {msg["content"]}'
self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'],
ats=",".join(ats))
elif msg['type'] == 'image':
self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
elif msg['type'] == 'WeChatMiniPrograms':
self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id']
, display_name=msg['display_name'], page_path=msg['page_path']
, cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name'])
elif msg['type'] == 'WeChatForwardMiniPrograms':
self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url'])
elif msg['type'] == 'WeChatEmoji':
self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id,
emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'])
elif msg['type'] == 'WeChatLink':
self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id
,title=msg['link_title'], desc=msg['link_desc']
, link_url=msg['link_url'], thumb_url=msg['link_thumb_url'])
async def reply_message(
self,
@@ -194,6 +400,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
content_list = await self.message_converter.yiri2target(message)
ats = [item["target"] for item in content_list if item["type"] == "at"]
target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"]
for msg in content_list:
if msg["type"] == "text":
@@ -212,8 +419,24 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
app_id=self.config["app_id"],
to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"],
content=msg["content"],
ats=','.join(ats)
ats=",".join(ats)
)
elif msg['type'] == 'image':
self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
elif msg['type'] == 'WeChatMiniPrograms':
self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id']
, display_name=msg['display_name'], page_path=msg['page_path']
, cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name'])
elif msg['type'] == 'WeChatForwardMiniPrograms':
self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url'])
elif msg['type'] == 'WeChatEmoji':
self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id,
emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'])
elif msg['type'] == 'WeChatLink':
self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id
, title=msg['link_title'], desc=msg['link_desc']
, link_url=msg['link_url'], thumb_url=msg['link_thumb_url'])
async def is_muted(self, group_id: int) -> bool:
pass
@@ -249,26 +472,28 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
self.config["token"]
)
app_id, error_msg = self.bot.login(self.config["app_id"])
if error_msg:
raise Exception(f"Gewechat 登录失败: {error_msg}")
def gewechat_login_process():
self.config["app_id"] = app_id
app_id, error_msg = self.bot.login(self.config["app_id"])
if error_msg:
raise Exception(f"Gewechat 登录失败: {error_msg}")
self.ap.logger.info(f"Gewechat 登录成功app_id: {app_id}")
self.config["app_id"] = app_id
await self.ap.platform_mgr.write_back_config(self, self.config)
self.ap.logger.info(f"Gewechat 登录成功app_id: {app_id}")
# 获取 nickname
profile = self.bot.get_profile(self.config["app_id"])
self.bot_account_id = profile["data"]["nickName"]
self.ap.platform_mgr.write_back_config('gewechat', self, self.config)
# 获取 nickname
profile = self.bot.get_profile(self.config["app_id"])
self.bot_account_id = profile["data"]["nickName"]
time.sleep(2)
def thread_set_callback():
time.sleep(3)
ret = self.bot.set_callback(self.config["token"], self.config["callback_url"])
print('设置 Gewechat 回调:', ret)
threading.Thread(target=thread_set_callback).start()
threading.Thread(target=gewechat_login_process).start()
async def shutdown_trigger_placeholder():
while True:
@@ -281,4 +506,4 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
)
async def kill(self) -> bool:
pass
pass

View File

@@ -17,6 +17,13 @@ spec:
type: string
required: true
default: ""
- name: gewechat_file_url
label:
en_US: GeWeChat file download URL
zh_CN: GeWeChat 文件下载URL
type: string
required: true
default: ""
- name: port
label:
en_US: Port

View File

@@ -328,6 +328,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
try:
data = await quart.request.json
self.ap.logger.debug(
f"Lark callback event: {data}"
)
if 'encrypt' in data:
cipher = AESCipher(self.config['encrypt-key'])
data = cipher.decrypt_string(data['encrypt'])
@@ -339,7 +343,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
type = context.header.event_type
if 'url_verification' == type:
print(data.get("challenge"))
# todo 验证verification token
return {
"challenge": data.get("challenge")

View File

@@ -2,21 +2,19 @@ from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
from pkg.core import app
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
import aiocqhttp
import aiohttp
from collections import deque
from libs.official_account_api.oaevent import OAEvent
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.official_account_api.api import OAClient
from libs.official_account_api.api import OAClientForLongerResponse
from pkg.core import app
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
@@ -24,9 +22,6 @@ from ..types import entities as platform_entities
from ...command.errors import ParamNotEnoughError
# 生成的ai回答
generated_content = {}
class OAMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(message_chain: platform_message.MessageChain):
@@ -70,7 +65,7 @@ class OAEventConverter(adapter.EventConverter):
class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
bot : OAClient
bot : OAClient | OAClientForLongerResponse
ap : app.Application
bot_account_id: str
message_converter: OAMessageConverter = OAMessageConverter()
@@ -88,27 +83,44 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
"EncodingAESKey",
"AppSecret",
"AppID",
"Mode",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员")
raise ParamNotEnoughError("微信公众号缺少相关配置项,请查看文档或联系管理员")
self.bot = OAClient(
token=config['token'],
EncodingAESKey=config['EncodingAESKey'],
Appsecret=config['AppSecret'],
AppID=config['AppID'],
)
if self.config['Mode'] == "drop":
self.bot = OAClient(
token=config['token'],
EncodingAESKey=config['EncodingAESKey'],
Appsecret=config['AppSecret'],
AppID=config['AppID'],
)
elif self.config['Mode'] == "passive":
self.bot = OAClientForLongerResponse(
token=config['token'],
EncodingAESKey=config['EncodingAESKey'],
Appsecret=config['AppSecret'],
AppID=config['AppID'],
LoadingMessage=config['LoadingMessage']
)
else:
raise KeyError("请设置微信公众号通信模式")
async def reply_message(self, message_source: platform_events.FriendMessage, message: platform_message.MessageChain, quote_origin: bool = False):
global generated_content
content = await OAMessageConverter.yiri2target(
message
)
if type(self.bot) == OAClient:
await self.bot.set_message(message_source.message_chain.message_id,content)
if type(self.bot) == OAClientForLongerResponse:
from_user = message_source.sender.id
await self.bot.set_message(from_user,message_source.message_chain.message_id,content)
generated_content[message_source.message_chain.message_id] = content
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):

View File

@@ -2,15 +2,14 @@ from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
import aiocqhttp
import aiohttp
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from pkg.core import app
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
@@ -74,7 +73,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
remark = "",
)
return platform_events.FriendMessage(
sender = friend,message_chain = yiri_chain,time = event.timestamp,
sender = friend,message_chain = yiri_chain,time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
),
source_platform_object=event
)
@@ -96,7 +99,7 @@ class QQOfficialEventConverter(adapter.EventConverter):
member_name= event.t,
permission= 'MEMBER',
group = platform_entities.Group(
id = 0,
id = event.group_openid,
name = 'MEMBER',
permission= platform_entities.Permission.Member
),
@@ -105,7 +108,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0,
mute_time_remaining=0
)
time = event.timestamp
time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
)
return platform_events.GroupMessage(
sender = sender,
message_chain=yiri_chain,
@@ -119,7 +126,7 @@ class QQOfficialEventConverter(adapter.EventConverter):
member_name=event.t,
permission= 'MEMBER',
group = platform_entities.Group(
id = 0,
id = event.channel_id,
name = 'MEMBER',
permission=platform_entities.Permission.Member
),
@@ -128,7 +135,11 @@ class QQOfficialEventConverter(adapter.EventConverter):
last_speak_timestamp=0,
mute_time_remaining=0
)
time = event.timestamp,
time = int(
datetime.datetime.strptime(
event.timestamp, "%Y-%m-%dT%H:%M:%S%z"
).timestamp()
)
return platform_events.GroupMessage(
sender =sender,
message_chain = yiri_chain,

View File

@@ -0,0 +1,204 @@
from __future__ import annotations
import typing
import asyncio
import traceback
import datetime
from libs.slack_api.api import SlackClient
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.slack_api.slackevent import SlackEvent
from pkg.core import app
from .. import adapter
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 ...command.errors import ParamNotEnoughError
from ...utils import image
class SlackMessageConverter(adapter.MessageConverter):
@staticmethod
async def yiri2target(message_chain:platform_message.MessageChain):
content_list = []
for msg in message_chain:
if type(msg) is platform_message.Plain:
content_list.append({
"content":msg.text,
})
return content_list
@staticmethod
async def target2yiri(message:str,message_id:str,pic_url:str,bot:SlackClient):
yiri_msg_list = []
yiri_msg_list.append(
platform_message.Source(id=message_id,time=datetime.datetime.now())
)
if pic_url is not None:
base64_url = await image.get_slack_image_to_base64(pic_url=pic_url,bot_token=bot.bot_token)
yiri_msg_list.append(
platform_message.Image(base64=base64_url)
)
yiri_msg_list.append(platform_message.Plain(text=message))
chain = platform_message.MessageChain(yiri_msg_list)
return chain
class SlackEventConverter(adapter.EventConverter):
@staticmethod
async def yiri2target(event:platform_events.MessageEvent) -> SlackEvent:
return event.source_platform_object
@staticmethod
async def target2yiri(event:SlackEvent,bot:SlackClient):
yiri_chain = await SlackMessageConverter.target2yiri(
message=event.text,message_id=event.message_id,pic_url=event.pic_url,bot=bot
)
if event.type == 'channel':
yiri_chain.insert(0, platform_message.At(target="SlackBot"))
sender = platform_entities.GroupMember(
id = event.user_id,
member_name= str(event.sender_name),
permission= 'MEMBER',
group = platform_entities.Group(
id = event.channel_id,
name = 'MEMBER',
permission= platform_entities.Permission.Member
),
special_title='',
join_timestamp=0,
last_speak_timestamp=0,
mute_time_remaining=0
)
time = int(datetime.datetime.utcnow().timestamp())
return platform_events.GroupMessage(
sender = sender,
message_chain=yiri_chain,
time = time,
source_platform_object=event
)
if event.type == 'im':
return platform_events.FriendMessage(
sender=platform_entities.Friend(
id=event.user_id,
nickname = event.sender_name,
remark=""
),
message_chain = yiri_chain,
time = float(datetime.datetime.now().timestamp()),
source_platform_object=event,
)
class SlackAdapter(adapter.MessagePlatformAdapter):
bot: SlackClient
ap: app.Application
bot_account_id: str
message_converter: SlackMessageConverter = SlackMessageConverter()
event_converter: SlackEventConverter = SlackEventConverter()
config: dict
def __init__(self,config:dict,ap:app.Application):
self.config = config
self.ap = ap
required_keys = [
"bot_token",
"signing_secret",
]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
raise ParamNotEnoughError("Slack机器人缺少相关配置项请查看文档或联系管理员")
self.bot = SlackClient(
bot_token=self.config["bot_token"],
signing_secret=self.config["signing_secret"]
)
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
):
slack_event = await SlackEventConverter.yiri2target(
message_source
)
content_list = await SlackMessageConverter.yiri2target(message)
for content in content_list:
if slack_event.type == 'channel':
await self.bot.send_message_to_channel(
content['content'],slack_event.channel_id
)
if slack_event.type == 'im':
await self.bot.send_message_to_one(
content['content'],slack_event.user_id
)
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
content_list = await SlackMessageConverter.yiri2target(message)
for content in content_list:
if target_type == 'person':
await self.bot.send_message_to_one(content['content'],target_id)
if target_type == 'group':
await self.bot.send_message_to_channel(content['content'],target_id)
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
callback: typing.Callable[
[platform_events.Event, adapter.MessagePlatformAdapter], None
],
):
async def on_message(event:SlackEvent):
self.bot_account_id = 'SlackBot'
try:
return await callback(
await self.event_converter.target2yiri(event,self.bot),self
)
except:
traceback.print_exc()
if event_type == platform_events.FriendMessage:
self.bot.on_message("im")(on_message)
elif event_type == platform_events.GroupMessage:
self.bot.on_message("channel")(on_message)
async def run_async(self):
async def shutdown_trigger_placeholder():
while True:
await asyncio.sleep(1)
await self.bot.run_task(
host="0.0.0.0",
port=self.config["port"],
shutdown_trigger=shutdown_trigger_placeholder,
)
async def kill(self) -> bool:
return False
async def unregister_listener(
self,
event_type: type,
callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None],
):
return super().unregister_listener(event_type, callback)

View File

@@ -0,0 +1,37 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: slack
label:
en_US: Slack API
zh_CN: Slack API
description:
en_US: Slack API
zh_CN: Slack API
spec:
config:
- name: bot_token
label:
en_US: Bot Token
zh_CN: 机器人令牌
type: string
required: true
default: ""
- name: signing_secret
label:
en_US: signing_secret
zh_CN: 密钥
type: string
required: true
default: ""
- name: port
label:
en_US: Port
zh_CN: 监听端口
type: int
required: true
default: 2288
execution:
python:
path: ./slack.py
attr: SlackAdapter

View File

@@ -4,7 +4,7 @@ import telegram
import telegram.ext
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
import telegramify_markdown
import typing
import asyncio
import traceback
@@ -86,9 +86,10 @@ class TelegramMessageConverter(adapter.MessageConverter):
if message.text:
message_text = message.text
message_components.extend(parse_message_text(message_text))
if message.photo:
message_components.extend(parse_message_text(message.caption))
if message.caption:
message_components.extend(parse_message_text(message.caption))
file = await message.photo[-1].get_file()
@@ -126,7 +127,7 @@ class TelegramEventConverter(adapter.EventConverter):
time=event.message.date.timestamp(),
source_platform_object=event
)
elif event.effective_chat.type == 'group':
elif event.effective_chat.type == 'group' or 'supergroup' :
return platform_events.GroupMessage(
sender=platform_entities.GroupMember(
id=event.effective_chat.id,
@@ -201,16 +202,23 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
for component in components:
if component['type'] == 'text':
if self.config['markdown_card'] is True:
content = telegramify_markdown.markdownify(
content= component['text'],
)
else:
content = component['text']
args = {
"chat_id": message_source.source_platform_object.effective_chat.id,
"text": component['text'],
"text": content,
}
if self.config['markdown_card'] is True:
args["parse_mode"] = "MarkdownV2"
if quote_origin:
args['reply_to_message_id'] = message_source.source_platform_object.message.id
if quote_origin:
args['reply_to_message_id'] = message_source.source_platform_object.message.id
await self.bot.send_message(**args)
await self.bot.send_message(**args)
async def is_muted(self, group_id: int) -> bool:
return False

View File

@@ -2,18 +2,15 @@ from __future__ import annotations
import typing
import asyncio
import traceback
import time
import datetime
import aiocqhttp
import aiohttp
from libs.wecom_api.api import WecomClient
from pkg.platform.adapter import MessagePlatformAdapter
from pkg.platform.types import events as platform_events, message as platform_message
from libs.wecom_api.wecomevent import WecomEvent
from pkg.core import app
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
@@ -200,7 +197,19 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
async def send_message(
self, target_type: str, target_id: str, message: platform_message.MessageChain
):
pass
"""企业微信目前只有发送给个人的方法,
构造target_id的方式为前半部分为账户id后半部分为agent_id,中间使用“|”符号隔开。
"""
content_list = await WecomMessageConverter.yiri2target(message, self.bot)
parts = target_id.split("|")
user_id = parts[0]
agent_id = int(parts[1])
if target_type == 'person':
for content in content_list:
if content["type"] == "text":
await self.bot.send_private_msg(user_id,agent_id,content["content"])
if content["type"] == "image":
await self.bot.send_image(user_id,agent_id,content["media"])
def register_listener(
self,

View File

@@ -13,27 +13,20 @@ import pydantic.v1 as pydantic
class Entity(pydantic.BaseModel):
"""实体,表示一个用户或群。"""
id: int
"""QQ 号或群号"""
@abc.abstractmethod
def get_avatar_url(self) -> str:
"""头像图片链接。"""
"""ID"""
@abc.abstractmethod
def get_name(self) -> str:
"""名称。"""
class Friend(Entity):
"""好友"""
"""私聊对象"""
id: typing.Union[int, str]
"""QQ 号"""
"""ID"""
nickname: typing.Optional[str]
"""昵称。"""
remark: typing.Optional[str]
"""备注。"""
def get_avatar_url(self) -> str:
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
def get_name(self) -> str:
return self.nickname or self.remark or ''
@@ -59,8 +52,6 @@ class Group(Entity):
"""群名称。"""
permission: Permission
"""Bot 在群中的权限。"""
def get_avatar_url(self) -> str:
return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/'
def get_name(self) -> str:
return self.name
@@ -69,11 +60,11 @@ class Group(Entity):
class GroupMember(Entity):
"""群成员。"""
id: typing.Union[int, str]
"""QQ 号"""
"""群员 ID"""
member_name: str
"""员名称。"""
"""群员名称。"""
permission: Permission
"""Bot 在群中的权限。"""
"""在群中的权限。"""
group: Group
"""群。"""
special_title: str = ''
@@ -84,61 +75,6 @@ class GroupMember(Entity):
"""最后一次发言的时间。"""
mute_time_remaining: int = 0
"""禁言剩余时间。"""
def get_avatar_url(self) -> str:
return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140'
def get_name(self) -> str:
return self.member_name
class Client(Entity):
"""来自其他客户端的用户。"""
id: typing.Union[int, str]
"""识别 id。"""
platform: str
"""来源平台。"""
def get_avatar_url(self) -> str:
raise NotImplementedError
def get_name(self) -> str:
return self.platform
class Subject(pydantic.BaseModel):
"""另一种实体类型表示。"""
id: typing.Union[int, str]
"""QQ 号或群号。"""
kind: typing.Literal['Friend', 'Group', 'Stranger']
"""类型。"""
class Config(pydantic.BaseModel):
"""配置项类型。"""
def modify(self, **kwargs) -> 'Config':
"""修改部分设置。"""
for k, v in kwargs.items():
if k in self.__fields__:
setattr(self, k, v)
else:
raise ValueError(f'未知配置项: {k}')
return self
class GroupConfigModel(Config):
"""群配置。"""
name: str
"""群名称。"""
confess_talk: bool
"""是否允许坦白说。"""
allow_member_invite: bool
"""是否允许成员邀请好友入群。"""
auto_approve: bool
"""是否开启自动审批入群。"""
anonymous_chat: bool
"""是否开启匿名聊天。"""
announcement: str = ''
"""群公告。"""
class MemberInfoModel(Config, GroupMember):
"""群成员信息。"""

View File

@@ -43,21 +43,6 @@ class Event(pydantic.BaseModel):
return Event
###############################
# Bot Event
class BotEvent(Event):
"""Bot 自身事件。
Args:
type: 事件名。
qq: Bot 的 QQ 号。
"""
type: str
"""事件名。"""
qq: int
"""Bot 的 QQ 号。"""
###############################
# Message Event
class MessageEvent(Event):
@@ -72,6 +57,9 @@ class MessageEvent(Event):
message_chain: platform_message.MessageChain
"""消息内容。"""
time: float | None = None
"""消息发送时间戳。"""
source_platform_object: typing.Optional[typing.Any] = None
"""原消息平台对象。
供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息,
@@ -79,7 +67,7 @@ class MessageEvent(Event):
class FriendMessage(MessageEvent):
"""好友消息。
"""私聊消息。
Args:
type: 事件名。
@@ -111,19 +99,3 @@ class GroupMessage(MessageEvent):
@property
def group(self) -> platform_entities.Group:
return self.sender.group
class StrangerMessage(MessageEvent):
"""陌生人消息。
Args:
type: 事件名。
sender: 发送消息的人。
message_chain: 消息内容。
"""
type: str = 'StrangerMessage'
"""事件名。"""
sender: platform_entities.Friend
"""发送消息的人。"""
message_chain: platform_message.MessageChain
"""消息内容。"""

View File

@@ -116,18 +116,6 @@ class MessageChain(PlatformBaseModel):
print('At Me')
```
消息链对索引操作进行了增强。以消息组件类型为索引,获取消息链中的全部该类型的消息组件。
```py
plain_list = message_chain[Plain]
'[Plain("Hello World!")]'
```
可以用加号连接两个消息链。
```py
MessageChain(['Hello World!']) + MessageChain(['Goodbye World!'])
# 返回 MessageChain([Plain("Hello World!"), Plain("Goodbye World!")])
```
"""
__root__: typing.List[MessageComponent]
@@ -488,9 +476,9 @@ class Quote(MessageComponent):
group_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息所接收的群号当为好友消息时为0。"""
sender_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的发送者的QQ号"""
"""被引用回复的原消息的发送者的ID"""
target_id: typing.Optional[typing.Union[int, str]] = None
"""被引用回复的原消息的接收者者的QQ号或群号"""
"""被引用回复的原消息的接收者者的ID或群ID"""
origin: MessageChain
"""被引用回复的原消息的消息链对象。"""
@@ -504,7 +492,7 @@ class At(MessageComponent):
type: str = "At"
"""消息组件类型。"""
target: typing.Union[int, str]
"""群员 QQ 号"""
"""群员 ID"""
display: typing.Optional[str] = None
"""At时显示的文字发送消息时无效自动使用群名片。"""
def __eq__(self, other):
@@ -527,9 +515,9 @@ class Image(MessageComponent):
type: str = "Image"
"""消息组件类型。"""
image_id: typing.Optional[str] = None
"""图片的 image_id群图片与好友图片格式不同。不为空时将忽略 url 属性。"""
"""图片的 image_id不为空时将忽略 url 属性。"""
url: typing.Optional[pydantic.HttpUrl] = None
"""图片的 URL发送时可作网络图片的链接接收时为腾讯图片服务器的链接,可用于图片下载。"""
"""图片的 URL发送时可作网络图片的链接接收时为图片的链接,可用于图片下载。"""
path: typing.Union[str, Path, None] = None
"""图片的路径,发送本地图片。"""
base64: typing.Optional[str] = None
@@ -663,7 +651,7 @@ class Voice(MessageComponent):
voice_id: typing.Optional[str] = None
"""语音的 voice_id不为空时将忽略 url 属性。"""
url: typing.Optional[str] = None
"""语音的 URL发送时可作网络语音的链接接收时为腾讯语音服务器的链接,可用于语音下载。"""
"""语音的 URL发送时可作网络语音的链接接收时为语音文件的链接,可用于语音下载。"""
path: typing.Optional[str] = None
"""语音的路径,发送本地语音。"""
base64: typing.Optional[str] = None
@@ -691,8 +679,6 @@ class Voice(MessageComponent):
):
"""下载语音到本地。
语音采用 silk v3 格式silk 格式的编码解码请使用 [graiax-silkcoder](https://pypi.org/project/graiax-silkcoder/)。
Args:
filename: 下载到本地的文件路径。与 `directory` 二选一。
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
@@ -750,13 +736,13 @@ class Voice(MessageComponent):
class ForwardMessageNode(pydantic.BaseModel):
"""合并转发中的一条消息。"""
sender_id: typing.Optional[typing.Union[int, str]] = None
"""发送人QQ号"""
"""发送人ID"""
sender_name: typing.Optional[str] = None
"""显示名称。"""
message_chain: typing.Optional[MessageChain] = None
"""消息内容。"""
message_id: typing.Optional[int] = None
"""消息的 message_id,可以只使用此属性,从缓存中读取消息内容"""
"""消息的 message_id。"""
time: typing.Optional[datetime] = None
"""发送时间。"""
@pydantic.validator('message_chain', check_fields=False)
@@ -821,6 +807,56 @@ class File(MessageComponent):
"""文件名称。"""
size: int
"""文件大小。"""
def __str__(self):
return f'[文件]{self.name}'
# ================ 个人微信专用组件 ================
class WeChatMiniPrograms(MessageComponent):
"""小程序。个人微信专用组件。"""
type: str = 'WeChatMiniPrograms'
"""小程序id"""
mini_app_id: str
"""小程序归属用户id"""
user_name: str
"""小程序名称"""
display_name: typing.Optional[str] = ''
"""打开地址"""
page_path: typing.Optional[str] = ''
"""小程序标题"""
title: typing.Optional[str] = ''
"""首页图片"""
image_url: typing.Optional[str] = ''
class WeChatForwardMiniPrograms(MessageComponent):
"""转发小程序。个人微信专用组件。"""
type: str = 'WeChatForwardMiniPrograms'
"""xml数据"""
xml_data: str
"""首页图片"""
image_url: typing.Optional[str] = None
class WeChatEmoji(MessageComponent):
"""emoji表情。个人微信专用组件。"""
type: str = 'WeChatEmoji'
"""emojimd5"""
emoji_md5: str
"""emoji大小"""
emoji_size: int
class WeChatLink(MessageComponent):
"""发送链接。个人微信专用组件。"""
type: str = 'WeChatLink'
"""标题"""
link_title: str = ''
"""链接描述"""
link_desc: str = ''
"""链接地址"""
link_url: str = ''
"""链接略缩图"""
link_thumb_url: str = ''

View File

@@ -4,6 +4,8 @@ import re
import os
import shutil
import zipfile
import ssl
import certifi
import aiohttp
import aiofiles
@@ -21,44 +23,39 @@ class GitHubRepoInstaller(installer.PluginInstaller):
def get_github_plugin_repo_label(self, repo_url: str) -> list[str]:
"""获取username, repo"""
# 提取 username/repo , 正则表达式
repo = re.findall(
r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
repo_url,
)
if len(repo) > 0: # github
if len(repo) > 0:
return repo[0].split("/")
else:
return None
async def download_plugin_source_code(self, repo_url: str, target_path: str, task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder()) -> str:
"""下载插件源码(全异步)"""
# 提取 username/repo , 正则表达式
repo = self.get_github_plugin_repo_label(repo_url)
target_path += repo[1]
if repo is None:
raise errors.PluginInstallerError('仅支持GitHub仓库地址')
target_path += repo[1]
self.ap.logger.debug("正在下载源码...")
task_context.trace("下载源码...", "download-plugin-source-code")
zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD"
zip_resp: bytes = None
# 创建自定义SSL上下文使用certifi提供的根证书
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
url=zipball_url,
timeout=aiohttp.ClientTimeout(total=300)
timeout=aiohttp.ClientTimeout(total=300),
ssl=ssl_context # 使用自定义SSL上下文来验证证书
) as resp:
if resp.status != 200:
raise errors.PluginInstallerError(f"下载源码失败: {resp.text}")
raise errors.PluginInstallerError(f"下载源码失败: {await resp.text()}")
zip_resp = await resp.read()
if await aiofiles_os.path.exists("temp/" + target_path):
@@ -80,15 +77,11 @@ class GitHubRepoInstaller(installer.PluginInstaller):
await aiofiles_os.remove("temp/" + target_path + "/source.zip")
import glob
unzip_dir = glob.glob("temp/" + target_path + "/*")[0]
await aioshutil.copytree(unzip_dir, target_path + "/")
await aioshutil.rmtree(unzip_dir)
self.ap.logger.debug("源码下载完成。")
return repo[1]
async def install_requirements(self, path: str):
@@ -100,20 +93,14 @@ class GitHubRepoInstaller(installer.PluginInstaller):
plugin_source: str,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
):
"""安装插件
"""
"""安装插件"""
task_context.trace("下载插件源码...", "install-plugin")
repo_label = await self.download_plugin_source_code(plugin_source, "plugins/", task_context)
task_context.trace("安装插件依赖...", "install-plugin")
await self.install_requirements("plugins/" + repo_label)
task_context.trace("完成.", "install-plugin")
await self.ap.plugin_mgr.setting.record_installed_plugin_source(
"plugins/"+repo_label+'/', plugin_source
"plugins/" + repo_label + '/', plugin_source
)
async def uninstall_plugin(
@@ -121,10 +108,8 @@ class GitHubRepoInstaller(installer.PluginInstaller):
plugin_name: str,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
):
"""卸载插件
"""
"""卸载插件"""
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is None:
raise errors.PluginInstallerError('插件不存在或未成功加载')
else:
@@ -135,24 +120,18 @@ class GitHubRepoInstaller(installer.PluginInstaller):
async def update_plugin(
self,
plugin_name: str,
plugin_source: str=None,
plugin_source: str = None,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
):
"""更新插件
"""
"""更新插件"""
task_context.trace("更新插件...", "update-plugin")
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is None:
raise errors.PluginInstallerError('插件不存在或未成功加载')
else:
if plugin_container.plugin_source:
plugin_source = plugin_container.plugin_source
task_context.trace("转交安装任务.", "update-plugin")
await self.install_plugin(plugin_source, task_context)
else:
raise errors.PluginInstallerError('插件无源码信息,无法更新')
raise errors.PluginInstallerError('插件无源码信息,无法更新')

View File

@@ -4,9 +4,9 @@ import aiohttp
from . import entities, requester
from ...core import app
from ...discover import engine
from . import token
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl
from .requesters import bailianchatcmpl, chatcmpl, anthropicmsgs, moonshotchatcmpl, deepseekchatcmpl, ollamachat, giteeaichatcmpl, volcarkchatcmpl, xaichatcmpl, zhipuaichatcmpl, lmstudiochatcmpl, siliconflowchatcmpl, volcarkchatcmpl, modelscopechatcmpl
FETCH_MODEL_LIST_URL = "https://api.qchatgpt.rockchin.top/api/v2/fetch/model_list"
@@ -16,6 +16,8 @@ class ModelManager:
ap: app.Application
requester_components: list[engine.Component]
model_list: list[entities.LLMModelInfo]
requesters: dict[str, requester.LLMAPIRequester]
@@ -38,14 +40,21 @@ class ModelManager:
async def initialize(self):
self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
# 初始化token_mgr, requester
for k, v in self.ap.provider_cfg.data['keys'].items():
self.token_mgrs[k] = token.TokenManager(k, v)
for api_cls in requester.preregistered_requesters:
# for api_cls in requester.preregistered_requesters:
# api_inst = api_cls(self.ap)
# await api_inst.initialize()
# self.requesters[api_inst.name] = api_inst
for component in self.requester_components:
api_cls = component.get_python_component_class()
api_inst = api_cls(self.ap)
await api_inst.initialize()
self.requesters[api_inst.name] = api_inst
self.requesters[component.metadata.name] = api_inst
# 尝试从api获取最新的模型信息
try:

View File

@@ -10,18 +10,6 @@ from . import entities as modelmgr_entities
from ..tools import entities as tools_entities
preregistered_requesters: list[typing.Type[LLMAPIRequester]] = []
def requester_class(name: str):
def decorator(cls: typing.Type[LLMAPIRequester]) -> typing.Type[LLMAPIRequester]:
cls.name = name
preregistered_requesters.append(cls)
return cls
return decorator
class LLMAPIRequester(metaclass=abc.ABCMeta):
"""LLM API请求器
"""

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import typing
import json
import traceback
import base64
@@ -16,7 +17,6 @@ from ...tools import entities as tools_entities
from ....utils import image
@requester.requester_class("anthropic-messages")
class AnthropicMessages(requester.LLMAPIRequester):
"""Anthropic Messages API 请求器"""
@@ -25,7 +25,7 @@ class AnthropicMessages(requester.LLMAPIRequester):
async def initialize(self):
httpx_client = anthropic._base_client.AsyncHttpxClientWrapper(
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'],
base_url=self.ap.provider_cfg.data['requester']['anthropic-messages']['base-url'].replace(' ', ''),
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=typing.cast(httpx.Timeout, self.ap.provider_cfg.data['requester']['anthropic-messages']['timeout']),
limits=anthropic._constants.DEFAULT_CONNECTION_LIMITS,
@@ -59,9 +59,11 @@ class AnthropicMessages(requester.LLMAPIRequester):
if m.role == "system":
system_role_message = m
messages.pop(i)
break
if system_role_message:
messages.pop(i)
if isinstance(system_role_message, llm_entities.Message) \
and isinstance(system_role_message.content, str):
args['system'] = system_role_message.content
@@ -69,11 +71,32 @@ class AnthropicMessages(requester.LLMAPIRequester):
req_messages = []
for m in messages:
if isinstance(m.content, str) and m.content.strip() != "":
req_messages.append(m.dict(exclude_none=True))
elif isinstance(m.content, list):
if m.role == 'tool':
tool_call_id = m.tool_call_id
msg_dict = m.dict(exclude_none=True)
req_messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_call_id,
"content": m.content
}
]
})
continue
msg_dict = m.dict(exclude_none=True)
if isinstance(m.content, str) and m.content.strip() != "":
msg_dict["content"] = [
{
"type": "text",
"text": m.content
}
]
elif isinstance(m.content, list):
for i, ce in enumerate(m.content):
@@ -90,25 +113,60 @@ class AnthropicMessages(requester.LLMAPIRequester):
}
msg_dict["content"][i] = alter_image_ele
req_messages.append(msg_dict)
if m.tool_calls:
for tool_call in m.tool_calls:
msg_dict["content"].append({
"type": "tool_use",
"id": tool_call.id,
"name": tool_call.function.name,
"input": json.loads(tool_call.function.arguments)
})
del msg_dict["tool_calls"]
req_messages.append(msg_dict)
args["messages"] = req_messages
if funcs:
tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs)
# anthropic的tools处在beta阶段sdk不稳定故暂时不支持
#
# if funcs:
# tools = await self.ap.tool_mgr.generate_tools_for_openai(funcs)
# if tools:
# args["tools"] = tools
if tools:
args["tools"] = tools
try:
# print(json.dumps(args, indent=4, ensure_ascii=False))
resp = await self.client.messages.create(**args)
return llm_entities.Message(
content=resp.content[0].text,
role=resp.role
)
args = {
'content': '',
'role': resp.role,
}
assert type(resp) is anthropic.types.message.Message
for block in resp.content:
if block.type == 'thinking':
args['content'] = '<think>' + block.thinking + '</think>\n' + args['content']
elif block.type == 'text':
args['content'] += block.text
elif block.type == 'tool_use':
assert type(block) is anthropic.types.tool_use_block.ToolUseBlock
tool_call = llm_entities.ToolCall(
id=block.id,
type="function",
function=llm_entities.FunctionCall(
name=block.name,
arguments=json.dumps(block.input)
)
)
if 'tool_calls' not in args:
args['tool_calls'] = []
args['tool_calls'].append(tool_call)
return llm_entities.Message(**args)
except anthropic.AuthenticationError as e:
raise errors.RequesterError(f'api-key 无效: {e.message}')
except anthropic.BadRequestError as e:

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: anthropic-messages
label:
en_US: Anthropic
zh_CN: Anthropic
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.anthropic.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./anthropicmsgs.py
attr: AnthropicMessages

View File

@@ -2,13 +2,12 @@ from __future__ import annotations
import openai
from . import chatcmpl
from . import chatcmpl, modelscopechatcmpl
from .. import requester
from ....core import app
@requester.requester_class("bailian-chat-completions")
class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
class BailianChatCompletions(modelscopechatcmpl.ModelScopeChatCompletions):
"""阿里云百炼大模型平台 ChatCompletion API 请求器"""
client: openai.AsyncClient
@@ -19,3 +18,4 @@ class BailianChatCompletions(chatcmpl.OpenAIChatCompletions):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['bailian-chat-completions']

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: bailian-chat-completions
label:
en_US: Aliyun Bailian
zh_CN: 阿里云百炼
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://dashscope.aliyuncs.com/compatible-mode/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./bailianchatcmpl.py
attr: BailianChatCompletions

View File

@@ -20,7 +20,6 @@ from ...tools import entities as tools_entities
from ....utils import image
@requester.requester_class("openai-chat-completions")
class OpenAIChatCompletions(requester.LLMAPIRequester):
"""OpenAI ChatCompletion API 请求器"""
@@ -37,7 +36,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
self.client = openai.AsyncClient(
api_key="",
base_url=self.requester_cfg['base-url'],
base_url=self.requester_cfg['base-url'].replace(' ', ''),
timeout=self.requester_cfg['timeout'],
http_client=httpx.AsyncClient(
trust_env=True,
@@ -48,8 +47,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
async def _req(
self,
args: dict,
extra_body: dict = {},
) -> chat_completion.ChatCompletion:
return await self.client.chat.completions.create(**args)
return await self.client.chat.completions.create(**args, extra_body=extra_body)
async def _make_msg(
self,
@@ -61,6 +61,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None:
chatcmpl_message['role'] = 'assistant'
reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None
# deepseek的reasoner模型
if reasoning_content is not None:
chatcmpl_message['content'] = "<think>\n" + reasoning_content + "\n</think>\n\n"+ chatcmpl_message['content']
message = llm_entities.Message(**chatcmpl_message)
return message
@@ -74,7 +80,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args = {}
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
if use_funcs:
@@ -100,7 +106,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
args["messages"] = messages
# 发送请求
resp = await self._req(args)
resp = await self._req(args, extra_body=self.requester_cfg['args'])
# 处理请求结果
message = await self._make_msg(resp)

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: openai-chat-completions
label:
en_US: OpenAI
zh_CN: OpenAI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.openai.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./chatcmpl.py
attr: OpenAIChatCompletions

View File

@@ -7,7 +7,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities
@requester.requester_class("deepseek-chat-completions")
class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Deepseek ChatCompletion API 请求器"""
@@ -24,7 +23,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args = {}
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
if use_funcs:
@@ -44,7 +43,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
args["messages"] = messages
# 发送请求
resp = await self._req(args)
resp = await self._req(args, extra_body=self.requester_cfg['args'])
if resp is None:
raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常')

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: deepseek-chat-completions
label:
en_US: DeepSeek
zh_CN: 深度求索
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.deepseek.com"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./deepseekchatcmpl.py
attr: DeepseekChatCompletions

View File

@@ -14,7 +14,6 @@ from ...tools import entities as tools_entities
from .. import entities as modelmgr_entities
@requester.requester_class("gitee-ai-chat-completions")
class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Gitee AI ChatCompletions API 请求器"""
@@ -31,7 +30,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args = {}
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
if use_funcs:
@@ -47,7 +46,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
args["messages"] = req_messages
resp = await self._req(args)
resp = await self._req(args, extra_body=self.requester_cfg['args'])
message = await self._make_msg(resp)

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: gitee-ai-chat-completions
label:
en_US: Gitee AI
zh_CN: Gitee AI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://ai.gitee.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./giteeaichatcmpl.py
attr: GiteeAIChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app
@requester.requester_class("lmstudio-chat-completions")
class LmStudioChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LMStudio ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: lmstudio-chat-completions
label:
en_US: LM Studio
zh_CN: LM Studio
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "http://127.0.0.1:1234/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./lmstudiochatcmpl.py
attr: LmStudioChatCompletions

View File

@@ -0,0 +1,207 @@
from __future__ import annotations
import asyncio
import typing
import json
import base64
from typing import AsyncGenerator
import openai
import openai.types.chat.chat_completion as chat_completion
import openai.types.chat.chat_completion_message_tool_call as chat_completion_message_tool_call
import httpx
import aiohttp
import async_lru
from .. import entities, errors, requester
from ....core import entities as core_entities, app
from ... import entities as llm_entities
from ...tools import entities as tools_entities
from ....utils import image
class ModelScopeChatCompletions(requester.LLMAPIRequester):
"""ModelScope ChatCompletion API 请求器"""
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']
async def initialize(self):
self.client = openai.AsyncClient(
api_key="",
base_url=self.requester_cfg['base-url'],
timeout=self.requester_cfg['timeout'],
http_client=httpx.AsyncClient(
trust_env=True,
timeout=self.requester_cfg['timeout']
)
)
async def _req(
self,
args: dict,
) -> chat_completion.ChatCompletion:
args["stream"] = True
chunk = None
pending_content = ""
tool_calls = []
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args)
async for chunk in resp_gen:
# print(chunk)
if not chunk or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta:
continue
if chunk.choices[0].delta.content is not None:
pending_content += chunk.choices[0].delta.content
if chunk.choices[0].delta.tool_calls is not None:
for tool_call in chunk.choices[0].delta.tool_calls:
for tc in tool_calls:
if tc.index == tool_call.index:
tc.function.arguments += tool_call.function.arguments
break
else:
tool_calls.append(tool_call)
if chunk.choices[0].finish_reason is not None:
break
real_tool_calls = []
for tc in tool_calls:
function = chat_completion_message_tool_call.Function(
name=tc.function.name,
arguments=tc.function.arguments
)
real_tool_calls.append(chat_completion_message_tool_call.ChatCompletionMessageToolCall(
id=tc.id,
function=function,
type="function"
))
return chat_completion.ChatCompletion(
id=chunk.id,
object="chat.completion",
created=chunk.created,
choices=[
chat_completion.Choice(
index=0,
message=chat_completion.ChatCompletionMessage(
role="assistant",
content=pending_content,
tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None
),
finish_reason=chunk.choices[0].finish_reason if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None else 'stop',
logprobs=chunk.choices[0].logprobs,
)
],
model=chunk.model,
service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None,
system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None,
usage=chunk.usage if hasattr(chunk, 'usage') else None
) if chunk else None
return await self.client.chat.completions.create(**args)
async def _make_msg(
self,
chat_completion: chat_completion.ChatCompletion,
) -> llm_entities.Message:
chatcmpl_message = chat_completion.choices[0].message.dict()
# 确保 role 字段存在且不为 None
if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None:
chatcmpl_message['role'] = 'assistant'
message = llm_entities.Message(**chatcmpl_message)
return message
async def _closure(
self,
query: core_entities.Query,
req_messages: list[dict],
use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None,
) -> 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
if use_funcs:
tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs)
if tools:
args["tools"] = tools
# 设置此次请求中的messages
messages = req_messages.copy()
# 检查vision
for msg in messages:
if 'content' in msg and isinstance(msg["content"], list):
for me in msg["content"]:
if me["type"] == "image_base64":
me["image_url"] = {
"url": me["image_base64"]
}
me["type"] = "image_url"
del me["image_base64"]
args["messages"] = messages
# 发送请求
resp = await self._req(args)
# 处理请求结果
message = await self._make_msg(resp)
return message
async def call(
self,
query: core_entities.Query,
model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None,
) -> llm_entities.Message:
req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
for m in messages:
msg_dict = m.dict(exclude_none=True)
content = msg_dict.get("content")
if isinstance(content, list):
# 检查 content 列表中是否每个部分都是文本
if all(isinstance(part, dict) and part.get("type") == "text" for part in content):
# 将所有文本部分合并为一个字符串
msg_dict["content"] = "\n".join(part["text"] for part in content)
req_messages.append(msg_dict)
try:
return await self._closure(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs)
except asyncio.TimeoutError:
raise errors.RequesterError('请求超时')
except openai.BadRequestError as e:
if 'context_length_exceeded' in e.message:
raise errors.RequesterError(f'上文过长,请重置会话: {e.message}')
else:
raise errors.RequesterError(f'请求参数错误: {e.message}')
except openai.AuthenticationError as e:
raise errors.RequesterError(f'无效的 api-key: {e.message}')
except openai.NotFoundError as e:
raise errors.RequesterError(f'请求路径错误: {e.message}')
except openai.RateLimitError as e:
raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}')
except openai.APIError as e:
raise errors.RequesterError(f'请求错误: {e.message}')

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: modelscope-chat-completions
label:
en_US: ModelScope
zh_CN: 魔搭社区
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api-inference.modelscope.cn/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./modelscopechatcmpl.py
attr: ModelScopeChatCompletions

View File

@@ -9,7 +9,6 @@ from ... import entities as llm_entities
from ...tools import entities as tools_entities
@requester.requester_class("moonshot-chat-completions")
class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
"""Moonshot ChatCompletion API 请求器"""
@@ -26,7 +25,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args = {}
args["model"] = use_model.name if use_model.model_name is None else use_model.model_name
if use_funcs:
@@ -43,13 +42,13 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
if 'content' in m and isinstance(m["content"], list):
m["content"] = " ".join([c["text"] for c in m["content"]])
# 删除空的
messages = [m for m in messages if m["content"].strip() != ""]
# 删除空的,不知道干嘛的,直接删了。
# messages = [m for m in messages if m["content"].strip() != "" and ('tool_calls' not in m or not m['tool_calls'])]
args["messages"] = messages
# 发送请求
resp = await self._req(args)
resp = await self._req(args, extra_body=self.requester_cfg['args'])
# 处理请求结果
message = await self._make_msg(resp)

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: moonshot-chat-completions
label:
en_US: Moonshot
zh_CN: 月之暗面
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.moonshot.com/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./moonshotchatcmpl.py
attr: MoonshotChatCompletions

View File

@@ -20,7 +20,6 @@ from ....utils import image
REQUESTER_NAME: str = "ollama-chat"
@requester.requester_class(REQUESTER_NAME)
class OllamaChatCompletions(requester.LLMAPIRequester):
"""Ollama平台 ChatCompletion API请求器"""
client: ollama.AsyncClient

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: ollama-chat
label:
en_US: Ollama
zh_CN: Ollama
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "http://127.0.0.1:11434"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./ollamachat.py
attr: OllamaChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app
@requester.requester_class("siliconflow-chat-completions")
class SiliconFlowChatCompletions(chatcmpl.OpenAIChatCompletions):
"""SiliconFlow ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: siliconflow-chat-completions
label:
en_US: SiliconFlow
zh_CN: 硅基流动
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.siliconflow.cn/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./siliconflowchatcmpl.py
attr: SiliconFlowChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app
@requester.requester_class("volcark-chat-completions")
class VolcArkChatCompletions(chatcmpl.OpenAIChatCompletions):
"""火山方舟大模型平台 ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: volcark-chat-completions
label:
en_US: Volc Engine Ark
zh_CN: 火山方舟
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://ark.cn-beijing.volces.com/api/v3"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./volcarkchatcmpl.py
attr: VolcArkChatCompletions

View File

@@ -7,7 +7,6 @@ from .. import requester
from ....core import app
@requester.requester_class("xai-chat-completions")
class XaiChatCompletions(chatcmpl.OpenAIChatCompletions):
"""xAI ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: xai-chat-completions
label:
en_US: xAI
zh_CN: xAI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.x.ai/v1"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./xaichatcmpl.py
attr: XaiChatCompletions

View File

@@ -7,7 +7,6 @@ from . import chatcmpl
from .. import requester
@requester.requester_class("zhipuai-chat-completions")
class ZhipuAIChatCompletions(chatcmpl.OpenAIChatCompletions):
"""智谱AI ChatCompletion API 请求器"""

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: zhipuai-chat-completions
label:
en_US: ZhipuAI
zh_CN: 智谱 AI
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://open.bigmodel.cn/api/paas/v4"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./zhipuaichatcmpl.py
attr: ZhipuAIChatCompletions

View File

@@ -167,6 +167,10 @@ class DashScopeAPIRunner(runner.RequestRunner):
image_ids = [] # 用户输入的图片ID列表 (暂不支持)
plain_text, image_ids = await self._preprocess_user_message(query)
biz_params = {}
biz_params.update(self.biz_params)
biz_params.update(query.variables)
#发送对话请求
response = dashscope.Application.call(
@@ -176,7 +180,7 @@ class DashScopeAPIRunner(runner.RequestRunner):
stream=True, # 流式输出
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
session_id=query.session.using_conversation.uuid, # 会话ID用于多轮对话
biz_params=self.biz_params # 工作流应用的自定义输入参数传递
biz_params=biz_params, # 工作流应用的自定义输入参数传递
# rag_options={ # 主要用于文件交互,暂不支持
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
# }

View File

@@ -5,6 +5,7 @@ import json
import uuid
import re
import base64
import datetime
import aiohttp
@@ -69,6 +70,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
"""
plain_text = ""
image_ids = []
if isinstance(query.user_message.content, list):
for ce in query.user_message.content:
if ce.type == "text":
@@ -109,8 +111,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
basic_mode_pending_chunk = ''
inputs = {}
inputs.update(query.variables)
async for chunk in self.dify_client.chat_messages(
inputs={},
inputs=inputs,
query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
conversation_id=cov_id,
@@ -160,8 +166,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
ignored_events = ["agent_message"]
inputs = {}
inputs.update(query.variables)
async for chunk in self.dify_client.chat_messages(
inputs={},
inputs=inputs,
query=plain_text,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
response_mode="streaming",
@@ -215,6 +225,8 @@ class DifyServiceAPIRunner(runner.RequestRunner):
role="assistant",
content=[llm_entities.ContentElement.from_image_url(image_url)],
)
if chunk['event'] == 'error':
raise errors.DifyAPIError("dify 服务错误: " + chunk['message'])
query.session.using_conversation.uuid = chunk["conversation_id"]
@@ -225,8 +237,8 @@ class DifyServiceAPIRunner(runner.RequestRunner):
if not query.session.using_conversation.uuid:
query.session.using_conversation.uuid = str(uuid.uuid4())
cov_id = query.session.using_conversation.uuid
query.variables["conversation_id"] = query.session.using_conversation.uuid
plain_text, image_ids = await self._preprocess_user_message(query)
@@ -241,12 +253,17 @@ class DifyServiceAPIRunner(runner.RequestRunner):
ignored_events = ["text_chunk", "workflow_started"]
inputs = { # these variables are legacy variables, we need to keep them for compatibility
"langbot_user_message_text": plain_text,
"langbot_session_id": query.variables["session_id"],
"langbot_conversation_id": query.variables["conversation_id"],
"langbot_msg_create_time": query.variables["msg_create_time"],
}
inputs.update(query.variables)
async for chunk in self.dify_client.workflow_run(
inputs={
"langbot_user_message_text": plain_text,
"langbot_session_id": f"{query.session.launcher_type.value}_{query.session.launcher_id}",
"langbot_conversation_id": cov_id,
},
inputs=inputs,
user=f"{query.session.launcher_type.value}_{query.session.launcher_id}",
files=files,
timeout=self.ap.provider_cfg.data["dify-service-api"]["workflow"]["timeout"],

View File

@@ -54,7 +54,6 @@ class SessionManager:
use_model=await self.ap.model_mgr.get_model_by_name(self.ap.provider_cfg.data['model']),
use_funcs=await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True,
plugin_status=plugin_context.RuntimeContainerStatus.INITIALIZED,
),
)
session.conversations.append(conversation)

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
import abc
import typing
from ...core import app, entities as core_entities
from . import entities as tools_entities
preregistered_loaders: list[typing.Type[ToolLoader]] = []
def loader_class(name: str):
"""注册一个工具加载器
"""
def decorator(cls: typing.Type[ToolLoader]) -> typing.Type[ToolLoader]:
cls.name = name
preregistered_loaders.append(cls)
return cls
return decorator
class ToolLoader(abc.ABC):
"""工具加载器"""
name: str = None
ap: app.Application
def __init__(self, ap: app.Application):
self.ap = ap
async def initialize(self):
pass
@abc.abstractmethod
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
"""获取所有工具"""
pass
@abc.abstractmethod
async def has_tool(self, name: str) -> bool:
"""检查工具是否存在"""
pass
@abc.abstractmethod
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
"""执行工具调用"""
pass
@abc.abstractmethod
async def shutdown(self):
"""关闭工具"""
pass

View File

View File

@@ -0,0 +1,161 @@
from __future__ import annotations
import typing
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.sse import sse_client
from .. import loader, entities as tools_entities
from ....core import app, entities as core_entities
class RuntimeMCPSession:
"""运行时 MCP 会话"""
ap: app.Application
server_name: str
server_config: dict
session: ClientSession
exit_stack: AsyncExitStack
functions: list[tools_entities.LLMFunction] = []
def __init__(self, server_name: str, server_config: dict, ap: app.Application):
self.server_name = server_name
self.server_config = server_config
self.ap = ap
self.session = None
self.exit_stack = AsyncExitStack()
self.functions = []
async def _init_stdio_python_server(self):
server_params = StdioServerParameters(
command=self.server_config["command"],
args=self.server_config["args"],
env=self.server_config["env"],
)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
stdio, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(stdio, write)
)
await self.session.initialize()
async def _init_sse_server(self):
sse_transport = await self.exit_stack.enter_async_context(
sse_client(
self.server_config["url"],
headers=self.server_config.get("headers", {}),
timeout=self.server_config.get("timeout", 10),
)
)
sseio, write = sse_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(sseio, write)
)
await self.session.initialize()
async def initialize(self):
self.ap.logger.debug(f"初始化 MCP 会话: {self.server_name} {self.server_config}")
if self.server_config["mode"] == "stdio":
await self._init_stdio_python_server()
elif self.server_config["mode"] == "sse":
await self._init_sse_server()
else:
raise ValueError(f"无法识别 MCP 服务器类型: {self.server_name}: {self.server_config}")
tools = await self.session.list_tools()
self.ap.logger.debug(f"获取 MCP 工具: {tools}")
for tool in tools.tools:
async def func(query: core_entities.Query, **kwargs):
result = await self.session.call_tool(tool.name, kwargs)
if result.isError:
raise Exception(result.content[0].text)
return result.content[0].text
func.__name__ = tool.name
self.functions.append(tools_entities.LLMFunction(
name=tool.name,
human_desc=tool.description,
description=tool.description,
parameters=tool.inputSchema,
func=func,
))
async def shutdown(self):
"""关闭工具"""
await self.session._exit_stack.aclose()
@loader.loader_class("mcp")
class MCPLoader(loader.ToolLoader):
"""MCP 工具加载器。
在此加载器中管理所有与 MCP Server 的连接。
"""
sessions: dict[str, RuntimeMCPSession] = {}
_last_listed_functions: list[tools_entities.LLMFunction] = []
def __init__(self, ap: app.Application):
super().__init__(ap)
self.sessions = {}
self._last_listed_functions = []
async def initialize(self):
for server_config in self.ap.provider_cfg.data.get("mcp", {}).get("servers", []):
if not server_config["enable"]:
continue
session = RuntimeMCPSession(server_config["name"], server_config, self.ap)
await session.initialize()
# self.ap.event_loop.create_task(session.initialize())
self.sessions[server_config["name"]] = session
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
all_functions = []
for session in self.sessions.values():
all_functions.extend(session.functions)
self._last_listed_functions = all_functions
return all_functions
async def has_tool(self, name: str) -> bool:
return name in [f.name for f in self._last_listed_functions]
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
for server_name, session in self.sessions.items():
for function in session.functions:
if function.name == name:
return await function.func(query, **parameters)
raise ValueError(f"未找到工具: {name}")
async def shutdown(self):
"""关闭工具"""
for session in self.sessions.values():
await session.shutdown()

View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import typing
import traceback
from .. import loader, entities as tools_entities
from ....core import app, entities as core_entities
from ....plugin import context as plugin_context
@loader.loader_class("plugin-tool-loader")
class PluginToolLoader(loader.ToolLoader):
"""插件工具加载器。
本加载器中不存储工具信息,仅负责从插件系统中获取工具信息。
"""
async def get_tools(self, enabled: bool=True) -> list[tools_entities.LLMFunction]:
# 从插件系统获取工具(内容函数)
all_functions: list[tools_entities.LLMFunction] = []
for plugin in self.ap.plugin_mgr.plugins(
enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
all_functions.extend(plugin.content_functions)
return all_functions
async def has_tool(self, name: str) -> bool:
"""检查工具是否存在"""
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return True
return False
async def _get_function_and_plugin(
self, name: str
) -> typing.Tuple[tools_entities.LLMFunction, plugin_context.BasePlugin]:
"""获取函数和插件实例"""
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return function, plugin.plugin_inst
return None, None
async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any:
try:
function, plugin = await self._get_function_and_plugin(name)
if function is None:
return None
parameters = parameters.copy()
parameters = {"query": query, **parameters}
return await function.func(plugin, **parameters)
except Exception as e:
self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}")
traceback.print_exc()
return f"error occurred when executing function {name}: {e}"
finally:
plugin = None
for p in self.ap.plugin_mgr.plugins():
if function in p.content_functions:
plugin = p
break
if plugin is not None:
await self.ap.ctr_mgr.usage.post_function_record(
plugin={
"name": plugin.plugin_name,
"remote": plugin.plugin_source,
"version": plugin.plugin_version,
"author": plugin.plugin_author,
},
function_name=function.name,
function_description=function.description,
)
async def shutdown(self):
"""关闭工具"""
pass

View File

@@ -4,8 +4,9 @@ import typing
import traceback
from ...core import app, entities as core_entities
from . import entities
from . import entities, loader as tools_loader
from ...plugin import context as plugin_context
from .loaders import plugin, mcp
class ToolManager:
@@ -13,33 +14,26 @@ class ToolManager:
ap: app.Application
loaders: list[tools_loader.ToolLoader]
def __init__(self, ap: app.Application):
self.ap = ap
self.all_functions = []
self.loaders = []
async def initialize(self):
pass
async def get_function_and_plugin(
self, name: str
) -> typing.Tuple[entities.LLMFunction, plugin_context.BasePlugin]:
"""获取函数和插件实例"""
for plugin in self.ap.plugin_mgr.plugins(
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
):
for function in plugin.content_functions:
if function.name == name:
return function, plugin.plugin_inst
return None, None
for loader_cls in tools_loader.preregistered_loaders:
loader_inst = loader_cls(self.ap)
await loader_inst.initialize()
self.loaders.append(loader_inst)
async def get_all_functions(self, plugin_enabled: bool=None, plugin_status: plugin_context.RuntimeContainerStatus=None) -> list[entities.LLMFunction]:
async def get_all_functions(self, plugin_enabled: bool=None) -> list[entities.LLMFunction]:
"""获取所有函数"""
all_functions: list[entities.LLMFunction] = []
for plugin in self.ap.plugin_mgr.plugins(
enabled=plugin_enabled, status=plugin_status
):
all_functions.extend(plugin.content_functions)
for loader in self.loaders:
all_functions.extend(await loader.get_tools(plugin_enabled))
return all_functions
@@ -102,38 +96,13 @@ class ToolManager:
) -> typing.Any:
"""执行函数调用"""
try:
for loader in self.loaders:
if await loader.has_tool(name):
return await loader.invoke_tool(query, name, parameters)
else:
raise ValueError(f"未找到工具: {name}")
function, plugin = await self.get_function_and_plugin(name)
if function is None:
return None
parameters = parameters.copy()
parameters = {"query": query, **parameters}
return await function.func(plugin, **parameters)
except Exception as e:
self.ap.logger.error(f"执行函数 {name} 时发生错误: {e}")
traceback.print_exc()
return f"error occurred when executing function {name}: {e}"
finally:
plugin = None
for p in self.ap.plugin_mgr.plugins():
if function in p.content_functions:
plugin = p
break
if plugin is not None:
await self.ap.ctr_mgr.usage.post_function_record(
plugin={
"name": plugin.plugin_name,
"remote": plugin.plugin_source,
"version": plugin.plugin_version,
"author": plugin.plugin_author,
},
function_name=function.name,
function_description=function.description,
)
async def shutdown(self):
"""关闭所有工具"""
for loader in self.loaders:
await loader.shutdown()

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.9.1"
semantic_version = "v3.4.13.1"
debug_mode = False

View File

@@ -8,6 +8,113 @@ import aiohttp
import PIL.Image
import httpx
import os
import aiofiles
import pathlib
import asyncio
from urllib.parse import urlparse
async def get_gewechat_image_base64(
gewechat_url: str,
gewechat_file_url: str,
app_id: str,
xml_content: str,
token: str,
image_type: int = 2,
) -> typing.Tuple[str, str]:
"""从gewechat服务器获取图片并转换为base64格式
Args:
gewechat_url (str): gewechat服务器地址用于获取图片URL
gewechat_file_url (str): gewechat文件下载服务地址
app_id (str): gewechat应用ID
xml_content (str): 图片的XML内容
token (str): Gewechat API Token
image_type (int, optional): 图片类型. Defaults to 2.
Returns:
typing.Tuple[str, str]: (base64编码, 图片格式)
Raises:
aiohttp.ClientTimeout: 请求超时15秒或连接超时2秒
Exception: 其他错误
"""
headers = {
'X-GEWE-TOKEN': token,
'Content-Type': 'application/json'
}
# 设置超时
timeout = aiohttp.ClientTimeout(
total=15.0, # 总超时时间15秒
connect=2.0, # 连接超时2秒
sock_connect=2.0, # socket连接超时2秒
sock_read=15.0 # socket读取超时15秒
)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
# 获取图片下载链接
try:
async with session.post(
f"{gewechat_url}/v2/api/message/downloadImage",
headers=headers,
json={
"appId": app_id,
"type": image_type,
"xml": xml_content
}
) as response:
if response.status != 200:
# print(response)
raise Exception(f"获取gewechat图片下载失败: {await response.text()}")
resp_data = await response.json()
if resp_data.get("ret") != 200:
raise Exception(f"获取gewechat图片下载链接失败: {resp_data}")
file_url = resp_data['data']['fileUrl']
except asyncio.TimeoutError:
raise Exception("获取图片下载链接超时")
except aiohttp.ClientError as e:
raise Exception(f"获取图片下载链接网络错误: {str(e)}")
# 解析原始URL并替换端口
base_url = gewechat_file_url
download_url = f"{base_url}/download/{file_url}"
# 下载图片
try:
async with session.get(download_url) as img_response:
if img_response.status != 200:
raise Exception(f"下载图片失败: {await img_response.text()}, URL: {download_url}")
image_data = await img_response.read()
content_type = img_response.headers.get('Content-Type', '')
if content_type:
image_format = content_type.split('/')[-1]
else:
image_format = file_url.split('.')[-1]
base64_str = base64.b64encode(image_data).decode('utf-8')
return base64_str, image_format
except asyncio.TimeoutError:
raise Exception(f"下载图片超时, URL: {download_url}")
except aiohttp.ClientError as e:
raise Exception(f"下载图片网络错误: {str(e)}, URL: {download_url}")
except Exception as e:
raise Exception(f"获取图片失败: {str(e)}") from e
async def get_wecom_image_base64(pic_url: str) -> tuple[str, str]:
"""
下载企业微信图片并转换为 base64
@@ -105,4 +212,19 @@ async def extract_b64_and_format(image_base64_data: str) -> typing.Tuple[str, st
"""
base64_str = image_base64_data.split(',')[-1]
image_format = image_base64_data.split(':')[-1].split(';')[0].split('/')[-1]
return base64_str, image_format
return base64_str, image_format
async def get_slack_image_to_base64(pic_url:str, bot_token:str):
headers = {"Authorization": f"Bearer {bot_token}"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(pic_url, headers=headers) as resp:
image_data = await resp.read()
return base64.b64encode(image_data).decode('utf-8')
except Exception as e:
raise(e)

View File

@@ -32,6 +32,10 @@ gewechat-client
dingtalk_stream
dashscope
python-telegram-bot
certifi
mcp
slack_sdk
telegramify-markdown
# indirect
taskgroup==0.0.0a4
taskgroup==0.0.0a4
python-socks

View File

@@ -74,19 +74,29 @@
"name": "claude-3-opus-latest",
"requester": "anthropic-messages",
"token_mgr": "anthropic",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "claude-3-5-sonnet-latest",
"requester": "anthropic-messages",
"token_mgr": "anthropic",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "claude-3-5-haiku-latest",
"requester": "anthropic-messages",
"token_mgr": "anthropic",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "claude-3-7-sonnet-latest",
"requester": "anthropic-messages",
"token_mgr": "anthropic",
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "moonshot-v1-8k",
@@ -156,61 +166,72 @@
{
"name": "glm-4-plus",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-0520",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-air",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-airx",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-long",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-flashx",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4-flash",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai"
"token_mgr": "zhipuai",
"tool_call_supported": true
},
{
"name": "glm-4v-plus",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "glm-4v",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "glm-4v-flash",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
},
{
"name": "glm-zero-preview",
"requester": "zhipuai-chat-completions",
"token_mgr": "zhipuai",
"vision_supported": true
"vision_supported": true,
"tool_call_supported": true
}
]
}

View File

@@ -16,7 +16,7 @@
},
"income-msg-check": true,
"ignore-rules": {
"prefix": ["/"],
"prefix": [],
"regexp": []
},
"check-sensitive-words": true,

View File

@@ -64,33 +64,45 @@
"adapter": "gewechat",
"enable": false,
"gewechat_url": "http://your-gewechat-server:2531",
"gewechat_file_url": "http://your-gewechat-server:2532",
"port": 2286,
"callback_url": "http://your-callback-url:2286/gewechat/callback",
"app_id": "",
"token": ""
},
{
"adapter":"officialaccount",
"adapter": "officialaccount",
"enable": false,
"token": "",
"EncodingAESKey":"",
"AppID":"",
"AppSecret":"",
"EncodingAESKey": "",
"AppID": "",
"AppSecret": "",
"Mode": "drop",
"LoadingMessage": "AI正在思考中请发送任意内容获取回复。",
"host": "0.0.0.0",
"port": 2287
},
{
"adapter":"dingtalk",
"adapter": "dingtalk",
"enable": false,
"client_id":"",
"client_secret":"",
"robot_code":"",
"robot_name":""
"client_id": "",
"client_secret": "",
"robot_code": "",
"robot_name": "",
"markdown_card": false
},
{
"adapter":"telegram",
"adapter": "telegram",
"enable": false,
"token":""
"token": "",
"markdown_card": false
},
{
"adapter": "slack",
"enable": false,
"bot_token": "",
"signing_secret": "",
"port": 2288
}
],
"track-function-calls": true,

View File

@@ -31,6 +31,9 @@
],
"volcark": [
"xxxxxxxx"
],
"modelscope": [
"xxxxxxxx"
]
},
"requester": {
@@ -95,12 +98,17 @@
"args": {},
"base-url": "https://ark.cn-beijing.volces.com/api/v3",
"timeout": 120
},
"modelscope-chat-completions": {
"base-url": "https://api-inference.modelscope.cn/v1",
"args": {},
"timeout": 120
}
},
"model": "gpt-4o",
"prompt-mode": "normal",
"prompt": {
"default": ""
"default": "You are a helpful assistant."
},
"runner": "local-agent",
"dify-service-api": {
@@ -138,5 +146,8 @@
"date": "2023-08-10"
}
}
},
"mcp": {
"servers": []
}
}

View File

@@ -23,7 +23,7 @@
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d)*$"
"pattern": "^(person|group)_.*$"
},
"default": []
},
@@ -34,7 +34,7 @@
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d)*$"
"pattern": "^(person|group)_.*$"
},
"default": []
}
@@ -99,7 +99,7 @@
}
},
"patternProperties": {
"^\\d+$": {
"^.*$": {
"type": "object",
"properties": {
"at": {

View File

@@ -325,6 +325,11 @@
"default": "",
"description": "gewechat 的 url"
},
"gewechat_file_url": {
"type": "string",
"default": "",
"description": "gewechat 文件下载URL"
},
"port": {
"type": "integer",
"default": 2286,
@@ -380,6 +385,17 @@
"default": "",
"description": "微信公众号的AppSecret"
},
"Mode": {
"type": "string",
"default": "drop",
"description": "对于超过15s的响应的处理模式",
"enum": ["drop", "passive"]
},
"LoadingMessage": {
"type": "string",
"default": "AI正在思考中请发送任意内容获取回复。",
"description": "当使用被动模式时,显示给用户的提示信息"
},
"host": {
"type": "string",
"default": "0.0.0.0",
@@ -430,6 +446,11 @@
"type": "string",
"default": "",
"description": "钉钉的robot_name"
},
"markdown_card": {
"type": "boolean",
"default": false,
"description": "是否使用 Markdown 卡片发送消息"
}
}
},
@@ -450,6 +471,41 @@
"type": "string",
"default": "",
"description": "Telegram 的 token"
},
"markdown_card": {
"type": "boolean",
"default": false,
"description": "是否使用 Markdown 卡片发送消息"
}
}
},
{
"title": "Slack 适配器",
"description": "用于接入 Slack",
"properties": {
"adapter": {
"type": "string",
"const": "slack"
},
"enable": {
"type": "boolean",
"default": false,
"description": "是否启用此适配器"
},
"bot_token": {
"type": "string",
"default": "",
"description": "Slack 的 bot_token"
},
"signing_secret": {
"type": "string",
"default": "",
"description": "Slack 的 signing_secret"
},
"port": {
"type": "integer",
"default": 2288,
"description": "监听的端口"
}
}
}

View File

@@ -368,7 +368,7 @@
"type": "string",
"title": "默认情景预设",
"description": "设置默认情景预设。值为空字符串时,将不使用情景预设(人格)",
"default": ""
"default": "You are a helpful assistant."
}
},
"patternProperties": {
@@ -520,6 +520,87 @@
}
}
}
},
"mcp": {
"type": "object",
"title": "MCP 配置",
"properties": {
"servers": {
"type": "array",
"title": "MCP 服务器配置",
"default": [],
"items": {
"type": "object",
"oneOf": [
{
"title": "Stdio 模式服务器",
"properties": {
"mode": {
"type": "string",
"title": "模式",
"const": "stdio"
},
"enable": {
"type": "boolean",
"title": "启用"
},
"name": {
"type": "string",
"title": "名称"
},
"command": {
"type": "string",
"title": "启动命令"
},
"args": {
"type": "array",
"title": "启动参数",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"default": {}
}
}
},
{
"title": "SSE 模式服务器",
"properties": {
"mode": {
"type": "string",
"title": "模式",
"const": "sse"
},
"enable": {
"type": "boolean",
"title": "启用"
},
"name": {
"type": "string",
"title": "名称"
},
"url": {
"type": "string",
"title": "URL"
},
"headers": {
"type": "object",
"default": {}
},
"timeout": {
"type": "number",
"title": "请求超时时间",
"default": 10
}
}
}
]
}
}
}
}
}
}

View File

@@ -9,7 +9,7 @@
"items": {
"type": "string",
"format": "regex",
"pattern": "^(person|group)_(\\d+)$"
"pattern": "^(person|group)_.*$"
},
"default": []
},
@@ -52,7 +52,7 @@
}
},
"patternProperties": {
"^(person|group)_(\\d+)$": {
"^(person|group)_.*$": {
"type": "integer"
}
}