Compare commits

...

184 Commits

Author SHA1 Message Date
lixinyue11
1b853aa893 隐性+情绪,BUG遗漏 (#267) 2026-01-30 19:09:43 +08:00
Mark
364e01ec7a Merge pull request #255 from SuanmoSuanyangTechnology/fix/model_TimeBomb
fix(model)
2026-01-30 14:26:25 +08:00
Timebomb2018
ffb7b0ba38 fix(model):
1. create a basic model to check if the name and provider are duplicated.
2. The result shows error models because the provider created API Keys for all matching models.
2026-01-30 14:23:35 +08:00
yingzhao
095dfc2879 Merge pull request #253 from SuanmoSuanyangTechnology/fix/codeNode_zy
feat(web): code node hidden
2026-01-30 13:51:06 +08:00
yingzhao
17dea9433e Merge pull request #252 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): change form message
2026-01-30 13:50:45 +08:00
yingzhao
c285444e2f Merge pull request #251 from SuanmoSuanyangTechnology/feature/memoryApi_zy
fix(web): the memoryContent field is compatible with numbers and strings
2026-01-30 13:50:28 +08:00
zhaoying
8ba402d080 feat(web): code node hidden 2026-01-30 13:47:34 +08:00
zhaoying
88ab86734d fix(web): the memoryContent field is compatible with numbers and strings 2026-01-30 12:19:23 +08:00
zhaoying
b0d5818351 fix(web): change form message 2026-01-30 12:08:36 +08:00
Mark
8826a01d32 [add] migration script 2026-01-30 11:17:20 +08:00
Mark
a651ae6ed4 [modify] migration script 2026-01-29 20:15:25 +08:00
lixinyue11
ee50b25d06 Add/develop memory (#247)
* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射
2026-01-29 19:27:02 +08:00
yingzhao
a67be85858 Merge pull request #245 from SuanmoSuanyangTechnology/feature/model_zy
feat(web): model ui update
2026-01-29 19:05:39 +08:00
zhaoying
59c5a3973a feat(web): model ui update 2026-01-29 19:04:57 +08:00
Mark
d76d7343ff Merge pull request #244 from SuanmoSuanyangTechnology/feature/agent-tool_xjn
fix(model)
2026-01-29 18:09:40 +08:00
Timebomb2018
2b9638e7d3 fix(model): bug fix 2026-01-29 18:06:32 +08:00
lixinyue11
3459a73705 Add/develop memory (#243)
* 遗漏的历史映射

* 遗漏的历史映射
2026-01-29 17:57:27 +08:00
yingzhao
bd480a466b Merge pull request #242 from SuanmoSuanyangTechnology/feature/model_zy
feat(web): model ui update
2026-01-29 17:51:41 +08:00
zhaoying
4c34cb55b6 feat(web): model ui update 2026-01-29 17:50:57 +08:00
yingzhao
e137e4a38a Merge pull request #241 from SuanmoSuanyangTechnology/feature/model_zy
feat(web): model ui update
2026-01-29 17:36:41 +08:00
zhaoying
b5989bbc25 feat(web): model ui update 2026-01-29 17:35:54 +08:00
Mark
c31ff7ceef Merge pull request #240 from SuanmoSuanyangTechnology/add/develop_memory
Add/develop memory
2026-01-29 17:28:17 +08:00
lixinyue
75066f2827 遗漏的历史映射 2026-01-29 17:05:49 +08:00
lixinyue
303f3aefef Merge branch 'refs/heads/develop' into add/develop_memory 2026-01-29 16:58:19 +08:00
lixinyue
44fb5e0fd5 遗漏的历史映射 2026-01-29 16:56:50 +08:00
lixinyue11
17a695120a Add/develop memory (#239)
* 遗漏的历史映射

* 遗漏的历史映射

* 遗漏的历史映射
2026-01-29 16:03:44 +08:00
Mark
6dc716eaf8 Merge pull request #238 from SuanmoSuanyangTechnology/feature/agent-tool_xjn
fix(model)
2026-01-29 16:03:34 +08:00
lixinyue
194be086d4 遗漏的历史映射 2026-01-29 15:58:11 +08:00
lixinyue
c49603c25b Merge branch 'refs/heads/develop' into add/develop_memory 2026-01-29 15:53:31 +08:00
lixinyue
8de85a4041 遗漏的历史映射 2026-01-29 15:52:32 +08:00
lixinyue
58a2135fa4 遗漏的历史映射 2026-01-29 15:33:37 +08:00
Timebomb2018
ab9a97db22 fix(model): bug fix 2026-01-29 15:25:25 +08:00
Timebomb2018
d291c241d5 fix(model): the model type does not allow modification, delete tts and speech2text type 2026-01-29 15:21:06 +08:00
yingzhao
24d4cb9b94 Merge pull request #237 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): model bugfix
2026-01-29 14:59:05 +08:00
zhaoying
5b9adb799f fix(web): model bugfix 2026-01-29 14:51:27 +08:00
Mark
38b41df36b [fix] api 2026-01-29 14:41:45 +08:00
Mark
34a9befe5c Merge branch 'develop' of github.com:SuanmoSuanyangTechnology/MemoryBear into develop 2026-01-29 14:03:29 +08:00
Mark
67fd579074 [fix] api 2026-01-29 14:03:21 +08:00
Mark
e2714b942d [add]migration script 2026-01-29 13:54:38 +08:00
Mark
6b2556f870 Merge pull request #236 from SuanmoSuanyangTechnology/feature/agent-tool_xjn
fix(model)
2026-01-29 13:51:14 +08:00
Timebomb2018
43e6e9d201 fix(model): bug fix 2026-01-29 12:33:40 +08:00
yingzhao
131e0cc4c7 Merge pull request #235 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): model list remove is_active
2026-01-29 12:18:33 +08:00
zhaoying
537be81b8f fix(web): model list remove is_active 2026-01-29 12:16:45 +08:00
yingzhao
765168db7f Merge pull request #233 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): model bugfix
2026-01-29 12:11:17 +08:00
zhaoying
1e16b06a24 fix(web): model bugfix 2026-01-29 12:10:19 +08:00
Mark
cd4c93a5cb [fix] web search set for v1 api 2026-01-29 11:52:59 +08:00
Mark
808961243d [fix] chat api for workflow 2026-01-29 11:47:39 +08:00
lixinyue11
4d80e119f7 提交遗漏 (#228) 2026-01-29 10:13:55 +08:00
yingzhao
10c87edae1 Merge pull request #230 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): model bugfix
2026-01-28 20:00:25 +08:00
zhaoying
0eb335d112 fix(web): model bugfix 2026-01-28 19:58:33 +08:00
yingzhao
b8b26ccfe5 Merge pull request #229 from SuanmoSuanyangTechnology/feature/model_zy
fix(web): model bugfix
2026-01-28 18:46:27 +08:00
zhaoying
e89c23da4d fix(web): model bugfix 2026-01-28 18:41:56 +08:00
Mark
ced087f8ae Merge pull request #225 from SuanmoSuanyangTechnology/fix/memory_bug_fix
Fix/memory bug fix
2026-01-28 16:10:58 +08:00
lixinyue
0f1eed0b1e 旧数据兼容 2026-01-28 16:07:53 +08:00
lixinyue
95f15b77a3 旧数据兼容 2026-01-28 16:05:54 +08:00
lixinyue
f9ccfd5ca0 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-28 16:05:46 +08:00
lixinyue
7207d7c847 旧数据兼容 2026-01-28 16:05:35 +08:00
lixinyue
00c4a524b7 旧数据兼容 2026-01-28 16:04:38 +08:00
Mark
3127c382a4 Merge pull request #219 from SuanmoSuanyangTechnology/fix/workflow-stream
fix(workflow): fix streaming output issues with multi-output End nodes
2026-01-28 15:32:48 +08:00
Eternity
1748a390ec perf(workflow): make memory write node backward-compatible and defer config validation 2026-01-28 15:30:36 +08:00
Mark
a7c0837049 Merge branch 'develop' of github.com:SuanmoSuanyangTechnology/MemoryBear into develop 2026-01-28 15:25:11 +08:00
Mark
44bf1eeae2 [add] migrations script 2026-01-28 15:24:55 +08:00
yingzhao
762b7a8ef1 Merge pull request #224 from SuanmoSuanyangTechnology/feature/memoryApi_zy
Revert "feat(web): update read_all_config select valueKey"
2026-01-28 15:22:08 +08:00
zhaoying
102712a16e Revert "feat(web): update read_all_config select valueKey"
This reverts commit 46f0f3cee9.
2026-01-28 15:20:31 +08:00
yingzhao
40810c59d7 Merge pull request #223 from SuanmoSuanyangTechnology/fix/agent_zy
fix(web): agent's knowledge_bases bugfix
2026-01-28 15:06:38 +08:00
zhaoying
35a10e86b5 fix(web): agent's knowledge_bases bugfix 2026-01-28 15:05:12 +08:00
yingzhao
c0c985494d Merge pull request #222 from SuanmoSuanyangTechnology/feature/app_statistics_zy
feat(web): add apps statistics api
2026-01-28 14:53:02 +08:00
zhaoying
8984ba7aef feat(web): add apps statistics api 2026-01-28 14:49:30 +08:00
yingzhao
179869d481 Merge pull request #221 from SuanmoSuanyangTechnology/feature/app_statistics_zy
feat(web): add app statistics
2026-01-28 14:47:32 +08:00
yingzhao
5f29956f2b Merge pull request #213 from SuanmoSuanyangTechnology/feature/model_zy
Feature/model zy
2026-01-28 14:46:09 +08:00
Eternity
dbc4ba84c2 fix(workflow): fix streaming output issues with multi-output End nodes
End nodes with multiple output segments could cause cursor errors or leave some
segments inactive, resulting in incorrect final outputs.
Unified _emit_active_chunks and _update_scope_activate to ensure all segments
are activated in order and streamed correctly.
2026-01-28 13:02:50 +08:00
zhaoying
9e4a527675 feat(web): add app statistics 2026-01-28 11:59:37 +08:00
lixinyue
45833542a7 memory_content暂时不修改 2026-01-28 11:57:17 +08:00
lixinyue
1be6de30d7 memory_content暂时不修改 2026-01-28 11:54:07 +08:00
lixinyue
981d78c8ba 统一字段为config_id_old 2026-01-28 11:47:52 +08:00
lixinyue
fbc7bedb6c 统一字段为config_id_old 2026-01-28 11:45:51 +08:00
lixinyue
4786b0c5d4 统一字段为config_id_old 2026-01-28 11:19:24 +08:00
lixinyue
17bed26096 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-28 11:19:09 +08:00
lixinyue
511e16f1d3 统一字段为config_id_old 2026-01-28 11:18:11 +08:00
zhaoying
18204bc1f7 fix(web): model loading update 2026-01-28 11:11:28 +08:00
lixinyue
b58d97fad3 应用层memory_content->memory_config 2026-01-28 10:59:38 +08:00
lixinyue
d2a67a53b5 应用层memory_content->memory_config 2026-01-28 10:58:46 +08:00
lixinyue
c0b556000c Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-28 10:58:06 +08:00
zhaoying
462c3b0696 fix(web): correct spelling 2026-01-28 10:57:45 +08:00
lixinyue
d34ad73439 应用层memory_content->memory_config 2026-01-28 10:56:41 +08:00
zhaoying
2c21712d58 feat(web): model logo update 2026-01-28 10:50:48 +08:00
zhaoying
ce01e588c9 feat(web): remove file url replace 2026-01-28 09:55:20 +08:00
lixinyue
2a23082203 config_id做映射+1 2026-01-27 21:15:38 +08:00
lixinyue
d373f924f6 config_id做映射+1 2026-01-27 21:10:32 +08:00
lixinyue
eaf46ee006 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-27 21:10:22 +08:00
lixinyue
d51355a0ad config_id做映射+1 2026-01-27 21:09:06 +08:00
zhaoying
1e481a311a feat(web): getModelListUrl add is_active param 2026-01-27 20:33:23 +08:00
lixinyue
46abb23ee8 config_id做映射 2026-01-27 20:24:05 +08:00
lixinyue
8555bb697c Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-27 20:23:57 +08:00
lixinyue
f821893653 config_id做映射 2026-01-27 20:22:14 +08:00
zhaoying
75b3ea1f05 feat(web): update model management 2026-01-27 20:07:53 +08:00
zhaoying
74f0018962 feat(web): add PageTabs component 2026-01-27 19:17:32 +08:00
zhaoying
3a0f07d36f feat(web): add PageEmpty component 2026-01-27 19:17:11 +08:00
lixinyue
a047cf2e91 修复宿主列表获取memory_config_idBUG 2026-01-27 14:32:48 +08:00
lixinyue
a8ae16e321 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-27 14:31:28 +08:00
lixinyue
a53be31765 检查需要更改的格式问题 2026-01-27 11:41:16 +08:00
lixinyue
4475be51cc Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-27 10:27:17 +08:00
lixinyue
d53cbe7868 Merge branch 'refs/heads/develop' into fix/memory_bug_fix
# Conflicts:
#	api/app/services/memory_storage_service.py
2026-01-26 17:50:05 +08:00
lixinyue
722746c78b user_id->显示为config_id_old传输 2026-01-26 17:47:05 +08:00
lixinyue
e1f5607836 user_id->显示为config_id_old传输 2026-01-26 17:37:40 +08:00
lixinyue
7cd0d78424 user_id->显示为config_id_old传输 2026-01-26 17:21:10 +08:00
lixinyue
d740559749 Merge branch 'refs/heads/develop' into fix/memory_bug_fix
# Conflicts:
#	api/app/services/memory_storage_service.py
2026-01-26 17:08:55 +08:00
lixinyue
399357f752 user_id->现实为config_id_old 2026-01-26 17:06:55 +08:00
lixinyue
9de6b4f151 感知meta_data字段BUG修复 2026-01-26 11:06:49 +08:00
lixinyue
94cced8323 修复遗留合并BUG 2026-01-23 18:36:33 +08:00
lixinyue
9b8ed16e37 修复遗留合并BUG 2026-01-23 18:35:40 +08:00
lixinyue
a5e44cd229 修复遗留合并BUG 2026-01-23 18:34:13 +08:00
lixinyue
eccc208229 修复遗留合并BUG 2026-01-23 18:34:06 +08:00
lixinyue
79cfabb45d end_user_id清理干净 2026-01-23 17:20:32 +08:00
lixinyue
af6e1e2b99 end_user_id清理干净 2026-01-23 17:20:07 +08:00
lixinyue
4ad51c1b24 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-23 17:15:22 +08:00
lixinyue
c44712167f 解决冲突 2026-01-23 15:03:39 +08:00
lixinyue
1aabaff1f2 解决冲突 2026-01-23 15:00:09 +08:00
lixinyue
21c0383efb Merge branch 'refs/heads/develop' into fix/memory_bug_fix
# Conflicts:
#	api/app/services/memory_agent_service.py
2026-01-23 14:57:25 +08:00
lixinyue
ebe018347b 检查项目,修复group_id的遗留问题 2026-01-23 10:39:10 +08:00
lixinyue
86fe6fe5ab 检查项目,修复group_id的遗留问题 2026-01-23 10:35:41 +08:00
lixinyue
9e828b1750 config_id字段改成UUID,与develop校对恢复 2026-01-22 21:53:15 +08:00
lixinyue
940d3d4567 config_id字段改成UUID 2026-01-22 20:48:51 +08:00
lixinyue
6bd7b2b8bb Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-22 20:47:23 +08:00
lixinyue
f2d6fd7b08 config_id字段改成UUID 2026-01-22 20:40:41 +08:00
lixinyue
b84c82880c config_id字段改成UUID 2026-01-22 18:45:26 +08:00
lixinyue
fcc418b4a0 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-22 18:44:30 +08:00
lixinyue
15c0bb4c9e Merge remote-tracking branch 'origin/develop' into develop 2026-01-22 18:43:53 +08:00
lixinyue
8db4f914d8 config_config替换成memory_config 2026-01-22 18:43:22 +08:00
lixinyue
f3f9211c9c config_config替换成memory_config 2026-01-22 16:59:40 +08:00
lixinyue
a2a69840f7 config_config替换成memory_config 2026-01-22 16:38:24 +08:00
lanceyq
3a4a7590c2 [fix]Fix the memory interface to use end_user_id. 2026-01-22 16:36:12 +08:00
lixinyue
bcc8b7ce3c config_config替换成memory_config 2026-01-22 16:11:48 +08:00
lixinyue
1c7fe6d134 config_config替换成memory_config 2026-01-22 14:59:01 +08:00
lixinyue
c4039f52bd 把group_id替换end_user_id_ 2026-01-22 12:12:41 +08:00
lixinyue
bd851d5e86 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-22 12:11:43 +08:00
lixinyue
00e448c5d6 Merge remote-tracking branch 'origin/develop' into develop 2026-01-22 12:11:17 +08:00
lixinyue
4aeec8afbf 把group_id替换end_user_id_ 2026-01-21 20:37:39 +08:00
lixinyue
f10432bf3f Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-21 20:35:04 +08:00
lixinyue
f0efed8aa1 把group_id替换end_user_id 2026-01-21 20:33:22 +08:00
lixinyue
4a4931bee2 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 19:37:03 +08:00
lixinyue
afcf12ebc9 Merge remote-tracking branch 'origin/develop' into develop 2026-01-21 19:16:04 +08:00
lixinyue
8f86d3417d Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-21 11:53:52 +08:00
lixinyue
92dfc54c4c Merge remote-tracking branch 'origin/develop' into develop 2026-01-21 11:53:25 +08:00
lixinyue
c93bcb8678 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 11:27:11 +08:00
lixinyue
98b2da9123 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 11:15:18 +08:00
lixinyue
cd5f1a1b28 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 11:05:56 +08:00
lixinyue
0e2e495d09 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 11:03:37 +08:00
lixinyue
84c6c7e2a6 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察)-接口添加翻译字段 2026-01-21 10:36:04 +08:00
lixinyue
c8ebf9c75a Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-20 20:12:53 +08:00
lixinyue
29852ff0a5 新增中翻英功能(记忆时间线)(用户摘要)(兴趣分布接口)(查询核心档案)(记忆洞察) 2026-01-20 20:12:14 +08:00
lixinyue
f06ca62589 Merge branch 'refs/heads/fix/memory_bug_fix' into develop 2026-01-20 20:09:29 +08:00
lixinyue
3f39a2be12 Merge remote-tracking branch 'origin/develop' into develop 2026-01-20 20:09:14 +08:00
lixinyue
575190a96d 读取接口内层嵌套BUG修复 2026-01-20 19:14:32 +08:00
lixinyue
78559d98eb 读取接口内层嵌套BUG修复 2026-01-20 19:11:40 +08:00
lixinyue
398964c747 读取接口内层嵌套BUG修复 2026-01-20 18:51:18 +08:00
lixinyue
a634565296 读取接口内层嵌套BUG修复 2026-01-20 18:46:53 +08:00
lixinyue
a5ecbec9a6 读取接口内层嵌套BUG修复 2026-01-20 16:32:52 +08:00
lixinyue
fe79978f88 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-20 16:32:46 +08:00
lixinyue
978ec8bc75 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	api/app/services/memory_reflection_service.py
2026-01-20 16:32:27 +08:00
lixinyue
6e77f5b068 反思优化测试接口 2026-01-20 11:11:45 +08:00
lixinyue
c9dbb64269 反思优化测试接口 2026-01-20 11:10:10 +08:00
lixinyue
546d32e3eb Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-20 10:47:32 +08:00
lixinyue
616f6401b4 反思优化1.0(优化隐私输出、时间检索) 2026-01-19 18:06:56 +08:00
lixinyue
d047190453 反思优化1.0(优化隐私输出、时间检索) 2026-01-19 18:06:19 +08:00
lixinyue
17504b1b9c Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-19 18:04:29 +08:00
lixinyue
5a0d3df689 反思优化1.0(优化隐私输出、时间检索) 2026-01-19 16:28:01 +08:00
lixinyue
871304c89b 输出数组 2026-01-15 21:48:08 +08:00
lixinyue
8155150e45 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-15 21:47:48 +08:00
lixinyue
d9fb8edaa9 读取的接口,去掉全局锁 2026-01-15 16:47:55 +08:00
lixinyue
dda61679bd Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-15 16:47:37 +08:00
lixinyue
6ac10a8297 Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-15 10:23:35 +08:00
lixinyue
0695c11739 用户详情优化 2026-01-14 18:25:55 +08:00
lixinyue
7a4297c4f1 Merge branch 'refs/heads/develop' into fix/memory_bug_fix
# Conflicts:
#	api/app/services/user_memory_service.py
2026-01-14 18:25:47 +08:00
lixinyue
2c9e5df27d 用户详情优化 2026-01-14 15:34:45 +08:00
lixinyue
6db37d35ed 用户详情优化 2026-01-14 15:25:04 +08:00
lixinyue
ceee4fe5cf 用户详情优化 2026-01-14 14:54:38 +08:00
lixinyue
130b4a57de Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-14 14:54:33 +08:00
lixinyue
1cee27e830 用户详情优化 2026-01-14 14:51:20 +08:00
lixinyue
ba2ff053f9 用户详情优化 2026-01-14 14:48:37 +08:00
lixinyue
227665439f Merge branch 'refs/heads/develop' into fix/memory_bug_fix 2026-01-14 14:47:15 +08:00
lixinyue
1a2e043ec2 图谱数据量限制数量去掉 2026-01-14 14:27:05 +08:00
lixinyue
89500df0ac 图谱数据量限制数量去掉 2026-01-14 12:20:27 +08:00
lixinyue
cb4e80f1bc 图谱数据量限制数量去掉 2026-01-14 12:15:35 +08:00
94 changed files with 4837 additions and 1292 deletions

View File

@@ -7,10 +7,11 @@ Routes:
GET /memory/config/emotion - 获取情绪引擎配置
POST /memory/config/emotion - 更新情绪引擎配置
"""
import uuid
from fastapi import APIRouter, Depends, Query, HTTPException, status
from pydantic import BaseModel, Field
from typing import Optional
from typing import Optional, Union
from sqlalchemy.orm import Session
from uuid import UUID
@@ -21,6 +22,7 @@ from app.schemas.response_schema import ApiResponse
from app.services.emotion_config_service import EmotionConfigService
from app.core.logging_config import get_api_logger
from app.db import get_db
from app.utils.config_utils import resolve_config_id
# 获取API专用日志器
api_logger = get_api_logger()
@@ -37,7 +39,7 @@ class EmotionConfigQuery(BaseModel):
class EmotionConfigUpdate(BaseModel):
"""情绪配置更新请求模型"""
config_id: UUID = Field(..., description="配置ID")
config_id: Union[uuid.UUID, int, str]= Field(..., description="配置ID")
emotion_enabled: bool = Field(..., description="是否启用情绪提取")
emotion_model_id: Optional[str] = Field(None, description="情绪分析专用模型ID")
emotion_extract_keywords: bool = Field(..., description="是否提取情绪关键词")
@@ -46,7 +48,7 @@ class EmotionConfigUpdate(BaseModel):
@router.get("/read_config", response_model=ApiResponse)
def get_emotion_config(
config_id: UUID = Query(..., description="配置ID"),
config_id: UUID|int = Query(..., description="配置ID"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -79,7 +81,7 @@ def get_emotion_config(
f"用户 {current_user.username} 请求获取情绪配置",
extra={"config_id": config_id}
)
config_id=resolve_config_id(config_id, db)
# 初始化服务
config_service = EmotionConfigService(db)
@@ -158,6 +160,7 @@ def update_emotion_config(
}
}
"""
config.config_id=resolve_config_id(config.config_id, db)
try:
api_logger.info(
f"用户 {current_user.username} 请求更新情绪配置",

View File

@@ -34,7 +34,7 @@ from app.schemas.memory_storage_schema import (
)
from app.schemas.response_schema import ApiResponse
from app.services.memory_forget_service import MemoryForgetService
from app.utils.config_utils import resolve_config_id
# 获取API专用日志器
api_logger = get_api_logger()
@@ -84,7 +84,8 @@ async def trigger_forgetting_cycle(
connected_config = get_end_user_connected_config(end_user_id, db)
config_id = connected_config.get("memory_config_id")
config_id = resolve_config_id((config_id), db)
if config_id is None:
api_logger.warning(f"终端用户 {end_user_id} 未关联记忆配置")
return fail(BizCode.INVALID_PARAMETER, f"终端用户 {end_user_id} 未关联记忆配置", "memory_config_id is None")
@@ -129,7 +130,7 @@ async def trigger_forgetting_cycle(
@router.get("/read_config", response_model=ApiResponse)
async def read_forgetting_config(
config_id: UUID,
config_id: UUID|int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
@@ -158,6 +159,7 @@ async def read_forgetting_config(
)
try:
config_id=resolve_config_id(config_id, db)
# 调用服务层读取配置
config = forget_service.read_forgetting_config(db=db, config_id=config_id)
@@ -195,6 +197,8 @@ async def update_forgetting_config(
ApiResponse: 包含更新结果的响应
"""
workspace_id = current_user.current_workspace_id
payload.config_id=resolve_config_id((payload.config_id), db)
# 检查用户是否已选择工作空间
if workspace_id is None:
@@ -255,12 +259,10 @@ async def get_forgetting_stats(
ApiResponse: 包含统计信息的响应
"""
workspace_id = current_user.current_workspace_id
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试获取遗忘引擎统计但未选择工作空间")
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
# 如果提供了 end_user_id通过它获取 config_id
config_id = None
if end_user_id:
@@ -269,6 +271,7 @@ async def get_forgetting_stats(
connected_config = get_end_user_connected_config(end_user_id, db)
config_id = connected_config.get("memory_config_id")
config_id = resolve_config_id(config_id, db)
if config_id is None:
api_logger.warning(f"终端用户 {end_user_id} 未关联记忆配置")
@@ -325,7 +328,7 @@ async def get_forgetting_curve(
ApiResponse: 包含遗忘曲线数据的响应
"""
workspace_id = current_user.current_workspace_id
request.config_id = resolve_config_id((request.config_id), db)
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试获取遗忘曲线但未选择工作空间")

View File

@@ -25,6 +25,8 @@ from fastapi import APIRouter, Depends, HTTPException, status,Header
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.utils.config_utils import resolve_config_id
load_dotenv()
api_logger = get_api_logger()
@@ -43,6 +45,7 @@ async def save_reflection_config(
"""Save reflection configuration to data_comfig table"""
try:
config_id = request.config_id
config_id = resolve_config_id(config_id, db)
if not config_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -157,17 +160,20 @@ async def start_workspace_reflection(
@router.get("/reflection/configs")
async def start_reflection_configs(
config_id: uuid.UUID,
config_id: uuid.UUID|int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
"""通过config_id查询memory_config表中的反思配置信息"""
config_id = resolve_config_id(config_id, db)
try:
config_id=resolve_config_id(config_id,db)
api_logger.info(f"用户 {current_user.username} 查询反思配置config_id: {config_id}")
result = MemoryConfigRepository.query_reflection_config_by_id(db, config_id)
memory_config_id = resolve_config_id(result.config_id, db)
# 构建返回数据
reflection_config = {
"config_id": result.config_id,
"config_id": memory_config_id,
"reflection_enabled": result.enable_self_reflexion,
"reflection_period_in_hours": result.iteration_period,
"reflexion_range": result.reflexion_range,
@@ -192,7 +198,7 @@ async def start_reflection_configs(
@router.get("/reflection/run")
async def reflection_run(
config_id: UUID,
config_id: UUID|int,
language_type: str = Header(default="zh", alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
@@ -200,7 +206,7 @@ async def reflection_run(
"""Activate the reflection function for all matching applications in the workspace"""
api_logger.info(f"用户 {current_user.username} 查询反思配置config_id: {config_id}")
config_id = resolve_config_id(config_id, db)
# 使用MemoryConfigRepository查询反思配置
result = MemoryConfigRepository.query_reflection_config_by_id(db, config_id)
if not result:

View File

@@ -35,6 +35,8 @@ from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.utils.config_utils import resolve_config_id
# Get API logger
api_logger = get_api_logger()
@@ -141,7 +143,6 @@ def create_config(
db: Session = Depends(get_db),
) -> dict:
workspace_id = current_user.current_workspace_id
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试创建配置但未选择工作空间")
@@ -161,12 +162,12 @@ def create_config(
@router.delete("/delete_config", response_model=ApiResponse) # 删除数据库中的内容(按配置名称)
def delete_config(
config_id: UUID,
config_id: UUID|int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
workspace_id = current_user.current_workspace_id
config_id=resolve_config_id(config_id, db)
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试删除配置但未选择工作空间")
@@ -188,7 +189,7 @@ def update_config(
db: Session = Depends(get_db),
) -> dict:
workspace_id = current_user.current_workspace_id
payload.config_id = resolve_config_id(payload.config_id, db)
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试更新配置但未选择工作空间")
@@ -211,7 +212,7 @@ def update_config_extracted(
db: Session = Depends(get_db),
) -> dict:
workspace_id = current_user.current_workspace_id
payload.config_id = resolve_config_id(payload.config_id, db)
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试更新提取配置但未选择工作空间")
@@ -233,12 +234,12 @@ def update_config_extracted(
@router.get("/read_config_extracted", response_model=ApiResponse) # 通过查询参数读取某条配置(固定路径) 没有意义的话就删除
def read_config_extracted(
config_id: UUID,
config_id: UUID | int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
workspace_id = current_user.current_workspace_id
config_id = resolve_config_id(config_id, db)
# 检查用户是否已选择工作空间
if workspace_id is None:
api_logger.warning(f"用户 {current_user.username} 尝试读取提取配置但未选择工作空间")
@@ -286,6 +287,7 @@ async def pilot_run(
f"Pilot run requested: config_id={payload.config_id}, "
f"dialogue_text_length={len(payload.dialogue_text)}"
)
payload.config_id = resolve_config_id(payload.config_id, db)
svc = DataConfigService(db)
return StreamingResponse(
svc.pilot_run_stream(payload),

View File

@@ -7,7 +7,7 @@ from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.db import get_db
from app.dependencies import get_current_user
from app.models.models_model import ModelProvider, ModelType
from app.models.models_model import ModelProvider, ModelType, LoadBalanceStrategy
from app.models.user_model import User
from app.repositories.model_repository import ModelConfigRepository
from app.schemas import model_schema
@@ -31,7 +31,12 @@ def get_model_types():
@router.get("/provider", response_model=ApiResponse)
def get_model_providers():
return success(msg="获取模型提供商成功", data=list(ModelProvider))
providers = [p for p in ModelProvider if p != ModelProvider.COMPOSITE]
return success(msg="获取模型提供商成功", data=providers)
@router.get("/strategy", response_model=ApiResponse)
def get_model_strategies():
return success(msg="获取模型策略成功", data=list(LoadBalanceStrategy))
@router.get("", response_model=ApiResponse)
@@ -91,7 +96,7 @@ def get_model_list(
@router.get("/new", response_model=ApiResponse)
def get_model_list(
def get_model_list_new(
type: Optional[list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING"),
provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于ModelConfig)"),
is_active: Optional[bool] = Query(None, description="激活状态筛选"),
@@ -147,7 +152,7 @@ def get_model_plaza_list(
type: Optional[ModelType] = Query(None, description="模型类型"),
provider: Optional[ModelProvider] = Query(None, description="供应商"),
is_official: Optional[bool] = Query(None, description="是否官方模型"),
is_deprecated: Optional[bool] = Query(False, description="是否弃用"),
is_deprecated: Optional[bool] = Query(None, description="是否弃用"),
search: Optional[str] = Query(None, description="搜索关键词"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
@@ -198,6 +203,10 @@ def update_model_base(
):
"""更新基础模型"""
# 不允许更改type类型
if data.type is not None or data.provider is not None:
raise BusinessException("不允许更改模型类型和供应商", BizCode.INVALID_PARAMETER)
result = ModelBaseService.update_model_base(db=db, model_base_id=model_base_id, data=data)
return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型更新成功")
@@ -318,6 +327,8 @@ async def update_composite_model(
api_logger.info(f"更新组合模型请求: model_id={model_id}, 用户: {current_user.username}")
try:
if model_data.type is not None:
raise BusinessException("不允许更改模型类型和供应商", BizCode.INVALID_PARAMETER)
result_orm = await ModelConfigService.update_composite_model(db=db, model_id=model_id, model_data=model_data, tenant_id=current_user.tenant_id)
api_logger.info(f"组合模型更新成功: {result_orm.name} (ID: {model_id})")
@@ -457,11 +468,13 @@ async def create_model_api_key_by_provider(
priority=api_key_data.priority,
model_config_ids=model_config_ids
)
created_keys = await ModelApiKeyService.create_api_key_by_provider(db=db, data=create_data)
created_keys, failed_models = await ModelApiKeyService.create_api_key_by_provider(db=db, data=create_data)
api_logger.info(f"API Key创建成功: 关联{len(created_keys)}个模型")
result_list = [model_schema.ModelApiKey.model_validate(key) for key in created_keys]
return success(data=result_list, msg=f"成功为 {len(created_keys)} 个模型创建API Key")
# result_list = [model_schema.ModelApiKey.model_validate(key) for key in created_keys]
result = "API Key已存在" if len(created_keys) == 0 and len(failed_models) == 0 else \
f"成功为 {len(created_keys)} 个模型创建API Key, 失败模型列表{failed_models}"
return success(data=result, msg=f"成功为 {len(created_keys)} 个模型创建API Key")
except Exception as e:
api_logger.error(f"创建API Key失败: {str(e)}")
raise

View File

@@ -235,11 +235,11 @@ async def chat(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=new_end_user.id, # 转换为字符串
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
web_search=web_search,
memory=memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
app_id=app.id,
@@ -268,11 +268,11 @@ async def chat(
message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=new_end_user.id, # 转换为字符串
user_id=end_user_id, # 转换为字符串
variables=payload.variables,
config=config,
web_search=payload.web_search,
memory=payload.memory,
web_search=web_search,
memory=memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
app_id=app.id,

View File

@@ -28,6 +28,8 @@ from langchain.agents import create_agent
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
from langchain_core.tools import BaseTool
from app.utils.config_utils import resolve_config_id
logger = get_business_logger()
@@ -175,7 +177,6 @@ class LangChainAgent:
# messagss_list.append(f'用户:{query}。AI回复:{aimessages}')
# retrieved_content.append({query: aimessages})
# return messagss_list,retrieved_content
async def write(self, storage_type, end_user_id, user_message, ai_message, user_rag_memory_id, actual_end_user_id, actual_config_id):
"""
写入记忆(支持结构化消息)
@@ -196,48 +197,54 @@ class LangChainAgent:
2. 如果只有 user_message创建单条用户消息 [user](用于历史记忆场景)
3. 每条消息会被转换为独立的 Chunk保留 speaker 字段
"""
if storage_type == "rag":
# RAG 模式:组合消息为字符串格式(保持原有逻辑)
combined_message = f"user: {user_message}\nassistant: {ai_message}"
await write_rag(end_user_id, combined_message, user_rag_memory_id)
logger.info(f'RAG_Agent:{end_user_id};{user_rag_memory_id}')
else:
# Neo4j 模式:使用结构化消息列表
structured_messages = []
# 始终添加用户消息(如果不为空)
if user_message:
structured_messages.append({"role": "user", "content": user_message})
db = next(get_db())
try:
actual_config_id=resolve_config_id(actual_config_id, db)
# 只有当 AI 回复不为空时才添加 assistant 消息
if ai_message:
structured_messages.append({"role": "assistant", "content": ai_message})
if storage_type == "rag":
# RAG 模式:组合消息为字符串格式(保持原有逻辑)
combined_message = f"user: {user_message}\nassistant: {ai_message}"
await write_rag(end_user_id, combined_message, user_rag_memory_id)
logger.info(f'RAG_Agent:{end_user_id};{user_rag_memory_id}')
else:
# Neo4j 模式:使用结构化消息列表
structured_messages = []
# 如果没有消息,直接返回
if not structured_messages:
logger.warning(f"No messages to write for user {actual_end_user_id}")
return
# 始终添加用户消息(如果不为空)
if user_message:
structured_messages.append({"role": "user", "content": user_message})
# 调用 Celery 任务,传递结构化消息列表
# 数据流:
# 1. structured_messages 传递给 write_message_task
# 2. write_message_task 调用 memory_agent_service.write_memory
# 3. write_memory 调用 write_tools.write传递 messages 参数
# 4. write_tools.write 调用 get_chunked_dialogs传递 messages 参数
# 5. get_chunked_dialogs 为每条消息创建独立的 Chunk设置 speaker 字段
# 6. 每个 Chunk 保存到 Neo4j包含 speaker 字段
logger.info(f"[WRITE] Submitting Celery task - user={actual_end_user_id}, messages={len(structured_messages)}, config={actual_config_id}")
write_id = write_message_task.delay(
actual_end_user_id, # end_user_id: 用户ID
structured_messages, # message: 结构化消息列表 [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
actual_config_id, # config_id: 配置ID
storage_type, # storage_type: "neo4j"
user_rag_memory_id # user_rag_memory_id: RAG记忆IDNeo4j模式下不使用
)
logger.info(f"[WRITE] Celery task submitted - task_id={write_id}")
write_status = get_task_memory_write_result(str(write_id))
logger.info(f'[WRITE] Task result - user={actual_end_user_id}, status={write_status}')
# 只有当 AI 回复不为空时才添加 assistant 消息
if ai_message:
structured_messages.append({"role": "assistant", "content": ai_message})
# 如果没有消息,直接返回
if not structured_messages:
logger.warning(f"No messages to write for user {actual_end_user_id}")
return
# 调用 Celery 任务,传递结构化消息列表
# 数据流:
# 1. structured_messages 传递给 write_message_task
# 2. write_message_task 调用 memory_agent_service.write_memory
# 3. write_memory 调用 write_tools.write传递 messages 参数
# 4. write_tools.write 调用 get_chunked_dialogs传递 messages 参数
# 5. get_chunked_dialogs 为每条消息创建独立的 Chunk设置 speaker 字段
# 6. 每个 Chunk 保存到 Neo4j包含 speaker 字段
logger.info(f"[WRITE] Submitting Celery task - user={actual_end_user_id}, messages={len(structured_messages)}, config={actual_config_id}")
write_id = write_message_task.delay(
actual_end_user_id, # end_user_id: 用户ID
structured_messages, # message: 结构化消息列表 [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
actual_config_id, # config_id: 配置ID
storage_type, # storage_type: "neo4j"
user_rag_memory_id # user_rag_memory_id: RAG记忆IDNeo4j模式下不使用
)
logger.info(f"[WRITE] Celery task submitted - task_id={write_id}")
write_status = get_task_memory_write_result(str(write_id))
logger.info(f'[WRITE] Task result - user={actual_end_user_id}, status={write_status}')
finally:
db.close()
async def chat(
self,
message: str,

View File

@@ -0,0 +1 @@
"""模型配置脚本模块"""

View File

@@ -0,0 +1,174 @@
provider: bedrock
enabled: true
models:
- name: ai21
type: llm
provider: bedrock
description: AI21 Labs大语言模型completion生成模式256000上下文窗口
is_deprecated: false
is_official: true
tags:
- 大语言模型
logo: bedrock
- name: amazon nova
type: llm
provider: bedrock
description: Amazon Nova大语言模型支持智能体思考、工具调用、流式工具调用、视觉能力300000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
- vision
logo: bedrock
- name: anthropic claude
type: llm
provider: bedrock
description: Anthropic Claude大语言模型支持智能体思考、视觉能力、工具调用、流式工具调用、文档处理200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- vision
- tool-call
- stream-tool-call
- document
logo: bedrock
- name: cohere
type: llm
provider: bedrock
description: Cohere大语言模型支持智能体思考、工具调用、流式工具调用128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
logo: bedrock
- name: deepseek
type: llm
provider: bedrock
description: DeepSeek大语言模型支持智能体思考、视觉能力、工具调用、流式工具调用32768上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- vision
- tool-call
- stream-tool-call
logo: bedrock
- name: meta
type: llm
provider: bedrock
description: Meta Llama大语言模型支持智能体思考、工具调用128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
logo: bedrock
- name: mistral
type: llm
provider: bedrock
description: Mistral AI大语言模型支持智能体思考、工具调用32000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
logo: bedrock
- name: openai
type: llm
provider: bedrock
description: OpenAI大语言模型支持智能体思考、工具调用、流式工具调用32768上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
logo: bedrock
- name: qwen
type: llm
provider: bedrock
description: Qwen大语言模型支持智能体思考、工具调用、流式工具调用32768上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
logo: bedrock
- name: amazon.rerank-v1:0
type: rerank
provider: bedrock
description: amazon.rerank-v1:0重排序模型5120上下文窗口
is_deprecated: false
is_official: true
tags:
- 重排序模型
logo: bedrock
- name: cohere.rerank-v3-5:0
type: rerank
provider: bedrock
description: cohere.rerank-v3-5:0重排序模型5120上下文窗口
is_deprecated: false
is_official: true
tags:
- 重排序模型
logo: bedrock
- name: amazon.nova-2-multimodal-embeddings-v1:0
type: embedding
provider: bedrock
description: amazon.nova-2-multimodal-embeddings-v1:0文本嵌入模型支持视觉能力8192上下文窗口
is_deprecated: false
is_official: true
tags:
- 文本嵌入模型
- vision
logo: bedrock
- name: amazon.titan-embed-text-v1
type: embedding
provider: bedrock
description: amazon.titan-embed-text-v1文本嵌入模型8192上下文窗口
is_deprecated: false
is_official: true
tags:
- 文本嵌入模型
logo: bedrock
- name: amazon.titan-embed-text-v2:0
type: embedding
provider: bedrock
description: amazon.titan-embed-text-v2:0文本嵌入模型8192上下文窗口
is_deprecated: false
is_official: true
tags:
- 文本嵌入模型
logo: bedrock
- name: cohere.embed-english-v3
type: embedding
provider: bedrock
description: Cohere Embed 3 English文本嵌入模型512上下文窗口
is_deprecated: false
is_official: true
tags:
- 文本嵌入模型
logo: bedrock
- name: cohere.embed-multilingual-v3
type: embedding
provider: bedrock
description: Cohere Embed 3 Multilingual文本嵌入模型512上下文窗口
is_deprecated: false
is_official: true
tags:
- 文本嵌入模型
logo: bedrock

View File

@@ -0,0 +1,820 @@
provider: dashscope
enabled: true
models:
- name: deepseek-r1-distill-qwen-14b
type: llm
provider: dashscope
description: DeepSeek-R1-Distill-Qwen-14B大语言模型支持智能体思考32000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-r1-distill-qwen-32b
type: llm
provider: dashscope
description: DeepSeek-R1-Distill-Qwen-32B大语言模型支持智能体思考32000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-r1
type: llm
provider: dashscope
description: DeepSeek-R1大语言模型支持智能体思考131072超大上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-v3.1
type: llm
provider: dashscope
description: DeepSeek-V3.1大语言模型支持智能体思考131072超大上下文窗口对话模式支持丰富生成参数调节
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-v3.2-exp
type: llm
provider: dashscope
description: DeepSeek-V3.2-exp实验版大语言模型支持智能体思考131072超大上下文窗口对话模式支持丰富生成参数调节
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-v3.2
type: llm
provider: dashscope
description: DeepSeek-V3.2大语言模型支持智能体思考131072超大上下文窗口对话模式支持丰富生成参数调节
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: deepseek-v3
type: llm
provider: dashscope
description: DeepSeek-V3大语言模型支持智能体思考64000上下文窗口对话模式支持文本与JSON格式输出
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
logo: dashscope
- name: farui-plus
type: llm
provider: dashscope
description: farui-plus大语言模型支持多工具调用、智能体思考、流式工具调用12288上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: glm-4.7
type: llm
provider: dashscope
description: GLM-4.7大语言模型支持多工具调用、智能体思考、流式工具调用202752超大上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qvq-max-latest
type: llm
provider: dashscope
description: qvq-max-latest大语言模型支持视觉、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- vision
- agent-thought
- stream-tool-call
logo: dashscope
- name: qvq-max
type: llm
provider: dashscope
description: qvq-max大语言模型支持视觉、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- vision
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-coder-turbo-0919
type: llm
provider: dashscope
description: qwen-coder-turbo-0919代码专用大语言模型支持智能体思考131072上下文窗口对话模式已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- 代码模型
- agent-thought
logo: dashscope
- name: qwen-max-latest
type: llm
provider: dashscope
description: qwen-max-latest大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-max-longcontext
type: llm
provider: dashscope
description: qwen-max-longcontext长上下文大语言模型支持多工具调用、智能体思考、流式工具调用32000上下文窗口对话模式已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-max
type: llm
provider: dashscope
description: qwen-max大语言模型支持多工具调用、智能体思考、流式工具调用32768上下文窗口对话模式支持联网搜索
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-mt-plus
type: llm
provider: dashscope
description: qwen-mt-plus多语言翻译大语言模型支持智能体思考16384上下文窗口对话模式支持多语种互译与领域翻译适配
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 翻译模型
- agent-thought
logo: dashscope
- name: qwen-mt-turbo
type: llm
provider: dashscope
description: qwen-mt-turbo轻量化多语言翻译大语言模型支持智能体思考16384上下文窗口对话模式支持多语种互译与领域翻译适配
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 翻译模型
- agent-thought
logo: dashscope
- name: qwen-plus-0112
type: llm
provider: dashscope
description: qwen-plus-0112大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-0125
type: llm
provider: dashscope
description: qwen-plus-0125大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-0723
type: llm
provider: dashscope
description: qwen-plus-0723大语言模型支持多工具调用、智能体思考、流式工具调用32000上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-0806
type: llm
provider: dashscope
description: qwen-plus-0806大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-0919
type: llm
provider: dashscope
description: qwen-plus-0919大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-1125
type: llm
provider: dashscope
description: qwen-plus-1125大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-1127
type: llm
provider: dashscope
description: qwen-plus-1127大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式支持联网搜索已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-plus-1220
type: llm
provider: dashscope
description: qwen-plus-1220大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen-vl-max
type: llm
provider: dashscope
description: qwen-vl-max多模态大模型支持视觉理解、智能体思考、视频理解131072上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen-vl-plus-0809
type: llm
provider: dashscope
description: qwen-vl-plus-0809多模态大模型支持视觉理解、智能体思考、视频理解32768上下文窗口对话模式已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen-vl-plus-2025-01-02
type: llm
provider: dashscope
description: qwen-vl-plus-2025-01-02多模态大模型支持视觉理解、智能体思考、视频理解32768上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen-vl-plus-2025-01-25
type: llm
provider: dashscope
description: qwen-vl-plus-2025-01-25多模态大模型支持视觉理解、智能体思考、视频理解131072上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen-vl-plus-latest
type: llm
provider: dashscope
description: qwen-vl-plus-latest多模态大模型支持视觉理解、智能体思考、视频理解131072上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen-vl-plus
type: llm
provider: dashscope
description: qwen-vl-plus多模态大模型支持视觉理解、智能体思考、视频理解131072上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen2.5-0.5b-instruct
type: llm
provider: dashscope
description: qwen2.5-0.5b-instruct大语言模型支持多工具调用、智能体思考、流式工具调用32768上下文窗口对话模式未废弃
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-14b
type: llm
provider: dashscope
description: qwen3-14b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-235b-a22b-instruct-2507
type: llm
provider: dashscope
description: qwen3-235b-a22b-instruct-2507大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-235b-a22b-thinking-2507
type: llm
provider: dashscope
description: qwen3-235b-a22b-thinking-2507大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-235b-a22b
type: llm
provider: dashscope
description: qwen3-235b-a22b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-30b-a3b-instruct-2507
type: llm
provider: dashscope
description: qwen3-30b-a3b-instruct-2507大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-30b-a3b
type: llm
provider: dashscope
description: qwen3-30b-a3b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-32b
type: llm
provider: dashscope
description: qwen3-32b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-4b
type: llm
provider: dashscope
description: qwen3-4b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-8b
type: llm
provider: dashscope
description: qwen3-8b大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-coder-30b-a3b-instruct
type: llm
provider: dashscope
description: qwen3-coder-30b-a3b-instruct大语言模型支持智能体思考262144上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 代码模型
- agent-thought
logo: dashscope
- name: qwen3-coder-480b-a35b-instruct
type: llm
provider: dashscope
description: qwen3-coder-480b-a35b-instruct大语言模型支持智能体思考262144上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 代码模型
- agent-thought
logo: dashscope
- name: qwen3-coder-plus-2025-09-23
type: llm
provider: dashscope
description: qwen3-coder-plus-2025-09-23大语言模型支持智能体思考1000000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 代码模型
- agent-thought
logo: dashscope
- name: qwen3-coder-plus
type: llm
provider: dashscope
description: qwen3-coder-plus大语言模型支持智能体思考1000000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 代码模型
- agent-thought
logo: dashscope
- name: qwen3-max-2025-09-23
type: llm
provider: dashscope
description: qwen3-max-2025-09-23大语言模型支持多工具调用、智能体思考、流式工具调用262144上下文窗口对话模式支持联网搜索
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- 联网搜索
logo: dashscope
- name: qwen3-max-2026-01-23
type: llm
provider: dashscope
description: qwen3-max-2026-01-23大语言模型支持多工具调用、智能体思考、流式工具调用262144上下文窗口对话模式支持联网搜索
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- 联网搜索
logo: dashscope
- name: qwen3-max-preview
type: llm
provider: dashscope
description: qwen3-max-preview大语言模型支持多工具调用、智能体思考、流式工具调用262144上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-max
type: llm
provider: dashscope
description: qwen3-max大语言模型支持多工具调用、智能体思考、流式工具调用262144上下文窗口对话模式支持联网搜索
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- 联网搜索
logo: dashscope
- name: qwen3-next-80b-a3b-instruct
type: llm
provider: dashscope
description: qwen3-next-80b-a3b-instruct大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-next-80b-a3b-thinking
type: llm
provider: dashscope
description: qwen3-next-80b-a3b-thinking大语言模型支持多工具调用、智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwen3-omni-flash-2025-12-01
type: llm
provider: dashscope
description: qwen3-omni-flash-2025-12-01多模态大语言模型支持视觉、智能体思考、视频、音频能力65536上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
- audio
logo: dashscope
- name: qwen3-vl-235b-a22b-instruct
type: llm
provider: dashscope
description: qwen3-vl-235b-a22b-instruct多模态大语言模型支持多工具调用、智能体思考、流式工具调用、视觉、视频能力131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- video
logo: dashscope
- name: qwen3-vl-235b-a22b-thinking
type: llm
provider: dashscope
description: qwen3-vl-235b-a22b-thinking多模态大语言模型支持多工具调用、智能体思考、流式工具调用、视觉、视频能力131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- video
logo: dashscope
- name: qwen3-vl-30b-a3b-instruct
type: llm
provider: dashscope
description: qwen3-vl-30b-a3b-instruct多模态大语言模型支持多工具调用、智能体思考、流式工具调用、视觉、视频能力131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- video
logo: dashscope
- name: qwen3-vl-30b-a3b-thinking
type: llm
provider: dashscope
description: qwen3-vl-30b-a3b-thinking多模态大语言模型支持多工具调用、智能体思考、流式工具调用、视觉、视频能力131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- video
logo: dashscope
- name: qwen3-vl-flash
type: llm
provider: dashscope
description: qwen3-vl-flash多模态大语言模型支持多工具调用、智能体思考、流式工具调用、视觉、视频能力131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- video
logo: dashscope
- name: qwen3-vl-plus-2025-09-23
type: llm
provider: dashscope
description: qwen3-vl-plus-2025-09-23多模态大语言模型支持视觉、智能体思考、视频能力262144上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwen3-vl-plus
type: llm
provider: dashscope
description: qwen3-vl-plus多模态大语言模型支持视觉、智能体思考、视频能力262144上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- 多模态模型
- vision
- agent-thought
- video
logo: dashscope
- name: qwq-32b
type: llm
provider: dashscope
description: qwq-32b大语言模型支持智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwq-plus-0305
type: llm
provider: dashscope
description: qwq-plus-0305大语言模型支持智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- stream-tool-call
logo: dashscope
- name: qwq-plus
type: llm
provider: dashscope
description: qwq-plus大语言模型支持智能体思考、流式工具调用131072上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- stream-tool-call
logo: dashscope
- name: gte-rerank-v2
type: rerank
provider: dashscope
description: gte-rerank-v2重排序模型4000上下文窗口
is_deprecated: false
is_official: true
tags:
- 重排序模型
logo: dashscope
- name: gte-rerank
type: rerank
provider: dashscope
description: gte-rerank重排序模型4000上下文窗口
is_deprecated: false
is_official: true
tags:
- 重排序模型
logo: dashscope
- name: multimodal-embedding-v1
type: embedding
provider: dashscope
description: multimodal-embedding-v1多模态嵌入模型支持视觉能力8192上下文窗口最大分块数10
is_deprecated: false
is_official: true
tags:
- 嵌入模型
- 多模态模型
- vision
logo: dashscope
- name: text-embedding-v1
type: embedding
provider: dashscope
description: text-embedding-v1文本嵌入模型2048上下文窗口最大分块数25
is_deprecated: false
is_official: true
tags:
- 嵌入模型
- 文本嵌入
logo: dashscope
- name: text-embedding-v2
type: embedding
provider: dashscope
description: text-embedding-v2文本嵌入模型2048上下文窗口最大分块数25
is_deprecated: false
is_official: true
tags:
- 嵌入模型
- 文本嵌入
logo: dashscope
- name: text-embedding-v3
type: embedding
provider: dashscope
description: text-embedding-v3文本嵌入模型8192上下文窗口最大分块数10
is_deprecated: false
is_official: true
tags:
- 嵌入模型
- 文本嵌入
logo: dashscope
- name: text-embedding-v4
type: embedding
provider: dashscope
description: text-embedding-v4文本嵌入模型8192上下文窗口最大分块数10
is_deprecated: false
is_official: true
tags:
- 嵌入模型
- 文本嵌入
logo: dashscope

View File

@@ -0,0 +1,143 @@
"""模型配置加载器 - 用于将预定义模型批量导入到数据库"""
import os
from pathlib import Path
from typing import Callable
import yaml
from sqlalchemy.orm import Session
from app.models.models_model import ModelBase, ModelProvider
def _load_yaml_config(provider: ModelProvider) -> list[dict]:
"""从YAML文件加载指定供应商的模型配置"""
config_dir = Path(__file__).parent
config_file = config_dir / f"{provider.value}_models.yaml"
if not config_file.exists():
return []
with open(config_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
# 检查是否需要加载(默认为 true
if not data.get('enabled', True):
return []
return data.get('models', [])
def _disable_yaml_config(provider: ModelProvider) -> None:
"""将YAML文件的enabled标志设置为false"""
config_dir = Path(__file__).parent
config_file = config_dir / f"{provider.value}_models.yaml"
if not config_file.exists():
return
with open(config_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
data['enabled'] = False
with open(config_file, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True, sort_keys=False)
def load_models(db: Session, providers: list[str] = None, silent: bool = False) -> dict:
"""
加载模型配置到数据库
Args:
db: 数据库会话
providers: 要加载的供应商列表None表示加载所有
silent: 是否静默模式(不输出详细日志)
Returns:
dict: 加载结果统计 {"success": int, "skipped": int, "failed": int}
"""
result = {"success": 0, "skipped": 0, "failed": 0}
# 确定要加载的供应商
if providers:
target_providers = [ModelProvider(p) if isinstance(p, str) else p for p in providers]
else:
target_providers = [p for p in ModelProvider if p != ModelProvider.COMPOSITE]
for provider in target_providers:
# 从YAML文件加载模型配置
models = _load_yaml_config(provider)
if not models:
if not silent:
print(f"警告: 供应商 '{provider.value}' 暂无预定义模型")
continue
if not silent:
print(f"\n正在加载 {provider.value}{len(models)} 个模型...")
# provider_success = 0
for model_data in models:
try:
# 检查模型是否已存在
existing = db.query(ModelBase).filter(
ModelBase.name == model_data["name"],
ModelBase.provider == model_data["provider"]
).first()
if existing:
# 更新现有模型配置
for key, value in model_data.items():
setattr(existing, key, value)
db.commit()
if not silent:
print(f"更新成功: {model_data['name']}")
result["success"] += 1
# provider_success += 1
else:
# 创建新模型
model = ModelBase(**model_data)
db.add(model)
db.commit()
if not silent:
print(f"添加成功: {model_data['name']}")
result["success"] += 1
# provider_success += 1
except Exception as e:
db.rollback()
if not silent:
print(f"添加失败: {model_data['name']} - {str(e)}")
result["failed"] += 1
# 如果该供应商的模型全部加载成功将enabled设置为false
# if provider_success == len(models):
_disable_yaml_config(provider)
return result
def load_models_by_provider(db: Session, provider: str) -> dict:
"""
加载指定供应商的模型配置
Args:
db: 数据库会话
provider: 供应商名称字符串或ModelProvider枚举
Returns:
dict: 加载结果统计
"""
provider_enum = ModelProvider(provider) if isinstance(provider, str) else provider
return load_models(db, providers=[provider_enum])
def get_available_providers() -> list[Callable[[], str]]:
"""获取所有可用的供应商列表从ModelProvider枚举获取排除COMPOSITE"""
return [p.value for p in ModelProvider if p != ModelProvider.COMPOSITE]
def get_models_by_provider(provider: str) -> list[dict]:
"""获取指定供应商的模型配置列表"""
provider_enum = ModelProvider(provider) if isinstance(provider, str) else provider
return _load_yaml_config(provider_enum)

View File

@@ -0,0 +1,294 @@
provider: openai
enabled: true
models:
- name: chatgpt-4o-latest
type: llm
provider: openai
description: chatgpt-4o-latest大语言模型支持多工具调用、智能体思考、流式工具调用、视觉能力128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
logo: openai
- name: gpt-3.5-turbo-0125
type: llm
provider: openai
description: gpt-3.5-turbo-0125大语言模型支持多工具调用、智能体思考、流式工具调用16385上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-3.5-turbo-1106
type: llm
provider: openai
description: gpt-3.5-turbo-1106大语言模型支持多工具调用、智能体思考、流式工具调用16385上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-3.5-turbo-16k
type: llm
provider: openai
description: gpt-3.5-turbo-16k大语言模型支持多工具调用、智能体思考、流式工具调用16385上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-3.5-turbo-instruct
type: llm
provider: openai
description: gpt-3.5-turbo-instruct大语言模型4096上下文窗口文本补全模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
logo: openai
- name: gpt-3.5-turbo
type: llm
provider: openai
description: gpt-3.5-turbo大语言模型支持多工具调用、智能体思考、流式工具调用16385上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-4-0125-preview
type: llm
provider: openai
description: gpt-4-0125-preview大语言模型支持多工具调用、智能体思考、流式工具调用128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-4-1106-preview
type: llm
provider: openai
description: gpt-4-1106-preview大语言模型支持多工具调用、智能体思考、流式工具调用128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-4-turbo-2024-04-09
type: llm
provider: openai
description: gpt-4-turbo-2024-04-09大语言模型支持多工具调用、智能体思考、流式工具调用、视觉能力128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
logo: openai
- name: gpt-4-turbo-preview
type: llm
provider: openai
description: gpt-4-turbo-preview大语言模型支持多工具调用、智能体思考、流式工具调用128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
logo: openai
- name: gpt-4-turbo
type: llm
provider: openai
description: gpt-4-turbo大语言模型支持多工具调用、智能体思考、流式工具调用、视觉能力128000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
logo: openai
- name: o1-preview
type: llm
provider: openai
description: o1-preview大语言模型支持智能体思考128000上下文窗口对话模式已废弃
is_deprecated: true
is_official: true
tags:
- 大语言模型
- agent-thought
logo: openai
- name: o1
type: llm
provider: openai
description: o1大语言模型支持多工具调用、智能体思考、流式工具调用、视觉能力、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- multi-tool-call
- agent-thought
- stream-tool-call
- vision
- structured-output
logo: openai
- name: o3-2025-04-16
type: llm
provider: openai
description: o3-2025-04-16大语言模型支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- vision
- stream-tool-call
- structured-output
logo: openai
- name: o3-mini-2025-01-31
type: llm
provider: openai
description: o3-mini-2025-01-31大语言模型支持智能体思考、工具调用、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
- structured-output
logo: openai
- name: o3-mini
type: llm
provider: openai
description: o3-mini大语言模型支持智能体思考、工具调用、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- stream-tool-call
- structured-output
logo: openai
- name: o3-pro-2025-06-10
type: llm
provider: openai
description: o3-pro-2025-06-10大语言模型支持智能体思考、工具调用、视觉能力、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- vision
- structured-output
logo: openai
- name: o3-pro
type: llm
provider: openai
description: o3-pro大语言模型支持智能体思考、工具调用、视觉能力、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- vision
- structured-output
logo: openai
- name: o3
type: llm
provider: openai
description: o3大语言模型支持智能体思考、视觉能力、工具调用、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- vision
- tool-call
- stream-tool-call
- structured-output
logo: openai
- name: o4-mini-2025-04-16
type: llm
provider: openai
description: o4-mini-2025-04-16大语言模型支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- vision
- stream-tool-call
- structured-output
logo: openai
- name: o4-mini
type: llm
provider: openai
description: o4-mini大语言模型支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出200000上下文窗口对话模式
is_deprecated: false
is_official: true
tags:
- 大语言模型
- agent-thought
- tool-call
- vision
- stream-tool-call
- structured-output
logo: openai
- name: text-embedding-3-large
type: embedding
provider: openai
description: text-embedding-3-large文本向量模型8191上下文窗口最大分块数32
is_deprecated: false
is_official: true
tags:
- 文本向量模型
logo: openai
- name: text-embedding-3-small
type: embedding
provider: openai
description: text-embedding-3-small文本向量模型8191上下文窗口最大分块数32
is_deprecated: false
is_official: true
tags:
- 文本向量模型
logo: openai
- name: text-embedding-ada-002
type: embedding
provider: openai
description: text-embedding-ada-002文本向量模型8097上下文窗口最大分块数32
is_deprecated: false
is_official: true
tags:
- 文本向量模型
logo: openai

View File

@@ -16,7 +16,6 @@ from app.core.workflow.graph_builder import GraphBuilder, StreamOutputConfig
from app.core.workflow.nodes import WorkflowState
from app.core.workflow.nodes.base_config import VariableType
from app.core.workflow.nodes.enums import NodeType
from app.core.workflow.template_renderer import render_template
logger = logging.getLogger(__name__)
@@ -157,12 +156,137 @@ class WorkflowExecutor:
"error": result.get("error"),
}
def _update_end_activate(self, node_id):
def _update_scope_activate(self, scope, status=None):
"""
Update the activation state of all End nodes based on a completed scope (node or variable).
Iterates over all End nodes in `self.end_outputs` and calls
`update_activate` on each, which may:
- Activate variable segments that depend on the completed node/scope.
- Activate the entire End node output if all control conditions are met.
If any End node becomes active and `self.activate_end` is not yet set,
this node will be marked as the currently active End node.
Args:
scope (str): The node ID or scope that has completed execution.
status (str | None): Optional status of the node (used for branch/control nodes).
"""
for node in self.end_outputs.keys():
self.end_outputs[node].update_activate(node_id)
self.end_outputs[node].update_activate(scope, status)
if self.end_outputs[node].activate and self.activate_end is None:
self.activate_end = node
def _update_stream_output_status(self, activate, data):
"""
Update the stream output state of End nodes based on workflow state updates.
This method checks which nodes/scopes are activated and propagates
activation to End nodes accordingly.
Args:
activate (dict): Mapping of node_id -> bool indicating which nodes/scopes are activated.
data (dict): Mapping of node_id -> node runtime data, including outputs.
Behavior:
For each node in `data`:
1. If the node is activated (`activate[node_id]` is True),
retrieve its output status from `runtime_vars`.
2. Call `_update_scope_activate` to propagate the activation
to all relevant End nodes and update `self.activate_end`.
"""
for node_id in data.keys():
if activate.get(node_id):
node_output_status = (
data[node_id]
.get('runtime_vars', {})
.get(node_id)
.get("output")
)
self._update_scope_activate(node_id, status=node_output_status)
async def _emit_active_chunks(
self,
node_outputs: dict,
variables: dict,
force=False
):
"""
Process and yield all currently active output segments for the currently active End node.
This method handles stream-mode output for an End node by iterating through its output segments
(`OutputContent`). Only segments marked as active (`activate=True`) are processed, unless
`force=True`, which allows all segments to be processed regardless of their activation state.
Behavior:
1. Iterates from the current `cursor` position to the end of the outputs list.
2. For each segment:
- If the segment is literal text (`is_variable=False`), append it directly.
- If the segment is a variable (`is_variable=True`), evaluate it using
`evaluate_expression` with the given `node_outputs` and `variables`,
then transform the result with `_trans_output_string`.
3. Yield a stream event of type "message" containing the processed chunk.
4. Move the `cursor` forward after processing each segment.
5. When all segments have been processed, remove this End node from `end_outputs`
and reset `activate_end` to None.
Args:
node_outputs (dict): Current runtime node outputs, used for variable evaluation.
variables (dict): Current runtime variables, used for variable evaluation.
force (bool, default=False): If True, process segments even if `activate=False`.
Yields:
dict: A stream event of type "message" containing the processed chunk.
Notes:
- Segments that fail evaluation (ValueError) are skipped with a warning logged.
- This method only processes the currently active End node (`self.activate_end`).
- Use `force=True` for final emission regardless of activation state.
"""
end_info = self.end_outputs[self.activate_end]
while end_info.cursor < len(end_info.outputs):
final_chunk = ''
current_segment = end_info.outputs[end_info.cursor]
if not current_segment.activate and not force:
# Stop processing until this segment becomes active
break
# Literal segment
if not current_segment.is_variable:
final_chunk += current_segment.literal
else:
# Variable segment: evaluate and transform
try:
chunk = evaluate_expression(
current_segment.literal,
variables=variables,
node_outputs=node_outputs
)
chunk = self._trans_output_string(chunk)
final_chunk += chunk
except ValueError:
# Log failed evaluation but continue streaming
logger.warning(f"[STREAM] Failed to evaluate segment: {current_segment.literal}")
if final_chunk:
yield {
"event": "message",
"data": {
"chunk": final_chunk
}
}
# Advance cursor after processing
end_info.cursor += 1
# Remove End node from active tracking if all segments have been processed
if end_info.cursor >= len(end_info.outputs):
self.end_outputs.pop(self.activate_end)
self.activate_end = None
@staticmethod
def _trans_output_string(content):
if isinstance(content, str):
@@ -218,14 +342,8 @@ class WorkflowExecutor:
result = await graph.ainvoke(initial_state, config=self.checkpoint_config)
full_content = ''
for end_info in self.end_outputs.values():
output_template = "".join([output.literal for output in end_info.outputs])
full_content += render_template(
output_template,
result.get("variables", {}),
result.get("runtime_vars", {}),
strict=False
)
for end_id in self.end_outputs.keys():
full_content += result.get('runtime_vars', {}).get(end_id, {}).get('output', '')
result["messages"].extend(
[
{
@@ -306,7 +424,7 @@ class WorkflowExecutor:
try:
chunk_count = 0
full_content = ''
self._update_scope_activate("sys")
async for event in graph.astream(
initial_state,
stream_mode=["updates", "debug", "custom"], # Use updates + debug + custom mode
@@ -333,9 +451,12 @@ class WorkflowExecutor:
if not end_info or end_info.cursor >= len(end_info.outputs):
continue
current_output = end_info.outputs[end_info.cursor]
if current_output.is_variable and current_output.depends_on_node(node_id):
if current_output.is_variable and current_output.depends_on_scope(node_id):
if data.get("done"):
end_info.cursor += 1
if end_info.cursor >= len(end_info.outputs):
self.end_outputs.pop(self.activate_end)
self.activate_end = None
else:
full_content += data.get("chunk")
yield {
@@ -415,91 +536,53 @@ class WorkflowExecutor:
elif mode == "updates":
# Handle state updates - store final state
for node_id in data.keys():
self._update_end_activate(node_id)
wait = False
state = graph.get_state(config=self.checkpoint_config)
node_outputs = state.values.get("runtime_vars", {})
for _ in data.keys():
node_outputs = node_outputs | data.get(_).get("runtime_vars", {})
state = graph.get_state(config=self.checkpoint_config).values
node_outputs = state.get("runtime_vars", {})
variables = state.get("variables", {})
activate = state.get("activate", {})
for _, node_data in data.items():
node_outputs |= node_data.get("runtime_vars", {})
variables |= node_data.get("variables", {})
self._update_stream_output_status(activate, data)
wait = False
while self.activate_end and not wait:
message = ''
logger.info(self.activate_end)
end_info = self.end_outputs[self.activate_end]
content = end_info.outputs[end_info.cursor]
while content.activate:
if not content.is_variable:
full_content += content.literal
message += content.literal
else:
try:
chunk = evaluate_expression(
content.literal,
variables={},
node_outputs=node_outputs
)
chunk = self._trans_output_string(chunk)
message += chunk
full_content += chunk
except ValueError:
pass
end_info.cursor += 1
if end_info.cursor == len(end_info.outputs):
break
content = end_info.outputs[end_info.cursor]
if end_info.cursor != len(end_info.outputs):
async for msg_event in self._emit_active_chunks(
node_outputs=node_outputs,
variables=variables
):
full_content += msg_event["data"]['chunk']
yield msg_event
if self.activate_end:
wait = True
else:
self.end_outputs.pop(self.activate_end)
self.activate_end = None
for node_id in data.keys():
self._update_end_activate(node_id)
if message:
yield {
"event": "message",
"data": {
"chunk": message
}
}
self._update_stream_output_status(activate, data)
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
f"- execution_id: {self.execution_id}")
result = graph.get_state(self.checkpoint_config).values
while self.activate_end:
message = ''
end_info = self.end_outputs[self.activate_end]
content = end_info.outputs[end_info.cursor]
if not content.is_variable:
message += content.literal
else:
node_outputs = result.get("runtime_vars", {})
variables = result.get("variables", {})
try:
chunk = evaluate_expression(
content.literal,
node_outputs = result.get("runtime_vars", {})
variables = result.get("variables", {})
self.end_outputs = {
node_id: node_info
for node_id, node_info in self.end_outputs.items()
if node_info.activate
}
if self.end_outputs or self.activate_end:
while self.activate_end:
async for msg_event in self._emit_active_chunks(
node_outputs=node_outputs,
variables=variables,
node_outputs=node_outputs
)
chunk = self._trans_output_string(chunk)
message += chunk
full_content += chunk
except ValueError:
pass
end_info.cursor += 1
if end_info.cursor == len(end_info.outputs):
self.end_outputs.pop(self.activate_end)
self.activate_end = None
if self.end_outputs:
force=True
):
full_content += msg_event["data"]['chunk']
yield msg_event
if not self.activate_end and self.end_outputs:
self.activate_end = list(self.end_outputs.keys())[0]
if message:
yield {
"event": "message",
"data": {
"chunk": message
}
}
# 计算耗时
end_time = datetime.datetime.now()

View File

@@ -53,114 +53,110 @@ class OutputContent(BaseModel):
)
)
def depends_on_node(self, node_id: str) -> bool:
def depends_on_scope(self, scope: str) -> bool:
"""
Check if this output segment depends on a specific node's variable.
This method examines the `literal` of the output segment to see if it
contains a variable placeholder referencing the given node in the form:
{{ node_id.field_name }}
It uses a regular expression to match the exact node ID, avoiding
false positives from substring matches (e.g., 'node1' should not match 'node10').
Check if this segment depends on a given scope.
Args:
node_id (str): The ID of the node to check for in this segment's variable placeholders.
scope (str): Node ID or special variable prefix (e.g., "sys").
Returns:
bool:
- True if the segment contains a variable referencing the given node.
- False otherwise.
Example:
literal = "{{node1.name}}"
depends_on_node("node1") -> True
depends_on_node("node2") -> False
Usage:
This method is primarily used in stream mode to determine whether
a particular variable output segment should be activated when a
specific upstream node completes execution.
bool: True if this segment references the given scope.
"""
variable_pattern = rf"\{{\{{\s*{re.escape(node_id)}\.[a-zA-Z0-9_]+\s*\}}\}}"
pattern = re.compile(variable_pattern)
match = pattern.search(self.literal)
if match:
return True
return False
pattern = rf"\{{\{{\s*{re.escape(scope)}\.[a-zA-Z0-9_]+\s*\}}\}}"
return bool(re.search(pattern, self.literal))
class StreamOutputConfig(BaseModel):
"""
Streaming output configuration for an End node.
This structure controls:
- whether the End node output is globally active
- which upstream branch nodes are responsible for activation
- how each output segment behaves in streaming mode
This configuration describes how the End node output behaves in streaming mode,
including:
- whether output emission is globally activated
- which upstream branch/control nodes gate the activation
- how each parsed output segment is streamed and activated
"""
activate: bool = Field(
...,
description=(
"Global activation state of the End node output.\n"
"If False, no output should be emitted until all control nodes are resolved."
"Global activation flag for the End node output.\n"
"When False, output segments should not be emitted even if available.\n"
"This flag typically becomes True once required control branch conditions "
"are satisfied."
)
)
control_nodes: list[str] = Field(
control_nodes: dict[str, str] = Field(
...,
description=(
"List of upstream branch node IDs that control this End node.\n"
"Each node must signal completion before output becomes active."
"Control branch conditions for this End node output.\n"
"Mapping of `branch_node_id -> expected_branch_label`.\n"
"The End node output becomes globally active when a controlling branch node "
"reports a matching completion status."
)
)
outputs: list[OutputContent] = Field(
...,
description="Ordered list of output segments parsed from the output template."
description=(
"Ordered list of output segments parsed from the output template.\n"
"Each segment represents either a literal text block or a variable placeholder "
"that may be activated independently."
)
)
cursor: int = Field(
...,
description=(
"Streaming cursor index.\n"
"Indicates how many output segments have already been emitted."
"Indicates the next output segment index to be emitted.\n"
"Segments with index < cursor are considered already streamed."
)
)
def update_activate(self, node_id):
def update_activate(self, scope: str, status=None):
"""
Update activation state based on an upstream node completion.
Update streaming activation state based on an upstream node or special variable.
This method is typically called when a branch/control node finishes execution.
Args:
scope (str):
Identifier of the completed upstream entity.
- If a control branch node, it should match a key in `control_nodes`.
- If a variable placeholder (e.g., "sys.xxx"), it may appear in output segments.
status (optional):
Completion status of the control branch node.
Required when `scope` refers to a control node.
Behavior:
1. If the node is a control node:
- Remove it from `control_nodes`
- If all control nodes are resolved, activate the entire output
1. Control branch nodes:
- If `scope` matches a key in `control_nodes` and `status` matches the expected
branch label, the End node output becomes globally active (`activate = True`).
2. Activate variable output segments that depend on this node:
- If an output segment is a variable
- And its literal references the completed node_id
- Mark that segment as active
2. Variable output segments:
- For each segment that is a variable (`is_variable=True`):
- If the segment literal references `scope`, mark the segment as active.
- This applies both to regular node variables (e.g., "node_id.field")
and special system variables (e.g., "sys.xxx").
Notes:
- This method does not emit output or advance the streaming cursor.
- It only updates activation flags based on upstream events or special variables.
"""
# Case 1: resolve control branch dependency
if node_id in self.control_nodes:
self.control_nodes.remove(node_id)
# All branch constraints resolved → enable output
if not self.control_nodes:
if scope in self.control_nodes.keys():
if status is None:
raise RuntimeError("[Stream Output] Control node activation status not provided")
if status == self.control_nodes[scope]:
self.activate = True
# Case 2: activate variable segments related to this node
for i in range(len(self.outputs)):
if (
self.outputs[i].is_variable
and self.outputs[i].depends_on_node(node_id)
and self.outputs[i].depends_on_scope(scope)
):
self.outputs[i].activate = True
@@ -184,11 +180,11 @@ class GraphBuilder:
self._find_upstream_branch_node = lru_cache(
maxsize=len(self.nodes) * 2
)(self._find_upstream_branch_node)
self._analyze_end_node_output()
self.graph = StateGraph(WorkflowState)
self.add_nodes()
self.add_edges()
self._analyze_end_node_output()
# EDGES MUST BE ADDED AFTER NODES ARE ADDED.
@property
@@ -216,30 +212,53 @@ class GraphBuilder:
except KeyError:
raise RuntimeError(f"Node not found: Id={node_id}")
def _find_upstream_branch_node(self, target_node: str) -> tuple[bool, tuple[str]]:
"""Find upstream branch nodes for a given target node in the workflow graph.
def _find_upstream_branch_node(self, target_node: str) -> tuple[bool, tuple[tuple[str, str]]]:
"""
Recursively find all upstream branch (control) nodes that influence the execution
of the given target node.
This method identifies all upstream control (branch) nodes that can affect
the execution of `target_node`. If `target_node` is reachable from a start
node (i.e., a node with no upstream nodes), the method returns an empty tuple.
This method walks upstream along the workflow graph starting from `target_node`.
It distinguishes between:
- branch nodes (node types listed in `BRANCH_NODES`)
- non-branch nodes (ordinary processing nodes)
The function distinguishes between branch nodes (defined in `BRANCH_NODES`)
and non-branch nodes, recursively traversing upstream through non-branch
nodes. If any non-branch upstream path does not lead to a branch node,
the result will indicate that no valid upstream branch node exists.
Traversal rules:
1. For each immediate upstream node:
- If it is a branch node, it is recorded as an affecting control node.
- If it is a non-branch node, the traversal continues recursively upstream.
2. If ANY upstream path reaches a START / CYCLE_START node without encountering
a branch node, the traversal is considered invalid:
- `has_branch` will be False
- no branch nodes are returned.
3. Only when ALL upstream non-branch paths eventually lead to at least one
branch node will `has_branch` be True.
Special case:
- If `target_node` has no upstream nodes AND its type is START or CYCLE_START,
it is considered directly reachable from the workflow entry, and therefore
has no controlling branch nodes.
Args:
target_node (str): The identifier of the target node.
target_node (str):
The identifier of the node whose upstream control branches
are to be resolved.
Returns:
tuple[bool, tuple[str]]:
- has_branch (bool): True if all upstream non-branch paths lead to at least
one branch node; False if any path reaches a start node without a branch.
- branch_nodes (tuple[str]): A deduplicated tuple of upstream branch node IDs
affecting `target_node`. Returns an empty tuple if `has_branch` is False.
tuple[bool, tuple[tuple[str, str]]]:
- has_branch (bool):
True if every upstream path from `target_node` encounters
at least one branch node.
False if any path reaches a start node without a branch.
- branch_nodes (tuple[tuple[str, str]]):
A deduplicated tuple of `(branch_node_id, branch_label)` pairs
representing all branch nodes that can influence `target_node`.
Returns an empty tuple if `has_branch` is False.
"""
source_nodes = [
edge.get("source")
{
"id": edge.get("source"),
"branch": edge.get("label")
}
for edge in self.edges
if edge.get("target") == target_node
]
@@ -249,11 +268,13 @@ class GraphBuilder:
branch_nodes = []
non_branch_nodes = []
for node_id in source_nodes:
if self.get_node_type(node_id) in BRANCH_NODES:
branch_nodes.append(node_id)
for node_info in source_nodes:
if self.get_node_type(node_info["id"]) in BRANCH_NODES:
branch_nodes.append(
(node_info["id"], node_info["branch"])
)
else:
non_branch_nodes.append(node_id)
non_branch_nodes.append(node_info["id"])
has_branch = True
for node_id in non_branch_nodes:
@@ -334,7 +355,7 @@ class GraphBuilder:
activate=not has_branch,
# Branch nodes that control activation of this End node
control_nodes=list(control_nodes),
control_nodes=dict(control_nodes),
# Convert output segments into OutputContent objects
outputs=list(
@@ -362,7 +383,7 @@ class GraphBuilder:
else:
self.end_node_map[end_node_id] = StreamOutputConfig(
activate=True,
control_nodes=[],
control_nodes={},
outputs=list(
[
OutputContent(

View File

@@ -25,6 +25,6 @@ class MemoryWriteNodeConfig(BaseNodeConfig):
...
)
config_id: UUID = Field(
config_id: UUID | int = Field(
...
)

View File

@@ -36,9 +36,10 @@ class MemoryReadNode(BaseNode):
class MemoryWriteNode(BaseNode):
def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]):
super().__init__(node_config, workflow_config)
self.typed_config = MemoryWriteNodeConfig(**self.config)
self.typed_config: MemoryWriteNodeConfig | None = None
async def execute(self, state: WorkflowState) -> Any:
self.typed_config = MemoryWriteNodeConfig(**self.config)
end_user_id = self.get_variable("sys.user_id", state)
if not end_user_id:

View File

@@ -16,6 +16,8 @@ from app.core.error_codes import BizCode, HTTP_MAPPING
from app.core.exceptions import BusinessException
from app.core.logging_config import LoggingConfig, get_logger
from app.core.response_utils import fail
from app.core.models.scripts.loader import load_models
from app.db import get_db_context
# Initialize logging system
LoggingConfig.setup_logging()
@@ -47,6 +49,15 @@ async def lifespan(app: FastAPI):
else:
logger.info("自动数据库升级已禁用 (DB_AUTO_UPGRADE=false)")
# 加载预定义模型
logger.info("开始加载预定义模型...")
try:
with get_db_context() as db:
result = load_models(db, silent=True)
logger.info(f"预定义模型加载完成: 成功{result['success']}个, 跳过{result['skipped']}个, 失败{result['failed']}")
except Exception as e:
logger.warning(f"加载预定义模型时出错: {str(e)}")
logger.info("应用程序启动完成")
yield
# 应用关闭事件

View File

@@ -5,6 +5,7 @@ from enum import StrEnum
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, UniqueConstraint, Integer, ARRAY, Table
from sqlalchemy.dialects.postgresql import UUID, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db import Base
@@ -23,6 +24,8 @@ class ModelType(StrEnum):
CHAT = "chat"
EMBEDDING = "embedding"
RERANK = "rerank"
# TTS = "tts"
# SPEECH2TEXT = "speech2text"
# IMAGE = "image"
# AUDIO = "audio"
# VISION = "vision"
@@ -48,8 +51,7 @@ class ModelProvider(StrEnum):
class LoadBalanceStrategy(StrEnum):
"""API Key负载均衡策略枚举"""
ROUND_ROBIN = "round_robin" # 轮询
WEIGHTED_ROUND_ROBIN = "weighted_round_robin" # 加权轮询
RANDOM = "random" # 随机
NONE = "none" #
# 多对多关联表
@@ -90,7 +92,8 @@ class ModelConfig(BaseModel):
# 状态管理
is_public = Column(Boolean, default=False, nullable=False, comment="是否公开")
load_balance_strategy = Column(String, nullable=True, comment="负载均衡策略")
load_balance_strategy = Column(String, nullable=True, comment="负载均衡策略", default=LoadBalanceStrategy.NONE,
server_default=LoadBalanceStrategy.NONE)
# 关联关系
model_base = relationship("ModelBase", back_populates="configs")
@@ -151,6 +154,7 @@ class ModelBase(Base):
is_official = Column(Boolean, default=True, comment="是否供应商官方模型(区分自定义)")
tags = Column(ARRAY(String), default=list, nullable=False, comment="模型标签(如['聊天', '创作']")
add_count = Column(Integer, default=0, nullable=False, comment="模型被用户添加的次数")
created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间", server_default=func.now())
# 关联关系
configs = relationship("ModelConfig", back_populates="model_base", cascade="all, delete-orphan")

View File

@@ -24,12 +24,16 @@ from app.schemas.memory_storage_schema import (
from sqlalchemy import desc, select
from sqlalchemy.orm import Session
from app.utils.config_utils import resolve_config_id
# 获取数据库专用日志器
db_logger = get_db_logger()
# 获取配置专用日志器
config_logger = get_config_logger()
TABLE_NAME = "memory_config"
class MemoryConfigRepository:
"""记忆配置Repository
@@ -168,6 +172,7 @@ class MemoryConfigRepository:
if not memory_config:
raise RuntimeError("reflection config not found")
return memory_config
@staticmethod
def query_reflection_config_by_workspace_id(db: Session, workspace_id: uuid.UUID) -> MemoryConfig:
"""构建查询所有配置的语句SQLAlchemy text() 命名参数)
@@ -187,7 +192,6 @@ class MemoryConfigRepository:
raise RuntimeError("reflection config not found")
return memory_config
@staticmethod
def build_select_all(workspace_id: uuid.UUID) -> Tuple[str, Dict]:
"""构建查询所有配置的语句SQLAlchemy text() 命名参数)
@@ -287,7 +291,6 @@ class MemoryConfigRepository:
db_logger.error(f"更新记忆配置失败: config_id={update.config_id} - {str(e)}")
raise
@staticmethod
def update_extracted(db: Session, update: ConfigUpdateExtracted) -> Optional[MemoryConfig]:
"""更新记忆萃取引擎配置
@@ -410,7 +413,7 @@ class MemoryConfigRepository:
raise
@staticmethod
def get_extracted_config(db: Session, config_id: UUID) -> Optional[Dict]:
def get_extracted_config(db: Session, config_id: UUID | int) -> Optional[Dict]:
"""获取萃取配置,通过主键查询某条配置
Args:
@@ -420,8 +423,8 @@ class MemoryConfigRepository:
Returns:
Optional[Dict]: 萃取配置字典不存在则返回None
"""
config_id = resolve_config_id(config_id, db)
db_logger.debug(f"查询萃取配置: config_id={config_id}")
try:
db_config = db.query(MemoryConfig).filter(MemoryConfig.config_id == config_id).first()
if not db_config:
@@ -514,26 +517,28 @@ class MemoryConfigRepository:
except Exception as e:
db_logger.error(f"根据ID查询记忆配置失败: config_id={config_id} - {str(e)}")
raise
@staticmethod
def get_config_with_workspace(db: Session, config_id: uuid.UUID) -> Optional[tuple]:
def get_config_with_workspace(db: Session, config_id: uuid.UUID | int | str) -> Optional[tuple]:
"""Get memory config and its associated workspace information
Args:
db: Database session
config_id: Configuration ID
Returns:
Optional[tuple]: (MemoryConfig, Workspace) tuple, None if not found
Raises:
ValueError: Raised when config exists but workspace doesn't
"""
import time
from app.models.workspace_model import Workspace
start_time = time.time()
config_id = resolve_config_id(config_id, db)
# Log configuration loading start
config_logger.info(
"Loading configuration with workspace",
@@ -542,17 +547,17 @@ class MemoryConfigRepository:
"config_id": config_id
}
)
db_logger.debug(f"Querying memory config and workspace: config_id={config_id}")
try:
# Use join query to get both config and workspace
result = db.query(MemoryConfig, Workspace).join(
Workspace, MemoryConfig.workspace_id == Workspace.id
).filter(MemoryConfig.config_id == config_id).first()
elapsed_ms = (time.time() - start_time) * 1000
if not result:
# Check if config exists but workspace is missing
config_only = db.query(MemoryConfig).filter(MemoryConfig.config_id == config_id).first()
@@ -581,9 +586,11 @@ class MemoryConfigRepository:
"elapsed_ms": elapsed_ms
}
)
db_logger.error(f"Memory config {config_id} references non-existent workspace {config_only.workspace_id}")
raise ValueError(f"Workspace {config_only.workspace_id} not found for configuration {config_id}")
db_logger.error(
f"Memory config {config_id} references non-existent workspace {config_only.workspace_id}")
raise ValueError(
f"Workspace {config_only.workspace_id} not found for configuration {config_id}")
config_logger.debug(
"Configuration not found",
extra={
@@ -595,9 +602,9 @@ class MemoryConfigRepository:
)
db_logger.debug(f"Memory config not found: config_id={config_id}")
return None
config, workspace = result
# Log successful configuration loading
config_logger.info(
"Configuration with workspace loaded successfully",
@@ -612,16 +619,17 @@ class MemoryConfigRepository:
"elapsed_ms": elapsed_ms
}
)
db_logger.debug(f"Memory config and workspace query successful: config={config.config_name}, workspace={workspace.name}")
db_logger.debug(
f"Memory config and workspace query successful: config={config.config_name}, workspace={workspace.name}")
return (config, workspace)
except ValueError:
# Re-raise known business exceptions
raise
except Exception as e:
elapsed_ms = (time.time() - start_time) * 1000
config_logger.error(
"Failed to load configuration with workspace",
extra={
@@ -634,9 +642,10 @@ class MemoryConfigRepository:
},
exc_info=True
)
db_logger.error(f"Failed to query memory config and workspace: config_id={config_id} - {str(e)}")
raise
@staticmethod
def get_all(db: Session, workspace_id: Optional[uuid.UUID] = None) -> List[MemoryConfig]:
"""获取所有配置参数

View File

@@ -165,7 +165,7 @@ class ModelConfigRepository:
total = base_query.count()
# 分页查询
models = base_query.order_by(desc(ModelConfig.updated_at)).offset(
models = base_query.order_by(desc(ModelConfig.created_at)).offset(
(query.page - 1) * query.pagesize
).limit(query.pagesize).all()
@@ -234,7 +234,7 @@ class ModelConfigRepository:
# 获取总数
total = base_query.count()
query_results = base_query.order_by(desc(ModelConfig.updated_at)).all()
query_results = base_query.order_by(desc(ModelConfig.created_at)).all()
provider_groups: Dict[str, List[ModelConfig]] = {}
for model_config in query_results:
@@ -433,6 +433,7 @@ class ModelConfigRepository:
ModelConfig.is_public
),
ModelBase.provider == provider,
ModelConfig.is_active,
~ModelConfig.is_composite
)
).distinct().all()
@@ -621,7 +622,7 @@ class ModelBaseRepository:
if filters:
q = q.filter(and_(*filters))
return q.order_by(ModelBase.add_count.desc()).all()
return q.order_by(ModelBase.add_count.desc(), ModelBase.created_at.desc()).all()
@staticmethod
def create(db: Session, data: dict) -> 'ModelBase':
@@ -629,6 +630,13 @@ class ModelBaseRepository:
db.add(model_base)
return model_base
@staticmethod
def get_by_name_and_provider(db: Session, name: str, provider: str) -> Optional['ModelBase']:
return db.query(ModelBase).filter(
ModelBase.name == name,
ModelBase.provider == provider
).first()
@staticmethod
def update(db: Session, model_base_id: uuid.UUID, data: dict) -> Optional['ModelBase']:
model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first()
@@ -636,6 +644,17 @@ class ModelBaseRepository:
return None
for key, value in data.items():
setattr(model_base, key, value)
# 同步更新绑定的非组合模型配置
if any(k in data for k in ['name', 'description', 'logo']):
db.query(ModelConfig).filter(
ModelConfig.model_id == model_base_id,
ModelConfig.is_composite == False
).update({
k: v for k, v in data.items()
if k in ['name', 'description', 'logo']
}, synchronize_session=False)
return model_base
@staticmethod

View File

@@ -1,5 +1,7 @@
import uuid
from pydantic import BaseModel, Field
from typing import Optional
from typing import Optional, Union
from uuid import UUID
from enum import Enum
@@ -10,7 +12,7 @@ class OptimizationStrategy(str, Enum):
ACCURACY_FIRST = "accuracy_first"
BALANCED = "balanced"
class Memory_Reflection(BaseModel):
config_id: Optional[UUID] = None
config_id: Union[uuid.UUID, int, str] = None
reflection_enabled: bool
reflection_period_in_hours: str
reflexion_range: Optional[str] = "partial"

View File

@@ -147,7 +147,7 @@ class ReflexionResultSchema(BaseModel):
# Composite key identifying a config row
class ConfigKey(BaseModel): # 配置参数键模型
model_config = ConfigDict(populate_by_name=True, extra="forbid")
config_id: uuid.UUID = Field("config_id", description="配置唯一标识UUID")
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置唯一标识UUID或int)")
user_id: str = Field("user_id", description="用户标识(字符串)")
apply_id: str = Field("apply_id", description="应用或场景标识(字符串)")
@@ -238,17 +238,17 @@ class ConfigParamsCreate(BaseModel): # 创建配置参数模型(仅 body
class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
# config_name: str = Field("配置名称", description="配置名称(字符串)")
config_id: uuid.UUID = Field("配置ID", description="配置IDUUID")
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置ID支持UUID、整数或字符串")
class ConfigUpdate(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
config_id: Optional[uuid.UUID] = None
config_id: Union[uuid.UUID, int, str] = None
config_name: str = Field("配置名称", description="配置名称(字符串)")
config_desc: str = Field("配置描述", description="配置描述(字符串)")
class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数时使用的模型
config_id: Optional[uuid.UUID] = None
config_id:Union[uuid.UUID, int, str] = None
llm_id: Optional[str] = Field(None, description="LLM模型配置ID")
embedding_id: Optional[str] = Field(None, description="嵌入模型配置ID")
rerank_id: Optional[str] = Field(None, description="重排序模型配置ID")
@@ -315,14 +315,14 @@ class ConfigUpdateExtracted(BaseModel): # 更新记忆萃取引擎配置参数
class ConfigUpdateForget(BaseModel): # 更新遗忘引擎配置参数时使用的模型
# 遗忘引擎配置参数更新模型
config_id: Optional[uuid.UUID] = None
config_id:Union[uuid.UUID, int, str] = None
lambda_time: Optional[float] = Field(0.5, ge=0.0, le=1.0, description="最低保持度0-1 小数;默认 0.5")
lambda_mem: Optional[float] = Field(0.5, ge=0.0, le=1.0, description="遗忘率0-1 小数;默认 0.5")
offset: Optional[float] = Field(0.0, ge=0.0, le=1.0, description="偏移度0-1 小数;默认 0.0")
class ConfigPilotRun(BaseModel): # 试运行触发请求模型
config_id: uuid.UUID = Field(..., description="配置ID唯一")
config_id:Union[uuid.UUID, int, str] = Field(..., description="配置ID唯一支持UUID、整数或字符串")
dialogue_text: str = Field(..., description="前端传入的对话文本,格式如 '用户: ...\nAI: ...' 可多行,试运行必填")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
@@ -330,7 +330,7 @@ class ConfigPilotRun(BaseModel): # 试运行触发请求模型
class ConfigFilter(BaseModel): # 查询配置参数时使用的模型
model_config = ConfigDict(populate_by_name=True, extra="forbid")
config_id: Optional[uuid.UUID] = None
config_id: Union[uuid.UUID, int, str] = None
user_id: Optional[str] = None
apply_id: Optional[str] = None
@@ -406,7 +406,7 @@ class ForgettingConfigResponse(BaseModel):
"""遗忘引擎配置响应模型"""
model_config = ConfigDict(populate_by_name=True, extra="forbid")
config_id: uuid.UUID = Field(..., description="配置ID")
config_id: Union[uuid.UUID, int, str] = Field(..., description="配置ID支持UUID、整数或字符串")
decay_constant: float = Field(..., description="衰减常数 d")
lambda_time: float = Field(..., description="时间衰减参数")
lambda_mem: float = Field(..., description="记忆衰减参数")
@@ -423,8 +423,8 @@ class ForgettingConfigResponse(BaseModel):
class ForgettingConfigUpdateRequest(BaseModel):
"""遗忘引擎配置更新请求模型"""
model_config = ConfigDict(populate_by_name=True, extra="forbid")
config_id: uuid.UUID = Field(..., description="配置ID")
config_id: Union[uuid.UUID, int,str] = Field(..., description="配置唯一标识UUID或int)")
decay_constant: Optional[float] = Field(None, ge=0.0, le=1.0, description="衰减常数 d")
lambda_time: Optional[float] = Field(None, ge=0.0, le=1.0, description="时间衰减参数")
lambda_mem: Optional[float] = Field(None, ge=0.0, le=1.0, description="记忆衰减参数")
@@ -499,7 +499,7 @@ class ForgettingCurveRequest(BaseModel):
importance_score: float = Field(0.5, ge=0.0, le=1.0, description="重要性分数0-1")
days: int = Field(60, ge=1, le=365, description="模拟天数默认60天")
config_id: Optional[uuid.UUID] = Field(None, description="配置ID可选如果为None则使用默认配置")
config_id: Union[uuid.UUID, int, str] = Field(..., description="配置唯一标识UUID或int)")
class ForgettingCurveResponse(BaseModel):

View File

@@ -3,14 +3,12 @@ from typing import Optional, List, Dict, Any
import datetime
import uuid
from app.models.models_model import ModelProvider, ModelType
from app.models.models_model import ModelProvider, ModelType, LoadBalanceStrategy
from app.core.logging_config import get_business_logger
schema_logger = get_business_logger()
# ModelConfig Schemas
class ModelConfigBase(BaseModel):
"""模型配置基础Schema"""
@@ -22,6 +20,7 @@ class ModelConfigBase(BaseModel):
config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数")
is_active: bool = Field(True, description="是否激活")
is_public: bool = Field(False, description="是否公开")
load_balance_strategy: Optional[str] = Field(LoadBalanceStrategy.NONE.value, description="负载均衡策略")
class ApiKeyCreateNested(BaseModel):
@@ -44,13 +43,14 @@ class ModelConfigCreate(ModelConfigBase):
class CompositeModelCreate(BaseModel):
"""创建组合模型Schema"""
name: str = Field(..., description="组合模型名称", max_length=255)
type: ModelType = Field(..., description="模型类型")
type: Optional[ModelType] = Field(None, description="模型类型")
logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255)
description: Optional[str] = Field(None, description="模型描述")
config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数")
is_active: bool = Field(True, description="是否激活")
is_public: bool = Field(False, description="是否公开")
api_key_ids: List[uuid.UUID] = Field(..., description="绑定的API Key ID列表")
load_balance_strategy: Optional[str] = Field(default=LoadBalanceStrategy.NONE.value, description="负载均衡策略")
class ModelConfigUpdate(BaseModel):

View File

@@ -17,12 +17,15 @@ from app.repositories.neo4j.neo4j_connector import Neo4jConnector
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.utils.config_utils import resolve_config_id
logger = get_business_logger()
class EmotionSuggestion(BaseModel):
"""情绪建议模型"""
type: str = Field(..., description="建议类型emotion_balance/activity_recommendation/social_connection/stress_management")
type: str = Field(...,
description="建议类型emotion_balance/activity_recommendation/social_connection/stress_management")
title: str = Field(..., description="建议标题")
content: str = Field(..., description="建议内容")
priority: str = Field(..., description="优先级high/medium/low")
@@ -37,33 +40,33 @@ class EmotionSuggestionsResponse(BaseModel):
class EmotionAnalyticsService:
"""情绪分析服务
提供情绪数据的分析和统计功能,包括:
- 情绪标签统计
- 情绪词云数据
- 情绪健康指数计算
- 个性化情绪建议生成
Attributes:
emotion_repo: 情绪数据仓储实例
"""
def __init__(self):
"""初始化情绪分析服务"""
connector = Neo4jConnector()
self.emotion_repo = EmotionRepository(connector)
logger.info("情绪分析服务初始化完成")
async def get_emotion_tags(
self,
end_user_id: str,
emotion_type: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: int = 10
self,
end_user_id: str,
emotion_type: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: int = 10
) -> Dict[str, Any]:
"""获取情绪标签统计
查询指定用户的情绪类型分布,包括计数、百分比和平均强度。
确保返回所有6个情绪维度joy、sadness、anger、fear、surprise、neutral
即使某些维度没有数据也会返回count=0的记录。
@@ -71,8 +74,8 @@ class EmotionAnalyticsService:
"""
try:
logger.info(f"获取情绪标签统计: user={end_user_id}, type={emotion_type}, "
f"start={start_date}, end={end_date}, limit={limit}")
f"start={start_date}, end={end_date}, limit={limit}")
# 调用仓储层查询
tags = await self.emotion_repo.get_emotion_tags(
end_user_id=end_user_id,
@@ -81,13 +84,13 @@ class EmotionAnalyticsService:
end_date=end_date,
limit=limit
)
# 定义所有6个情绪维度
all_emotion_types = ['joy', 'sadness', 'anger', 'fear', 'surprise', 'neutral']
# 将查询结果转换为字典,方便查找
tags_dict = {tag["emotion_type"]: tag for tag in tags}
# 补全缺失的情绪维度
complete_tags = []
for emotion in all_emotion_types:
@@ -101,52 +104,52 @@ class EmotionAnalyticsService:
"percentage": 0.0,
"avg_intensity": 0.0
})
# 计算总数
total_count = sum(tag["count"] for tag in complete_tags)
# 如果有数据重新计算百分比因为补全了0值项
if total_count > 0:
for tag in complete_tags:
if tag["count"] > 0:
tag["percentage"] = round((tag["count"] / total_count) * 100, 2)
# 构建时间范围信息
time_range = {}
if start_date:
time_range["start_date"] = start_date
if end_date:
time_range["end_date"] = end_date
# 格式化响应
response = {
"tags": complete_tags,
"total_count": total_count,
"time_range": time_range if time_range else None
}
logger.info(f"情绪标签统计完成: total_count={total_count}, tags_count={len(complete_tags)}")
return response
except Exception as e:
logger.error(f"获取情绪标签统计失败: {str(e)}", exc_info=True)
raise
async def get_emotion_wordcloud(
self,
end_user_id: str,
emotion_type: Optional[str] = None,
limit: int = 50
self,
end_user_id: str,
emotion_type: Optional[str] = None,
limit: int = 50
) -> Dict[str, Any]:
"""获取情绪词云数据
查询情绪关键词及其频率,用于生成词云可视化。
Args:
end_user_id: 宿主ID用户组ID
emotion_type: 可选的情绪类型过滤
limit: 返回关键词的最大数量
Returns:
Dict: 包含情绪词云数据的响应:
- keywords: 关键词列表
@@ -154,39 +157,39 @@ class EmotionAnalyticsService:
"""
try:
logger.info(f"获取情绪词云数据: user={end_user_id}, type={emotion_type}, limit={limit}")
# 调用仓储层查询
keywords = await self.emotion_repo.get_emotion_wordcloud(
end_user_id=end_user_id,
emotion_type=emotion_type,
limit=limit
)
# 计算总关键词数量
total_keywords = len(keywords)
# 格式化响应
response = {
"keywords": keywords,
"total_keywords": total_keywords
}
logger.info(f"情绪词云数据获取完成: total_keywords={total_keywords}")
return response
except Exception as e:
logger.error(f"获取情绪词云数据失败: {str(e)}", exc_info=True)
raise
def _calculate_positivity_rate(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""计算积极率
根据情绪类型分类正面、负面和中性情绪,计算积极率。
公式:(正面数 / (正面数 + 负面数)) * 100
Args:
emotions: 情绪数据列表,每个包含 emotion_type 字段
Returns:
Dict: 包含积极率计算结果:
- score: 积极率分数0-100
@@ -197,38 +200,38 @@ class EmotionAnalyticsService:
# 定义情绪分类
positive_emotions = {'joy', 'surprise'}
negative_emotions = {'sadness', 'anger', 'fear'}
# 统计各类情绪数量
positive_count = sum(1 for e in emotions if e.get('emotion_type') in positive_emotions)
negative_count = sum(1 for e in emotions if e.get('emotion_type') in negative_emotions)
neutral_count = sum(1 for e in emotions if e.get('emotion_type') == 'neutral')
# 计算积极率
total_non_neutral = positive_count + negative_count
if total_non_neutral > 0:
score = (positive_count / total_non_neutral) * 100
else:
score = 50.0 # 如果没有非中性情绪默认为50
logger.debug(f"积极率计算: positive={positive_count}, negative={negative_count}, "
f"neutral={neutral_count}, score={score:.2f}")
f"neutral={neutral_count}, score={score:.2f}")
return {
"score": round(score, 2),
"positive_count": positive_count,
"negative_count": negative_count,
"neutral_count": neutral_count
}
def _calculate_stability(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""计算稳定性
基于情绪强度的标准差计算情绪稳定性。
公式:(1 - min(std_deviation, 1.0)) * 100
Args:
emotions: 情绪数据列表,每个包含 emotion_intensity 字段
Returns:
Dict: 包含稳定性计算结果:
- score: 稳定性分数0-100
@@ -236,7 +239,7 @@ class EmotionAnalyticsService:
"""
# 提取所有情绪强度
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
# 计算标准差
if len(intensities) >= 2:
std_deviation = statistics.stdev(intensities)
@@ -244,29 +247,29 @@ class EmotionAnalyticsService:
std_deviation = 0.0 # 只有一个数据点标准差为0
else:
std_deviation = 0.0 # 没有数据标准差为0
# 计算稳定性分数
# 标准差越小,稳定性越高
score = (1 - min(std_deviation, 1.0)) * 100
logger.debug(f"稳定性计算: intensities_count={len(intensities)}, "
f"std_deviation={std_deviation:.3f}, score={score:.2f}")
f"std_deviation={std_deviation:.3f}, score={score:.2f}")
return {
"score": round(score, 2),
"std_deviation": round(std_deviation, 3)
}
def _calculate_resilience(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""计算恢复力
分析情绪转换模式,统计从负面情绪恢复到正面情绪的能力。
公式:(负面到正面转换次数 / 总负面情绪数) * 100
Args:
emotions: 情绪数据列表,每个包含 emotion_type 和 created_at 字段
应该按时间顺序排列
Returns:
Dict: 包含恢复力计算结果:
- score: 恢复力分数0-100
@@ -275,24 +278,24 @@ class EmotionAnalyticsService:
# 定义情绪分类
positive_emotions = {'joy', 'surprise'}
negative_emotions = {'sadness', 'anger', 'fear'}
# 统计负面到正面的转换次数
recovery_count = 0
negative_count = 0
for i in range(len(emotions)):
current_emotion = emotions[i].get('emotion_type')
# 统计负面情绪总数
if current_emotion in negative_emotions:
negative_count += 1
# 检查下一个情绪是否为正面
if i + 1 < len(emotions):
next_emotion = emotions[i + 1].get('emotion_type')
if next_emotion in positive_emotions:
recovery_count += 1
# 计算恢复力分数
if negative_count > 0:
recovery_rate = recovery_count / negative_count
@@ -301,28 +304,28 @@ class EmotionAnalyticsService:
# 如果没有负面情绪恢复力设为100最佳状态
recovery_rate = 1.0
score = 100.0
logger.debug(f"恢复力计算: negative_count={negative_count}, "
f"recovery_count={recovery_count}, score={score:.2f}")
f"recovery_count={recovery_count}, score={score:.2f}")
return {
"score": round(score, 2),
"recovery_rate": round(recovery_rate, 3)
}
async def calculate_emotion_health_index(
self,
end_user_id: str,
time_range: str = "30d"
self,
end_user_id: str,
time_range: str = "30d"
) -> Dict[str, Any]:
"""计算情绪健康指数
综合积极率、稳定性和恢复力计算情绪健康指数。
Args:
end_user_id: 宿主ID用户组ID
time_range: 时间范围7d/30d/90d
Returns:
Dict: 包含情绪健康指数的完整响应:
- health_score: 综合健康分数0-100
@@ -336,13 +339,13 @@ class EmotionAnalyticsService:
"""
try:
logger.info(f"计算情绪健康指数: user={end_user_id}, time_range={time_range}")
# 获取时间范围内的情绪数据
emotions = await self.emotion_repo.get_emotions_in_range(
end_user_id=end_user_id,
time_range=time_range
)
# 如果没有数据,返回默认值
if not emotions:
logger.warning(f"用户 {end_user_id} 在时间范围 {time_range} 内没有情绪数据")
@@ -357,20 +360,20 @@ class EmotionAnalyticsService:
"emotion_distribution": {},
"time_range": time_range
}
# 计算各维度指标
positivity_rate = self._calculate_positivity_rate(emotions)
stability = self._calculate_stability(emotions)
resilience = self._calculate_resilience(emotions)
# 计算综合健康分数
# 公式positivity_rate * 0.4 + stability * 0.3 + resilience * 0.3
health_score = (
positivity_rate["score"] * 0.4 +
stability["score"] * 0.3 +
resilience["score"] * 0.3
positivity_rate["score"] * 0.4 +
stability["score"] * 0.3 +
resilience["score"] * 0.3
)
# 确定健康等级
if health_score >= 80:
level = "优秀"
@@ -380,13 +383,13 @@ class EmotionAnalyticsService:
level = "一般"
else:
level = "较差"
# 统计情绪分布
emotion_distribution = {}
for emotion_type in ['joy', 'sadness', 'anger', 'fear', 'surprise', 'neutral']:
count = sum(1 for e in emotions if e.get('emotion_type') == emotion_type)
emotion_distribution[emotion_type] = count
# 格式化响应
response = {
"health_score": round(health_score, 2),
@@ -399,22 +402,22 @@ class EmotionAnalyticsService:
"emotion_distribution": emotion_distribution,
"time_range": time_range
}
logger.info(f"情绪健康指数计算完成: score={health_score:.2f}, level={level}")
return response
except Exception as e:
logger.error(f"计算情绪健康指数失败: {str(e)}", exc_info=True)
raise
def _analyze_emotion_patterns(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""分析情绪模式
识别主要负面情绪、情绪触发因素和波动时段。
Args:
emotions: 情绪数据列表,每个包含 emotion_type、emotion_intensity、created_at 字段
Returns:
Dict: 包含情绪模式分析结果:
- dominant_negative_emotion: 主要负面情绪类型
@@ -422,19 +425,19 @@ class EmotionAnalyticsService:
- emotion_volatility: 情绪波动性(高/中/低)
"""
negative_emotions = {'sadness', 'anger', 'fear'}
# 统计负面情绪分布
negative_emotion_counts = {}
for emotion in emotions:
emotion_type = emotion.get('emotion_type')
if emotion_type in negative_emotions:
negative_emotion_counts[emotion_type] = negative_emotion_counts.get(emotion_type, 0) + 1
# 识别主要负面情绪
dominant_negative_emotion = None
if negative_emotion_counts:
dominant_negative_emotion = max(negative_emotion_counts, key=negative_emotion_counts.get)
# 识别高强度情绪(强度 >= 0.7
high_intensity_emotions = [
{
@@ -445,7 +448,7 @@ class EmotionAnalyticsService:
for e in emotions
if e.get('emotion_intensity', 0) >= 0.7
]
# 评估情绪波动性
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
if len(intensities) >= 2:
@@ -458,29 +461,29 @@ class EmotionAnalyticsService:
volatility = ""
else:
volatility = "未知"
logger.debug(f"情绪模式分析: dominant_negative={dominant_negative_emotion}, "
f"high_intensity_count={len(high_intensity_emotions)}, volatility={volatility}")
f"high_intensity_count={len(high_intensity_emotions)}, volatility={volatility}")
return {
"dominant_negative_emotion": dominant_negative_emotion,
"high_intensity_emotions": high_intensity_emotions[:5], # 最多返回5个
"emotion_volatility": volatility
}
async def generate_emotion_suggestions(
self,
end_user_id: str,
db: Session,
self,
end_user_id: str,
db: Session,
) -> Dict[str, Any]:
"""生成个性化情绪建议
基于情绪健康数据和用户画像生成个性化建议。
Args:
end_user_id: 宿主ID用户组ID
db: 数据库会话
Returns:
Dict: 包含个性化建议的响应:
- health_summary: 健康状态摘要
@@ -488,17 +491,17 @@ class EmotionAnalyticsService:
"""
try:
logger.info(f"生成个性化情绪建议: user={end_user_id}")
# 1. 从 end_user_id 获取关联的 memory_config_id
llm_client = None
try:
from app.services.memory_agent_service import (
get_end_user_connected_config,
)
connected_config = get_end_user_connected_config(end_user_id, db)
config_id = connected_config.get("memory_config_id")
config_id = resolve_config_id(config_id, db)
if config_id is not None:
from app.services.memory_config_service import (
MemoryConfigService,
@@ -513,35 +516,35 @@ class EmotionAnalyticsService:
llm_client = factory.get_llm_client(str(memory_config.llm_model_id))
except Exception as e:
logger.warning(f"无法获取 end_user {end_user_id} 的配置,将使用默认配置: {e}")
# 2. 获取情绪健康数据
health_data = await self.calculate_emotion_health_index(end_user_id, time_range="30d")
# 3. 获取情绪数据用于模式分析
emotions = await self.emotion_repo.get_emotions_in_range(
end_user_id=end_user_id,
time_range="30d"
)
# 4. 分析情绪模式
patterns = self._analyze_emotion_patterns(emotions)
# 5. 获取用户画像数据简化版直接从Neo4j获取
user_profile = await self._get_simple_user_profile(end_user_id)
# 6. 构建LLM prompt
prompt = await self._build_suggestion_prompt(health_data, patterns, user_profile)
# 7. 调用LLM生成建议使用配置中的LLM
if llm_client is None:
# 无法获取配置时,抛出错误而不是使用默认配置
raise ValueError("无法获取LLM配置请确保end_user关联了有效的memory_config")
# 将 prompt 转换为 messages 格式
messages = [
{"role": "user", "content": prompt}
]
# 8. 使用结构化输出直接获取 Pydantic 模型
try:
suggestions_response = await llm_client.response_structured(
@@ -552,7 +555,7 @@ class EmotionAnalyticsService:
logger.error(f"LLM 结构化输出失败: {str(e)}")
# 返回默认建议
suggestions_response = self._get_default_suggestions(health_data)
# 8. 验证建议数量3-5条
if len(suggestions_response.suggestions) < 3:
logger.warning(f"建议数量不足: {len(suggestions_response.suggestions)}")
@@ -560,7 +563,7 @@ class EmotionAnalyticsService:
elif len(suggestions_response.suggestions) > 5:
logger.warning(f"建议数量过多: {len(suggestions_response.suggestions)}")
suggestions_response.suggestions = suggestions_response.suggestions[:5]
# 9. 格式化响应
response = {
"health_summary": suggestions_response.health_summary,
@@ -575,26 +578,26 @@ class EmotionAnalyticsService:
for s in suggestions_response.suggestions
]
}
logger.info(f"个性化建议生成完成: suggestions_count={len(response['suggestions'])}")
return response
except Exception as e:
logger.error(f"生成个性化建议失败: {str(e)}", exc_info=True)
raise
async def _get_simple_user_profile(self, end_user_id: str) -> Dict[str, Any]:
"""获取简化的用户画像数据
Args:
end_user_id: 用户ID
Returns:
Dict: 用户画像数据
"""
try:
connector = Neo4jConnector()
# 查询用户的实体和标签
query = """
MATCH (e:Entity)
@@ -603,59 +606,59 @@ class EmotionAnalyticsService:
ORDER BY e.created_at DESC
LIMIT 20
"""
entities = await connector.execute_query(query, end_user_id=end_user_id)
# 提取兴趣标签
interests = [e["name"] for e in entities if e.get("type") in ["INTEREST", "HOBBY"]][:5]
# 后期会引入用户的习惯。。
return {
"interests": interests if interests else ["未知"]
}
except Exception as e:
logger.error(f"获取用户画像失败: {str(e)}")
return {"interests": ["未知"]}
async def _build_suggestion_prompt(
self,
health_data: Dict[str, Any],
patterns: Dict[str, Any],
user_profile: Dict[str, Any]
self,
health_data: Dict[str, Any],
patterns: Dict[str, Any],
user_profile: Dict[str, Any]
) -> str:
"""构建情绪建议生成的prompt
Args:
health_data: 情绪健康数据
patterns: 情绪模式分析结果
user_profile: 用户画像数据
Returns:
str: LLM prompt
"""
from app.core.memory.utils.prompt.prompt_utils import (
render_emotion_suggestions_prompt,
)
prompt = await render_emotion_suggestions_prompt(
health_data=health_data,
patterns=patterns,
user_profile=user_profile
)
return prompt
def _get_default_suggestions(self, health_data: Dict[str, Any]) -> EmotionSuggestionsResponse:
"""获取默认建议当LLM调用失败时使用
Args:
health_data: 情绪健康数据
Returns:
EmotionSuggestionsResponse: 默认建议
"""
health_score = health_data.get('health_score', 0)
if health_score >= 80:
summary = "您的情绪健康状况优秀,请继续保持积极的生活态度。"
elif health_score >= 60:
@@ -664,7 +667,7 @@ class EmotionAnalyticsService:
summary = "您的情绪健康需要关注,建议采取一些改善措施。"
else:
summary = "您的情绪健康需要重点关注,建议寻求专业帮助。"
suggestions = [
EmotionSuggestion(
type="emotion_balance",
@@ -700,54 +703,54 @@ class EmotionAnalyticsService:
]
)
]
return EmotionSuggestionsResponse(
health_summary=summary,
suggestions=suggestions
)
async def get_cached_suggestions(
self,
end_user_id: str,
db: Session,
self,
end_user_id: str,
db: Session,
) -> Optional[Dict[str, Any]]:
"""从 Redis 缓存获取个性化情绪建议
Args:
end_user_id: 宿主ID用户组ID
db: 数据库会话(保留参数以保持接口兼容性)
Returns:
Dict: 缓存的建议数据,如果不存在或已过期返回 None
"""
try:
from app.cache.memory.emotion_memory import EmotionMemoryCache
logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}")
# 从 Redis 获取缓存
cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id)
if cached_data is None:
logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期")
return None
logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}")
return cached_data
except Exception as e:
logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True)
return None
async def save_suggestions_cache(
self,
end_user_id: str,
suggestions_data: Dict[str, Any],
db: Session,
expires_hours: int = 24
self,
end_user_id: str,
suggestions_data: Dict[str, Any],
db: Session,
expires_hours: int = 24
) -> None:
"""保存建议到 Redis 缓存
Args:
end_user_id: 宿主ID用户组ID
suggestions_data: 建议数据
@@ -756,24 +759,24 @@ class EmotionAnalyticsService:
"""
try:
from app.cache.memory.emotion_memory import EmotionMemoryCache
logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
# 计算过期时间(秒)
expire_seconds = expires_hours * 3600
# 保存到 Redis
success = await EmotionMemoryCache.set_emotion_suggestions(
user_id=end_user_id,
suggestions_data=suggestions_data,
expire=expire_seconds
)
if success:
logger.info(f"建议缓存保存成功: user={end_user_id}")
else:
logger.warning(f"建议缓存保存失败: user={end_user_id}")
except Exception as e:
logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True)
# 不抛出异常,缓存失败不应影响主流程

View File

@@ -334,7 +334,9 @@ class MemoryAgentService:
langchain_messages.append(HumanMessage(content=msg['content']))
elif msg['role'] == 'assistant':
langchain_messages.append(AIMessage(content=msg['content']))
print(100*'-')
print(langchain_messages)
print(100*'-')
# 初始状态 - 包含所有必要字段
initial_state = {
"messages": langchain_messages,

View File

@@ -18,6 +18,7 @@ from app.repositories.neo4j.neo4j_connector import Neo4jConnector
from app.models.app_model import App
from app.models.app_release_model import AppRelease
from app.models.end_user_model import EndUser
from app.utils.config_utils import resolve_config_id
api_logger = get_api_logger()
@@ -88,38 +89,36 @@ class WorkspaceAppService:
for release in app_releases:
memory_content = self._extract_memory_content(release.config)
memory_content=resolve_config_id(memory_content, self.db)
if memory_content and memory_content in processed_configs:
continue
release_info = {
"app_id": str(release.app_id),
"config": memory_content
}
if memory_content:
processed_configs.add(memory_content)
memory_config_info = self._get_memory_config(memory_content)
if memory_config_info:
if not any(dc["config_id"] == memory_config_info["config_id"] for dc in app_info["memory_configs"]):
app_info["memory_configs"].append(memory_config_info)
app_info["releases"].append(release_info)
def _extract_memory_content(self, config: Any) -> str:
"""Extract memory_comtent from config"""
if not config or not isinstance(config, dict):
return None
memory_obj = config.get('memory')
if memory_obj and isinstance(memory_obj, dict):
return memory_obj.get('memory_content')
return None
def _get_memory_config(self, memory_content: str) -> Dict[str, Any]:
"""Retrieve memory_config information based on memory_content"""
try:
@@ -129,7 +128,7 @@ class WorkspaceAppService:
# memory_config_result = self.db.execute(text(memory_config_query), memory_config_params).fetchone()
# if memory_config_result is None:
# return None
if memory_config_result:
return {
"config_id": memory_config_result.config_id,
@@ -144,20 +143,22 @@ class WorkspaceAppService:
}
except Exception as e:
api_logger.warning(f"查询memory_config失败memory_content: {memory_content}, 错误: {str(e)}")
return None
def _process_end_users(self, app: App, app_info: Dict[str, Any]) -> None:
"""Processing end-user information for applications"""
end_users = self.db.query(EndUser).filter(EndUser.app_id == app.id).all()
for end_user in end_users:
end_user_info = {
"id": str(end_user.id),
"app_id": str(end_user.app_id)
}
app_info["end_users"].append(end_user_info)
print(100*'-')
print(app_info)
def get_end_user_reflection_time(self, end_user_id: str) -> Optional[Any]:
"""
Read the reflection time of end users
@@ -176,7 +177,7 @@ class WorkspaceAppService:
except Exception as e:
api_logger.error(f"读取用户反思时间失败end_user_id: {end_user_id}, 错误: {str(e)}")
return None
def update_end_user_reflection_time(self, end_user_id: str) -> bool:
"""
Update the reflection time of end users to the current time
@@ -189,7 +190,7 @@ class WorkspaceAppService:
"""
try:
from datetime import datetime
end_user = self.db.query(EndUser).filter(EndUser.id == end_user_id).first()
if end_user:
end_user.reflection_time = datetime.now()
@@ -207,7 +208,7 @@ class WorkspaceAppService:
class MemoryReflectionService:
"""Memory reflection service category"""
def __init__(self,db: Session = Depends(get_db)):
self.db=db
@@ -252,22 +253,22 @@ class MemoryReflectionService:
"end_user_id": end_user_id,
"config_data": config_data
}
async def start_reflection_from_data(self, config_data: Dict[str, Any], end_user_id: str) -> Dict[str, Any]:
"""
Starting Reflection from Configuration Data
Args:
config_data: Configure data dictionary, including reflective configuration information
end_user_id: end_user_id
Returns:
Reflect on the execution results
"""
try:
config_id = config_data.get("config_id")
api_logger.info(f"从配置数据启动反思config_id: {config_id}, end_user_id: {end_user_id}")
if not config_data.get("enable_self_reflexion", False):
return {
@@ -277,7 +278,7 @@ class MemoryReflectionService:
"end_user_id": end_user_id,
"config_data": config_data
}
config_data_id=config_data['config_id']
reflection_config=WorkspaceAppService(self.db)._get_memory_config(config_data_id)

View File

@@ -347,7 +347,9 @@ class ModelConfigService:
"is_public": model_data.is_public,
"is_composite": True
}
if "load_balance_strategy" in model_data.model_fields_set:
model_config_data["load_balance_strategy"] = model_data.load_balance_strategy
model = ModelConfigRepository.create(db, model_config_data)
db.flush()
@@ -380,7 +382,7 @@ class ModelConfigService:
for model_config in api_key.model_configs:
compatible_types = {ModelType.LLM, ModelType.CHAT}
config_type = model_config.type
request_type = model_data.type
request_type = existing_model.type
if not (config_type == request_type or
(config_type in compatible_types and request_type in compatible_types)):
@@ -391,12 +393,14 @@ class ModelConfigService:
# 更新基本信息
existing_model.name = model_data.name
existing_model.type = model_data.type
# existing_model.type = model_data.type
existing_model.logo = model_data.logo
existing_model.description = model_data.description
existing_model.config = model_data.config
existing_model.is_active = model_data.is_active
existing_model.is_public = model_data.is_public
if "load_balance_strategy" in model_data.model_fields_set:
existing_model.load_balance_strategy = model_data.load_balance_strategy
# 更新 API Keys 关联
existing_model.api_keys.clear()
@@ -453,9 +457,11 @@ class ModelApiKeyService:
return ModelApiKeyRepository.get_by_model_config(db, model_config_id, is_active)
@staticmethod
async def create_api_key_by_provider(db: Session, data: model_schema.ModelApiKeyCreateByProvider) -> List[ModelApiKey]:
async def create_api_key_by_provider(db: Session, data: model_schema.ModelApiKeyCreateByProvider) -> tuple[
list[Any], list[Any]]:
"""根据provider为多个ModelConfig创建API Key"""
created_keys = []
failed_models = [] # 记录验证失败的模型
for model_config_id in data.model_config_ids:
model_config = ModelConfigRepository.get_by_id(db, model_config_id)
@@ -501,10 +507,9 @@ class ModelApiKeyService:
test_message="Hello"
)
if not validation_result["valid"]:
raise BusinessException(
f"模型配置验证失败: {validation_result['error']}",
BizCode.INVALID_PARAMETER
)
# 记录验证失败的模型,但不抛出异常
failed_models.append(model_name)
continue
# 创建API Key
api_key_data = ModelApiKeyCreate(
@@ -526,7 +531,7 @@ class ModelApiKeyService:
for key in created_keys:
db.refresh(key)
return created_keys
return created_keys, failed_models
@staticmethod
async def create_api_key(db: Session, api_key_data: ModelApiKeyCreate) -> ModelApiKey:
@@ -684,6 +689,9 @@ class ModelBaseService:
@staticmethod
def create_model_base(db: Session, data: model_schema.ModelBaseCreate):
existing = ModelBaseRepository.get_by_name_and_provider(db, data.name, data.provider)
if existing:
raise BusinessException("模型已存在", BizCode.DUPLICATE_NAME)
model_base = ModelBaseRepository.create(db, data.model_dump())
db.commit()
db.refresh(model_base)

View File

@@ -16,7 +16,7 @@ from app.models.prompt_optimizer_model import (
PromptOptimizerSession,
RoleType
)
from app.repositories.model_repository import ModelConfigRepository
from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository
from app.repositories.prompt_optimizer_repository import (
PromptOptimizerSessionRepository
)
@@ -168,7 +168,8 @@ class PromptOptimizerService:
logger.info(f"Prompt optimization started, user_id={user_id}, session_id={session_id}")
# Create LLM instance
api_config: ModelApiKey = model_config.api_keys[0]
api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config.id)
api_config: ModelApiKey = api_keys[0] if api_keys else None
llm = RedBearLLM(RedBearModelConfig(
model_name=api_config.model_name,
provider=api_config.provider,

View File

@@ -0,0 +1,46 @@
"""
Configuration utility functions
Shared utilities for configuration handling to avoid circular imports.
"""
from uuid import UUID
from sqlalchemy.orm import Session
def resolve_config_id(config_id: UUID | int|str, db: Session) -> UUID:
"""
解析 config_id如果是整数则通过 config_id_old 查找对应的 UUID
Args:
config_id: 配置IDUUID 或整数)
db: 数据库会话
Returns:
UUID: 解析后的配置ID
Raises:
ValueError: 当找不到对应的配置时
"""
from app.models.memory_config_model import MemoryConfig
if isinstance(config_id, UUID):
return config_id
if isinstance(config_id, str) and len(config_id)<=6:
memory_config = db.query(MemoryConfig).filter(
MemoryConfig.config_id_old == int(config_id)
).first()
print(memory_config)
if not memory_config:
raise ValueError(f"STR 未找到 config_id_old={config_id} 对应的配置")
return memory_config.config_id
if isinstance(config_id, int):
memory_config = db.query(MemoryConfig).filter(
MemoryConfig.config_id_old == config_id
).first()
if not memory_config:
raise ValueError(f"INT 未找到 config_id_old={config_id} 对应的配置")
return memory_config.config_id
return config_id

View File

@@ -28,7 +28,15 @@ def upgrade() -> None:
op.drop_constraint('data_config_pkey', 'memory_config', type_='primary')
op.alter_column('memory_config', 'config_id', new_column_name='config_id_old', nullable=True)
op.add_column('memory_config', sa.Column('config_id', sa.UUID(), nullable=True))
op.execute("UPDATE memory_config SET config_id = apply_id::uuid")
# Handle rows where apply_id might be NULL or invalid - generate new UUIDs for those
op.execute("""
UPDATE memory_config
SET config_id = CASE
WHEN apply_id IS NOT NULL AND apply_id ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
THEN apply_id::uuid
ELSE gen_random_uuid()
END
""")
op.alter_column('memory_config', 'config_id', nullable=False)
op.create_primary_key('memory_config_pkey', 'memory_config', ['config_id'])
op.execute("ALTER TABLE memory_config ALTER COLUMN config_id_old DROP DEFAULT")

View File

@@ -0,0 +1,30 @@
"""202601291352
Revision ID: 5ca246ee7dd4
Revises: 915bed077f8d
Create Date: 2026-01-29 13:52:47.647306
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '5ca246ee7dd4'
down_revision: Union[str, None] = '915bed077f8d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('model_bases', sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('model_bases', 'created_at')
# ### end Alembic commands ###

View File

@@ -0,0 +1,80 @@
"""20260129212722
Revision ID: 5de9b1e28509
Revises: 5ca246ee7dd4
Create Date: 2026-01-29 21:34:30.978031
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '5de9b1e28509'
down_revision: Union[str, None] = '5ca246ee7dd4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Neo4j migration: rename group_id to end_user_id
import asyncio
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
async def run_neo4j_upgrade():
connector = Neo4jConnector()
try:
async def transaction_func(tx):
result = await tx.run("""
MATCH (n)
WHERE n.group_id IS NOT NULL
SET n.end_user_id = n.group_id
REMOVE n.group_id
WITH count(n) AS node_count
MATCH ()-[r]->()
WHERE r.group_id IS NOT NULL
SET r.end_user_id = r.group_id
REMOVE r.group_id
RETURN node_count, count(r) AS rel_count
""")
return await result.data()
await connector.execute_write_transaction(transaction_func)
finally:
await connector.close()
asyncio.run(run_neo4j_upgrade())
def downgrade() -> None:
# Neo4j migration: rename end_user_id back to group_id
import asyncio
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
async def run_neo4j_downgrade():
connector = Neo4jConnector()
try:
async def transaction_func(tx):
result = await tx.run("""
MATCH (n)
WHERE n.end_user_id IS NOT NULL
SET n.group_id = n.end_user_id
REMOVE n.end_user_id
WITH count(n) AS node_count
MATCH ()-[r]->()
WHERE r.end_user_id IS NOT NULL
SET r.group_id = r.end_user_id
REMOVE r.end_user_id
RETURN node_count, count(r) AS rel_count
""")
return await result.data()
await connector.execute_write_transaction(transaction_func)
finally:
await connector.close()
asyncio.run(run_neo4j_downgrade())

View File

@@ -0,0 +1,224 @@
"""202601281340
Revision ID: 915bed077f8d
Revises: 75f0ec80e50b
Create Date: 2026-01-28 13:38:49.471560
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '915bed077f8d'
down_revision: Union[str, None] = '75f0ec80e50b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
BACKUP_TABLE_NAME = 'model_api_keys_backup_20260123'
def get_temp_models():
"""创建临时模型,用于迁移过程中查询数据"""
metadata = sa.MetaData()
# 临时ModelApiKey表仅包含需要的字段
ModelApiKey = sa.Table(
'model_api_keys', metadata,
sa.Column('id', sa.UUID(), primary_key=True),
sa.Column('model_config_id', sa.UUID(), nullable=True),
)
# 临时关联表(和升级脚本创建的表结构一致)
ModelConfigApiKeyAssociation = sa.Table(
'model_config_api_key_association', metadata,
sa.Column('model_config_id', sa.UUID(), nullable=False),
sa.Column('api_key_id', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
)
ModelApiKeyBackup = sa.Table(
BACKUP_TABLE_NAME, metadata,
sa.Column('id', sa.UUID(), primary_key=True),
sa.Column('model_name', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=True),
sa.Column('provider', sa.String(), nullable=False),
sa.Column('api_key', sa.String(), nullable=False),
sa.Column('api_base', sa.String(), nullable=True),
sa.Column('config', sa.JSON(), nullable=True),
sa.Column('usage_count', sa.String(), default="0"),
sa.Column('last_used_at', sa.DateTime(), nullable=True),
sa.Column('priority', sa.String(), default="1"),
sa.Column('model_config_id', sa.UUID(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('is_active', sa.Boolean(), default=True),
)
return ModelApiKey, ModelConfigApiKeyAssociation, ModelApiKeyBackup
def backup_model_api_keys():
"""备份model_api_keys表的结构和数据"""
connection = op.get_bind()
# 检查备份表是否已存在
result = connection.execute(sa.text(f"""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = '{BACKUP_TABLE_NAME}'
);
""")).scalar()
if result:
# 备份表已存在,先删除再重建(确保结构一致)
op.execute(f"DROP TABLE IF EXISTS {BACKUP_TABLE_NAME};")
# 直接复制表结构和数据PostgreSQL专用一步完成
op.execute(f"""
CREATE TABLE {BACKUP_TABLE_NAME} AS
SELECT * FROM model_api_keys;
""")
# 统计行数
backup_count = connection.execute(sa.text(f"SELECT COUNT(*) FROM {BACKUP_TABLE_NAME}")).scalar()
original_count = connection.execute(sa.text("SELECT COUNT(*) FROM model_api_keys")).scalar()
print(
f"已备份model_api_keys表到 {BACKUP_TABLE_NAME} \n"
f" 原表数据行数:{original_count} | 备份表数据行数:{backup_count}"
)
# def restore_model_api_keys_from_backup():
# """从备份表恢复model_api_keys数据可选用于回滚失败时手动恢复"""
# # 1. 清空原表(谨慎使用!)
# # op.execute("TRUNCATE TABLE model_api_keys;")
#
# # 2. 从备份表恢复数据
# op.execute(f"""
# INSERT INTO model_api_keys
# SELECT * FROM {BACKUP_TABLE_NAME}
# ON CONFLICT (id) DO UPDATE SET
# model_name = EXCLUDED.model_name,
# description = EXCLUDED.description,
# provider = EXCLUDED.provider,
# api_key = EXCLUDED.api_key,
# api_base = EXCLUDED.api_base,
# config = EXCLUDED.config,
# usage_count = EXCLUDED.usage_count,
# last_used_at = EXCLUDED.last_used_at,
# priority = EXCLUDED.priority,
# model_config_id = EXCLUDED.model_config_id,
# created_at = EXCLUDED.created_at,
# updated_at = EXCLUDED.updated_at,
# is_active = EXCLUDED.is_active;
# """)
# print(f"✅ 已从 {BACKUP_TABLE_NAME} 恢复model_api_keys表数据")
def upgrade() -> None:
backup_model_api_keys()
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('model_bases',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('logo', sa.String(length=255), nullable=True, comment='模型logo图片URL'),
sa.Column('name', sa.String(), nullable=False, comment='模型唯一标识如gpt-3.5-turbo'),
sa.Column('type', sa.String(), nullable=False, comment='模型类型'),
sa.Column('provider', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True, comment='模型描述'),
sa.Column('is_deprecated', sa.Boolean(), nullable=False, comment='是否弃用'),
sa.Column('is_official', sa.Boolean(), nullable=True, comment='是否供应商官方模型(区分自定义)'),
sa.Column('tags', sa.ARRAY(sa.String()), nullable=False, comment="模型标签(如['聊天', '创作']"),
sa.Column('add_count', sa.Integer(), nullable=False, comment='模型被用户添加的次数'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'provider', name='uk_model_name_provider')
)
op.create_index(op.f('ix_model_bases_id'), 'model_bases', ['id'], unique=False)
op.create_index(op.f('ix_model_bases_provider'), 'model_bases', ['provider'], unique=False)
op.create_index(op.f('ix_model_bases_type'), 'model_bases', ['type'], unique=False)
op.create_table('model_config_api_key_association',
sa.Column('model_config_id', sa.UUID(), nullable=False),
sa.Column('api_key_id', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['api_key_id'], ['model_api_keys.id'], ),
sa.ForeignKeyConstraint(['model_config_id'], ['model_configs.id'], ),
sa.PrimaryKeyConstraint('model_config_id', 'api_key_id')
)
op.add_column('model_api_keys', sa.Column('description', sa.String(), nullable=True, comment='备注'))
op.add_column('model_configs', sa.Column('model_id', sa.UUID(), nullable=True, comment='基础模型ID'))
op.add_column('model_configs', sa.Column('logo', sa.String(length=255), nullable=True, comment='模型logo图片URL'))
op.add_column('model_configs', sa.Column('provider', sa.String(), server_default='composite', nullable=False, comment='供应商'))
op.add_column('model_configs', sa.Column('is_composite', sa.Boolean(), server_default='true', nullable=False, comment='是否为组合模型'))
op.add_column('model_configs', sa.Column('load_balance_strategy', sa.String(), nullable=True, comment='负载均衡策略'))
op.create_index(op.f('ix_model_configs_model_id'), 'model_configs', ['model_id'], unique=False)
op.create_foreign_key("model_configs_model_id_fkey", 'model_configs', 'model_bases', ['model_id'], ['id'])
connection = op.get_bind()
ModelApiKey, ModelConfigApiKeyAssociation, _ = get_temp_models()
# 查询所有有model_config_id的API Key
api_keys = connection.execute(
sa.select(ModelApiKey.c.id, ModelApiKey.c.model_config_id)
.where(ModelApiKey.c.model_config_id.isnot(None))
).fetchall()
# 批量插入到多对多表
if api_keys:
association_data = [
{
'model_config_id': row.model_config_id,
'api_key_id': row.id
}
for row in api_keys
]
connection.execute(ModelConfigApiKeyAssociation.insert(), association_data)
op.drop_constraint(op.f('model_api_keys_model_config_id_fkey'), 'model_api_keys', type_='foreignkey')
op.drop_column('model_api_keys', 'model_config_id')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("model_configs_model_id_fkey", 'model_configs', type_='foreignkey')
op.drop_index(op.f('ix_model_configs_model_id'), table_name='model_configs')
op.drop_column('model_configs', 'load_balance_strategy')
op.drop_column('model_configs', 'is_composite')
op.drop_column('model_configs', 'provider')
op.drop_column('model_configs', 'logo')
op.drop_column('model_configs', 'model_id')
op.add_column('model_api_keys', sa.Column('model_config_id', sa.UUID(), autoincrement=False, nullable=True, comment='模型配置ID'))
connection = op.get_bind()
ModelApiKey, ModelConfigApiKeyAssociation, _ = get_temp_models()
# 查询多对多表中的关联数据取每个API Key的第一个关联的model_config_id
association_data = connection.execute(
sa.select(
ModelConfigApiKeyAssociation.c.api_key_id,
ModelConfigApiKeyAssociation.c.model_config_id
).distinct(ModelConfigApiKeyAssociation.c.api_key_id)
).fetchall()
# 批量更新model_api_keys表
if association_data:
for api_key_id, model_config_id in association_data:
connection.execute(
sa.update(ModelApiKey)
.where(ModelApiKey.c.id == api_key_id)
.values(model_config_id=model_config_id)
)
op.execute(
"UPDATE model_api_keys SET model_config_id = '00000000-0000-0000-0000-000000000000' WHERE model_config_id IS NULL")
op.alter_column('model_api_keys', 'model_config_id', nullable=False)
op.create_foreign_key(op.f('model_api_keys_model_config_id_fkey'), 'model_api_keys', 'model_configs', ['model_config_id'], ['id'])
op.drop_column('model_api_keys', 'description')
op.drop_table('model_config_api_key_association')
# ### 可选:回滚时恢复备份(如需)###
# restore_model_api_keys_from_backup()
print(
f"回滚完成!备份表 {BACKUP_TABLE_NAME} 仍保留,如需手动恢复可执行 restore_model_api_keys_from_backup() 函数")
op.drop_index(op.f('ix_model_bases_type'), table_name='model_bases')
op.drop_index(op.f('ix_model_bases_provider'), table_name='model_bases')
op.drop_index(op.f('ix_model_bases_id'), table_name='model_bases')
op.drop_table('model_bases')
# ### end Alembic commands ###

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""API Key认证MCP服务器"""
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import uvicorn
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
app = FastAPI(title="API Key MCP Server", version="1.0.0")
# API Key配置
API_KEYS = {"test-api-key", "demo-key-123"}
def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""验证API Key"""
if x_api_key and x_api_key in API_KEYS:
return True
raise HTTPException(status_code=401, detail="Invalid API Key")
@app.get("/")
async def root():
return {"name": "API Key MCP Server", "version": "1.0.0", "auth_type": "api_key"}
@app.get("/health")
async def health():
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "api_key"}
@app.post("/mcp")
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_api_key)):
return await handle_mcp_request(request, "API Key MCP Server")
if __name__ == "__main__":
print("启动API Key认证MCP服务器...")
print("访问 http://localhost:8004 查看服务状态")
print("MCP端点: http://localhost:8004/mcp")
print("认证方式: API Key (Header: X-API-Key)")
print("测试API Keys: test-api-key, demo-key-123")
uvicorn.run(app, host="0.0.0.0", port=8004)

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env python3
"""Basic Auth认证MCP服务器"""
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import uvicorn
import base64
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
app = FastAPI(title="Basic Auth MCP Server", version="1.0.0")
# Basic Auth配置
BASIC_AUTH_USERS = {"admin": "password", "user": "secret"}
def verify_basic_auth(authorization: Optional[str] = Header(None)):
"""验证Basic Auth"""
if authorization and authorization.startswith("Basic "):
try:
credentials = base64.b64decode(authorization.split(" ")[1]).decode()
username, password = credentials.split(":", 1)
if username in BASIC_AUTH_USERS and BASIC_AUTH_USERS[username] == password:
return True
except:
pass
raise HTTPException(status_code=401, detail="Invalid Basic Auth")
@app.get("/")
async def root():
return {"name": "Basic Auth MCP Server", "version": "1.0.0", "auth_type": "basic_auth"}
@app.get("/health")
async def health():
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "basic_auth"}
@app.post("/mcp")
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_basic_auth)):
return await handle_mcp_request(request, "Basic Auth MCP Server")
if __name__ == "__main__":
print("启动Basic Auth认证MCP服务器...")
print("访问 http://localhost:8006 查看服务状态")
print("MCP端点: http://localhost:8006/mcp")
print("认证方式: Basic Auth (Header: Authorization: Basic <base64>)")
print("测试用户: admin:password, user:secret")
uvicorn.run(app, host="0.0.0.0", port=8006)

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python3
"""Bearer Token认证MCP服务器"""
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import uvicorn
from mcp_base import MCPRequest, handle_mcp_request, TOOLS
app = FastAPI(title="Bearer Token MCP Server", version="1.0.0")
# Bearer Token配置
BEARER_TOKENS = {"bearer-token-123", "demo-bearer-token"}
def verify_bearer_token(authorization: Optional[str] = Header(None)):
"""验证Bearer Token"""
if authorization and authorization.startswith("Bearer "):
token = authorization.split(" ")[1]
if token in BEARER_TOKENS:
return True
raise HTTPException(status_code=401, detail="Invalid Bearer Token")
@app.get("/")
async def root():
return {"name": "Bearer Token MCP Server", "version": "1.0.0", "auth_type": "bearer_token"}
@app.get("/health")
async def health():
return {"status": "healthy", "tools": len(TOOLS), "auth_type": "bearer_token"}
@app.post("/mcp")
async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_bearer_token)):
return await handle_mcp_request(request, "Bearer Token MCP Server")
if __name__ == "__main__":
print("启动Bearer Token认证MCP服务器...")
print("访问 http://localhost:8005 查看服务状态")
print("MCP端点: http://localhost:8005/mcp")
print("认证方式: Bearer Token (Header: Authorization: Bearer <token>)")
print("测试Bearer Tokens: bearer-token-123, demo-bearer-token")
uvicorn.run(app, host="0.0.0.0", port=8005)

View File

@@ -1,111 +0,0 @@
#!/usr/bin/env python3
"""MCP服务器基础模块 - 共享的模型和处理逻辑"""
from pydantic import BaseModel
from typing import Dict, Any
class MCPRequest(BaseModel):
jsonrpc: str = "2.0"
id: str
method: str
params: Dict[str, Any] = {}
class MCPResponse(BaseModel):
jsonrpc: str = "2.0"
id: str
result: Any = None
error: Dict[str, Any] = None
# 工具定义
TOOLS = [
{
"name": "calculator",
"description": "简单计算器",
"inputSchema": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式"}
},
"required": ["expression"]
}
},
{
"name": "echo",
"description": "回显工具",
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "要回显的消息"}
},
"required": ["message"]
}
}
]
async def handle_mcp_request(request: MCPRequest, server_name: str = "MCP Server"):
"""处理MCP请求"""
try:
if request.method == "initialize":
return MCPResponse(
id=request.id,
result={
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {"listChanged": True}},
"serverInfo": {"name": server_name, "version": "1.0.0"}
}
)
elif request.method == "tools/list":
return MCPResponse(
id=request.id,
result={"tools": TOOLS}
)
elif request.method == "tools/call":
tool_name = request.params.get("name")
arguments = request.params.get("arguments", {})
if tool_name == "calculator":
try:
expression = arguments.get("expression", "")
result = eval(expression)
return MCPResponse(
id=request.id,
result={"content": [{"type": "text", "text": f"结果: {result}"}]}
)
except Exception as e:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"计算错误: {str(e)}"}
)
elif tool_name == "echo":
message = arguments.get("message", "")
return MCPResponse(
id=request.id,
result={"content": [{"type": "text", "text": f"Echo: {message}"}]}
)
else:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"未知工具: {tool_name}"}
)
elif request.method == "ping":
return MCPResponse(
id=request.id,
result={"status": "pong"}
)
else:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"未知方法: {request.method}"}
)
except Exception as e:
return MCPResponse(
id=request.id,
error={"code": -1, "message": str(e)}
)

View File

@@ -1,130 +0,0 @@
#!/usr/bin/env python3
"""简化的MCP服务器 - 用于测试MCP工具集成"""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict, Any, List
import uvicorn
app = FastAPI(title="Simple MCP Server", version="1.0.0")
class MCPRequest(BaseModel):
jsonrpc: str = "2.0"
id: str
method: str
params: Dict[str, Any] = {}
class MCPResponse(BaseModel):
jsonrpc: str = "2.0"
id: str
result: Any = None
error: Dict[str, Any] = None
# 可用工具定义
TOOLS = [
{
"name": "calculator",
"description": "简单计算器",
"inputSchema": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式"}
},
"required": ["expression"]
}
},
{
"name": "echo",
"description": "回显工具",
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "要回显的消息"}
},
"required": ["message"]
}
}
]
@app.get("/")
async def root():
return {"name": "Simple MCP Server", "version": "1.0.0"}
@app.get("/health")
async def health():
return {"status": "healthy", "tools": len(TOOLS)}
@app.post("/mcp")
async def mcp_handler(request: MCPRequest):
"""处理MCP请求"""
try:
if request.method == "initialize":
return MCPResponse(
id=request.id,
result={
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {"listChanged": True}},
"serverInfo": {"name": "Simple MCP Server", "version": "1.0.0"}
}
)
elif request.method == "tools/list":
return MCPResponse(
id=request.id,
result={"tools": TOOLS}
)
elif request.method == "tools/call":
tool_name = request.params.get("name")
arguments = request.params.get("arguments", {})
if tool_name == "calculator":
try:
expression = arguments.get("expression", "")
result = eval(expression) # 注意生产环境不要用eval
return MCPResponse(
id=request.id,
result={"content": [{"type": "text", "text": f"结果: {result}"}]}
)
except Exception as e:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"计算错误: {str(e)}"}
)
elif tool_name == "echo":
message = arguments.get("message", "")
return MCPResponse(
id=request.id,
result={"content": [{"type": "text", "text": f"Echo: {message}"}]}
)
else:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"未知工具: {tool_name}"}
)
elif request.method == "ping":
return MCPResponse(
id=request.id,
result={"status": "pong"}
)
else:
return MCPResponse(
id=request.id,
error={"code": -1, "message": f"未知方法: {request.method}"}
)
except Exception as e:
return MCPResponse(
id=request.id,
error={"code": -1, "message": str(e)}
)
if __name__ == "__main__":
print("启动简化MCP服务器...")
print("访问 http://localhost:8002 查看服务状态")
print("MCP端点: http://localhost:8002/mcp")
uvicorn.run(app, host="0.0.0.0", port=8002)

View File

@@ -108,4 +108,8 @@ export const getShareToken = (share_token: string, user_id: string) => {
// 复制应用
export const copyApplication = (app_id: string, new_name: string) => {
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
}
}
// 数据统计
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
return request.get(`/apps/${app_id}/statistics`, data)
}

View File

@@ -0,0 +1,25 @@
import { request, API_PREFIX } from '@/utils/request'
// Upload filefile storage has expiration period
export const fileUploadUrl = `${API_PREFIX}/storage/files`
export const fileUpload = (formData?: unknown) => {
return request.uploadFile('/storage/files', formData)
}
// Get file access URL (no token required)
export const getFileUrl = (file_id: string) => `/storage/files/${file_id}/url`
export const getFileLink = (fileId: string, data: { permanent?: boolean } = { permanent: true }) => {
return request.get(getFileUrl(fileId), data)
}
// Get file internally
export const getInternalFileUrl = (file_id: string) => `/storage/files/${file_id}`
export const getInternalFile = (fileId: string) => {
return request.get(getInternalFileUrl(fileId))
}
// Delete file
export const deleteFileUrl = (file_id: string) => `/storage/files/${file_id}`
export const deleteFile = (fileId: string) => {
return request.delete(deleteFileUrl(fileId))
}

View File

@@ -65,7 +65,7 @@ export const getModelTypeList = async () => {
};
// 获取模型列表
export const getModelList = async (pageInfo: PageRequest) => {
const response = await request.get(`${apiPrefix}/models`, pageInfo);
const response = await request.get(`${apiPrefix}/models`, { ...pageInfo, is_active: true });
return response as any;
};
//获取模型提供者

View File

@@ -1,23 +1,68 @@
import { request } from '@/utils/request'
import type { ModelFormData } from '@/views/ModelManagement/types'
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'
// 模型列表
// Model list
export const getModelListUrl = '/models'
export const getModelList = (data: { type: string; pagesize: number; page: number; }) => {
export const getModelList = (data: Query) => {
return request.get(getModelListUrl, data)
}
// 创建模型
export const addModel = (data: ModelFormData) => {
return request.post('/models', data)
}
// 更新模型
export const updateModel = (apiKeyId: string, data: ModelFormData) => {
return request.put(`/models/apikeys/${apiKeyId}`, data)
}
// 模型类型列表
// Model type list
export const modelTypeUrl = '/models/type'
// 模型供应商列表
// Model provider list
export const modelProviderUrl = '/models/provider'
export const getModelProviderList = () => {
return request.get(modelProviderUrl)
}
// New model list
export const getModelNewListUrl = '/models/new'
export const getModelNewList = (data: Query) => {
return request.get(getModelNewListUrl, data)
}
// Get model information
export const getModelInfo = (model_id: string) => {
return request.get(`/models/${model_id}`)
}
// Create composite model
export const addCompositeModel = (data: CompositeModelForm) => {
return request.post('/models/composite', data)
}
// Update composite model
export const updateCompositeModel = (model_id: string, data: CompositeModelForm) => {
return request.put(`/models/composite/${model_id}`, data)
}
// Delete composite model
export const deleteCompositeModel = (model_id: string) => {
return request.delete(`/models/composite/${model_id}`)
}
// Create API keys for all matching models by provider
export const updateProviderApiKeys = (data: KeyConfigModalForm) => {
return request.post('/models/provider/apikeys', data)
}
// Create model API key
export const addModelApiKey = (model_id: string, data: MultiKeyForm) => {
return request.post(`/models/${model_id}/apikeys`, data)
}
// Delete model API key
export const deleteModelApiKey = (api_key_id: string) => {
return request.delete(`/models/apikeys/${api_key_id}`)
}
// Update model status
export const updateModelStatus = (model_id: string, data: { is_active: boolean; }) => {
return request.put(`/models/${model_id}`, data)
}
// Model plaza list
export const getModelPlaza = (data: { search?: string; provider?: string; }) => {
return request.get('/models/model_plaza', data)
}
// Add model to plaza
export const addModelPlaza = (model_base_id: string) => {
return request.post(`/models/model_plaza/${model_base_id}/add`)
}
// Create custom model
export const addCustomModel = (data: CustomModelForm) => {
return request.post('/models/model_plaza', data)
}
// Update custom model
export const updateCustomModel = (model_base_id: string, data: CustomModelForm) => {
return request.put(`/models/model_plaza/${model_base_id}`, data)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_16762_59518)">
<path d="M12.6667 0H3.33333C1.49238 0 0 1.49238 0 3.33333V12.6667C0 14.5076 1.49238 16 3.33333 16H12.6667C14.5076 16 16 14.5076 16 12.6667V3.33333C16 1.49238 14.5076 0 12.6667 0Z" fill="url(#paint0_linear_16762_59518)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 12.093L6.3825 12.6323L5.75184 12.2116L6.4385 11.9823L6.22784 11.3503L5.04917 11.743L4.6665 11.4883V9.66631C4.6665 9.54031 4.59517 9.42497 4.4825 9.3683L3.33317 8.79364V7.20564L4.33317 6.70564L5.33317 7.20564V8.33297C5.33317 8.45964 5.4045 8.57497 5.51717 8.63164L6.8505 9.29831L7.14917 8.70164L5.99984 8.12697V7.20564L7.14917 6.63164C7.26184 6.57497 7.33317 6.45964 7.33317 6.33297V5.33297H6.6665V6.12697L5.6665 6.62697L4.6665 6.12697V4.51164L5.33317 4.06697V5.33297H5.99984V3.62297L6.3825 3.36764L7.99984 3.90697V12.093ZM11.6665 11.333C11.8498 11.333 11.9998 11.4823 11.9998 11.6663C11.9998 11.8503 11.8498 11.9996 11.6665 11.9996C11.4832 11.9996 11.3332 11.8503 11.3332 11.6663C11.3332 11.4823 11.4832 11.333 11.6665 11.333ZM10.9998 3.99964C11.1832 3.99964 11.3332 4.14897 11.3332 4.33297C11.3332 4.51697 11.1832 4.6663 10.9998 4.6663C10.8165 4.6663 10.6665 4.51697 10.6665 4.33297C10.6665 4.14897 10.8165 3.99964 10.9998 3.99964ZM12.3332 7.99964C12.5165 7.99964 12.6665 8.14897 12.6665 8.33297C12.6665 8.51697 12.5165 8.66631 12.3332 8.66631C12.1498 8.66631 11.9998 8.51697 11.9998 8.33297C11.9998 8.14897 12.1498 7.99964 12.3332 7.99964ZM11.3945 8.66631C11.5325 9.05364 11.8992 9.33297 12.3332 9.33297C12.8845 9.33297 13.3332 8.88497 13.3332 8.33297C13.3332 7.78164 12.8845 7.33297 12.3332 7.33297C11.8992 7.33297 11.5325 7.61297 11.3945 7.99964H8.6665V6.66631H10.9998C11.1838 6.66631 11.3332 6.51764 11.3332 6.33297V5.27164C11.7205 5.13364 11.9998 4.76697 11.9998 4.33297C11.9998 3.78164 11.5512 3.33297 10.9998 3.33297C10.4485 3.33297 9.99984 3.78164 9.99984 4.33297C9.99984 4.76697 10.2792 5.13364 10.6665 5.27164V5.99964H8.6665V3.6663C8.6665 3.52297 8.5745 3.39564 8.4385 3.3503L6.4385 2.68364C6.3405 2.65097 6.23384 2.66564 6.1485 2.7223L4.1485 4.05564C4.05584 4.11764 3.99984 4.22164 3.99984 4.33297V6.12697L2.8505 6.70164C2.73784 6.75831 2.6665 6.87364 2.6665 6.99964V8.99964C2.6665 9.12631 2.73784 9.24164 2.8505 9.29831L3.99984 9.87231V11.6663C3.99984 11.7776 4.05584 11.8823 4.1485 11.9436L6.1485 13.277C6.20384 13.3143 6.26784 13.333 6.33317 13.333C6.3685 13.333 6.40384 13.3276 6.4385 13.3156L8.4385 12.649C8.5745 12.6043 8.6665 12.477 8.6665 12.333V10.6663H10.1952L10.7638 11.2356L10.7725 11.227C10.7072 11.3603 10.6665 11.5083 10.6665 11.6663C10.6665 12.2176 11.1152 12.6663 11.6665 12.6663C12.2178 12.6663 12.6665 12.2176 12.6665 11.6663C12.6665 11.115 12.2178 10.6663 11.6665 10.6663C11.5078 10.6663 11.3598 10.707 11.2272 10.773L11.2358 10.7643L10.5692 10.0976C10.5065 10.035 10.4218 9.99964 10.3332 9.99964H8.6665V8.66631H11.3945Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_16762_59518" x1="0" y1="1600" x2="1600" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#055F4E"/>
<stop offset="1" stop-color="#56C0A7"/>
</linearGradient>
<clipPath id="clip0_16762_59518">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,24 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Xorbits Square" clip-path="url(#clip0_9850_26870)">
<path id="Vector" d="M8.00391 12.3124C8.69334 13.0754 9.47526 13.7494 10.3316 14.3188C11.0667 14.8105 11.8509 15.2245 12.6716 15.5541C14.1617 14.1465 15.3959 12.4907 16.3192 10.6606L21.7051 0L12.3133 7.38353C10.5832 8.74456 9.12178 10.416 8.00391 12.3124Z" fill="url(#paint0_linear_9850_26870)"/>
<path id="Vector_2" d="M7.23504 18.9512C6.56092 18.5012 5.92386 18.0265 5.3221 17.5394L2.06445 24L7.91975 19.3959C7.69034 19.2494 7.46092 19.103 7.23504 18.9512Z" fill="url(#paint1_linear_9850_26870)"/>
<path id="Vector_3" d="M19.3161 8.57474C21.0808 10.9147 21.5961 13.5159 20.3996 15.3053C18.6526 17.9189 13.9161 17.8183 9.82024 15.0812C5.72435 12.3441 3.82024 8.0065 5.56729 5.39297C6.76377 3.60356 9.36318 3.0865 12.2008 3.81886C7.29318 1.73474 2.62376 1.94121 0.813177 4.64474C-1.45976 8.04709 1.64435 14.1177 7.74494 18.1889C13.8455 22.26 20.6361 22.8124 22.9091 19.4118C24.7179 16.703 23.1173 12.3106 19.3161 8.57474Z" fill="url(#paint2_linear_9850_26870)"/>
</g>
<defs>
<linearGradient id="paint0_linear_9850_26870" x1="2.15214" y1="24.3018" x2="21.2921" y2="0.0988218" gradientUnits="userSpaceOnUse">
<stop stop-color="#E9A85E"/>
<stop offset="1" stop-color="#F52B76"/>
</linearGradient>
<linearGradient id="paint1_linear_9850_26870" x1="2.06269" y1="24.2294" x2="21.2027" y2="0.028252" gradientUnits="userSpaceOnUse">
<stop stop-color="#E9A85E"/>
<stop offset="1" stop-color="#F52B76"/>
</linearGradient>
<linearGradient id="paint2_linear_9850_26870" x1="-0.613606" y1="3.843" x2="21.4449" y2="18.7258" gradientUnits="userSpaceOnUse">
<stop stop-color="#6A0CF5"/>
<stop offset="1" stop-color="#AB66F3"/>
</linearGradient>
<clipPath id="clip0_9850_26870">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -15,7 +15,7 @@ interface ApiResponse<T> {
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
url: string;
params?: Record<string, unknown>;
valueKey?: string | string[];
valueKey?: string;
labelKey?: string;
placeholder?: string;
hasAll?: boolean;
@@ -66,18 +66,11 @@ const CustomSelect: FC<CustomSelectProps> = ({
{...props}
>
{hasAll && <Select.Option value={null}>{allTitle || t('common.all')}</Select.Option>}
{displayOptions.map((option) => {
const getValue = () => {
if (typeof valueKey === 'string') return option[valueKey];
return valueKey.find(key => option[key] != null) ? option[valueKey.find(key => option[key] != null)!] : undefined;
};
const value = getValue();
return (
<Select.Option key={value} value={value}>
{String(option[labelKey])}
</Select.Option>
);
})}
{displayOptions.map((option) => (
<Select.Option key={option[valueKey]} value={option[valueKey]}>
{String(option[labelKey])}
</Select.Option>
))}
</Select>
);
};

View File

@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next'
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
import Empty from './index'
const PageEmpty = ({ size = [240, 210] }: { size?: number | number[] }) => {
const { t } = useTranslation()
return (
<Empty
url={pageEmptyIcon}
title={t('empty.pageEmpty')}
subTitle={t('empty.pageEmptyDesc')}
size={size}
className="rb:h-full"
/>
)
}
export default PageEmpty;

View File

@@ -0,0 +1,13 @@
.page-tabs:global(.ant-segmented) {
background-color: rgba(91, 97, 103, 0.08);
padding: 4px;
}
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
line-height: 24px;
min-height: 24px;
padding: 0 12px;
}
.page-tabs:global(.ant-segmented .ant-segmented-item-selected) {
box-shadow: 0px 2px 4px 0px rgba(33, 35, 50, 0.16);
}

View File

@@ -0,0 +1,18 @@
import { type FC } from 'react';
import { Segmented, type SegmentedProps } from 'antd';
import styles from './index.module.css';
const PageTabs: FC<SegmentedProps> = ({
value,
options,
onChange
}) => {
return <Segmented
value={value}
options={options}
onChange={onChange}
className={styles.pageTabs}
/>;
};
export default PageTabs;

View File

@@ -1,5 +1,5 @@
import { type FC, type ReactNode } from 'react'
import { Card } from 'antd';
import { Card, Tooltip } from 'antd';
import clsx from 'clsx';
interface RbCardProps {
@@ -9,7 +9,7 @@ interface RbCardProps {
extra?: ReactNode;
children?: ReactNode;
avatar?: ReactNode;
avatarUrl?: string;
avatarUrl?: string | null;
bodyPadding?: string;
bodyClassName?: string;
headerType?: 'border' | 'borderless' | 'borderBL' | 'borderL';
@@ -50,7 +50,7 @@ const RbCard: FC<RbCardProps> = ({
<Card
{...props}
title={typeof title === 'function' ? title() : title ?
<div className="rb:flex rb:items-center">
<div className="rb:flex rb:items-center rb:gap-2">
{avatarUrl
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
: avatar ? avatar : null
@@ -59,11 +59,11 @@ const RbCard: FC<RbCardProps> = ({
clsx(
{
'rb:max-w-full': !avatarUrl && !avatar,
'rb:max-w-[calc(100%-60px)]': avatarUrl || avatar,
'rb:max-w-[calc(100%-80px)]': avatarUrl || avatar,
}
)
}>
<div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div>
<Tooltip title={title}><div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div></Tooltip>
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
</div>
</div> : null

View File

@@ -1,23 +1,23 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Upload, Modal, Image, App } from 'antd';
import { Upload, Image, App } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
// import { UploadOutlined, } from '@ant-design/icons';
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next';
import PlusIcon from '@/assets/images/plus.svg'
import { cookieUtils } from '@/utils/request'
import { fileUploadUrl } from '@/api/fileStorage'
import styles from './index.module.less'
const { confirm } = Modal;
interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
interface UploadImagesProps extends Omit<UploadProps, 'onChange' | 'fileList'> {
/** 上传接口地址 */
action?: string;
/** 是否支持多选 */
multiple?: boolean;
/** 已上传的文件列表 */
fileList?: UploadFile[];
fileList?: UploadFile[] | UploadFile;
/** 文件列表变化回调 */
onChange?: (fileList: UploadFile[]) => void;
onChange?: (fileList?: UploadFile[] | UploadFile) => void;
/** 禁用上传 */
disabled?: boolean;
/** 文件大小限制MB */
@@ -28,6 +28,7 @@ interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
isAutoUpload?: boolean;
/** 最大上传文件数 */
maxCount?: number;
className?: string;
}
const ALL_FILE_TYPE: {
[key: string]: string;
@@ -59,7 +60,7 @@ const getBase64 = (file: FileType): Promise<string> => {
* 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能
*/
const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
action = '/api/upload',
action = fileUploadUrl,
multiple = false,
fileList: propFileList = [],
onChange,
@@ -68,27 +69,42 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
fileType = ['png', 'jpg', 'gif'],
isAutoUpload = true,
maxCount = 1,
className = 'rb:size-24! rb:leading-1!',
...props
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
const { message, modal } = App.useApp()
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [accept, setAccept] = useState<string | undefined>();
// const [loading, setLoading] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
useEffect(() => {
if (!Array.isArray(propFileList) && typeof propFileList === 'object') {
setFileList([propFileList]);
}
}, [propFileList])
const updateValue = (list: UploadFile[]) => {
if (maxCount === 1) {
onChange?.(list[0])
} else {
onChange?.(list)
}
}
// 处理文件移除
const handleRemove = (file: UploadFile) => {
confirm({
title: '确定要删除此文件吗?',
okText: '确定',
modal.confirm({
title: t('common.confirmRemoveFile'),
okText: `${t('common.confirm')}`,
okType: 'danger',
cancelText: '取消',
cancelText: `${t('common.cancel')}`,
onOk: () => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
onChange?.(newFileList);
updateValue(newFileList)
},
});
return false; // 阻止默认删除行为由confirm控制
@@ -100,7 +116,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
if (fileSize && file.size) {
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
if (!isLtMaxSize) {
message.error(`文件大小不能超过 ${fileSize}MB`);
message.error(t('common.fileSizeTip', { size: fileSize }));
return Upload.LIST_IGNORE;
}
}
@@ -108,7 +124,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
if (accept && accept.length > 0 && file.type) {
const isAccept = accept.includes(file.type);
if (!isAccept) {
message.error(`不支持的文件类型: ${file.type}`);
message.error(`${t('common.fileAcceptTip')}${file.type}`);
return Upload.LIST_IGNORE;
}
}
@@ -119,7 +135,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
}
const newFileList = [...fileList, file];
setFileList(newFileList);
onChange?.(newFileList);
updateValue(newFileList);
return Upload.LIST_IGNORE; // 阻止自动上传
}
@@ -129,17 +145,13 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
// 处理上传状态变化
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
if (onChange) {
onChange(newFileList);
}
updateValue(newFileList);
};
// 清空已上传文件
const clearFiles = () => {
setFileList([]);
if (onChange) {
onChange([]);
}
updateValue([]);
}
const handlePreview = async (file: UploadFile) => {
@@ -167,7 +179,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
fileList,
beforeUpload,
headers: {
authorization: cookieUtils.get('authToken') || '',
authorization: `Bearer ${cookieUtils.get('authToken') }`,
},
onPreview: handlePreview,
onRemove: handleRemove,
@@ -180,6 +192,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
showRemoveIcon: true,
showDownloadIcon: false,
},
className: `${styles.imageUpload} ${className}`,
...props,
};
@@ -193,16 +206,9 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
<>
<Upload
{...uploadProps}
style={{
width: '136px',
height: '136px',
}}
>
{fileList.length < maxCount && (
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center">
<img src={PlusIcon} className="rb:w-[32px] rb:h-[32px]" />
<div className="rb:mt-[12px] rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">{t('common.clickUploadIcon')}</div>
</div>
<img src={PlusIcon} className="rb:size-7" />
)}
</Upload>
{previewImage && (

View File

@@ -0,0 +1,7 @@
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container) {
width: 96px;
height: 96px;
}

View File

@@ -419,6 +419,9 @@ export const en = {
statusEnabled: 'Available',
statusDisabled: 'Unavailable',
remove: 'Remove',
fileSizeTip: 'File size cannot exceed {{size}}MB',
fileAcceptTip: 'Unsupported file type:'
},
model: {
searchPlaceholder: 'search model…',
@@ -510,6 +513,64 @@ export const en = {
gpustack: "Gpustack",
bedrock: "Bedrock"
},
modelNew: {
group: 'Model Group',
list: 'Model List',
square: 'Model Plaza',
createGroupModel: 'Create Model Group',
groupSearchPlaceholder: 'Search model groups',
listSearchPlaceholder: 'Search available models',
squareSearchPlaceholder: 'Search platform models',
status: 'Model Status',
created_at: 'Created At',
configureBtn: 'Click to Configure',
showModel: 'Show Model',
keyConfig: 'Configure KEY',
modelConfiguration: 'Model Configuration',
logo: 'Model LOGO',
name: 'Model Name',
type: 'Model Type',
modelImplement: 'Model Implementation',
addImplement: 'Add Implementation',
noAuth: 'Unauthorized (Limited to 1 implementation)',
implementConfig: 'Configure Model Implementation',
provider: 'Model Provider',
api_key_ids: 'Select Model',
viewAll: 'More',
modelCount: 'Total {{count}} models',
modelList: 'Model List',
added: ' Added',
addSuccess: 'Added successfully',
model_name: 'Model Name',
tags: 'Tags',
createCustomModel: 'Add Custom Model',
edit: 'Edit',
selectOneTip: 'Model API KEY not configured, please configure it in the model list first',
load_balance_strategy: 'Concurrency Strategy',
round_robin: 'Sequential Execution - Call each model in order',
none: 'None',
api_key: 'API KEY',
api_base: 'API Base URL',
description: 'Description',
add: 'Add',
item: 'item',
apiKeyNum: ' API Keys',
official: 'Official',
deprecated: 'Deprecated',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
knowledgeBase: {
pleaseUploadFileFirst: 'Please upload file first',
shareSuccess: 'Share successfully',
@@ -1175,6 +1236,12 @@ export const en = {
priority: 'Structured Integration',
addTool: 'Add Tool',
tool: 'Tool',
statistics: 'Data Statistics',
daily_conversations: 'Daily Conversations',
daily_new_users: 'Daily New Users',
daily_api_calls: 'Daily API Calls',
daily_tokens: 'Token Consumption',
},
userMemory: {
userMemory: 'User Memory',
@@ -1534,7 +1601,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
noPermissionDesc: ' Please contact the administrator to grant permission',
tableEmpty: 'No data available.',
loadingEmpty: 'The content is loading…',
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen',
pageEmpty: 'Oops No search results available at the moment',
pageEmptyDesc: "Red Bear tilts its head and waits for you to change a new keyword, let's explore together.",
},
apiKey: {
name: 'Project Name',

View File

@@ -658,7 +658,13 @@ export const zh = {
priority: '结构化整合',
addTool: '添加工具',
tool: '工具',
variableConfig: '配置变量'
variableConfig: '配置变量',
statistics: '数据统计',
daily_conversations: '消息会话数',
daily_new_users: '新增用户数',
daily_api_calls: '调用次数',
daily_tokens: 'Token消耗',
},
role: {
roleManagement: '角色管理',
@@ -967,6 +973,9 @@ export const zh = {
statusEnabled: '可用',
statusDisabled: '不可用',
remove: '删除',
fileSizeTip: '文件大小不能超过 {{size}}MB',
fileAcceptTip: '不支持的文件类型:'
},
product: {
applicationManagement: '应用管理',
@@ -1076,6 +1085,64 @@ export const zh = {
gpustack: "Gpustack",
bedrock: "Bedrock"
},
modelNew: {
group: '模型组合',
list: '模型列表',
square: '模型广场',
createGroupModel: '创建模型组合',
groupSearchPlaceholder: '搜索模型组合',
listSearchPlaceholder: '搜索可用模型',
squareSearchPlaceholder: '搜索平台模型',
status: '模型状态',
created_at: '创建时间',
configureBtn: '点击配置',
showModel: '显示模型',
keyConfig: '配置 KEY',
modelConfiguration: '模型配置',
logo: '模型LOGO',
name: '模型名称',
type: '模型类型',
modelImplement: '模型实现',
addImplement: '添加实现',
noAuth: '未授权(限1个实现)',
implementConfig: '配置模型实现',
provider: '模型供应商',
api_key_ids: '选择模型',
viewAll: '更多',
modelCount: '共 {{count}} 个模型',
modelList: '模型列表',
added: ' 已添加',
addSuccess: '添加成功',
model_name: '模型名称',
tags: '标签',
createCustomModel: '添加自定义模型',
edit: '编辑',
selectOneTip: '模型未配置API KEY请先在模型列表配置',
load_balance_strategy: '并发策略',
round_robin: '顺序执行 - 按顺序依次调用每个模型',
none: '无',
api_key: 'API KEY',
api_base: 'API Base URL',
description: '描述',
add: '添加',
item: '个',
apiKeyNum: '个 API Key',
official: '官方',
deprecated: '已弃用',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
timezones: {
'Asia/Shanghai': '中国标准时间 (UTC+8)',
'Asia/Kolkata': '印度标准时间 (UTC+5:30)',
@@ -1607,7 +1674,9 @@ export const zh = {
noPermissionDesc: '请联系管理员授予权限',
tableEmpty: '目前没有数据',
loadingEmpty: '内容正在加载中…',
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上',
pageEmpty: '哎呀!暂无搜索结果',
pageEmptyDesc: '红熊歪着头等待您更换新的关键词,让我们一起探索吧。',
},
home: {

View File

@@ -22,7 +22,7 @@ export const lightTheme: ThemeConfig = {
// colorBgContainer: '#FBFDFF',
colorError: '#FF5D34',
sizeSM: 12,
fontSizeSM: 12,
fontSizeSM: 12,
},
components: {
Layout: {
@@ -105,6 +105,9 @@ export const lightTheme: ThemeConfig = {
},
Select: {
lineHeightSM: 26
},
Upload: {
pictureCardSize: 96,
}
}
};

View File

@@ -23,9 +23,10 @@ interface data {
}
export const API_PREFIX = '/api'
// 创建axios实例
const service = axios.create({
baseURL: '/api', // 与vite.config.ts中的代理配置对应
baseURL: API_PREFIX, // 与vite.config.ts中的代理配置对应
// timeout: 10000, // 请求超时时间
withCredentials: false,
headers: {
@@ -126,7 +127,7 @@ service.interceptors.response.use(
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
return Promise.reject(error);
}
// 处理网络错误、超时等
let msg = error.response?.data?.error || error.response?.error;
const status = error?.response ? error.response.status : error;

View File

@@ -20,7 +20,7 @@ import type {
} from './types'
import type { Variable } from './components/VariableList/types'
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
import { getModelList } from '@/api/models';
import { saveAgentConfig } from '@/api/application'
import Knowledge from './components/Knowledge/Knowledge'
@@ -79,7 +79,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
placeholder={t('common.pleaseSelect')}
url={url}
hasAll={false}
valueKey={['config_id_old', 'config_id']}
valueKey='config_id'
labelKey="config_name"
/>
</Form.Item>
@@ -96,8 +96,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const [loading, setLoading] = useState(false)
const [data, setData] = useState<Config | null>(null);
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
const [modelList, setModelList] = useState<Model[]>([])
const [defaultModel, setDefaultModel] = useState<Model | null>(null)
const [modelList, setModelList] = useState<ModelListItem[]>([])
const [defaultModel, setDefaultModel] = useState<ModelListItem | null>(null)
const [chatList, setChatList] = useState<ChatData[]>([])
const values = Form.useWatch<Config>([], form)
const [isSave, setIsSave] = useState(false)
@@ -127,13 +127,15 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const response = res as Config
let allTools = Array.isArray(response.tools) ? response.tools : []
const memoryContent = response.memory?.memory_content
const convertedMemoryContent = memoryContent && !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
const parsedMemoryContent = memoryContent === null || memoryContent === ''
? undefined
: !isNaN(Number(memoryContent)) ? Number(memoryContent) : memoryContent
form.setFieldsValue({
...response,
tools: allTools,
memory: {
...response.memory,
memory_content: convertedMemoryContent
memory_content: parsedMemoryContent
}
})
setData({
@@ -214,7 +216,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
...data.knowledge_retrieval,
...knowledgeRest,
knowledge_bases: knowledge_bases.map(item => ({
kb_id: item.id,
kb_id: item.kb_id || item.id,
...(item.config || {})
}))
} as KnowledgeConfig : null,
@@ -239,9 +241,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
})
}
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}
@@ -251,7 +253,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
useEffect(() => {
if (values?.default_model_config_id && modelList.length > 0) {
const filterValue = modelList.find(item => item.id === values.default_model_config_id)
setDefaultModel(filterValue as Model | null)
setDefaultModel(filterValue as ModelListItem | null)
setChatList([{
label: filterValue?.name || '',
model_config_id: filterValue?.id || '',

View File

@@ -225,7 +225,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
<Form.Item name="default_model_config_id" noStyle>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100 }}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -0,0 +1,86 @@
import { type FC, useState, useEffect } from 'react';
import { Row, Col, Flex, DatePicker } from 'antd';
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
import type { Application } from '@/views/ApplicationManagement/types'
import { getAppStatistics } from '@/api/application';
import LineCard from './components/LineCard'
import type { StatisticsData, StatisticsItem } from './types'
const TotalObj: Record<string, keyof StatisticsData> = {
daily_conversations: 'total_conversations',
daily_new_users: 'total_new_users',
daily_api_calls: 'total_api_calls',
daily_tokens: 'total_tokens',
}
const Statistics: FC<{ application: Application | null }> = ({ application }) => {
const [data, setData] = useState<StatisticsData>({
daily_conversations: [],
total_conversations: 0,
daily_new_users: [],
total_new_users: 0,
daily_api_calls: [],
total_api_calls: 0,
daily_tokens: [],
total_tokens: 0
})
const [query, setQuery] = useState({
start_date: dayjs().subtract(6, 'd'),
end_date: dayjs().subtract(0, 'd'),
})
useEffect(() => {
getData()
}, [application, query])
const getData = () => {
if (!application?.id) {
return
}
const params = {
start_date: query.start_date.startOf('d').valueOf(),
end_date: query.end_date.endOf('d').valueOf(),
}
getAppStatistics(application.id, params)
.then(res => {
setData(res as StatisticsData)
})
}
const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => {
if (!date || !date[0] || !date[1]) return
setQuery({
start_date: date[0],
end_date: date[1],
})
}
return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
<Row gutter={[16, 16]}>
<Col span={24}>
<Flex justify="end">
<RangePicker defaultValue={[query.start_date, query.end_date]} onChange={handleChange} />
</Flex>
</Col>
{Object.entries(data).map(([key, value]) => {
if (key.includes('total')) {
return null
}
const totalKey = TotalObj[key];
return (
<Col span={12} key={key}>
<LineCard
type={key}
total={totalKey ? (data[totalKey] as number) : 0}
chartData={value as StatisticsItem[]}
/>
</Col>
)
})}
</Row>
</div>
);
}
export default Statistics;

View File

@@ -181,7 +181,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100 }}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -17,7 +17,7 @@ import CopyModal from './CopyModal'
const { Header } = Layout;
const tabKeys = ['arrangement', 'api', 'release']
const tabKeys = ['arrangement', 'api', 'release', 'statistics']
const menuIcons: Record<string, string> = {
edit: editIcon,
copy: copyIcon,

View File

@@ -97,7 +97,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -0,0 +1,127 @@
import { type FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import Empty from '@/components/Empty'
import Card from './Card'
import type { StatisticsItem } from '../types'
interface LineCardProps {
chartData: StatisticsItem[];
type: string;
total: number;
}
const SeriesConfig = {
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 3
},
showSymbol: true,
label: {
show: false,
position: 'top'
},
emphasis: {
focus: 'series'
},
}
const ColorObj: Record<string, string> = {
daily_conversations: '#FFB048',
daily_new_users: '#4DA8FF',
daily_api_calls: '#155EEF',
daily_tokens: '#AD88FF'
}
const LineCard: FC<LineCardProps> = ({ chartData, type, total }) => {
const { t } = useTranslation()
const chartRef = useRef<ReactEcharts>(null);
useEffect(() => {
}, [chartData])
const getSeries = () => {
return [{
...SeriesConfig,
name: t(`application.${type}`),
data: chartData.map(vo => vo.count),
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: ColorObj[type] },
{ offset: 1, color: '#FFFFFF' }
])
},
}]
}
return (
<Card
title={<div>{t(`application.${type}`)} <span className="rb:text-[#155EEF] rb:font-medium rb:text-[18px]">{total}</span></div>}
>
{chartData && chartData.length > 0 ? (
<ReactEcharts
ref={chartRef}
option={{
color: [ColorObj[type]],
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
},
label: {
show: false
}
},
},
grid: {
top: 10,
left: 15,
right: 40,
bottom: 0,
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.map(item => item.date),
boundaryGap: false,
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC',
align: 'right',
lineHeight: 17,
},
axisLine: {
lineStyle: {
color: '#EBEBEB',
}
},
},
series: getSeries()
}}
style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }}
opts={{ renderer: 'canvas' }}
notMerge={true}
lazyUpdate={true}
/>
) : <Empty size={120} className="rb:mt-12 rb:mb-20.25" />}
</Card>
)
}
export default LineCard

View File

@@ -9,6 +9,7 @@ import ReleasePage from './ReleasePage'
import Cluster from './Cluster'
import { getApplication } from '@/api/application'
import Workflow from '@/views/Workflow';
import Statistics from './Statistics'
const ApplicationConfig: React.FC = () => {
const { id } = useParams();
@@ -68,6 +69,7 @@ const ApplicationConfig: React.FC = () => {
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}
</>
);
};

View File

@@ -150,4 +150,19 @@ export interface AiPromptForm {
}
export interface ChatVariableConfigModalRef {
handleOpen: (values: Variable[]) => void;
}
export interface StatisticsItem {
count: number;
date: string;
}
export interface StatisticsData {
daily_conversations: StatisticsItem[];
daily_new_users: StatisticsItem[];
daily_api_calls: StatisticsItem[];
daily_tokens: StatisticsItem[];
total_conversations: number;
total_new_users: number;
total_api_calls: number;
total_tokens: number;
}

View File

@@ -20,7 +20,7 @@ const configList = [
key: 'emotion_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
},
{
key: 'emotion_min_intensity',

View File

@@ -39,7 +39,7 @@ const MemberManagement: React.FC = () => {
onOk: () => {
deleteMember(member.id)
.then(() => {
message.success(t('member.deleteSuccess'));
message.success(t('common.deleteSuccess'));
refreshTable();
})
}
@@ -93,7 +93,7 @@ const MemberManagement: React.FC = () => {
return (
<>
<div className="rb:flex rb:justify-end rb:mb-[12px]">
<div className="rb:flex rb:justify-end rb:mb-3">
<Button type="primary" onClick={() => handleEdit()}>
{t('member.createMember')}
</Button>

View File

@@ -1,14 +1,14 @@
import { type FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
import clsx from 'clsx'
import Card from './components/Card'
import type { ConfigForm, Variable } from './types'
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
import Markdown from '@/components/Markdown'
import { getModelList } from '@/api/models';
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
import { configList } from './constant'
import Result from './components/Result'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
@@ -43,7 +43,7 @@ const MemoryExtractionEngine: FC = () => {
const values = Form.useWatch<ConfigForm>([], form)
const [loading, setLoading] = useState(false)
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
const [modelList, setModelList] = useState<Model[]>([])
const [modelList, setModelList] = useState<ModelListItem[]>([])
useEffect(() => {
if (values?.reflexion_range === 'database') {
@@ -55,9 +55,9 @@ const MemoryExtractionEngine: FC = () => {
}, [values])
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}

View File

@@ -0,0 +1,92 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
import { Button } from 'antd'
import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import { formatDateTime } from '@/utils/format';
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation();
const [list, setList] = useState<ModelListItem[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelNewList({
...query,
is_composite: true,
is_active: true,
})
.then(res => {
const response = res as ProviderModelItem[]
setList(response[0]?.models || [])
})
}
const formatData = (data: ModelListItem) => {
return [
{
key: 'type',
label: t(`modelNew.type`),
children: data.type ? t(`modelNew.${data.type}`) : '-',
},
{
key: 'is_active',
label: t(`modelNew.status`),
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
},
{
key: 'created_at',
label: t(`modelNew.created_at`),
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
},
]
}
useImperativeHandle(ref, () => ({
getList,
}));
return (
<>
{list.length === 0
? <PageEmpty />
:(
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
{formatData(item)?.map((description: DescriptionItem) => (
<div
key={description.key}
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
<span className={clsx({
"rb:text-[#212332]": description.key !== 'is_active',
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
})}>{(description.children as string)}</span>
</div>
))}
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>
</RbCard>
))}
</div>
)
}
</>
)
})
export default Group

View File

@@ -0,0 +1,86 @@
import { useRef, useState, useEffect, type FC } from 'react';
import { Button, Flex, Row, Col } from 'antd'
import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag';
import KeyConfigModal from './components/KeyConfigModal'
import ModelListDetail from './components/ModelListDetail'
import { getLogoUrl } from './utils'
const ModelList: FC<{ query: any }> = ({ query }) => {
const { t } = useTranslation();
const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
const modelListDetailRef = useRef<ModelListDetailRef>(null)
const [list, setList] = useState<ProviderModelItem[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelNewList({
...query,
is_composite: false,
})
.then(res => {
setList((res || []) as ProviderModelItem[])
})
}
const handleShowModel = (vo: ProviderModelItem) => {
modelListDetailRef.current?.handleOpen(vo)
}
const handleKeyConfig = (vo: ProviderModelItem) => {
keyConfigModalRef.current?.handleOpen(vo)
}
return (
<>
{list.length === 0
? <PageEmpty />
:(
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
{list.map(item => (
<RbCard
key={item.provider}
title={t(`modelNew.${item.provider}`)}
avatarUrl={getLogoUrl(item.logo)}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.provider[0].toUpperCase()}
</div>
}
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
>
<Flex gap={8} wrap>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Flex>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Row gutter={12}>
<Col span={12}>
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
</Col>
<Col span={12}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>
</div>
</RbCard>
))}
</div>
)
}
<KeyConfigModal
ref={keyConfigModalRef}
refresh={getList}
/>
<ModelListDetail
ref={modelListDetailRef}
refresh={getList}
/>
</>
)
}
export default ModelList

View File

@@ -0,0 +1,104 @@
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Button, Space, App, Divider, Flex, Tooltip } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelPlaza, addModelPlaza } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag';
import ModelSquareDetail from './components/ModelSquareDetail'
import { getLogoUrl } from './utils'
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
const [list, setList] = useState<ModelPlaza[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelPlaza(query)
.then(res => {
setList((res as ModelPlaza[]) || [])
})
}
const handleMore = (vo: ModelPlaza) => {
modelSquareDetailRef.current?.handleOpen(vo)
}
const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id)
.then(() => {
message.success(`${item.name}${t('modelNew.addSuccess')}`)
getList()
})
}
useImperativeHandle(ref, () => ({
getList,
}));
return (
<>
{list.length === 0
? <PageEmpty />
: list.map(vo => (
<div key={vo.provider}>
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">
<div className="rb:font-medium">{t(`modelNew.${vo.provider}`)}</div>
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})&gt;</Button>
</div>
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
{vo.models.slice(0, 6).map(item => (
<RbCard
key={item.id}
title={item.name}
subTitle={<Space size={8}>
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
</Space>}
avatarUrl={getLogoUrl(item.logo)}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
>
<Tooltip title={item.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
</Tooltip>
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Divider size="middle" />
<Flex justify="space-between">
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
<Space>
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
{item.is_added
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
}
</Space>
</Flex>
</div>
</RbCard>
))}
</div>
</div>
))
}
<ModelSquareDetail
ref={modelSquareDetailRef}
refresh={getList}
handleEdit={handleEdit}
/>
</>
)
})
export default ModelSquare

View File

@@ -1,171 +0,0 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelFormData, Model, ConfigModalRef, ConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { updateModel, addModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<Model>({} as Model);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<ModelFormData>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch<ModelFormData>([], form);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setModel({} as Model);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: Model) => {
if (model) {
setIsEdit(true);
setModel(model);
// 设置表单值
const apiKeyInfo = model.api_keys[0]
form.setFieldsValue({
provider: apiKeyInfo.provider,
model_name: apiKeyInfo.model_name,
api_key: apiKeyInfo.api_key,
api_base: apiKeyInfo.api_base
});
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
const data = {
name: values.name,
type: values.type,
api_keys: {
provider: values.provider,
model_name: values.model_name,
api_key: values.api_key,
api_base: values.api_base
},
}
setLoading(true)
const res = isEdit
? updateModel(model.api_keys[0].id, {
provider: values.provider,
model_name: values.model_name,
api_key: values.api_key,
api_base: values.api_base
} as ModelFormData)
: addModel(data as ModelFormData)
res.then(() => {
if (refresh) {
refresh();
}
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={isEdit ? `${model.name} - ${t('model.modelConfiguration')}` : t('model.createModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={{}}
>
{!isEdit && (
<>
<Form.Item
name="name"
label={t('model.displayName')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.displayName') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('model.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
</>
)}
<Form.Item
name="provider"
label={t('model.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.provider') }) }]}
>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
<Form.Item
name="model_name"
label={t('model.modelName')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.modelName') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_key"
label={t('model.apiKey')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.apiKey') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('model.apiEndpoint')}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
</Form>
</RbModal>
);
});
export default ConfigModal;

View File

@@ -0,0 +1,168 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import type { CustomModelForm, ModelPlazaItem, CustomModelModalRef, CustomModelModalProps } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import UploadImages from '@/components/Upload/UploadImages'
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
import { getFileLink } from '@/api/fileStorage'
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelPlazaItem>({} as ModelPlazaItem);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<CustomModelForm>();
const [loading, setLoading] = useState(false)
const formValues = Form.useWatch([], form)
const handleClose = () => {
setModel({} as ModelPlazaItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: ModelPlazaItem) => {
if (model) {
setIsEdit(true);
setModel(model);
form.setFieldsValue({
...model,
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
});
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
const handleUpdate = (data: CustomModelForm) => {
setLoading(true)
const { type, provider, ...rest} = data
const res = isEdit ? updateCustomModel(model.id, rest) : addCustomModel(data)
res.then(() => {
refresh && refresh()
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
}
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
const { logo, ...rest } = values;
let formData: CustomModelForm = {
...rest
}
formData.is_official = false;
if (typeof logo === 'object' && logo?.response?.data.file_id) {
getFileLink(logo?.response?.data.file_id)
.then(res => {
const logoRes = res as { url: string }
formData.logo = logoRes.url
handleUpdate(formData)
})
.catch(() => {
handleUpdate(formData)
})
} else {
formData.logo = typeof logo === 'string' ? logo : logo.url
handleUpdate(formData)
}
})
.catch((err) => {
console.log('err', err)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
console.log('formValues', formValues)
return (
<RbModal
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<UploadImages />
</Form.Item>
<Form.Item
name="name"
label={t('modelNew.name')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.name') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('modelNew.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
disabled={isEdit}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/>
</Form.Item>
<Form.Item
name="provider"
label={t('modelNew.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
disabled={isEdit}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/>
</Form.Item>
<Form.Item
name="description"
label={t('modelNew.description')}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="tags"
label={t('modelNew.tags')}
>
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
</Form.Item>
</Form>
</RbModal>
);
});
export default CustomModelModal;

View File

@@ -0,0 +1,173 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/models'
import UploadImages from '@/components/Upload/UploadImages'
import ModelImplement from './ModelImplement'
import { getFileLink } from '@/api/fileStorage'
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<CompositeModelForm>();
const [loading, setLoading] = useState(false)
const type = Form.useWatch(['type'], form)
const handleClose = () => {
setModel({} as ModelListItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: ModelListItem) => {
if (model) {
setIsEdit(true);
setModel(model);
form.setFieldsValue({
...model,
api_key_ids: model.api_keys,
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
})
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
const handleSave = () => {
form
.validateFields()
.then((values) => {
const { api_key_ids = [], logo, ...rest } = values
const formData: CompositeModelForm = {
...rest,
api_key_ids: api_key_ids.map(vo => (vo as ModelApiKey).id)
}
if (logo?.response?.data.file_id) {
getFileLink(logo?.response?.data.file_id).then(res => {
const logoRes = res as { url: string }
formData.logo = logoRes.url
handleUpdate(formData)
}).catch(() => {
handleUpdate(formData)
})
} else {
formData.logo = typeof logo === 'string' ? logo : logo.url
handleUpdate(formData)
}
})
.catch((err) => {
console.log('err', err)
});
}
const handleUpdate = (data: CompositeModelForm) => {
setLoading(true)
const { type, ...rest } = data
const res = isEdit
? updateCompositeModel(model.id, { ...rest })
: addCompositeModel(data)
res.then(() => {
refresh?.();
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createGroupModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={{ balance_strategy: 'none' }}
>
<Form.Item
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<UploadImages />
</Form.Item>
<Form.Item
name="name"
label={t('modelNew.name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('modelNew.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
value: typeof item === 'object' ? item.value : item
}))}
disabled={isEdit}
/>
</Form.Item>
<Form.Item
name="description"
label={t('modelNew.description')}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="load_balance_strategy"
label={t('modelNew.load_balance_strategy')}
>
<Select
options={['round_robin', 'none'].map(key => ({
label: t(`modelNew.${key}`),
value: key
}))}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
<Form.Item name="api_key_ids">
<ModelImplement type={type} />
</Form.Item>
</Form>
</RbModal>
);
});
export default GroupModelModal;

View File

@@ -0,0 +1,92 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import { updateProviderApiKeys } from '@/api/models'
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ProviderModelItem>({} as ProviderModelItem);
const [form] = Form.useForm<KeyConfigModalForm>();
const [loading, setLoading] = useState(false)
const handleClose = () => {
setModel({} as ProviderModelItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (vo: ProviderModelItem) => {
setVisible(true);
setModel(vo);
};
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
updateProviderApiKeys({
...values,
provider: model.provider
}).then((res) => {
if (refresh) {
refresh();
}
handleClose()
message.success(res as string)
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={`${model.provider} - ${t('modelNew.keyConfig')}`}
open={visible}
onCancel={handleClose}
okText={t(`common.save`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="api_key"
label={t('modelNew.api_key')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('modelNew.api_base')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
</Form>
</RbModal>
);
});
export default KeyConfigModal;

View File

@@ -0,0 +1,181 @@
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import { Form, Cascader, App, type CascaderProps } from 'antd';
import { useTranslation } from 'react-i18next';
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { modelProviderUrl, getModelNewList } from '@/api/models'
import type { ProviderModelItem } from '../../types'
const { SHOW_CHILD } = Cascader;
interface Option {
value: string | number;
label: string;
children?: Option[];
[key: string]: any;
}
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
refresh,
type,
groupedByProvider
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<SubModelModalForm>();
const [selecteds, setSelecteds] = useState<any[]>([])
const [modelList, setModelList] = useState<Option[]>([])
const provider = Form.useWatch(['provider'], form)
useEffect(() => {
if (provider && groupedByProvider) {
const lastModels = groupedByProvider[provider] || []
const list = lastModels.map(vo => [{ name: vo.model_name, id: vo.model_config_ids[0], value: vo.model_config_ids[0], provider }, { value: vo.id }])
setSelecteds(list)
form.setFieldValue('api_key_ids', lastModels.map(vo => [vo.model_config_ids[0], vo.id]))
}
}, [groupedByProvider, provider])
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
form.resetFields();
setVisible(false);
setSelecteds([])
setModelList([])
};
const handleOpen = () => {
form.resetFields()
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
refresh?.(selecteds.map(vo => ({
...vo[0],
model_name: vo[0].name,
model_config_ids: [vo[0].id],
id: vo[1].value,
api_key: vo[1].label
})))
handleClose()
})
}
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
const lastFilterLit = value.filter(vo => vo.length !== 1)
if (filterList.length) {
message.warning(`${filterList.map(vo => vo.label)}${t('modelNew.selectOneTip')}`)
form.setFieldValue('api_key_ids', lastFilterLit)
}
setSelecteds(selectedOptions)
}
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
form.setFieldValue('api_key_ids', undefined)
if (provider) {
getModelNewList({
provider: provider,
is_composite: false,
is_active: true,
type
})
.then(res => {
const response = res as ProviderModelItem[]
const list = response[0]?.models || []
setModelList(list.map(vo => {
const children = vo.api_keys.map(item => ({
label: item.api_key,
value: item.id,
}))
return {
...vo,
label: vo.name,
value: vo.id,
children: children
}
}))
if (api_key_ids?.length) {
form.setFieldsValue({
api_key_ids: api_key_ids
})
}
})
} else {
setModelList([])
}
}
const displayRender: CascaderProps<Option>['displayRender'] = (labels, selectedOptions = []) =>
labels.map((label, i) => {
const option = selectedOptions[i];
if (i === labels.length - 1) {
return (
<span key={option?.value || i}>
{label}
</span>
);
}
return <span key={option?.value || i}>{label} / </span>;
});
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('modelNew.implementConfig')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="provider"
label={t('modelNew.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
value: typeof item === 'object' ? item.value : item
}))}
onChange={(value) => handleChangeProvider(value)}
/>
</Form.Item>
<Form.Item
name="api_key_ids"
label={t('modelNew.api_key_ids')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.api_key_ids') }) }]}
>
<Cascader
placeholder={t('common.pleaseSelect')}
options={modelList}
onChange={handleChange}
multiple
autoClearSearchValue
className="rb:w-full!"
showCheckedStrategy={SHOW_CHILD}
changeOnSelect
displayRender={displayRender}
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default SubModelModal;

View File

@@ -0,0 +1,99 @@
import { type FC, useRef } from "react";
import { useTranslation } from 'react-i18next';
import { Flex, Button, Space, App } from 'antd'
import type { SubModelModalRef, ModelList } from './types'
import SubModelModal from './SubModelModal'
import Empty from '@/components/Empty'
import Tag from '@/components/Tag'
interface ModelImplementProps {
type?: string;
value?: any;
onChange?: (value: any) => void;
}
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
const { t } = useTranslation();
const { modal, message } = App.useApp();
const subModelModalRef = useRef<SubModelModalRef>(null)
const handleAdd = () => {
if (!type || type.trim() === '') {
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
return
}
subModelModalRef.current?.handleOpen()
}
const handleDelete = (vo: any) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
content: t('application.apiKeyDeleteContent'),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
onChange?.(value?.filter((item: any) => item.id !== vo.id))
}
})
}
const handleRefresh = (list: ModelList[]) => {
const existingModels = value || [];
let updatedModels = [...existingModels];
const provider = list[0].provider
updatedModels = updatedModels.filter(item => item.provider !== provider)
updatedModels = [...updatedModels, ...list]
onChange?.([...updatedModels]);
}
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
const provider = item.provider || 'unknown';
if (!acc[provider]) acc[provider] = [];
acc[provider].push(item);
return acc;
}, {} as Record<string, ModelList[]>);
return (
<div>
<Flex justify="space-between" align="center">
{t('modelNew.modelImplement')}
<Space>
<Button type="primary" onClick={handleAdd} className="rb:px-2! rb:h-6!">+ {t('modelNew.addImplement')}</Button>
<Button size="small" className="rb:px-2! rb:h-6!">{t('modelNew.noAuth')}</Button>
</Space>
</Flex>
<div className="rb:bg-[#F5F6F7] rb:rounded-lg rb:p-3 rb:mt-2">
{!value || value.length === 0
? <Empty size={88} />
: value.map((item: any) => {
return (
<div key={item.id} className="rb:mb-4 rb:last:rb:mb-0 rb:bg-[#FBFDFF] rb:rounded-lg rb:p-3">
<Flex gap={8} justify="space-between" align="center" className="rb:mb-2 rb:last:rb:mb-0">
<div className="rb:font-medium">{item.model_name}</div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDelete(item)}
></div>
</Flex>
<div className="rb:text-[#5B6167] rb:my-2">{item.api_key}</div>
<Tag className="rb:mb-2">{t(`modelNew.${item.provider}`)}</Tag>
</div>
)
})}
</div>
<SubModelModal
ref={subModelModalRef}
refresh={handleRefresh}
type={type}
groupedByProvider={groupedByProvider}
/>
</div>
)
}
export default ModelImplement

View File

@@ -0,0 +1,17 @@
import type { ModelListItem } from '../../types'
export interface ModelList extends ModelListItem {
api_key_id: string;
}
export interface SubModelModalForm {
provider: string;
api_key_ids: string[][];
}
export interface SubModelModalRef {
handleOpen: () => void;
}
export interface SubModelModalProps {
type?: string;
refresh?: (vo: ModelList[]) => void;
groupedByProvider?: Record<string, ModelList[]>
}

View File

@@ -0,0 +1,142 @@
import { useState, useImperativeHandle, forwardRef, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Switch, Row, Col, Space, Tooltip } from 'antd'
import type { ProviderModelItem, ModelListItem, ModelListDetailRef, MultiKeyConfigModalRef } from '../types';
import RbDrawer from '@/components/RbDrawer';
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
import MultiKeyConfigModal from './MultiKeyConfigModal'
import { getModelNewList, updateModelStatus, modelTypeUrl } from '@/api/models'
import { getLogoUrl } from '../utils'
import CustomSelect from '@/components/CustomSelect'
interface ModelListDetailProps {
refresh?: () => void;
}
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [data, setData] = useState<ProviderModelItem>({} as ProviderModelItem)
const [list, setList] = useState<ModelListItem[]>([])
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
const [loading, setLoading] = useState(false)
const [type, setType] = useState<string | undefined | null>(null)
const handleOpen = (vo: ProviderModelItem) => {
setType(null)
setOpen(true)
getData(vo)
}
const getData = (vo: ProviderModelItem) => {
if (!vo.provider) return
getModelNewList({
provider: vo.provider
})
.then(res => {
const response = res as ProviderModelItem[]
setData(response[0])
setList(response[0].models)
})
}
const handleKeyConfig = (vo: ModelListItem) => {
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
}
const handleChange = (vo: ModelListItem) => {
setLoading(true)
updateModelStatus(vo.id, { is_active: !vo.is_active })
.finally(() => {
getData(data)
setLoading(false)
})
}
const handleClose = () => {
setType(null)
setOpen(false)
refresh?.()
}
const handleRefresh = () => {
getData(data)
}
const handleTypeChange = (value: string) => {
setType(value)
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
const filterList = useMemo(() => {
if (!type) return list
return list.filter(vo => vo.type === type)
}, [type, list])
return (
<RbDrawer
title={<>{t(`modelNew.${data.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
onClose={handleClose}
>
<Row gutter={16}>
<Col span={12}>
<CustomSelect
value={type}
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleTypeChange}
className="rb:w-full"
allowClear={true}
placeholder={t('modelNew.type')}
/>
</Col>
</Row>
{filterList.length === 0
? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
{filterList.map(item => (
<RbCard
key={item.id}
title={item.name}
subTitle={<Space className="rb:mt-1!">
<Tag>{t(`modelNew.${item.type}`)}</Tag>
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
</Space>}
avatarUrl={getLogoUrl(item.logo)}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
extra={<Switch defaultChecked={item.is_active} disabled={loading} onChange={() => handleChange(item)} />}
bodyClassName="rb:relative rb:pb-[64px]! rb:h-[calc(100%-64px)]!"
>
<Tooltip title={item.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2">{item.description}</div>
</Tooltip>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Row gutter={12}>
<Col span={24}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>
</div>
</RbCard>
))}
</div>
}
<MultiKeyConfigModal
ref={multiKeyConfigModalRef}
refresh={handleRefresh}
/>
</RbDrawer>
);
});
export default ModelListDetail;

View File

@@ -0,0 +1,106 @@
import { useState, useImperativeHandle, forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Space, App, Flex, Tooltip, Divider } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons';
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
import RbDrawer from '@/components/RbDrawer';
import { getModelPlaza, addModelPlaza } from '@/api/models'
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
import { getLogoUrl } from '../utils'
interface ModelSquareDetailProps {
refresh: () => void;
handleEdit: (vo: ModelPlazaItem) => void;
}
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
const [open, setOpen] = useState(false);
const [list, setList] = useState<ModelPlazaItem[]>([])
const handleOpen = (vo: ModelPlaza) => {
setModel(vo)
setOpen(true)
getList(vo)
}
const handleClose = () => {
setOpen(false)
refresh()
}
const getList = (vo: ModelPlaza) => {
getModelPlaza({ provider: vo.provider })
.then(res => {
const response = res as ModelPlaza[]
setList(response.length > 0 ? response[0].models : [])
})
}
const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id)
.then(() => {
message.success(`${item.name}${t('modelNew.addSuccess')}`)
getList(model)
})
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbDrawer
title={<>{t(`modelNew.${model.provider}`)} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
onClose={handleClose}
>
<div className="rb:h-full rb:overflow-y-auto">
{list.length === 0
? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
subTitle={<Space size={8}>
<Tag className="rb:mt-1">{t(`modelNew.${item.type}`)}</Tag>
{item.is_official && <Tag color="success" className="rb:mt-1">{t(`modelNew.official`)}</Tag>}
</Space>}
avatarUrl={getLogoUrl(item.logo)}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
bodyClassName="rb:relative rb:pb-[80px]! rb:h-[calc(100%-64px)]!"
>
<Tooltip title={item.description}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:font-regular rb:wrap-break-word rb:line-clamp-2 rb:mt-3">{item.description}</div>
</Tooltip>
<Flex gap={8} wrap className="rb:mt-3!">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Flex>
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Divider size="middle" />
<Flex justify="space-between">
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
<Space>
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
{item.is_added
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>{item.is_deprecated ? t('modelNew.deprecated') : `+ ${t('common.add')}`}</Button>
}
</Space>
</Flex>
</div>
</RbCard>
))}
</div>
}
</div>
</RbDrawer>
);
});
export default ModelSquareDetail;

View File

@@ -0,0 +1,122 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Button } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import { addModelApiKey, deleteModelApiKey, getModelInfo } from '@/api/models'
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
const [form] = Form.useForm<MultiKeyForm>();
const [loading, setLoading] = useState(false)
const handleClose = () => {
setModel({} as ModelListItem);
refresh?.()
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (vo: ModelListItem) => {
setVisible(true);
getData(vo)
};
const getData = (vo: ModelListItem) => {
if (!vo.id) return
getModelInfo(vo?.id)
.then(res => {
setModel(res as ModelListItem)
})
}
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
addModelApiKey(model.id, {
...values,
model_config_id: model.id,
model_name: model.name,
provider: model.provider,
}).then(() => {
message.success(t('common.saveSuccess'))
form.resetFields();
getData(model)
})
.finally(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
const handleDelete = (api_key_id: string) => {
deleteModelApiKey(api_key_id)
.then(() => {
message.success(t('common.deleteSuccess'))
getData(model)
})
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={`${model.name} - ${t('modelNew.keyConfig')}`}
open={visible}
onCancel={handleClose}
footer={null}
confirmLoading={loading}
>
{model.api_keys && model.api_keys.length > 0 && (
<div className="rb:mb-4">
{model.api_keys.map((key) => (
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
<div>
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
</div>
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>
</div>
))}
</div>
)}
<Form
form={form}
layout="vertical"
>
<Form.Item
name="api_key"
label={t('modelNew.api_key')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('modelNew.api_base')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
<Form.Item>
<Button type="primary" block onClick={handleSave} loading={loading}>+ {t('modelNew.add')}</Button>
</Form.Item>
</Form>
</RbModal>
);
});
export default MultiKeyConfigModal;

View File

@@ -1,99 +1,124 @@
import { useState, useRef, type FC } from 'react';
import { Row, Col, Button } from 'antd'
import { Button, Flex, Space, type SegmentedProps, Form } from 'antd'
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import ConfigModal from './components/ConfigModal'
import type { Model, DescriptionItem, ConfigModalRef } from './types'
import RbCard from '@/components/RbCard/Card'
import GroupModelModal from './components/GroupModelModal'
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef, Query } from './types'
import SearchInput from '@/components/SearchInput'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { getModelListUrl } from '@/api/models'
import { formatDateTime } from '@/utils/format';
import PageTabs from '@/components/PageTabs'
import GroupModel from './Group'
import ModelList from './List'
import ModelSquare from './Square'
import CustomModelModal from './components/CustomModelModal'
import CustomSelect from '@/components/CustomSelect'
import { modelTypeUrl, modelProviderUrl } from '@/api/models'
const tabKeys = ['group', 'list', 'square']
const ModelManagement: FC = () => {
const { t } = useTranslation();
const [query, setQuery] = useState({})
const configModalRef = useRef<ConfigModalRef>(null)
const scrollListRef = useRef<PageScrollListRef>(null)
const [activeTab, setActiveTab] = useState('group');
const configModalRef = useRef<GroupModelModalRef>(null)
const customModelModalRef = useRef<CustomModelModalRef>(null)
const groupRef = useRef<BaseRef>(null)
const squareRef = useRef<BaseRef>(null)
const [form] = Form.useForm<Query>()
const query = Form.useWatch([], form)
const formatData = (data: Model) => {
return [
{
key: 'type',
label: t(`model.type`),
children: data.type || '-',
},
{
key: 'provider',
label: t(`model.provider`),
children: data.api_keys[0].provider || '-',
},
{
key: 'is_active',
label: t(`model.status`),
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
},
{
key: 'created',
label: t(`model.created`),
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
},
]
const formatTabItems = () => {
return tabKeys.map(value => ({
value,
label: t(`modelNew.${value}`),
}))
}
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
form.resetFields()
}
const handleEdit = (model?: Model) => {
configModalRef?.current?.handleOpen(model)
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
switch(activeTab) {
case 'group':
configModalRef?.current?.handleOpen(vo as ModelListItem)
break
case 'square':
customModelModalRef?.current?.handleOpen(vo as ModelPlazaItem)
break
}
}
const handleSearch = (value?: string) => {
setQuery({ search: value })
const handleRefresh = () => {
switch (activeTab) {
case 'group':
groupRef.current?.getList()
break
case 'square':
squareRef.current?.getList()
break
}
}
return (
<div className="rb:w-full">
<Row className='rb:mb-[16px] rb:w-full'>
<Col span={6}>
<SearchInput
placeholder={t('model.searchPlaceholder')}
onSearch={handleSearch}
style={{width: '100%'}}
/>
</Col>
<Col span={18} className="rb:text-right">
<Button type="primary" onClick={() => handleEdit()}>{t('model.createModel')}</Button>
</Col>
</Row>
<>
<Flex justify="space-between" align="center">
<PageTabs
value={activeTab}
options={formatTabItems()}
onChange={handleChangeTab}
/>
<PageScrollList
ref={scrollListRef}
url={getModelListUrl}
query={query}
renderItem={(item: Model) => (
<RbCard
title={item.name}
>
{formatData(item)?.map((description: DescriptionItem) => (
<div
key={description.key}
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]"
>
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
<span className={clsx({
"rb:text-[#212332]": description.key !== 'is_active',
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
})}>{(description.children as string)}</span>
</div>
))}
<Button className="rb:mt-[8px]" type="primary" ghost block onClick={() => handleEdit(item)}>{t('model.configureBtn')}</Button>
</RbCard>
)}
/>
<Form form={form}>
<Space size={12}>
{activeTab === 'list' &&
<Form.Item name="type" noStyle>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.type')}
/>
</Form.Item>
}
{(activeTab === 'list' || activeTab === 'square') &&
<Form.Item name="provider" noStyle>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.provider')}
/>
</Form.Item>
}
{activeTab !== 'list' &&
<Form.Item name="search" noStyle>
<SearchInput
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
className="rb:w-70!"
/>
</Form.Item>
}
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
</Space>
</Form>
</Flex>
<ConfigModal
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
{activeTab === 'list' && <ModelList query={query} />}
{activeTab === 'square' && <ModelSquare ref={squareRef} query={query} handleEdit={handleEdit} />}
</div>
<GroupModelModal
ref={configModalRef}
refresh={() => scrollListRef?.current?.refresh()}
refresh={handleRefresh}
/>
</div>
<CustomModelModal
ref={customModelModalRef}
refresh={handleRefresh}
/>
</>
)
}

View File

@@ -1,70 +1,139 @@
// 模型表单数据类型
export interface ModelFormData extends ApiKey {
name: string;
type: string;
api_keys: ApiKey;
}
export interface Query {
type?: string;
provider?: string;
is_active?: boolean;
is_public?: boolean;
is_composite?: boolean;
search?: string;
pagesize?: number;
page?: number;
}
export interface DescriptionItem {
key: string;
label: string;
children: string;
}
export interface CompositeModelForm {
logo?: any;
name: string;
type?: string;
description: string;
api_key_ids: ModelApiKey[] | string[];
}
export interface GroupModelModalRef {
handleOpen: (model?: ModelListItem) => void;
}
export interface GroupModelModalProps {
refresh?: () => void;
}
export interface ModelListDetailRef {
handleOpen: (vo: ProviderModelItem) => void;
}
// 模型类型定义
export interface Model {
export interface ModelApiKey {
model_name: string;
description: string | null;
provider: string;
api_key: string;
api_base: string;
config: any;
is_active: boolean;
priority: string;
id: string;
usage_count: string;
last_used_at: number;
created_at: number;
updated_at: number;
model_config_ids: string[];
}
export interface ModelListItem {
model_name?: string;
model_config_ids: string[];
name: string;
type: string;
logo: string;
description: string;
provider: string;
config: any;
is_active: boolean;
is_public: boolean;
id: string;
created_at: number;
updated_at: number;
api_keys: ModelApiKey[]
}
export interface ProviderModelItem {
provider: string;
logo?: string;
tags: string[];
models: ModelListItem[];
}
export interface KeyConfigModalForm {
provider: string;
api_key: string;
api_base: string;
}
export interface KeyConfigModalRef {
handleOpen: (vo: ProviderModelItem) => void;
}
export interface KeyConfigModalProps {
refresh?: () => void;
}
export interface MultiKeyForm {
model_config_id?: string;
model_name: string;
provider: string;
api_key: string;
api_base: string;
}
export interface MultiKeyConfigModalRef {
handleOpen: (vo: ModelListItem, provider?: string) => void;
}
export interface MultiKeyConfigModalProps {
refresh?: () => void;
}
export interface ModelPlaza {
provider: string;
models: ModelPlazaItem[];
}
export interface ModelPlazaItem {
id: string;
name: string;
type: string;
description?: string;
config: Record<string, unknown>;
is_active: boolean;
is_public: boolean;
created_at: string | number;
updated_at: string | number;
api_keys: ApiKey[];
// provider: string;
// temperature: number,
// topP: number,
// status: string;
// vectorDimension: number;
// batchSize: number;
// truncateStrategy: string;
// created: string;
// updatedAt: string;
// descriptionItems?: Record<string, unknown>[];
// basicParameters?: string;
// normalization?: string;
// maxInputLength?: number;
// encodingFormat?: string;
// enablePooling?: boolean;
// poolingStrategy?: string;
// apiKey?: string;
// apiEndpoint?: string;
// timeout?: number;
// autoRetry?: boolean;
// retryCount?: number;
}
interface ApiKey {
model_name?: string;
provider: string;
api_key?: string;
api_base?: string;
config?: Record<string, unknown>;
is_active?: boolean;
priority?: string;
id: string;
model_config_id?: string;
usage_count?: string;
last_used_at?: string | null;
created_at?: string;
updated_at?: string;
logo: string;
description: string;
is_deprecated: boolean;
is_official: boolean;
tags: string[];
add_count: number;
is_added: boolean;
}
// 定义组件暴露的方法接口
export interface ConfigModalRef {
handleOpen: (model?: Model) => void;
export interface ModelSquareDetailRef {
handleOpen: (vo: ModelPlaza) => void;
}
export interface ConfigModalProps {
export interface CustomModelForm {
name: string;
type?: string;
provider?: string;
logo?: any;
description: string;
is_official: boolean;
tags: string[];
}
export interface CustomModelModalRef {
handleOpen: (vo?: ModelPlazaItem) => void;
}
export interface CustomModelModalProps {
refresh?: () => void;
}
export interface BaseRef {
getList: () => void;
}

View File

@@ -0,0 +1,26 @@
import bedrockIcon from '@/assets/images/model/bedrock.svg'
import dashscopeIcon from '@/assets/images/model/dashscope.png'
import gpustackIcon from '@/assets/images/model/gpustack.png'
import ollamaIcon from '@/assets/images/model/ollama.svg'
import openaiIcon from '@/assets/images/model/openai.svg'
import xinferenceIcon from '@/assets/images/model/xinference.svg'
export const ICONS = {
bedrock: bedrockIcon,
dashscope: dashscopeIcon,
gpustack: gpustackIcon,
ollama: ollamaIcon,
openai: openaiIcon,
xinference: xinferenceIcon
}
export const getLogoUrl = (logo?: string) => {
if (!logo) {
return undefined
}
if (logo.startsWith('http')) {
return logo
}
return ICONS[logo as keyof typeof ICONS] || undefined
}

View File

@@ -24,7 +24,7 @@ const configList = [
key: 'reflection_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
},
// 迭代周期
{

View File

@@ -66,7 +66,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm', pagesize: 100 }}
params={{ type: 'llm', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -80,7 +80,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
params={{ type: 'embedding', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -94,7 +94,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -8,7 +8,7 @@ import { createWorkspace } from '@/api/workspaces'
import RadioGroupCard from '@/components/RadioGroupCard'
import { getModelListUrl, getModelList } from '@/api/models'
import CustomSelect from '@/components/CustomSelect'
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
const FormItem = Form.Item;
@@ -29,7 +29,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
const [form] = Form.useForm<SpaceModalData>();
const [loading, setLoading] = useState(false)
const [editVo, setEditVo] = useState<Space | null>(null)
const [modelList, setModelList] = useState<Model[]>([])
const [modelList, setModelList] = useState<ModelListItem[]>([])
const values = Form.useWatch([], form);
@@ -80,9 +80,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
}, [])
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}
@@ -134,7 +134,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
params={{ type: 'embedding', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -148,7 +148,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -98,7 +98,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -105,7 +105,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -200,7 +200,7 @@ export const nodeLibrary: NodeLibrary[] = [
config_id: {
type: 'customSelect',
url: memoryConfigListUrl,
valueKey: ['config_id_old', 'config_id'],
valueKey: 'config_id',
labelKey: 'config_name'
},
search_switch: {
@@ -223,7 +223,7 @@ export const nodeLibrary: NodeLibrary[] = [
config_id: {
type: 'customSelect',
url: memoryConfigListUrl,
valueKey: ['config_id_old', 'config_id'],
valueKey: 'config_id',
labelKey: 'config_name'
}
}
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -431,32 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [
}
}
},
{ type: "code", icon: codeExecutionIcon,
config: {
input_variables: {
type: 'inputList',
defaultValue: [{ name: 'arg1' }, { name: 'arg2' }]
},
language: {
type: 'select',
defaultValue: 'python3'
},
code: {
type: 'messageEditor',
isArray: false,
language: ['python3', 'javascript'],
titleVariant: 'borderless',
defaultValue: `def main(arg1: str, arg2: str):
return {
"result": arg1 + arg2,
}`
},
output_variables: {
type: 'outputList',
defaultValue: [{name: 'result', type: 'string'}]
},
}
},
// { type: "code", icon: codeExecutionIcon,
// config: {
// input_variables: {
// type: 'inputList',
// defaultValue: [{ name: 'arg1' }, { name: 'arg2' }]
// },
// language: {
// type: 'select',
// defaultValue: 'python3'
// },
// code: {
// type: 'messageEditor',
// isArray: false,
// language: ['python3', 'javascript'],
// titleVariant: 'borderless',
// defaultValue: `def main(arg1: str, arg2: str):
// return {
// "result": arg1 + arg2,
// }`
// },
// output_variables: {
// type: 'outputList',
// defaultValue: [{name: 'result', type: 'string'}]
// },
// }
// },
{ type: "jinja-render", icon: templateRenderingIcon,
config: {
mapping: {

View File

@@ -14,7 +14,7 @@ export interface NodeConfig {
url?: string;
params?: { [key: string]: unknown; }
valueKey?: string | string[];
valueKey?: string;
labelKey?: string;
defaultValue?: any;