195 Commits

Author SHA1 Message Date
npc0-hue ff78ea6936 fix: 1.2.0版本修复 React Server Components拒绝服务漏洞 (CVE-2025-55184) 和 CVE-2025-54880 2025-12-16 09:13:21 +08:00
FamousMai b4bf4a59c6 fix: 修复react漏洞 cve-2025-55182 2025-12-07 16:11:21 +08:00
FamousMai 27918dfb03 Update README.md
更新微信交流群二维码
2025-04-20 00:50:02 +08:00
FamousMai 54174e07b2 fix: 修正docker启动plugin_daemon失败 2025-04-20 00:15:01 +08:00
FamousMai 82abb76c5e fix:加上二开迁移命令的运行 2025-04-19 17:14:37 +08:00
FamousMai 6286dbf489 fix:二开新增迁移文件,字段调整 2025-04-19 17:14:05 +08:00
FamousMai 72d9a95a86 fix:后台菜单权限调整 2025-04-19 16:42:43 +08:00
FamousMai 63e6a025ee fix: 部分页面夜间模式显示样式调整 2025-04-19 16:42:05 +08:00
FamousMai 345ebde80e fix: 后台修改默认头像 2025-04-19 16:40:41 +08:00
FamousMai df715829ed feat: 修改镜像tag 2025-04-19 12:29:01 +08:00
crazywoola 5dfbfd4e88 fix: doc error 2025-04-19 12:18:42 +08:00
npc0-hue db321be482 fix: 修正后台钉钉回调域名显示问题 2025-04-19 12:17:40 +08:00
npc0-hue 64c7f3de25 feat:新增oauth2.0登录 2025-04-19 12:17:18 +08:00
npc0-hue 385b3d5aaa fix: 修正批量上传功能,模版经过wps保存后编码格式出错问题 2025-04-19 12:16:54 +08:00
npc0-hue b65b809711 fix: 处理后台注册bug 2025-04-19 12:16:08 +08:00
npc0-hue f667aff9db fix: 无限跳转 2025-04-16 10:49:59 +08:00
FamousMai d4fbeeda86 fix: 修正api调用出错 2025-04-16 10:43:56 +08:00
FamousMai 12f8bcbd0e ci: 优化一下docker-compose文件,保持与dify的docker-compose文件基本一致,只比原来多3个镜像 2025-04-11 19:55:02 +08:00
FamousMai 8caeac58cd fix: 修复web页面应用打开404 2025-04-11 17:08:16 +08:00
FamousMai 83ccfcbdd7 fix: 修复额度首页显示异常 2025-04-11 14:11:36 +08:00
FamousMai bddd355d9a fix:更新lock依赖文件 2025-04-11 12:04:51 +08:00
FamousMai 6c20f04f4e fix: 修复启动报错 2025-04-11 11:39:31 +08:00
FamousMai b361b83368 refactor(extend db upgrade): 重新整合二开新增表的hash历史,以及新增迁移命令 2025-04-11 11:38:42 +08:00
FamousMai 906f5a0da5 refactor(extend db upgrade): 删除掉一些之前合并版本的无关迁移文件。精简一下迁移文件,把改字段的统一都放到创建表里面去 2025-04-10 15:21:03 +08:00
FamousMai 706729aaae refactor(extend db upgrade): 先把二开新增的数据库迁移文件挪到新的目录 2025-04-10 14:47:23 +08:00
FamousMai 73e81323fc 合并dify1.2.0版本代码
# Conflicts:
#	api/controllers/service_api/app/workflow.py
#	api/controllers/service_api/wraps.py
#	web/.env.example
#	web/app/components/share/utils.ts
#	web/app/signin/page.module.css
#	web/context/debug-configuration.ts
2025-04-09 19:57:08 +08:00
-LAN- 2c2efe2e1e chore(*): bump version to 1.2.0 (#17675)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-04-09 17:12:40 +08:00
-LAN- ec29bcf013 feat(graph_engine): yield control to other threads before node run. (#17689)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-04-09 17:02:47 +08:00
Joel c9f18aae0f chore: find code with high complexity (#17679) 2025-04-09 15:39:12 +08:00
KVOJJJin df03c89a48 Chore: remove beta tag of app type (#17676) 2025-04-09 15:10:08 +08:00
crazywoola eb8584613b fix: Account.query => db.session.query(Account) (#17667) 2025-04-09 14:07:32 +08:00
quicksand 9000f4ad05 feat: add plugin daemon oss env config (#17663) 2025-04-09 14:02:17 +08:00
Bowen Liang 0b1259fc4a chore: add script for running mypy type checks and speed up mypy checks in CI jobs (#17489) 2025-04-09 13:03:53 +08:00
FangHao eb0e51d44d optimize: docker-compose.middleware.yaml update env_file dependence (#17646)
Co-authored-by: fanghao <fanghao@sci99.com>
2025-04-09 12:16:48 +08:00
yusheng chen f633d1ee92 chore: add 'no-empty-function': 'error' to eslint.config.mjs (#17656) 2025-04-09 12:10:17 +08:00
Han f1e4d5ed6c Fix Performance Issues: (#17083)
Co-authored-by: Wang Han <wanghan@zhejianglab.org>
2025-04-09 11:22:53 +08:00
Han b5498a373a Accelerate migration (#17088)
Co-authored-by: Wang Han <wanghan@zhejianglab.org>
2025-04-09 10:12:16 +08:00
Bowen Liang b73607da80 chore: bump Nodejs in web image from 20 to 22 LTS (#13341) 2025-04-09 09:40:11 +08:00
Lao 106604682a Fixed the model-modal titles not being clearly distinguished between "Add" and "Setup" (#17634) 2025-04-08 21:00:00 +08:00
Wu Tianwei cd7ac20d80 feat: enhance index type handling and add error notification for missing embedding model (#16836) 2025-04-08 18:01:43 +08:00
Bowen Liang be3ebea45b chore: bump pnpm to v10 in web dockerfile (#17611) 2025-04-08 17:12:25 +08:00
Joel 5a6219c726 chore: add unit test to high frequency hooks (#17617) 2025-04-08 16:39:11 +08:00
IAOTW 4124e804a0 fix(transport): add missing verify parameter to httpx.HTTPTransport (#17612) 2025-04-08 16:04:50 +08:00
Joel 8d1a34bbb9 fix: Sass @import warning (#17604) 2025-04-08 14:33:55 +08:00
Steven Li abead647e2 fix: Extract docx file fails when the file contains an invalid link (#17576) 2025-04-08 13:59:33 +08:00
huangzhuo1949 07ed728605 fix: segment keywords bug (#17599)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
2025-04-08 13:57:07 +08:00
Marcelo Díaz d796fcc0e7 feat: support select-type variables in Metadata Filtering (#17440 (#17445) 2025-04-08 11:15:07 +08:00
Good Wood 7b7ac7a495 fix: compatibility issues for currentStrategy.features is null (#17581) 2025-04-08 09:24:42 +08:00
Jyong c8145ce581 deal db session in celery worker (#17549) 2025-04-07 20:31:26 +08:00
诗浓 da7f8ad936 fix: remove wrong padding-bottom=0 (#17551) 2025-04-07 18:16:01 +08:00
Junjie.M 41df910771 fix: only the owner can edit the member information (#17538) 2025-04-07 17:42:12 +08:00
kenwoodjw 01704a4c1b fix ChatCompletionResponse docs (#17534)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-04-07 17:42:05 +08:00
诗浓 8ab9eb9857 fix: update QA document default language from "Chinese" to "Chinese Simplified" (#17541) 2025-04-07 17:38:29 +08:00
Jyong e1439c4a5b deal dataset metadata and metadata binding (#17544) 2025-04-07 17:38:06 +08:00
Junyan Qin (Chin) e7f1d0deea feat(plugin/endpoint): add supports for provider to register array[tool] type field (#17523) 2025-04-07 16:15:20 +08:00
GeorgeCaoJ 0998b01321 fix: loop and interation node not showing tracing entry in chatflow (#17500) 2025-04-07 15:56:25 +08:00
crazywoola 743071d9bb chore: add missing i18n (#17517) 2025-04-07 13:11:42 +08:00
Amir Mohsen Asaran f54905e685 feat: Integrate WaterCrawl.dev as a new knowledge base provider (#16396)
Co-authored-by: crazywoola <427733928@qq.com>
2025-04-07 12:43:23 +08:00
Jasonfish 0afad94378 fix: Correct "The job is not exist" to "The job does not exist" (#17516) 2025-04-07 12:32:25 +08:00
zxhlyh e350511102 fix: chat api doc (#17515) 2025-04-07 12:32:15 +08:00
Jasonfish fd443941a2 feat(improve-api-endpoints): Added Datasets and Annotation APIs (#12237) 2025-04-07 10:36:58 +08:00
quicksand b146aaaeb7 optimize: plugin permission (#17507) 2025-04-07 10:17:53 +08:00
yusheng chen dd4b03e812 refactor & perf of file var-reference-vars.tsx (#17444) 2025-04-07 10:13:22 +08:00
yusheng chen bf69b97639 refactor & perf: declare const object outside component ConstantField (#17495) 2025-04-07 10:12:35 +08:00
yusheng chen 6d59b8d85b refactor: type improvement of component AddVariablePopupWithPosition (#17497) 2025-04-07 10:12:20 +08:00
yusheng chen e3dc9f3c31 refactor & perf: declare const object outside component VarReferencePicker (#17496) 2025-04-07 10:12:10 +08:00
wlleiiwang 42a42a7962 FEAT: support Tencent vectordb to full text search (#16865)
Co-authored-by: wlleiiwang <wlleiiwang@tencent.com>
2025-04-07 09:50:03 +08:00
yusheng chen c05e03fc09 refactor & perf: import { noop } from 'lodash-es' across web (#17439) 2025-04-06 17:56:08 +08:00
KVOJJJin 7016ccef10 Fix(webapp): long table scroll issue of workflow (#17491) 2025-04-06 17:54:41 +08:00
GuanMu 6f636093b6 fix: adjust styling for active and disabled states in Stepper component (#17485) 2025-04-06 00:04:56 +08:00
crazywoola 3e698074e7 Fix/17466 cannot create a knowledge base by adding files (#17470) 2025-04-06 00:03:05 +08:00
yusheng chen dc9194ca00 fix: change Switch.props.ref to optional prop to align with OriginalSwitch (#17443) 2025-04-05 14:56:54 +08:00
Alter-xyz 6efa882ca3 chore: remove outdated contact (#17467) 2025-04-05 11:36:40 +08:00
Bowen Liang fcd5fcca83 fix: skip file format check of mime types for generated files by tools (#17455) 2025-04-05 10:29:14 +08:00
yusheng chen 95212af935 refactor: type improvement of file oneMoreStep.tsx (#17431) 2025-04-04 21:11:33 +08:00
crazywoola da2113bde9 fix: client side error (#17428) 2025-04-04 15:59:02 +08:00
Panpan fc3f14c0ee fix: keep image url (#17430) 2025-04-04 15:55:48 +08:00
yusheng chen 296e2ef90f doc: add JSDoc to file format.ts (#17426) 2025-04-03 22:48:54 +08:00
非法操作 dcb8939c7f feat: add resize debug panel width (#17427) 2025-04-03 21:30:04 +08:00
Joel 31a6aabfe5 chore: add unit test to high frequency component (#17423) 2025-04-03 18:19:11 +08:00
Dongyu Li 2e9997110a Fix/dsl kb encrypt (#17353) 2025-04-03 17:29:34 +08:00
Yongtao Huang e1304dc0c3 Chore: fix some wrong annotations (#17413) 2025-04-03 17:27:07 +08:00
bravomark 5aa82629dd fix: #15548 Resolve errors in SQL queries caused by SELECT fields not appearing in the GROUP BY clause. (#17408) 2025-04-03 16:48:50 +08:00
Novice dcdec98c8e feat: agent node add memory (#15976) 2025-04-03 16:40:58 +08:00
XiaoBa 3d76f09c3a feat: hide 'Find More in Marketplace in Tools' (#16955)
Co-authored-by: Xiaoba Yu <xb1823725853@gmail.com>
2025-04-03 16:35:59 +08:00
Good Wood b3f4e90862 feat: remove file and file[] restriction in code node (#17382) 2025-04-03 16:23:19 +08:00
crazywoola 4902ddaf87 Feat/change workspace name (#17402) 2025-04-03 16:05:55 +08:00
Tonny a83318cf4b feat: add langfuse llm node input and output (#17368) 2025-04-03 16:05:37 +08:00
GuanMu 12faecdf89 fix: update tag input border styles for special mode and focus state (#17393) 2025-04-03 14:03:07 +08:00
crazywoola c92bc84316 Fix/15429 forgotpasswordresetapi session management (#17390) 2025-04-03 13:54:28 +08:00
Jyong 48c2168dff fix annotation failed when embedding model and dimension changed (#17347) 2025-04-03 13:03:22 +08:00
Perfecto 16c722d1d8 fix: move hardcoded text to language settings (#16990) (#17133) 2025-04-02 22:35:51 +08:00
akou 5cffcd6336 fix: improve error handling and default values in fetchPlan function (#17320) 2025-04-02 21:44:09 +08:00
Jyong 0bf816f2e8 fix duplicate documents returned by Get document list service API (#17351) 2025-04-02 21:39:28 +08:00
Jyong 6104b91d3f add doc support in knowledge base for unstructured (#17352) 2025-04-02 21:35:01 +08:00
GuanMu 33c8cb7b3b fix: update docker-compose commands $17355 (#17356) 2025-04-02 21:33:33 +08:00
诗浓 4f286c9073 fix: remove setPublishedAt on handleRestoreFromPublishedWorkflow (#17358) 2025-04-02 21:32:21 +08:00
Jiang fd1e40d22e Lindorm VDB bugfix (#17357)
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
2025-04-02 21:31:59 +08:00
Benjamin e2b8f40275 fix: update permission logic to ensure administrators always have access (#17360) 2025-04-02 21:29:14 +08:00
zxhlyh 78409dfec1 fix: loop single run log (#17329) 2025-04-02 15:22:24 +08:00
Joel 98345c0f65 fix: sandbox can upload multiple files by upload (#17323) 2025-04-02 14:02:44 +08:00
Kindy Lin 95c6bd1c8a feat: add i18n checker (ui) (#17283) 2025-04-02 13:58:31 +08:00
Dongyu Li 8c77f2dc03 Feat/loop node (#17273) 2025-04-02 13:53:26 +08:00
liguochuan 11e95d2a61 Fix (api): Fix the processing logic of the retriever_resources field. (#17304)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-02 12:44:29 +08:00
Jian Guo abaefe22bc [Documentation] Update README under docker folder for middleware components. (#17284) 2025-04-02 11:02:05 +08:00
1Ckpwee bda06df668 fix: add app quota check to import API (#17295)
Co-authored-by: crazywoola <427733928@qq.com>
2025-04-02 10:20:46 +08:00
Bowen Liang 4a505c19df fix: enlarge workspace selector width to show current workspace (#17288) 2025-04-02 09:27:37 +08:00
csurong d417ccaf49 fix: web docker file config (#17130) 2025-04-01 21:16:14 +08:00
Joel b4aa1900e2 feat: add zod (#17277) 2025-04-01 18:10:11 +08:00
zxhlyh 713902dc47 Feat/loop break node (#17268) 2025-04-01 16:52:07 +08:00
Jyong 627a9e2ce1 SaaS: batch upload limit check for sandbox plan (#17264) 2025-04-01 16:45:31 +08:00
NFish 2ae7a70be9 fix: web app form<RunOnce> component is changing an uncontrolled inpu… (#17269) 2025-04-01 16:30:41 +08:00
zxhlyh e58703877b chore: slice workflow store (#17254) 2025-04-01 16:02:52 +08:00
Xiyuan Chen 9c4be5d098 Feat/education api (#17168) 2025-04-01 02:45:34 -04:00
KVOJJJin d1801b1f2e Feat:edu frontend (#17251)
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
2025-04-01 13:58:10 +08:00
HuDenghui 4b5ec242e7 fix: Fix lodash module not found issue (#16953) 2025-04-01 12:16:49 +08:00
liuzhenghua d5b48a0aa3 fix: keywords field not persist in segements api (#17151) 2025-04-01 11:20:32 +08:00
Obada Khalili 6372cb7b41 Support variables in question classifier classes (#17227) 2025-04-01 11:19:36 +08:00
GuanMu 931d3390f0 Fix api document (#17178) 2025-04-01 11:07:47 +08:00
Jiang ff388fe3e6 optimize lindorm vdb add_texts (#17212)
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
2025-04-01 11:06:35 +08:00
Arcaner ef1c1a12d2 fix: enhance validation of workflow file types (#16203) 2025-03-31 20:05:20 +08:00
Panpan 24b1a625b3 feat: allow the embedding in websites to customize sys.user_id (#16062) 2025-03-31 18:55:42 +08:00
shirukai 6cf258a809 fix: code block syntax cannot be displayed correctly in react mode (#16904) 2025-03-31 16:27:29 +08:00
KVOJJJin c66fda7c71 chore: independent page style update (#17176) 2025-03-31 15:44:04 +08:00
诗浓 ac850e559f feat: organize button adds organization of nodes inside iteration/loop nodes (#17068) 2025-03-31 15:17:17 +08:00
Joel 7df36fe9f5 fix: run frontend test failed and enable run test in CI (#17017) 2025-03-31 14:36:01 +08:00
NFish 161724fb17 Fix/workspace logo style (#17164) 2025-03-31 14:26:43 +08:00
KVOJJJin cf05e9cf78 Fix: iframe mount delay of embedded chatbot (#17167) 2025-03-31 14:25:39 +08:00
非法操作 44f911a0a8 chore: docstring not match the function parameter (#17162) 2025-03-31 13:19:15 +08:00
zxhlyh 32527b26d5 fix: model page switch marketplace (#17147) 2025-03-31 11:13:05 +08:00
KVOJJJin e008faf729 Feat: dark mode for independent pages (#17045) 2025-03-31 10:28:19 +08:00
Yingchun Lai 46d235bca0 feat: poolize the ops trace instance (#15947) 2025-03-30 13:20:23 +08:00
horochx a91b780936 perf: optimizing db WorkflowAppLog index (#14710) 2025-03-30 13:17:23 +08:00
jiangbo721 aa4c6874f1 fix: When a WorkSpaceNotAllowedCreateError occurs, may account is not defined (#11144)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-03-30 13:16:07 +08:00
adru 6d7a54915e Added CONTRIBUTING.md translations for multiple languages (#17108) 2025-03-30 13:12:13 +08:00
GuanMu f30b1c2358 fix: update border radius of ListboxOptions for improved UI consistency #17101 (#17102) 2025-03-29 23:43:18 +08:00
Good Wood 42968cb945 fix: fix cannot select stream-tool-call in agent modal (#17015) 2025-03-29 14:47:28 +08:00
ClSlaid 87034e26ae chore: add .env-local to gitigonre (#17042)
Signed-off-by: cl <cailue@apache.org>
2025-03-29 14:23:58 +08:00
Arcaner becd03a4aa fix: enhance file extension condition check for if-else node (#17060) 2025-03-29 14:20:18 +08:00
jiangbo721 a1aa325ce3 Chore/code format and Repair commit_id 3254018d more deleted codes and Fix naming error ambiguity between workflow_run_id and workflow_id (#17075)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-03-29 14:15:53 +08:00
jiangbo721 34cba83ac4 fix: bug that overwrote the llm model thought process when final_answ… (#17074)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-03-28 23:41:32 +08:00
tyounami fb11264f42 docs: correct type desc (#17043)
Co-authored-by: bo.zhao <bo.zhao@iglooinsure.com>
2025-03-28 19:24:45 +08:00
Bowen Liang bad31dfff1 ci: skip setting up opengauss for vdb tests (#17016) 2025-03-28 19:23:46 +08:00
Novice dd3844d1d3 fix(answer): The chat interface outputs twice (#17033) 2025-03-28 18:18:29 +08:00
GuanMu bc22076ad8 fix: update account dropdown border radius for improved UI consistency #17030 (#17031) 2025-03-28 16:01:59 +08:00
過世秋風 754e646b0c fix: _build_from_remote_url get extension is .bin (#17020) 2025-03-28 15:44:23 +08:00
KVOJJJin 9feafb6dbd fix: show build app limit in app creation modal (#16940) 2025-03-28 15:39:21 +08:00
GuanMu c7fcfc863d fix: add overflow hidden to Collapse component #17009 (#17011) 2025-03-28 14:39:16 +08:00
Chenhe Gu d565802ea1 remove business contact info in license (#16985) 2025-03-28 10:03:36 +08:00
crazywoola ea1d459423 Revert "feat: add langfuse llm node input and output" (#16947) 2025-03-27 17:42:12 +08:00
過世秋風 377d11d13b fix: WorkflowRunDetailApi created_at、finished_at types changed to timestamps (#16821) 2025-03-27 17:23:30 +08:00
GuanMu d65da600e5 fix: enhance filename validation and extraction in FileService #16867 (#16869) 2025-03-27 16:46:10 +08:00
Joe 82189e1bc5 feat: add langfuse llm node input and output (#16924) 2025-03-27 16:32:12 +08:00
诗浓 7f8bfb6e4a fix(ui): improve chart text spacing and prevent unnecessary truncation (#16894) 2025-03-27 15:58:23 +08:00
wanttobeamaster 7f70cadacb feat: support Tablestore vector database (#16601)
Co-authored-by: xiaozhiqing.xzq <xiaozhiqing.xzq@alibaba-inc.com>
2025-03-27 15:53:33 +08:00
Yongtao Huang 98f2e2c729 Fix wrong allowed extensions (#16893) 2025-03-27 15:42:12 +08:00
Joel 395fdc4960 fix: some parallel logs missing (#16923) 2025-03-27 15:42:01 +08:00
Good Wood 094b049c94 fix: fix the bug where pressing Enter in Chinese input mode on Safari… (#16914) 2025-03-27 15:41:06 +08:00
Alex 5d77730c78 feat(api): optimize conversation name generation logic in message app (#16917)
Co-authored-by: yuanlong <yuanlong@boco.com.cn>
Co-authored-by: crazywoola <427733928@qq.com>
2025-03-27 15:18:45 +08:00
wlleiiwang a743d5dc71 feat: tencent vectordb: use grpc client and set upsert batch size (#16016)
Co-authored-by: wlleiiwang <wlleiiwang@tencent.com>
2025-03-27 12:20:16 +08:00
KVOJJJin c23135c9e8 feat: webapp support change inputs after conversation started (#16901) 2025-03-27 11:58:16 +08:00
Novice 0722beeb0b fix(agent app): moderation feature can't work as expected (#16794) 2025-03-27 11:41:14 +08:00
JimintheBox 8b89447549 Fix Custom Tool File Upload: Resolve Multiple Files Recognition and Multipart Boundary Issues (#14014)
Co-authored-by: crazywoola <427733928@qq.com>
2025-03-27 10:47:35 +08:00
非法操作 8047d08b3b chore: remove third party models (#13885) 2025-03-27 10:22:32 +08:00
Jyong 30792a1e1a install pandoc (#16825) 2025-03-26 22:34:10 +08:00
非法操作 91db2207b3 fix: tool's number and secet input display issue (#16834) 2025-03-26 21:17:04 +08:00
yourchanges 59a86dabee fix: fix missing oceanbase config enable_hybrid_search init (#16852)
Co-authored-by: 李远军 <4842@9ji.com>
2025-03-26 21:15:54 +08:00
非法操作 d87d66ab88 fix missing agent node help link (#16841) 2025-03-26 16:28:32 +08:00
Joel 032d849f17 chore: handle Textarea component ref warning in react 19 (#16818) 2025-03-26 14:55:02 +08:00
XiaoBa c451f54925 feat: add the maximum number of iterations to env (#16805)
Co-authored-by: Xiaoba Yu <xb1823725853@gmail.com>
2025-03-26 14:17:59 +08:00
Jyong 6a857e01f6 fix multiple metadata filter's confusing setting (#16771) 2025-03-26 14:16:21 +08:00
Hanqing Zhao 2da780e4dc Add and modify jp translation (#16807)
Co-authored-by: crazywoola <427733928@qq.com>
2025-03-26 13:29:04 +08:00
Good Wood 71edaba9df fix: fix ui layout when in mobile mode (#16793) 2025-03-26 11:01:13 +08:00
Yongtao Huang 37134c5987 Remove the useless excluded item in mypy.ini (#16777) 2025-03-26 09:02:45 +08:00
taokuizu 0c2a459c30 fix typo in _process_metadata_filter_func (#16780) 2025-03-26 09:01:41 +08:00
Bowen Liang e0fc7f69dd fix the major and minor version of stub types declaration with build date stripped (#16709) 2025-03-25 21:23:44 +08:00
crazywoola bbf1639c63 feat: add jp_ja for knowledge api (#16766) 2025-03-25 20:31:01 +08:00
liuzhenghua cb12b4436f fix: provider credentials load error (#16695) 2025-03-25 18:56:53 +08:00
Yongtao Huang 0277a37fca Fix some typos in CONTRIBUTING.md (#16761) 2025-03-25 18:53:09 +08:00
Novice ac3577bc56 chore(slider): fix the slider lint error (#16746) 2025-03-25 17:09:59 +08:00
GuanMu 0811a23cd4 fix: trim whitespace from URL input in file uploader component #16700 (#16722) 2025-03-25 16:48:25 +08:00
Novice 05c6d57f29 fix(slider): the slider's style is not as expected (#16711) 2025-03-25 16:47:23 +08:00
Jyong 58c62f0a34 fix full-doc mode document doesn't reindex after enable or un_archive (#16737) 2025-03-25 16:26:14 +08:00
Jyong 2174225259 fix milvus filter search (#16725) 2025-03-25 16:22:43 +08:00
Wu Tianwei 6a0f1dad7f fix: update htmlContent type and wrap button in div for manualClose (#16716) 2025-03-25 14:58:42 +08:00
David 40cadab8a6 fix: fix icon generation error on Windows platform (#16254) 2025-03-25 14:54:49 +08:00
hsiong 6157f57872 feat: Add OceanBase hybrid search features (#16652)
Co-authored-by: 李远军 <4842@9ji.com>
Co-authored-by: yourchanges <yourchanges@gmail.com>
2025-03-25 14:32:00 +08:00
zhangkun-21 c4bb07184d Update prompts.py:Fix conversation title language for Italian conversations (#16696) 2025-03-25 14:29:08 +08:00
诗浓 af5d6ca27d fix: regex expressions distorted in code block (#16690) 2025-03-25 14:28:35 +08:00
Jyong 78f2ec8f32 mark weight type as optional (#16701) 2025-03-25 14:19:26 +08:00
kenwoodjw a113356695 fix: pgvector metadata filter (#16688)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-03-25 11:34:33 +08:00
Yongtao Huang 43753c8e9a Fix function's name mismatch (#16681) 2025-03-25 10:25:15 +08:00
Jiang fc8c765215 Fix/vdb lindorm (#16660)
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
2025-03-25 09:19:06 +08:00
Jyong 86a1859d02 Metadata variable value fix (#16665) 2025-03-25 09:07:11 +08:00
GuanMu 360986f38d Feat add childchunk api (#16094) 2025-03-24 20:58:28 +08:00
691 changed files with 22309 additions and 5900 deletions
+3 -3
View File
@@ -1,13 +1,13 @@
#!/bin/bash
npm add -g pnpm@9.12.2
npm add -g pnpm@10.8.0
cd web && pnpm install
pipx install poetry
echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify down"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc
source /home/vscode/.bashrc
+7 -2
View File
@@ -53,9 +53,14 @@ jobs:
- name: Run dify config tests
run: poetry run -P api python dev/pytest/pytest_config_tests.py
- name: Cache MyPy
uses: actions/cache@v4
with:
path: api/.mypy_cache
key: mypy-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('api/poetry.lock') }}
- name: Run mypy
run: |
poetry run -C api python -m mypy --install-types --non-interactive .
run: dev/run-mypy
- name: Set up dotenvs
run: |
+2 -1
View File
@@ -82,7 +82,7 @@ jobs:
uses: actions/setup-node@v4
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 20
node-version: 22
cache: pnpm
cache-dependency-path: ./web/package.json
@@ -153,6 +153,7 @@ jobs:
env:
BASH_SEVERITY: warning
DEFAULT_BRANCH: main
FILTER_REGEX_INCLUDE: pnpm-lock.yaml
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IGNORE_GENERATED_FILES: true
IGNORE_GITIGNORED_FILES: true
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
node-version: [16, 18, 20]
node-version: [16, 18, 20, 22]
defaults:
run:
@@ -33,7 +33,7 @@ jobs:
- name: Set up Node.js
if: env.FILES_CHANGED == 'true'
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
-1
View File
@@ -76,7 +76,6 @@ jobs:
milvus-standalone
pgvecto-rs
pgvector
opengauss
chroma
elasticsearch
+20 -19
View File
@@ -31,25 +31,26 @@ jobs:
uses: tj-actions/changed-files@v45
with:
files: web/**
# to run pnpm, should install package canvas, but it always install failed on amd64 under ubuntu-latest
# - name: Install pnpm
# uses: pnpm/action-setup@v4
# with:
# version: 10
# run_install: false
# - name: Setup Node.js
# uses: actions/setup-node@v4
# if: steps.changed-files.outputs.any_changed == 'true'
# with:
# node-version: 20
# cache: pnpm
# cache-dependency-path: ./web/package.json
- name: Install pnpm
if: steps.changed-files.outputs.any_changed == 'true'
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
# - name: Install dependencies
# if: steps.changed-files.outputs.any_changed == 'true'
# run: pnpm install --frozen-lockfile
- name: Setup Node.js
uses: actions/setup-node@v4
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 22
cache: pnpm
cache-dependency-path: ./web/package.json
# - name: Run tests
# if: steps.changed-files.outputs.any_changed == 'true'
# run: pnpm test
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm install --frozen-lockfile
- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm test
+1
View File
@@ -103,6 +103,7 @@ celerybeat.pid
# Environments
.env
.env-local
.venv
env/
venv/
+3 -3
View File
@@ -18,7 +18,7 @@ Need to update an existing model runtime, tool, or squash some bugs? Head over t
Join the fun, contribute, and let's build something awesome together! 💡✨
Don't forget to link an existing issue or open an new issue in the PR's description.
Don't forget to link an existing issue or open a new issue in the PR's description.
### Bug reports
@@ -68,7 +68,7 @@ How we prioritize:
4. Please add tests for your changes accordingly
5. Ensure your code passes the existing tests
6. Please link the issue in the PR description, `fixes #<issue_number>`
7. Get merrged!
7. Get merged!
### Setup the project
#### Frontend
@@ -90,4 +90,4 @@ We recommend reviewing this document carefully before proceeding with the setup,
Feel free to reach out if you encounter any issues during the setup process.
## Getting Help
If you ever get stuck or got a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
+93
View File
@@ -0,0 +1,93 @@
# CONTRIBUIR
Así que estás buscando contribuir a Dify - eso es fantástico, estamos ansiosos por ver lo que haces. Como una startup con personal y financiación limitados, tenemos grandes ambiciones de diseñar el flujo de trabajo más intuitivo para construir y gestionar aplicaciones LLM. Cualquier ayuda de la comunidad cuenta, realmente.
Necesitamos ser ágiles y enviar rápidamente dado donde estamos, pero también queremos asegurarnos de que colaboradores como tú obtengan una experiencia lo más fluida posible al contribuir. Hemos elaborado esta guía de contribución con ese propósito, con el objetivo de familiarizarte con la base de código y cómo trabajamos con los colaboradores, para que puedas pasar rápidamente a la parte divertida.
Esta guía, como Dify mismo, es un trabajo en constante progreso. Agradecemos mucho tu comprensión si a veces se queda atrás del proyecto real, y damos la bienvenida a cualquier comentario para que podamos mejorar.
En términos de licencia, por favor tómate un minuto para leer nuestro breve [Acuerdo de Licencia y Colaborador](./LICENSE). La comunidad también se adhiere al [código de conducta](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md).
## Antes de empezar
¿Buscas algo en lo que trabajar? Explora nuestros [buenos primeros issues](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) y elige uno para comenzar.
¿Tienes un nuevo modelo o herramienta genial para añadir? Abre un PR en nuestro [repositorio de plugins](https://github.com/langgenius/dify-plugins) y muéstranos lo que has construido.
¿Necesitas actualizar un modelo existente, herramienta o corregir algunos errores? Dirígete a nuestro [repositorio oficial de plugins](https://github.com/langgenius/dify-official-plugins) y haz tu magia.
¡Únete a la diversión, contribuye y construyamos algo increíble juntos! 💡✨
No olvides vincular un issue existente o abrir uno nuevo en la descripción del PR.
### Informes de errores
> [!IMPORTANT]
> Por favor, asegúrate de incluir la siguiente información al enviar un informe de error:
- Un título claro y descriptivo
- Una descripción detallada del error, incluyendo cualquier mensaje de error
- Pasos para reproducir el error
- Comportamiento esperado
- **Logs**, si están disponibles, para problemas del backend, esto es realmente importante, puedes encontrarlos en los logs de docker-compose
- Capturas de pantalla o videos, si es aplicable
Cómo priorizamos:
| Tipo de Issue | Prioridad |
| ------------------------------------------------------------ | --------------- |
| Errores en funciones principales (servicio en la nube, no poder iniciar sesión, aplicaciones que no funcionan, fallos de seguridad) | Crítica |
| Errores no críticos, mejoras de rendimiento | Prioridad Media |
| Correcciones menores (errores tipográficos, UI confusa pero funcional) | Prioridad Baja |
### Solicitudes de funcionalidades
> [!NOTE]
> Por favor, asegúrate de incluir la siguiente información al enviar una solicitud de funcionalidad:
- Un título claro y descriptivo
- Una descripción detallada de la funcionalidad
- Un caso de uso para la funcionalidad
- Cualquier otro contexto o capturas de pantalla sobre la solicitud de funcionalidad
Cómo priorizamos:
| Tipo de Funcionalidad | Prioridad |
| ------------------------------------------------------------ | --------------- |
| Funcionalidades de alta prioridad etiquetadas por un miembro del equipo | Prioridad Alta |
| Solicitudes populares de funcionalidades de nuestro [tablero de comentarios de la comunidad](https://github.com/langgenius/dify/discussions/categories/feedbacks) | Prioridad Media |
| Funcionalidades no principales y mejoras menores | Prioridad Baja |
| Valiosas pero no inmediatas | Futura-Funcionalidad |
## Enviando tu PR
### Proceso de Pull Request
1. Haz un fork del repositorio
2. Antes de redactar un PR, por favor crea un issue para discutir los cambios que quieres hacer
3. Crea una nueva rama para tus cambios
4. Por favor añade pruebas para tus cambios en consecuencia
5. Asegúrate de que tu código pasa las pruebas existentes
6. Por favor vincula el issue en la descripción del PR, `fixes #<número_del_issue>`
7. ¡Fusiona tu código!
### Configuración del proyecto
#### Frontend
Para configurar el servicio frontend, por favor consulta nuestra [guía completa](https://github.com/langgenius/dify/blob/main/web/README.md) en el archivo `web/README.md`. Este documento proporciona instrucciones detalladas para ayudarte a configurar el entorno frontend correctamente.
#### Backend
Para configurar el servicio backend, por favor consulta nuestras [instrucciones detalladas](https://github.com/langgenius/dify/blob/main/api/README.md) en el archivo `api/README.md`. Este documento contiene una guía paso a paso para ayudarte a poner en marcha el backend sin problemas.
#### Otras cosas a tener en cuenta
Recomendamos revisar este documento cuidadosamente antes de proceder con la configuración, ya que contiene información esencial sobre:
- Requisitos previos y dependencias
- Pasos de instalación
- Detalles de configuración
- Consejos comunes de solución de problemas
No dudes en contactarnos si encuentras algún problema durante el proceso de configuración.
## Obteniendo Ayuda
Si alguna vez te quedas atascado o tienes una pregunta urgente mientras contribuyes, simplemente envíanos tus consultas a través del issue relacionado de GitHub, o únete a nuestro [Discord](https://discord.gg/8Tpq4AcN9c) para una charla rápida.
+93
View File
@@ -0,0 +1,93 @@
# CONTRIBUER
Vous cherchez donc à contribuer à Dify - c'est fantastique, nous avons hâte de voir ce que vous allez faire. En tant que startup avec un personnel et un financement limités, nous avons de grandes ambitions pour concevoir le flux de travail le plus intuitif pour construire et gérer des applications LLM. Toute aide de la communauté compte, vraiment.
Nous devons être agiles et livrer rapidement compte tenu de notre position, mais nous voulons aussi nous assurer que des contributeurs comme vous obtiennent une expérience aussi fluide que possible lors de leur contribution. Nous avons élaboré ce guide de contribution dans ce but, visant à vous familiariser avec la base de code et comment nous travaillons avec les contributeurs, afin que vous puissiez rapidement passer à la partie amusante.
Ce guide, comme Dify lui-même, est un travail en constante évolution. Nous apprécions grandement votre compréhension si parfois il est en retard par rapport au projet réel, et nous accueillons tout commentaire pour nous aider à nous améliorer.
En termes de licence, veuillez prendre une minute pour lire notre bref [Accord de Licence et de Contributeur](./LICENSE). La communauté adhère également au [code de conduite](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md).
## Avant de vous lancer
Vous cherchez quelque chose à réaliser ? Parcourez nos [problèmes pour débutants](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) et choisissez-en un pour commencer !
Vous avez un nouveau modèle ou un nouvel outil à ajouter ? Ouvrez une PR dans notre [dépôt de plugins](https://github.com/langgenius/dify-plugins) et montrez-nous ce que vous avez créé.
Vous devez mettre à jour un modèle existant, un outil ou corriger des bugs ? Rendez-vous sur notre [dépôt officiel de plugins](https://github.com/langgenius/dify-official-plugins) et faites votre magie !
Rejoignez l'aventure, contribuez, et construisons ensemble quelque chose d'extraordinaire ! 💡✨
N'oubliez pas de lier un problème existant ou d'ouvrir un nouveau problème dans la description de votre PR.
### Rapports de bugs
> [!IMPORTANT]
> Veuillez vous assurer d'inclure les informations suivantes lors de la soumission d'un rapport de bug :
- Un titre clair et descriptif
- Une description détaillée du bug, y compris tous les messages d'erreur
- Les étapes pour reproduire le bug
- Comportement attendu
- **Logs**, si disponibles, pour les problèmes de backend, c'est vraiment important, vous pouvez les trouver dans les logs de docker-compose
- Captures d'écran ou vidéos, si applicable
Comment nous priorisons :
| Type de Problème | Priorité |
| ------------------------------------------------------------ | --------------- |
| Bugs dans les fonctions principales (service cloud, impossibilité de se connecter, applications qui ne fonctionnent pas, failles de sécurité) | Critique |
| Bugs non critiques, améliorations de performance | Priorité Moyenne |
| Corrections mineures (fautes de frappe, UI confuse mais fonctionnelle) | Priorité Basse |
### Demandes de fonctionnalités
> [!NOTE]
> Veuillez vous assurer d'inclure les informations suivantes lors de la soumission d'une demande de fonctionnalité :
- Un titre clair et descriptif
- Une description détaillée de la fonctionnalité
- Un cas d'utilisation pour la fonctionnalité
- Tout autre contexte ou captures d'écran concernant la demande de fonctionnalité
Comment nous priorisons :
| Type de Fonctionnalité | Priorité |
| ------------------------------------------------------------ | --------------- |
| Fonctionnalités hautement prioritaires étiquetées par un membre de l'équipe | Priorité Haute |
| Demandes populaires de fonctionnalités de notre [tableau de feedback communautaire](https://github.com/langgenius/dify/discussions/categories/feedbacks) | Priorité Moyenne |
| Fonctionnalités non essentielles et améliorations mineures | Priorité Basse |
| Précieuses mais non immédiates | Fonctionnalité Future |
## Soumettre votre PR
### Processus de Pull Request
1. Forkez le dépôt
2. Avant de rédiger une PR, veuillez créer un problème pour discuter des changements que vous souhaitez apporter
3. Créez une nouvelle branche pour vos changements
4. Veuillez ajouter des tests pour vos changements en conséquence
5. Assurez-vous que votre code passe les tests existants
6. Veuillez lier le problème dans la description de la PR, `fixes #<numéro_du_problème>`
7. Faites fusionner votre code !
### Configuration du projet
#### Frontend
Pour configurer le service frontend, veuillez consulter notre [guide complet](https://github.com/langgenius/dify/blob/main/web/README.md) dans le fichier `web/README.md`. Ce document fournit des instructions détaillées pour vous aider à configurer correctement l'environnement frontend.
#### Backend
Pour configurer le service backend, veuillez consulter nos [instructions détaillées](https://github.com/langgenius/dify/blob/main/api/README.md) dans le fichier `api/README.md`. Ce document contient un guide étape par étape pour vous aider à faire fonctionner le backend sans problème.
#### Autres choses à noter
Nous recommandons de revoir attentivement ce document avant de procéder à la configuration, car il contient des informations essentielles sur :
- Prérequis et dépendances
- Étapes d'installation
- Détails de configuration
- Conseils courants de dépannage
N'hésitez pas à nous contacter si vous rencontrez des problèmes pendant le processus de configuration.
## Obtenir de l'aide
Si jamais vous êtes bloqué ou avez une question urgente en contribuant, envoyez-nous simplement vos questions via le problème GitHub concerné, ou rejoignez notre [Discord](https://discord.gg/8Tpq4AcN9c) pour une discussion rapide.
+93
View File
@@ -0,0 +1,93 @@
# 기여하기
Dify에 기여하려고 하시는군요 - 정말 멋집니다, 당신이 무엇을 할지 기대가 됩니다. 인력과 자금이 제한된 스타트업으로서, 우리는 LLM 애플리케이션을 구축하고 관리하기 위한 가장 직관적인 워크플로우를 설계하고자 하는 큰 야망을 가지고 있습니다. 커뮤니티의 모든 도움은 정말 중요합니다.
우리는 현재 상황에서 민첩하게 빠르게 배포해야 하지만, 동시에 당신과 같은 기여자들이 기여하는 과정에서 최대한 원활한 경험을 얻을 수 있도록 하고 싶습니다. 우리는 이러한 목적으로 이 기여 가이드를 작성했으며, 여러분이 코드베이스와 우리가 기여자들과 어떻게 협업하는지에 대해 친숙해질 수 있도록 돕고, 빠르게 재미있는 부분으로 넘어갈 수 있도록 하고자 합니다.
이 가이드는 Dify 자체와 마찬가지로 끊임없이 진행 중인 작업입니다. 때로는 실제 프로젝트보다 뒤처질 수 있다는 점을 이해해 주시면 감사하겠으며, 개선을 위한 피드백은 언제든지 환영합니다.
라이센스 측면에서, 간략한 [라이센스 및 기여자 동의서](./LICENSE)를 읽어보는 시간을 가져주세요. 커뮤니티는 또한 [행동 강령](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md)을 준수합니다.
## 시작하기 전에
처리할 작업을 찾고 계신가요? [초보자를 위한 이슈](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)를 살펴보고 시작할 것을 선택하세요!
추가할 새로운 모델 런타임이나 도구가 있나요? 우리의 [플러그인 저장소](https://github.com/langgenius/dify-plugins)에 PR을 열고 당신이 만든 것을 보여주세요.
기존 모델 런타임, 도구를 업데이트하거나 버그를 수정해야 하나요? 우리의 [공식 플러그인 저장소](https://github.com/langgenius/dify-official-plugins)로 가서 당신의 마법을 펼치세요!
함께 즐기고, 기여하고, 멋진 것을 함께 만들어 봅시다! 💡✨
PR 설명에 기존 이슈를 연결하거나 새 이슈를 여는 것을 잊지 마세요.
### 버그 보고
> [!IMPORTANT]
> 버그 보고서를 제출할 때 다음 정보를 포함해 주세요:
- 명확하고 설명적인 제목
- 오류 메시지를 포함한 버그에 대한 상세한 설명
- 버그를 재현하는 단계
- 예상되는 동작
- 가능한 경우 **로그**, 백엔드 이슈의 경우 매우 중요합니다. docker-compose 로그에서 찾을 수 있습니다
- 해당되는 경우 스크린샷 또는 비디오
우선순위 결정 방법:
| 이슈 유형 | 우선순위 |
| ------------------------------------------------------------ | --------------- |
| 핵심 기능의 버그(클라우드 서비스, 로그인 불가, 애플리케이션 작동 불능, 보안 취약점) | 중대 |
| 비중요 버그, 성능 향상 | 중간 우선순위 |
| 사소한 수정(오타, 혼란스럽지만 작동하는 UI) | 낮은 우선순위 |
### 기능 요청
> [!NOTE]
> 기능 요청을 제출할 때 다음 정보를 포함해 주세요:
- 명확하고 설명적인 제목
- 기능에 대한 상세한 설명
- 해당 기능의 사용 사례
- 기능 요청에 관한 기타 컨텍스트 또는 스크린샷
우선순위 결정 방법:
| 기능 유형 | 우선순위 |
| ------------------------------------------------------------ | --------------- |
| 팀 구성원에 의해 레이블이 지정된 고우선순위 기능 | 높은 우선순위 |
| 우리의 [커뮤니티 피드백 보드](https://github.com/langgenius/dify/discussions/categories/feedbacks)에서 인기 있는 기능 요청 | 중간 우선순위 |
| 비핵심 기능 및 사소한 개선 | 낮은 우선순위 |
| 가치 있지만 즉시 필요하지 않은 기능 | 미래 기능 |
## PR 제출하기
### Pull Request 프로세스
1. 저장소를 포크하세요
2. PR을 작성하기 전에, 변경하고자 하는 내용에 대해 논의하기 위한 이슈를 생성해 주세요
3. 변경 사항을 위한 새 브랜치를 만드세요
4. 변경 사항에 대한 테스트를 적절히 추가해 주세요
5. 코드가 기존 테스트를 통과하는지 확인하세요
6. PR 설명에 이슈를 연결해 주세요, `fixes #<이슈_번호>`
7. 병합 완료!
### 프로젝트 설정하기
#### 프론트엔드
프론트엔드 서비스를 설정하려면, `web/README.md` 파일에 있는 우리의 [종합 가이드](https://github.com/langgenius/dify/blob/main/web/README.md)를 참조하세요. 이 문서는 프론트엔드 환경을 적절히 설정하는 데 도움이 되는 자세한 지침을 제공합니다.
#### 백엔드
백엔드 서비스를 설정하려면, `api/README.md` 파일에 있는 우리의 [상세 지침](https://github.com/langgenius/dify/blob/main/api/README.md)을 참조하세요. 이 문서는 백엔드를 원활하게 실행하는 데 도움이 되는 단계별 가이드를 포함하고 있습니다.
#### 기타 참고 사항
설정을 진행하기 전에 이 문서를 주의 깊게 검토하는 것을 권장합니다. 다음과 같은 필수 정보가 포함되어 있습니다:
- 필수 조건 및 종속성
- 설치 단계
- 구성 세부 정보
- 일반적인 문제 해결 팁
설정 과정에서 문제가 발생하면 언제든지 연락해 주세요.
## 도움 받기
기여하는 동안 막히거나 긴급한 질문이 있으면, 관련 GitHub 이슈를 통해 질문을 보내거나, 빠른 대화를 위해 우리의 [Discord](https://discord.gg/8Tpq4AcN9c)에 참여하세요.
+93
View File
@@ -0,0 +1,93 @@
# CONTRIBUINDO
Então você está procurando contribuir para o Dify - isso é incrível, mal podemos esperar para ver o que você vai fazer. Como uma startup com equipe e financiamento limitados, temos grandes ambições de projetar o fluxo de trabalho mais intuitivo para construir e gerenciar aplicações LLM. Qualquer ajuda da comunidade conta, verdadeiramente.
Precisamos ser ágeis e entregar rapidamente considerando onde estamos, mas também queremos garantir que colaboradores como você tenham uma experiência o mais tranquila possível ao contribuir. Montamos este guia de contribuição com esse propósito, visando familiarizá-lo com a base de código e como trabalhamos com os colaboradores, para que você possa rapidamente passar para a parte divertida.
Este guia, como o próprio Dify, é um trabalho em constante evolução. Agradecemos muito a sua compreensão se às vezes ele ficar atrasado em relação ao projeto real, e damos as boas-vindas a qualquer feedback para que possamos melhorar.
Em termos de licenciamento, por favor, dedique um minuto para ler nosso breve [Acordo de Licença e Contribuidor](./LICENSE). A comunidade também adere ao [código de conduta](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md).
## Antes de começar
Procurando algo para resolver? Navegue por nossos [problemas para iniciantes](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) e escolha um para começar!
Tem um novo modelo ou ferramenta para adicionar? Abra um PR em nosso [repositório de plugins](https://github.com/langgenius/dify-plugins) e mostre-nos o que você construiu.
Precisa atualizar um modelo existente, ferramenta ou corrigir alguns bugs? Vá para nosso [repositório oficial de plugins](https://github.com/langgenius/dify-official-plugins) e faça sua mágica!
Junte-se à diversão, contribua e vamos construir algo incrível juntos! 💡✨
Não se esqueça de vincular um problema existente ou abrir um novo problema na descrição do PR.
### Relatórios de bugs
> [!IMPORTANT]
> Por favor, certifique-se de incluir as seguintes informações ao enviar um relatório de bug:
- Um título claro e descritivo
- Uma descrição detalhada do bug, incluindo quaisquer mensagens de erro
- Passos para reproduzir o bug
- Comportamento esperado
- **Logs**, se disponíveis, para problemas de backend, isso é realmente importante, você pode encontrá-los nos logs do docker-compose
- Capturas de tela ou vídeos, se aplicável
Como priorizamos:
| Tipo de Problema | Prioridade |
| ------------------------------------------------------------ | --------------- |
| Bugs em funções centrais (serviço em nuvem, não conseguir fazer login, aplicações não funcionando, falhas de segurança) | Crítica |
| Bugs não críticos, melhorias de desempenho | Prioridade Média |
| Correções menores (erros de digitação, interface confusa mas funcional) | Prioridade Baixa |
### Solicitações de recursos
> [!NOTE]
> Por favor, certifique-se de incluir as seguintes informações ao enviar uma solicitação de recurso:
- Um título claro e descritivo
- Uma descrição detalhada do recurso
- Um caso de uso para o recurso
- Qualquer outro contexto ou capturas de tela sobre a solicitação de recurso
Como priorizamos:
| Tipo de Recurso | Prioridade |
| ------------------------------------------------------------ | --------------- |
| Recursos de alta prioridade conforme rotulado por um membro da equipe | Prioridade Alta |
| Solicitações populares de recursos do nosso [quadro de feedback da comunidade](https://github.com/langgenius/dify/discussions/categories/feedbacks) | Prioridade Média |
| Recursos não essenciais e melhorias menores | Prioridade Baixa |
| Valiosos mas não imediatos | Recurso Futuro |
## Enviando seu PR
### Processo de Pull Request
1. Faça um fork do repositório
2. Antes de elaborar um PR, por favor crie um problema para discutir as mudanças que você quer fazer
3. Crie um novo branch para suas alterações
4. Por favor, adicione testes para suas alterações conforme apropriado
5. Certifique-se de que seu código passa nos testes existentes
6. Por favor, vincule o problema na descrição do PR, `fixes #<número_do_problema>`
7. Faça o merge do seu código!
### Configurando o projeto
#### Frontend
Para configurar o serviço frontend, por favor consulte nosso [guia abrangente](https://github.com/langgenius/dify/blob/main/web/README.md) no arquivo `web/README.md`. Este documento fornece instruções detalhadas para ajudá-lo a configurar o ambiente frontend adequadamente.
#### Backend
Para configurar o serviço backend, por favor consulte nossas [instruções detalhadas](https://github.com/langgenius/dify/blob/main/api/README.md) no arquivo `api/README.md`. Este documento contém um guia passo a passo para ajudá-lo a colocar o backend em funcionamento sem problemas.
#### Outras coisas a observar
Recomendamos revisar este documento cuidadosamente antes de prosseguir com a configuração, pois ele contém informações essenciais sobre:
- Pré-requisitos e dependências
- Etapas de instalação
- Detalhes de configuração
- Dicas comuns de solução de problemas
Sinta-se à vontade para entrar em contato se encontrar quaisquer problemas durante o processo de configuração.
## Obtendo Ajuda
Se você ficar preso ou tiver uma dúvida urgente enquanto contribui, simplesmente envie suas perguntas através do problema relacionado no GitHub, ou entre no nosso [Discord](https://discord.gg/8Tpq4AcN9c) para uma conversa rápida.
+93
View File
@@ -0,0 +1,93 @@
# KATKIDA BULUNMAK
Demek Dify'a katkıda bulunmak istiyorsunuz - bu harika, ne yapacağınızı görmek için sabırsızlanıyoruz. Sınırlı personel ve finansmana sahip bir startup olarak, LLM uygulamaları oluşturmak ve yönetmek için en sezgisel iş akışını tasarlama konusunda büyük hedeflerimiz var. Topluluktan gelen her türlü yardım gerçekten önemli.
Bulunduğumuz noktada çevik olmamız ve hızlı hareket etmemiz gerekiyor, ancak sizin gibi katkıda bulunanların mümkün olduğunca sorunsuz bir deneyim yaşamasını da sağlamak istiyoruz. Bu katkı rehberini bu amaçla hazırladık; sizi kod tabanıyla ve katkıda bulunanlarla nasıl çalıştığımızla tanıştırmayı, böylece hızlıca eğlenceli kısma geçebilmenizi hedefliyoruz.
Bu rehber, Dify'ın kendisi gibi, sürekli gelişen bir çalışmadır. Bazen gerçek projenin gerisinde kalırsa anlayışınız için çok minnettarız ve gelişmemize yardımcı olacak her türlü geri bildirimi memnuniyetle karşılıyoruz.
Lisanslama konusunda, lütfen kısa [Lisans ve Katkıda Bulunan Anlaşmamızı](./LICENSE) okumak için bir dakikanızı ayırın. Topluluk ayrıca [davranış kurallarına](https://github.com/langgenius/.github/blob/main/CODE_OF_CONDUCT.md) da uyar.
## Başlamadan Önce
Üzerinde çalışacak bir şey mi arıyorsunuz? [İlk katkıda bulunanlar için iyi sorunlarımıza](https://github.com/langgenius/dify/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) göz atın ve başlamak için birini seçin!
Eklenecek harika bir yeni model runtime'ı veya aracınız mı var? [Eklenti depomuzda](https://github.com/langgenius/dify-plugins) bir PR açın ve ne yaptığınızı bize gösterin.
Mevcut bir model runtime'ını, aracı güncellemek veya bazı hataları düzeltmek mi istiyorsunuz? [Resmi eklenti depomuza](https://github.com/langgenius/dify-official-plugins) gidin ve sihrinizi gösterin!
Eğlenceye katılın, katkıda bulunun ve birlikte harika bir şeyler inşa edelim! 💡✨
PR açıklamasında mevcut bir sorunu bağlamayı veya yeni bir sorun açmayı unutmayın.
### Hata Raporları
> [!IMPORTANT]
> Lütfen bir hata raporu gönderirken aşağıdaki bilgileri dahil ettiğinizden emin olun:
- Net ve açıklayıcı bir başlık
- Hata mesajları dahil hatanın ayrıntılı bir açıklaması
- Hatayı tekrarlamak için adımlar
- Beklenen davranış
- Mümkünse **Loglar**, backend sorunları için, bu gerçekten önemlidir, bunları docker-compose loglarında bulabilirsiniz
- Uygunsa ekran görüntüleri veya videolar
Nasıl önceliklendiriyoruz:
| Sorun Türü | Öncelik |
| ------------------------------------------------------------ | --------------- |
| Temel işlevlerdeki hatalar (bulut hizmeti, giriş yapamama, çalışmayan uygulamalar, güvenlik açıkları) | Kritik |
| Kritik olmayan hatalar, performans artışları | Orta Öncelik |
| Küçük düzeltmeler (yazım hataları, kafa karıştırıcı ama çalışan UI) | Düşük Öncelik |
### Özellik İstekleri
> [!NOTE]
> Lütfen bir özellik isteği gönderirken aşağıdaki bilgileri dahil ettiğinizden emin olun:
- Net ve açıklayıcı bir başlık
- Özelliğin ayrıntılı bir açıklaması
- Özellik için bir kullanım durumu
- Özellik isteği hakkında diğer bağlamlar veya ekran görüntüleri
Nasıl önceliklendiriyoruz:
| Özellik Türü | Öncelik |
| ------------------------------------------------------------ | --------------- |
| Bir ekip üyesi tarafından etiketlenen Yüksek Öncelikli Özellikler | Yüksek Öncelik |
| [Topluluk geri bildirim panosundan](https://github.com/langgenius/dify/discussions/categories/feedbacks) popüler özellik istekleri | Orta Öncelik |
| Temel olmayan özellikler ve küçük geliştirmeler | Düşük Öncelik |
| Değerli ama acil olmayan | Gelecek-Özellik |
## PR'nizi Göndermek
### Pull Request Süreci
1. Depoyu fork edin
2. Bir PR taslağı oluşturmadan önce, yapmak istediğiniz değişiklikleri tartışmak için lütfen bir sorun oluşturun
3. Değişiklikleriniz için yeni bir dal oluşturun
4. Lütfen değişiklikleriniz için uygun testler ekleyin
5. Kodunuzun mevcut testleri geçtiğinden emin olun
6. Lütfen PR açıklamasında sorunu bağlayın, `fixes #<sorun_numarası>`
7. Kodunuzu birleştirin!
### Projeyi Kurma
#### Frontend
Frontend hizmetini kurmak için, lütfen `web/README.md` dosyasındaki kapsamlı [rehberimize](https://github.com/langgenius/dify/blob/main/web/README.md) bakın. Bu belge, frontend ortamını düzgün bir şekilde kurmanıza yardımcı olacak ayrıntılı talimatlar sağlar.
#### Backend
Backend hizmetini kurmak için, lütfen `api/README.md` dosyasındaki detaylı [talimatlarımıza](https://github.com/langgenius/dify/blob/main/api/README.md) bakın. Bu belge, backend'i sorunsuz bir şekilde çalıştırmanıza yardımcı olacak adım adım bir kılavuz içerir.
#### Dikkat Edilecek Diğer Şeyler
Kuruluma geçmeden önce bu belgeyi dikkatlice incelemenizi öneririz, çünkü şunlar hakkında temel bilgiler içerir:
- Ön koşullar ve bağımlılıklar
- Kurulum adımları
- Yapılandırma detayları
- Yaygın sorun giderme ipuçları
Kurulum süreci sırasında herhangi bir sorunla karşılaşırsanız bizimle iletişime geçmekten çekinmeyin.
## Yardım Almak
Katkıda bulunurken takılırsanız veya yanıcı bir sorunuz olursa, sorularınızı ilgili GitHub sorunu aracılığıyla bize gönderin veya hızlı bir sohbet için [Discord'umuza](https://discord.gg/8Tpq4AcN9c) katılın.
-2
View File
@@ -10,8 +10,6 @@ a. Multi-tenant service: Unless explicitly authorized by Dify in writing, you ma
b. LOGO and copyright information: In the process of using Dify's frontend, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend.
- Frontend Definition: For the purposes of this license, the "frontend" of Dify includes all components located in the `web/` directory when running Dify from the raw source code, or the "web" image when running Dify with Docker.
Please contact business@dify.ai by email to inquire about licensing matters.
2. As a contributor, you should agree that:
a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
+13 -3
View File
@@ -128,17 +128,27 @@ Dify-Plus,该名字不是说比 Dify 项目牛的意思,意思是想说比 D
- toxingwang@gmail.com
- 906631095@qq.com
### 微信交流
1. 防止广告进群,添加微信,输入以下代码执行结果
### 微信交流
1. 微信交流群
- Dify-plus官方交流群1(已满)
- Dify-plus官方交流群2(已满)
- Dify-plus官方交流群3
<img width="200" alt="image" src="https://github.com/user-attachments/assets/228f948c-ed4b-40dd-9ca9-0997a97b98e6" />
2. 防止广告进群,添加微信,输入以下代码执行结果
```python
encoded_str = "5Yqg5YWlZGlmeS1wbHVz5Lqk5rWB576kMgo="
decoded_bytes = base64.b64decode(encoded_str)
decoded_str = decoded_bytes.decode('utf-8')
print(decoded_str)
```
2. 微信二维码:
3. 微信二维码:
<img width="200" alt="image" src="https://github.com/user-attachments/assets/b5aa106e-4bd9-40d7-925f-05c3f5265ef6" />
备注:微信交流群若加不进去,可添加我们微信,我们拉你进群
### 请作者喝咖啡~
<img width="200" alt="image" src="https://github.com/user-attachments/assets/9a1ce3d4-3101-46eb-8a72-0a39db5b836b" />
-2
View File
@@ -254,8 +254,6 @@ docker compose up -d
- [Discord](https://discord.gg/FngNHpbcY7)。👉:分享您的应用程序并与社区交流。
- [X(Twitter)](https://twitter.com/dify_ai)。👉:分享您的应用程序并与社区交流。
- [商业许可](mailto:business@dify.ai?subject=[GitHub]Business%20License%20Inquiry)。👉:有关商业用途许可 Dify.AI 的商业咨询。
- [微信]() 👉:扫描下方二维码,添加微信好友,备注 Dify,我们将邀请您加入 Dify 社区。
<img src="./images/wechat.png" alt="wechat" width="100"/>
## 安全问题
+1
View File
@@ -8,6 +8,7 @@ type ApiGroup struct {
TenantsApi
SystemApi
TestApi
SystemOAuth2Api
}
var (
+8 -2
View File
@@ -1,6 +1,7 @@
package gaia
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
@@ -19,8 +20,12 @@ type SystemApi struct{}
// @Success 200 {object} response.Response{data=gaia.Tenants,msg=string} "查询成功"
// @Router /gaia/system/dingtalk [get]
func (systemApi *SystemApi) GetDingTalk(c *gin.Context) {
var host string
var config = make(map[string]interface{})
config["host"] = global.GVA_CONFIG.Gaia.Url
if host, _ = global.GVA_Dify_REDIS.Get(context.Background(), "api_host").Result(); len(host) == 0 {
host = global.GVA_CONFIG.Gaia.Url
}
config["host"] = host
config["config"] = systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk)
response.OkWithData(config, c)
}
@@ -43,7 +48,8 @@ func (systemApi *SystemApi) SetDingTalk(c *gin.Context) {
}
// update
req.Classify = gaia.SystemIntegrationDingTalk
if err = systemIntegratedService.SetIntegratedConfig(req, req.Test); err != nil {
if err = systemIntegratedService.SetIntegratedConfig(
req, "", req.Test); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
+91
View File
@@ -0,0 +1,91 @@
package gaia
import (
"context"
"encoding/json"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type SystemOAuth2Api struct{}
// GetOAuth2Config
// @Tags System
// @Summary 获取OAuth2集成配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
// @Router /gaia/system/oauth2 [get]
func (s *SystemOAuth2Api) GetOAuth2Config(c *gin.Context) {
var configMap request.SystemOAuth2Request
var config = make(map[string]interface{})
// 直接使用service层获取request.SystemOAuth2Request结构
integrated := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationOAuth2)
_ = json.Unmarshal([]byte(integrated.Config), &configMap)
// setting
configMap.AppID = integrated.AppID
configMap.Status = integrated.Status
configMap.Classify = integrated.Classify
configMap.AppSecret = integrated.AppSecret
var host string
if host, _ = global.GVA_Dify_REDIS.Get(context.Background(), "api_host").Result(); len(host) == 0 {
host = global.GVA_CONFIG.Gaia.Url
}
config["host"] = host
config["config"] = configMap
response.OkWithData(config, c)
}
// SetOAuth2Config
// @Tags System
// @Summary 修改OAuth2集成配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.SystemOAuth2Request true "修改数据"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
// @Router /gaia/system/oauth2 [post]
func (s *SystemOAuth2Api) SetOAuth2Config(c *gin.Context) {
var req request.SystemOAuth2Request
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 序列化为JSON
configBytes, err := json.Marshal(&map[string]string{
"server_url": req.ServerURL,
"authorize_url": req.AuthorizeURL,
"token_url": req.TokenURL,
"userinfo_url": req.UserinfoURL,
"logout_url": req.LogoutURL,
"user_name_field": req.UserNameField,
"user_email_field": req.UserEmailField,
"user_id_field": req.UserIDField,
})
if err != nil {
global.GVA_LOG.Error("序列化OAuth2配置失败!", zap.Error(err))
response.FailWithMessage("配置序列化失败", c)
return
}
// 更新配置
if err = systemIntegratedService.SetIntegratedConfig(gaia.SystemIntegration{
Classify: gaia.SystemIntegrationOAuth2,
Config: string(configBytes),
AppSecret: req.AppSecret,
Status: req.Status,
AppID: req.AppID,
}, req.Code, req.Test); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData("设置成功", c)
}
@@ -4,6 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
@@ -156,3 +158,29 @@ func (b *BaseApi) OaLogin(c *gin.Context) {
return
}
// Extend Start: oAuth2 callback verification
// OAuth2Callback
// @Tags Base
// @Summary oAuth2回调校验
// @Produce application/json
// @Param code query string true "授权码"
// @Success 200 {string} string "返回HTML内容,包含授权码"
// @Router /base/auth2/callback [get]
func (b *BaseApi) OAuth2Callback(c *gin.Context) {
// 获取授权码
code := c.Request.URL.Query().Get("code")
if code == "" {
global.GVA_LOG.Error("OAuth2回调未获取到授权码")
c.String(http.StatusBadRequest, "授权码不能为空")
return
}
// 返回HTML内容,通过BroadcastChannel将授权码传递给前端
htmlContent := fmt.Sprintf(`<html><body><script>const channel = new BroadcastChannel('oAuth2');channel.postMessage({code: '%s', timestamp: Date.now() });channel.close();window.close();</script></body></html>`, code)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, htmlContent)
}
// Extend Stop: oAuth2 callback verification
+2 -1
View File
@@ -1,11 +1,12 @@
package initialize
import (
"os"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/example"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"os"
"go.uber.org/zap"
"gorm.io/gorm"
+25
View File
@@ -0,0 +1,25 @@
package request
// SystemOAuth2Error OAuth2 错误返回
type SystemOAuth2Error struct {
Code int `json:"code" gorm:"comment:分类"` // 错误代码
Info string `json:"info" gorm:"comment:错误详情"` // 错误详情
}
// SystemOAuth2Request OAuth2 集成配置
type SystemOAuth2Request struct {
Classify uint `json:"classify" gorm:"comment:分类"` // 分类
Status bool `json:"status" gorm:"comment:状态"` // 状态
ServerURL string `json:"server_url" gorm:"comment:服务器地址"` // OAuth2 服务器地址
AuthorizeURL string `json:"authorize_url" gorm:"comment:申请认证的URL"` // 申请认证的URL
TokenURL string `json:"token_url" gorm:"comment:获取Token的URL"` // 获取Token的URL
UserinfoURL string `json:"userinfo_url" gorm:"comment:获取用户信息URL"` // 获取用户信息的URL
LogoutURL string `json:"logout_url" gorm:"comment:退出登录回调URL"` // 退出登录回调URL
AppID string `json:"app_id" gorm:"comment:Client ID"` // Client ID
AppSecret string `json:"app_secret" gorm:"comment:Client Secret"` // Client Secret
UserNameField string `json:"user_name_field" gorm:"comment:用户名字段"` // 用户名字段
UserEmailField string `json:"user_email_field" gorm:"comment:邮箱字段"` // 邮箱字段
UserIDField string `json:"user_id_field" gorm:"comment:用户唯一标识字段"` // 用户唯一标识字段
Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"` // 是否测试链接联通性
Code string `json:"code" gorm:"default:0;comment:code代码"` // code代码
}
@@ -3,6 +3,7 @@ package gaia
const SystemIntegrationDingTalk = uint(1) // 钉钉集成
const SystemIntegrationWeiXin = uint(2) // 微信集成
const SystemIntegrationFeiShu = uint(3) // 飞书集成
const SystemIntegrationOAuth2 = uint(4) // OAuth2集成
// SystemIntegration 系统集成表
type SystemIntegration struct {
@@ -15,6 +16,7 @@ type SystemIntegration struct {
AppKey string `json:"app_key" gorm:"default:;comment:加密key"`
AppSecret string `json:"app_secret" gorm:"default:;comment:加密密钥"`
Test bool `json:"test" gorm:"default:0;comment:是否测试链接联通性"`
Config string `json:"config" gorm:"type:text;default:;comment:其他配置"`
}
// TableName system_integration_extend表 SystemIntegration自定义表名 system_integration_extend
@@ -45,15 +45,15 @@ type SetUserAuthorities struct {
}
type ChangeUserInfo struct {
ID uint `gorm:"primarykey"` // 主键ID
NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称
Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号
AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID
Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像
SideMode string `json:"sideMode" gorm:"comment:用户侧边主题"` // 用户侧边主题
Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户
GlobalCode bool `json:"global_code" gorm:"comment:全局代码执行权限"` //全局代码执行权限 Extend global code
ID uint `gorm:"primarykey"` // 主键ID
NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称
Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号
AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID
Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/1576554439myAvatar.png;comment:用户头像"` // 用户头像
SideMode string `json:"sideMode" gorm:"comment:用户侧边主题"` // 用户侧边主题
Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户
GlobalCode bool `json:"global_code" gorm:"comment:全局代码执行权限"` //全局代码执行权限 Extend global code
Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"`
}
+3 -2
View File
@@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
"github.com/gofrs/uuid/v5"
"time"
)
type Login interface {
@@ -29,7 +30,7 @@ type SysUser struct {
Username string `json:"userName" gorm:"index;comment:用户登录名"` // 用户登录名
Password string `json:"-" gorm:"comment:用户登录密码"` // 用户登录密码
NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像
HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/1576554439myAvatar.png;comment:用户头像"` // 用户头像
AuthorityId uint `json:"authorityId" gorm:"default:888;comment:用户角色ID"` // 用户角色ID
Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"` // 用户角色
Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"` // 多用户角色
+1
View File
@@ -14,6 +14,7 @@ var (
dashboardApi = api.ApiGroupApp.GaiaApiGroup.DashboardApi
tenantsApi = api.ApiGroupApp.GaiaApiGroup.TenantsApi
)
var systemOAuth2Api = api.ApiGroupApp.GaiaApiGroup.SystemOAuth2Api
var systemApi = api.ApiGroupApp.GaiaApiGroup.SystemApi
var quotaApi = api.ApiGroupApp.GaiaApiGroup.QuotaApi
var testApi = api.ApiGroupApp.GaiaApiGroup.TestApi
+7 -5
View File
@@ -6,11 +6,13 @@ import (
type SystemRouter struct{}
// InitSystemRouter 初始化 Dify 系统 关联系统表 路由信息
func (d *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) {
dashboardRouterWithoutRecord := Router.Group("gaia/system")
// InitSystemRouter 初始化系统路由
func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) {
systemRouter := Router.Group("gaia/system")
{
dashboardRouterWithoutRecord.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置
dashboardRouterWithoutRecord.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置
systemRouter.GET("dingtalk", systemApi.GetDingTalk) // 获取钉钉系统配置
systemRouter.POST("dingtalk", systemApi.SetDingTalk) // 设置钉钉系统配置
systemRouter.GET("oauth2", systemOAuth2Api.GetOAuth2Config) // 获取OAuth2配置
systemRouter.POST("oauth2", systemOAuth2Api.SetOAuth2Config) // 设置OAuth2配置
}
}
+2 -1
View File
@@ -11,7 +11,8 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
{
baseRouter.POST("login", baseApi.Login)
baseRouter.POST("captcha", baseApi.Captcha)
baseRouter.POST("oaLogin", baseApi.OaLogin) // 新增OA登录
baseRouter.POST("oaLogin", baseApi.OaLogin) // 新增OA登录
baseRouter.GET("auth2/callback", baseApi.OAuth2Callback) // 新增oAuth2回调校验
}
return baseRouter
}
+7 -8
View File
@@ -6,15 +6,16 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"go.uber.org/zap"
"io"
"net/http"
"regexp"
"time"
)
// IsUserPasswordValid
@@ -170,16 +171,15 @@ func RegisterUser(u system.SysUser, token string) (err error) {
return err
}
global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1))
_ = res.Body.Close()
if err = json.Unmarshal(bodyByte, &bodyMap); err != nil {
global.GVA_LOG.Debug("json unmarshal error:", zap.Any("error", err.Error()))
return err
}
global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1))
// result
if result, ok := bodyMap["result"]; !ok && result != "success" {
return errors.New("failed to create user")
return errors.New(fmt.Sprintf("failed to create user: %s", bodyMap["error"]))
}
// 修改密码
var account gaia.Account
@@ -187,7 +187,6 @@ func RegisterUser(u system.SysUser, token string) (err error) {
return err
}
global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1))
// 修改密码
global.GVA_DB.Model(&account).Updates(&map[string]interface{}{
"password": passwordHashed,
+113 -13
View File
@@ -1,17 +1,22 @@
package gaia
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"fmt"
"github.com/faabiosr/cachego/file"
"github.com/fastwego/dingding"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia"
"github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/google/uuid"
"go.uber.org/zap"
"io"
"net/http"
"net/url"
"os"
"strings"
)
type SystemIntegratedService struct{}
@@ -48,7 +53,10 @@ func (e *SystemIntegratedService) GetIntegratedConfig(classID uint) (integrate g
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemIntegration, test bool) (err error) {
// @param: integrate gaia.SystemIntegration, code string, test bool
// @return: err error
func (e *SystemIntegratedService) SetIntegratedConfig(
integrate gaia.SystemIntegration, code string, test bool) (err error) {
// classID是否在
var log gaia.SystemIntegration
if err = global.GVA_DB.Where("classify = ?", integrate.Classify).First(&log).Error; err != nil {
@@ -74,7 +82,6 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg
}
}
// CorpID
var ding *dingding.Client
if utils.AddAsteriskToString(log.CorpID) != integrate.CorpID {
log.CorpID = integrate.CorpID
}
@@ -82,12 +89,9 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg
log.AppID = integrate.AppID
// 关闭不需要请求
if integrate.Status || test {
if ding, err = e.DingTalkConfigAvailable(integrate); err != nil {
return errors.New("钉钉链接失败" + err.Error())
}
// token
if _, err = ding.AccessTokenManager.GetAccessToken(); err != nil {
return errors.New("钉钉token获取失败:" + err.Error())
// 测试连接
if err = e.TestConnection(integrate, code); err != nil {
return errors.New("连接失败:" + err.Error())
}
}
// Test completed
@@ -95,7 +99,9 @@ func (e *SystemIntegratedService) SetIntegratedConfig(integrate gaia.SystemInteg
return err
}
// save
if err = global.GVA_DB.Model(&gaia.SystemIntegration{}).Where("id=?", log.Id).Updates(&map[string]interface{}{
if err = global.GVA_DB.Model(&gaia.SystemIntegration{}).Where(
"id=?", log.Id).Updates(&map[string]interface{}{
"config": integrate.Config,
"status": integrate.Status,
"agent_id": integrate.AgentID,
"app_key": integrate.AppKey,
@@ -134,3 +140,97 @@ func (e *SystemIntegratedService) DingTalkConfigAvailable(req gaia.SystemIntegra
},
}), err
}
// TestConnection 测试连接
// @Tags System Integrated
// @Summary 测试系统集成连接
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @param: integrate gaia.SystemIntegration, code string
// @return: error
func (e *SystemIntegratedService) TestConnection(integrate gaia.SystemIntegration, code string) error {
switch integrate.Classify {
case gaia.SystemIntegrationDingTalk:
// 测试钉钉连接
if _, err := e.DingTalkConfigAvailable(integrate); err != nil {
return errors.New("钉钉链接失败: " + err.Error())
}
return nil
case gaia.SystemIntegrationOAuth2:
// 测试OAuth2连接
return e.TestOAuth2Connection(integrate, code)
default:
return errors.New("不支持的集成类型")
}
}
// TestOAuth2Connection 测试OAuth2连接
// @Tags System Integrated
// @Summary 测试OAuth2连接
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @param: integrate gaia.SystemIntegration, code string
// @return: error
func (e *SystemIntegratedService) TestOAuth2Connection(integrate gaia.SystemIntegration, code string) (err error) {
// 解析Config字段
var configMap request.SystemOAuth2Request
if err = json.Unmarshal([]byte(integrate.Config), &configMap); err != nil {
global.GVA_LOG.Error("解析OAuth2配置失败!", zap.Error(err))
return err
}
// 没有code的(保存操作)
if len(code) == 0 {
return nil
}
// 检查必要字段
if configMap.ServerURL == "" || configMap.TokenURL == "" || integrate.AppID == "" || integrate.AppSecret == "" {
return errors.New("请填写完整的 OAuth2 配置信息")
}
// 合成请求byte
formData := url.Values{}
formData.Set("grant_type", "authorization_code")
formData.Set("client_secret", integrate.AppSecret)
formData.Set("client_id", integrate.AppID)
formData.Set("redirect_uri", "")
formData.Set("code", code)
// 发送请求
var req *http.Request
client := &http.Client{}
req, err = http.NewRequest("POST", fmt.Sprintf(
"%s%s", configMap.ServerURL, configMap.TokenURL), strings.NewReader(formData.Encode()))
if err != nil {
global.GVA_LOG.Error("创建测试请求失败", zap.Error(err))
return errors.New(fmt.Sprintf("创建测试请求失败: %s", err.Error()))
}
// 设置Content-Type
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// 发送请求
resp, err := client.Do(req)
if err != nil {
global.GVA_LOG.Error("测试 OAuth2 连接失败", zap.Error(err))
return errors.New(fmt.Sprintf("连接 OAuth2 服务器失败: %s", err.Error()))
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
global.GVA_LOG.Error("测试 OAuth2 连接失败", zap.Int("status", resp.StatusCode))
return errors.New(fmt.Sprintf("OAuth2 服务器返回错误状态码: %d", resp.StatusCode))
}
var bodyByte []byte
if bodyByte, err = io.ReadAll(resp.Body); err != nil {
return fmt.Errorf("OAuth2 request io.ReadAll: %s", resp.Status)
}
var tokenMap request.SystemOAuth2Error
if err = json.Unmarshal(bodyByte, &tokenMap); err == nil && tokenMap.Code != 0 {
return fmt.Errorf("OAuth2 Eroor: %s", tokenMap.Info)
}
return nil
}
+5
View File
@@ -202,6 +202,11 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
{ApiGroup: "应用集成配置", Method: "GET", Path: "/gaia/system/dingtalk", Description: "获取钉钉系统配置"},
{ApiGroup: "应用集成配置", Method: "POST", Path: "/gaia/system/dingtalk", Description: "设置钉钉系统配置"},
// Extend Stop: system integration
// Extend Start: oauth2
{ApiGroup: "应用集成配置", Method: "GET", Path: "/gaia/system/oauth2", Description: "设置OAuth2配置"},
{ApiGroup: "应用集成配置", Method: "POST", Path: "/gaia/system/oauth2", Description: "获取OAuth2集成配置"},
// Extend Stop: oauth2
}
if err := db.Create(&entities).Error; err != nil {
return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!")
@@ -45,7 +45,10 @@ func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Co
}
next = ctx
// 888
if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus[31:39]); err != nil {
if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus[31:34]); err != nil {
return next, err
}
if err = db.Model(&authorities[0]).Association("SysBaseMenus").Append(menus[37:40]); err != nil {
return next, err
}
if err = db.Model(&authorities[0]).Association("SysBaseMenus").Append(menus[2:5]); err != nil {
+5
View File
@@ -288,6 +288,11 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
{Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/gaia/system/dingtalk", V2: "POST"},
// Extend Stop: system integration
// Extend Start: oauth2
{Ptype: "p", V0: "888", V1: "/gaia/system/oauth2", V2: "GET"},
{Ptype: "p", V0: "888", V1: "/gaia/system/oauth2", V2: "POST"},
// Extend Stop: oauth2
}
if err := db.Create(&entities).Error; err != nil {
return ctx, errors.Wrap(err, "Casbin 表 ("+i.InitializerName()+") 数据初始化失败!")
+41 -42
View File
@@ -3,6 +3,7 @@ package system
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
. "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
"github.com/pkg/errors"
@@ -51,49 +52,47 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
return ctx, system.ErrMissingDBContext
}
entities := []SysBaseMenu{
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}},
{MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}},
{MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}},
{MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}},
{MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}},
{MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}},
{MenuLevel: 0, Hidden: false, ParentId: 24, Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}},
{MenuLevel: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
{MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}},
{MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}},
{MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}},
{MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}},
{MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}},
{GVA_MODEL: global.GVA_MODEL{ID: 1}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}},
{GVA_MODEL: global.GVA_MODEL{ID: 2}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}},
{GVA_MODEL: global.GVA_MODEL{ID: 3}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}},
{GVA_MODEL: global.GVA_MODEL{ID: 4}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}},
{GVA_MODEL: global.GVA_MODEL{ID: 5}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}},
{GVA_MODEL: global.GVA_MODEL{ID: 6}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}},
{GVA_MODEL: global.GVA_MODEL{ID: 7}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}},
{GVA_MODEL: global.GVA_MODEL{ID: 8}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}},
{GVA_MODEL: global.GVA_MODEL{ID: 9}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}},
{GVA_MODEL: global.GVA_MODEL{ID: 10}, MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}},
{GVA_MODEL: global.GVA_MODEL{ID: 11}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}},
{GVA_MODEL: global.GVA_MODEL{ID: 12}, MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}},
{GVA_MODEL: global.GVA_MODEL{ID: 13}, MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}},
{GVA_MODEL: global.GVA_MODEL{ID: 14}, MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}},
{GVA_MODEL: global.GVA_MODEL{ID: 15}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}},
{GVA_MODEL: global.GVA_MODEL{ID: 16}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}},
{GVA_MODEL: global.GVA_MODEL{ID: 17}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}},
{GVA_MODEL: global.GVA_MODEL{ID: 18}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}},
{GVA_MODEL: global.GVA_MODEL{ID: 19}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}},
{GVA_MODEL: global.GVA_MODEL{ID: 20}, MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}},
{GVA_MODEL: global.GVA_MODEL{ID: 21}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}},
{GVA_MODEL: global.GVA_MODEL{ID: 22}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}},
{GVA_MODEL: global.GVA_MODEL{ID: 23}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}},
{GVA_MODEL: global.GVA_MODEL{ID: 24}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}},
{GVA_MODEL: global.GVA_MODEL{ID: 25}, MenuLevel: 0, Hidden: false, ParentId: 24, Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}},
{GVA_MODEL: global.GVA_MODEL{ID: 26}, MenuLevel: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
{GVA_MODEL: global.GVA_MODEL{ID: 27}, MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}},
{GVA_MODEL: global.GVA_MODEL{ID: 28}, MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}},
{GVA_MODEL: global.GVA_MODEL{ID: 29}, MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}},
{GVA_MODEL: global.GVA_MODEL{ID: 30}, MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}},
{GVA_MODEL: global.GVA_MODEL{ID: 31}, MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}},
// 二开部分
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "gaiaDashboard", Name: "gaiaDashboard", Component: "view/gaia/dashboard/index.vue", Sort: 0, Meta: Meta{Title: "费用报表", Icon: "odometer"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "UserList", Name: "UserList", Component: "view/user/index.vue", Sort: 1, Meta: Meta{Title: "用户列表", Icon: "user"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "QuotaList", Name: "QuotaList", Component: "view/quota/index.vue", Sort: 2, Meta: Meta{Title: "额度管理", Icon: "wallet"}},
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SuperTest", Name: "SuperTest", Component: "view/test/index.vue", Sort: 2, Meta: Meta{Title: "测试管理", Icon: "management"}},
{MenuLevel: 0, Hidden: false, ParentId: 35, Path: "AppRequestTestBatch", Name: "AppRequestTestBatch", Component: "view/test/appRequest/index.vue", Sort: 1, Meta: Meta{Title: "测试批次", Icon: "list"}},
{MenuLevel: 0, Hidden: true, ParentId: 35, Path: "AppRequestTestList", Name: "AppRequestTestList", Component: "view/test/appRequest/list.vue", Sort: 1, Meta: Meta{Title: "测试列表", Icon: "list"}},
// Extend Start: system integration
{MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SystemIntegrated", Name: "SystemIntegrated", Component: "view/systemIntegrated/index.vue", Sort: 1, Meta: Meta{Title: "系统集成", Icon: "box"}},
{MenuLevel: 0, Hidden: false, ParentId: 38, Path: "IntegratedDingTalk", Name: "IntegratedDingTalk", Component: "view/systemIntegrated/dingTalk/index.vue", Sort: 1, Meta: Meta{Title: "钉钉", Icon: "turn-off"}},
// Extend Stop: system integration
{GVA_MODEL: global.GVA_MODEL{ID: 32}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "gaiaDashboard", Name: "gaiaDashboard", Component: "view/gaia/dashboard/index.vue", Sort: 0, Meta: Meta{Title: "费用报表", Icon: "odometer"}},
{GVA_MODEL: global.GVA_MODEL{ID: 33}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "UserList", Name: "UserList", Component: "view/user/index.vue", Sort: 1, Meta: Meta{Title: "用户列表", Icon: "user"}},
{GVA_MODEL: global.GVA_MODEL{ID: 34}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "QuotaList", Name: "QuotaList", Component: "view/quota/index.vue", Sort: 2, Meta: Meta{Title: "额度管理", Icon: "wallet"}},
{GVA_MODEL: global.GVA_MODEL{ID: 35}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SuperTest", Name: "SuperTest", Component: "view/test/index.vue", Sort: 2, Meta: Meta{Title: "测试管理", Icon: "management"}},
{GVA_MODEL: global.GVA_MODEL{ID: 36}, MenuLevel: 0, Hidden: false, ParentId: 35, Path: "AppRequestTestBatch", Name: "AppRequestTestBatch", Component: "view/test/appRequest/index.vue", Sort: 1, Meta: Meta{Title: "测试批次", Icon: "list"}},
{GVA_MODEL: global.GVA_MODEL{ID: 37}, MenuLevel: 0, Hidden: true, ParentId: 35, Path: "AppRequestTestList", Name: "AppRequestTestList", Component: "view/test/appRequest/list.vue", Sort: 1, Meta: Meta{Title: "测试列表", Icon: "list"}},
{GVA_MODEL: global.GVA_MODEL{ID: 38}, MenuLevel: 0, Hidden: false, ParentId: 0, Path: "SystemIntegrated", Name: "SystemIntegrated", Component: "view/systemIntegrated/index.vue", Sort: 1, Meta: Meta{Title: "系统集成", Icon: "box"}},
{GVA_MODEL: global.GVA_MODEL{ID: 39}, MenuLevel: 0, Hidden: false, ParentId: 38, Path: "IntegratedDingTalk", Name: "IntegratedDingTalk", Component: "view/systemIntegrated/dingTalk/index.vue", Sort: 1, Meta: Meta{Title: "钉钉", Icon: "turn-off"}},
{GVA_MODEL: global.GVA_MODEL{ID: 40}, MenuLevel: 0, Hidden: false, ParentId: 38, Path: "IntegratedOAuth2", Name: "IntegratedOAuth2", Component: "view/systemIntegrated/oauth2/index.vue", Sort: 2, Meta: Meta{Title: "OAuth2", Icon: "share"}},
// 二开部分
}
if err = db.Create(&entities).Error; err != nil {
+2 -1
View File
@@ -2,6 +2,7 @@ package system
import (
"context"
sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
@@ -60,7 +61,7 @@ func (i *initUser) InitializeData(ctx context.Context) (next context.Context, er
Username: "admin",
Password: adminPassword,
NickName: "Mr.奇淼",
HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg",
HeaderImg: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png",
AuthorityId: 888,
Phone: "17611111111",
Email: "333333333@qq.com",
+27
View File
@@ -26,3 +26,30 @@ export const setSystemDingTalk = (data) => {
data,
})
}
// @Tags systrm
// @Summary 获取OAuth2集成配置
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
// @Router /gaia/system/oauth2 [get]
export const getSystemOAuth2 = () => {
return service({
url: '/gaia/system/oauth2',
method: 'get'
})
}
// @Tags systrm
// @Summary 修改OAuth2集成配置
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
// @Router /gaia/system/oauth2 [post]
export const setSystemOAuth2 = (data) => {
return service({
url: '/gaia/system/oauth2',
method: 'post',
data,
})
}
+1 -1
View File
@@ -25,7 +25,6 @@
"/src/view/gaia/dashboard/components/charts-people-numbers.vue": "ChartsPeopleNumbers",
"/src/view/gaia/dashboard/components/charts.vue": "Charts",
"/src/view/gaia/dashboard/index.vue": "GaiaDashboard",
"/src/view/gaia/providers/providers.vue": "ProviderManage",
"/src/view/gaia/tenants/tenants.vue": "TenantList",
"/src/view/init/index.vue": "Init",
"/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",
@@ -64,6 +63,7 @@
"/src/view/system/state.vue": "State",
"/src/view/systemIntegrated/dingTalk/index.vue": "IntegratedDingTalk",
"/src/view/systemIntegrated/index.vue": "SystemIntegrated",
"/src/view/systemIntegrated/oauth2/index.vue": "IntegratedOAuth2",
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
"/src/view/systemTools/autoCode/index.vue": "AutoCode",
@@ -57,7 +57,7 @@
配置链接应用信息
</el-button>
</div>
<div class="bg-gray-50 p-5 border rounded-lg">
<div class="bg-gray-50 dark:bg-slate-800 p-5 border dark:border-slate-700 rounded-lg">
<div class="flex items-center mb-4">
<span class="info-label">CorpID:</span>
<el-input v-if="openEdit" v-model="config.corp_id" class="info-value flex-1" />
@@ -251,7 +251,7 @@ const goToSecuritySettings = () => {
}
.card {
@apply bg-white rounded-lg shadow-sm border border-gray-100 p-5;
@apply bg-white dark:bg-slate-900 rounded-lg shadow-sm border border-gray-100 dark:border-slate-700 p-5;
}
.card-header {
@@ -267,19 +267,19 @@ const goToSecuritySettings = () => {
}
.info-label {
@apply text-gray-600 w-28;
@apply text-gray-600 dark:text-gray-400 w-28;
}
.info-value {
@apply font-mono text-gray-800;
@apply font-mono text-gray-800 dark:text-gray-200;
}
.tips-content {
@apply bg-amber-50 p-4 rounded-lg;
@apply bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg;
}
.tip-item {
@apply mb-2 text-gray-700;
@apply mb-2 text-gray-700 dark:text-gray-300;
}
.config-btn {
@@ -0,0 +1,349 @@
<template>
<div id="oauth2" class="system">
<el-form
ref="form"
:model="config"
label-width="240px"
>
<div class="page-header mb-6">
<h2 class="text-xl font-bold">
OAuth2 应用集成配置
</h2>
<p class="text-gray-500 mt-2">
配置 OAuth2 单点登录相关参数
</p>
</div>
<el-tabs class="oauth2-tabs">
<div class="card">
<div class="card-header flex items-center justify-between">
<span class="text-lg font-medium">启用状态</span>
<div class="flex items-center">
<el-switch
v-model="config.status"
active-text="已启用"
:disabled="!isConfigValid"
@change="handleStatusChange"
/>
</div>
</div>
<el-divider />
<div class="card-section">
<div class="section-title">
OAuth2 回调域名配置
</div>
<div class="text-gray-600 mb-3">
<p>回调域名此信息将在创建 OAuth2 授权应用时使用</p>
</div>
<div class="flex items-center">
<el-input v-model="host" disabled readonly class="flex-1" />
<el-button type="primary" class="ml-2" icon="copy-document" @click="copyHost">
复制
</el-button>
</div>
</div>
<el-divider />
<div class="card-section">
<div class="section-title">
应用信息配置
</div>
<div class="mb-4">
<el-button v-if="!openEdit" type="primary" class="config-btn w-full" icon="setting" @click="openConfig">
配置链接应用信息
</el-button>
</div>
<div class="bg-gray-50 dark:bg-slate-800 p-5 border dark:border-slate-700 rounded-lg">
<div class="flex items-center mb-4">
<span class="info-label">OAuth2 服务器地址:</span>
<el-input v-if="openEdit" v-model="config.server_url" class="info-value flex-1" placeholder="例如: https://oauth2.example.com" />
<span v-else class="info-value">{{ config.server_url || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">授权页面地址:</span>
<el-input v-if="openEdit" v-model="config.authorize_url" class="info-value flex-1" placeholder="例如: /oauth/authorize" />
<span v-else class="info-value">{{ config.authorize_url }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">获取 Token URL:</span>
<el-input v-if="openEdit" v-model="config.token_url" class="info-value flex-1" placeholder="例如: /oauth2/token" />
<span v-else class="info-value">{{ config.token_url || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">获取用户信息 URL:</span>
<el-input v-if="openEdit" v-model="config.userinfo_url" class="info-value flex-1" placeholder="例如: /oauth2/userinfo" />
<span v-else class="info-value">{{ config.userinfo_url || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">退出登录回调 URL:</span>
<el-input v-if="openEdit" v-model="config.logout_url" class="info-value flex-1" placeholder="例如: /oauth2/logout" />
<span v-else class="info-value">{{ config.logout_url || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">Client ID:</span>
<el-input v-if="openEdit" v-model="config.app_id" class="info-value flex-1" />
<span v-else class="info-value">{{ config.app_id || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">Client Secret:</span>
<el-input v-if="openEdit" v-model="config.app_secret" class="info-value flex-1" type="text" />
<span v-else class="info-value">{{ config.app_secret ? config.app_secret : '未配置' }}</span>
</div>
<el-divider />
<div class="section-title mb-4">
用户信息映射配置
</div>
<div class="flex items-center mb-4">
<span class="info-label">用户名字段:</span>
<el-input v-if="openEdit" v-model="config.user_name_field" class="info-value flex-1" placeholder="例如: data.name" />
<span v-else class="info-value">{{ config.user_name_field || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">邮箱字段:</span>
<el-input v-if="openEdit" v-model="config.user_email_field" class="info-value flex-1" placeholder="例如: data.email" />
<span v-else class="info-value">{{ config.user_email_field || '未配置' }}</span>
</div>
<div class="flex items-center mb-4">
<span class="info-label">用户唯一标识字段:</span>
<el-input v-if="openEdit" v-model="config.user_id_field" class="info-value flex-1" placeholder="例如: data.sub 或 data.id" />
<span v-else class="info-value">{{ config.user_id_field || '未配置' }}</span>
</div>
<div class="float-right">
<el-button type="primary" plain icon="connection" @click="testConnection">
测试连接
</el-button>
<el-button v-if="openEdit" type="primary" icon="goods-filled" @click="update">
保存
</el-button>
</div>
<div class="clear-both" />
</div>
</div>
<el-divider />
<div class="card-section">
<div class="section-title text-amber-500">
<el-icon><warning-filled /></el-icon>
<span>温馨提示</span>
</div>
<div class="tips-content">
<p class="tip-item">
1. 请确保您的 OAuth2 服务器已正确配置并支持授权码模式
</p>
<p class="tip-item">
2. Client ID Client Secret 是应用在 OAuth2 服务器中的唯一标识
</p>
<p class="tip-item">
3. 用户信息映射字段应与 OAuth2 服务器返回的用户信息字段一致
</p>
<p class="tip-item">
4. 请确保在 OAuth2 服务器上正确配置回调域名否则会导致认证失败
</p>
</div>
</div>
</div>
</el-tabs>
</el-form>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { getSystemOAuth2, setSystemOAuth2 } from "@/api/gaia/system";
import { WarningFilled } from '@element-plus/icons-vue'
defineOptions({
name: 'IntegratedOAuth2',
})
const bc = ref()
const host = ref("")
const openEdit = ref(false)
const config = ref({
id: 0,
classify: 0,
status: false,
server_url: "",
token_url: "",
userinfo_url: "",
logout_url: "",
app_id: "",
app_app: "",
authorize_url: "",
app_secret: "",
user_name_field: "",
user_email_field: "",
user_id_field: "",
test: false,
})
// 验证配置是否有效
const isConfigValid = computed(() => {
return !!(
config.value.server_url &&
config.value.token_url &&
config.value.userinfo_url &&
config.value.app_id &&
config.value.app_secret &&
config.value.user_id_field
);
})
// 处理状态变更
const handleStatusChange = (val) => {
if (val && !isConfigValid.value) {
config.value.status = false;
ElMessage({
type: 'warning',
message: '请先填写应用信息配置'
});
return;
}
update();
}
// 打开编辑
const openConfig = () => {
openEdit.value = true
}
// 复制回调URL
const copyHost = () => {
navigator.clipboard.writeText(host.value);
ElMessage({
type: 'success',
message: '复制成功'
});
}
// 测试连接
const testConnection = async () => {
let host = config.value.server_url
let authorizeUrl = `${host}${config.value.authorize_url}`
let redirectUri = encodeURIComponent(`${location.protocol}//${location.host}/api/base/auth2/callback`)
window.open(`${authorizeUrl}?client_id=${config.value.app_id}&response_type=code&scope=all&redirect_uri=${redirectUri}`)
}
const initForm = async() => {
const res = await getSystemOAuth2()
if (res.code === 0) {
host.value = res.data.host
config.value = res.data.config
}
}
const update = async() => {
config.value.test = false;
if (config.value.status && !isConfigValid.value) {
config.value.status = false;
ElMessage({
type: 'warning',
message: '请先填写应用信息配置'
});
return;
}
const res = await setSystemOAuth2(config.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '设置成功',
})
await initForm()
openEdit.value = false
}
}
// start
onMounted(() => {
initForm()
bc.value = new BroadcastChannel('oAuth2');
bc.value.onmessage = async function (event) {
// 尝试激活标签
window.focus();
config.value.test = true;
config.value.code = event.data.code;
if (config.value.status && !isConfigValid.value) {
config.value.status = false;
ElMessage({
type: 'warning',
message: '请先填写应用信息配置'
});
return;
}
const res = await setSystemOAuth2(config.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '链接成功',
})
}
};
})
// clone
onBeforeUnmount(() => {
console.log('clone')
bc.value.close()
})
</script>
<style lang="scss" scoped>
.system {
@apply bg-white p-9 rounded dark:bg-slate-900;
}
.page-header {
margin-bottom: 20px;
}
.card {
@apply bg-white dark:bg-slate-900 rounded-lg overflow-hidden;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
}
.card-header {
padding: 16px 20px;
}
.card-section {
padding: 20px;
}
.section-title {
@apply text-lg font-medium mb-4 flex items-center;
.el-icon {
margin-right: 8px;
}
}
.info-label {
width: 180px;
@apply text-gray-600 dark:text-gray-400;
}
.info-value {
@apply font-medium dark:text-gray-200;
}
.tips-content {
@apply text-gray-600 dark:text-gray-400;
}
.tip-item {
margin-bottom: 8px;
line-height: 1.6;
}
.config-btn {
margin-bottom: 16px;
}
</style>
+9 -1
View File
@@ -137,7 +137,7 @@ WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
# Vector database configuration
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss, tablestore
VECTOR_STORE=weaviate
# Weaviate configuration
@@ -189,6 +189,7 @@ TENCENT_VECTOR_DB_USERNAME=dify
TENCENT_VECTOR_DB_DATABASE=dify
TENCENT_VECTOR_DB_SHARD=1
TENCENT_VECTOR_DB_REPLICAS=2
TENCENT_VECTOR_DB_ENABLE_HYBRID_SEARCH=false
# ElasticSearch configuration
ELASTICSEARCH_HOST=127.0.0.1
@@ -212,6 +213,12 @@ PGVECTOR_DATABASE=postgres
PGVECTOR_MIN_CONNECTION=1
PGVECTOR_MAX_CONNECTION=5
# TableStore Vector configuration
TABLESTORE_ENDPOINT=https://instance-name.cn-hangzhou.ots.aliyuncs.com
TABLESTORE_INSTANCE_NAME=instance-name
TABLESTORE_ACCESS_KEY_ID=xxx
TABLESTORE_ACCESS_KEY_SECRET=xxx
# Tidb Vector configuration
TIDB_VECTOR_HOST=xxx.eu-central-1.xxx.aws.tidbcloud.com
TIDB_VECTOR_PORT=4000
@@ -297,6 +304,7 @@ OCEANBASE_VECTOR_USER=root@test
OCEANBASE_VECTOR_PASSWORD=difyai123456
OCEANBASE_VECTOR_DATABASE=test
OCEANBASE_MEMORY_LIMIT=6G
OCEANBASE_ENABLE_HYBRID_SEARCH=false
# openGauss configuration
OPENGAUSS_HOST=127.0.0.1
+67
View File
@@ -276,6 +276,7 @@ def migrate_knowledge_vector_database():
VectorType.ORACLE,
VectorType.ELASTICSEARCH,
VectorType.OPENGAUSS,
VectorType.TABLESTORE,
}
lower_collection_vector_types = {
VectorType.ANALYTICDB,
@@ -813,3 +814,69 @@ def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[
ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids)
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green"))
@click.group("extend_db", help="管理二开扩展表的数据库迁移")
def extend_db():
"""管理二开扩展表的数据库迁移"""
pass
@extend_db.command("upgrade", help="将数据库升级到最新版本")
@click.option("--revision", default="head", help="目标版本,默认为最新版本(head)")
def extend_db_upgrade(revision):
"""将数据库升级到指定版本(默认为最新版本)"""
_run_alembic_command_extend("upgrade", revision)
@extend_db.command("downgrade", help="回滚数据库到指定版本")
@click.option("--revision", required=True, help="目标版本")
def extend_db_downgrade(revision):
"""回滚数据库到指定版本"""
_run_alembic_command_extend("downgrade", revision)
@extend_db.command("current", help="显示当前数据库版本")
def extend_db_current():
"""显示当前数据库版本"""
_run_alembic_command_extend("current")
@extend_db.command("history", help="显示迁移历史")
def extend_db_history():
"""显示迁移历史"""
_run_alembic_command_extend("history")
@extend_db.command("heads", help="显示最新的迁移版本")
def extend_db_heads():
"""显示最新的迁移版本"""
_run_alembic_command_extend("heads")
def _run_alembic_command_extend(command, *args):
"""运行 alembic 命令"""
import os
import sys
from flask import current_app
from alembic.config import Config
from alembic import command as alembic_command
# 获取 api 目录的绝对路径
api_dir = os.path.abspath(os.path.dirname(__file__))
migrations_extend_dir = os.path.join(api_dir, 'migrations_extend')
# 创建alembic配置
alembic_cfg = Config(os.path.join(migrations_extend_dir, 'alembic.ini'))
alembic_cfg.set_main_option('script_location', migrations_extend_dir)
# 获取相应的alembic命令函数
cmd_func = getattr(alembic_command, command)
# 在Flask应用上下文中执行alembic命令
with current_app.app_context():
# 执行命令
if args:
cmd_func(alembic_cfg, *args)
else:
cmd_func(alembic_cfg)
+5
View File
@@ -848,6 +848,11 @@ class AccountConfig(BaseSettings):
default=5,
)
EDUCATION_ENABLED: bool = Field(
description="whether to enable education identity",
default=False,
)
class FeatureConfig(
# place the configs in alphabet order
@@ -1,6 +1,6 @@
from typing import Optional
from pydantic import Field, NonNegativeInt, computed_field
from pydantic import Field, NonNegativeInt
from pydantic_settings import BaseSettings
+2
View File
@@ -33,6 +33,7 @@ from .vdb.pgvector_config import PGVectorConfig
from .vdb.pgvectors_config import PGVectoRSConfig
from .vdb.qdrant_config import QdrantConfig
from .vdb.relyt_config import RelytConfig
from .vdb.tablestore_config import TableStoreConfig
from .vdb.tencent_vector_config import TencentVectorDBConfig
from .vdb.tidb_on_qdrant_config import TidbOnQdrantConfig
from .vdb.tidb_vector_config import TiDBVectorConfig
@@ -283,5 +284,6 @@ class MiddlewareConfig(
OceanBaseVectorConfig,
BaiduVectorDBConfig,
OpenGaussConfig,
TableStoreConfig,
):
pass
@@ -33,3 +33,9 @@ class OceanBaseVectorConfig(BaseSettings):
description="Name of the OceanBase Vector database to connect to",
default=None,
)
OCEANBASE_ENABLE_HYBRID_SEARCH: bool = Field(
description="Enable hybrid search features (requires OceanBase >= 4.3.5.1). Set to false for compatibility "
"with older versions",
default=False,
)
@@ -0,0 +1,30 @@
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings
class TableStoreConfig(BaseSettings):
"""
Configuration settings for TableStore.
"""
TABLESTORE_ENDPOINT: Optional[str] = Field(
description="Endpoint address of the TableStore server (e.g. 'https://instance-name.cn-hangzhou.ots.aliyuncs.com')",
default=None,
)
TABLESTORE_INSTANCE_NAME: Optional[str] = Field(
description="Instance name to access TableStore server (eg. 'instance-name')",
default=None,
)
TABLESTORE_ACCESS_KEY_ID: Optional[str] = Field(
description="AccessKey id for the instance name",
default=None,
)
TABLESTORE_ACCESS_KEY_SECRET: Optional[str] = Field(
description="AccessKey secret for the instance name",
default=None,
)
@@ -48,3 +48,8 @@ class TencentVectorDBConfig(BaseSettings):
description="Name of the specific Tencent Vector Database to connect to",
default=None,
)
TENCENT_VECTOR_DB_ENABLE_HYBRID_SEARCH: bool = Field(
description="Enable hybrid search features",
default=False,
)
+1 -1
View File
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="1.1.3",
default="1.2.0",
)
COMMIT_SHA: str = Field(
@@ -1,5 +1,3 @@
from typing import Optional
from pydantic import Field
from .apollo import ApolloSettingsSourceInfo
@@ -8,6 +8,7 @@ from werkzeug.exceptions import Forbidden
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
setup_required,
)
from extensions.ext_database import db
@@ -23,6 +24,7 @@ class AppImportApi(Resource):
@login_required
@account_initialization_required
@marshal_with(app_import_fields)
@cloud_edition_billing_resource_check("apps")
def post(self):
# Check user role first
if not current_user.is_editor:
@@ -61,7 +61,7 @@ class PassportResourceExtend(Resource):
print("app_model", app_model, flush=True)
raise NotFound()
endUser_ta = EndUser.query.filter_by(id=user_id).first()
endUser_ta = db.session.query(EndUser).filter(EndUser.id == user_id).first()
if not endUser_ta:
end_user = EndUser(
id=user_id,
+46 -35
View File
@@ -99,53 +99,64 @@ class ForgotPasswordResetApi(Resource):
parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json")
args = parser.parse_args()
new_password = args["new_password"]
password_confirm = args["password_confirm"]
if str(new_password).strip() != str(password_confirm).strip():
# Validate passwords match
if args["new_password"] != args["password_confirm"]:
raise PasswordMismatchError()
token = args["token"]
reset_data = AccountService.get_reset_password_data(token)
if reset_data is None:
# Validate token and get reset data
reset_data = AccountService.get_reset_password_data(args["token"])
if not reset_data:
raise InvalidTokenError()
AccountService.revoke_reset_password_token(token)
# Revoke token to prevent reuse
AccountService.revoke_reset_password_token(args["token"])
# Generate secure salt and hash password
salt = secrets.token_bytes(16)
base64_salt = base64.b64encode(salt).decode()
password_hashed = hash_password(args["new_password"], salt)
password_hashed = hash_password(new_password, salt)
base64_password_hashed = base64.b64encode(password_hashed).decode()
email = reset_data.get("email", "")
with Session(db.engine) as session:
account = session.execute(select(Account).filter_by(email=reset_data.get("email"))).scalar_one_or_none()
if account:
account.password = base64_password_hashed
account.password_salt = base64_salt
db.session.commit()
tenant = TenantService.get_join_tenants(account)
if not tenant and not FeatureService.get_system_features().is_allow_create_workspace:
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
else:
try:
account = AccountService.create_account_and_tenant(
email=reset_data.get("email", ""),
name=reset_data.get("email", ""),
password=password_confirm,
interface_language=languages[0],
)
except WorkSpaceNotAllowedCreateError:
pass
except AccountRegisterError:
raise AccountInFreezeError()
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
if account:
self._update_existing_account(account, password_hashed, salt, session)
else:
self._create_new_account(email, args["password_confirm"])
return {"result": "success"}
def _update_existing_account(self, account, password_hashed, salt, session):
# Update existing account credentials
account.password = base64.b64encode(password_hashed).decode()
account.password_salt = base64.b64encode(salt).decode()
session.commit()
# Create workspace if needed
if (
not TenantService.get_join_tenants(account)
and FeatureService.get_system_features().is_allow_create_workspace
):
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
def _create_new_account(self, email, password):
# Create new account if allowed
try:
AccountService.create_account_and_tenant(
email=email,
name=email,
password=password,
interface_language=languages[0],
)
except WorkSpaceNotAllowedCreateError:
pass
except AccountRegisterError:
raise AccountInFreezeError()
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
+2 -9
View File
@@ -45,16 +45,9 @@ def get_oauth_providers():
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/google",
)
if not dify_config.OAUTH2_CLIENT_ID or not dify_config.OAUTH2_CLIENT_SECRET:
oa_oauth = None
else:
oa_oauth = OaOAuth(
client_id=dify_config.OAUTH2_CLIENT_ID,
client_secret=dify_config.OAUTH2_CLIENT_SECRET,
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/oauth2",
)
oauth2 = OaOAuth(client_id='', client_secret='', redirect_uri='') # Extend: oauth2
OAUTH_PROVIDERS = {"github": github_oauth, "google": google_oauth, "oauth2": oa_oauth}
OAUTH_PROVIDERS = {"github": github_oauth, "google": google_oauth, "oauth2": oauth2}
return OAUTH_PROVIDERS
@@ -35,12 +35,12 @@ class AdminRegisterApi(Resource):
# 解析jwt
if not ("AuthorityId" in decoded_jwt.keys() and int(
decoded_jwt["AuthorityId"]) == int(dify_config.ADMIN_GROUP_ID)):
return {"error": "User already exists."}, 351
return {"error": "Unable to add a new backend super admin groups."}, 351
try:
if Account.query.filter_by(email=args.email).first() is not None:
return {"error": "User already exists."}, 351
if db.session.query(Account).filter_by(email=args.email).first() is not None:
return {"error": "User already exists"}, 352
except:
return {"error": "User already exists."}, 351
return {"error": "Can't find the relevant user email."}, 353
account = AccountService.create_account(
password=str(uuid.uuid4()).replace('-', ''),
interface_language="zh-Hans",
+6 -4
View File
@@ -641,12 +641,10 @@ class DatasetRetrievalSettingApi(Resource):
VectorType.RELYT
| VectorType.TIDB_VECTOR
| VectorType.CHROMA
| VectorType.TENCENT
| VectorType.PGVECTO_RS
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
| VectorType.OCEANBASE
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
@@ -664,6 +662,9 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.COUCHBASE
| VectorType.MILVUS
| VectorType.OPENGAUSS
| VectorType.OCEANBASE
| VectorType.TABLESTORE
| VectorType.TENCENT
):
return {
"retrieval_method": [
@@ -687,12 +688,10 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.RELYT
| VectorType.TIDB_VECTOR
| VectorType.CHROMA
| VectorType.TENCENT
| VectorType.PGVECTO_RS
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
| VectorType.OCEANBASE
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
@@ -708,6 +707,9 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.PGVECTOR
| VectorType.LINDORM
| VectorType.OPENGAUSS
| VectorType.OCEANBASE
| VectorType.TABLESTORE
| VectorType.TENCENT
):
return {
"retrieval_method": [
+9 -2
View File
@@ -14,7 +14,12 @@ class WebsiteCrawlApi(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument(
"provider", type=str, choices=["firecrawl", "jinareader"], required=True, nullable=True, location="json"
"provider",
type=str,
choices=["firecrawl", "watercrawl", "jinareader"],
required=True,
nullable=True,
location="json",
)
parser.add_argument("url", type=str, required=True, nullable=True, location="json")
parser.add_argument("options", type=dict, required=True, nullable=True, location="json")
@@ -34,7 +39,9 @@ class WebsiteCrawlStatusApi(Resource):
@account_initialization_required
def get(self, job_id: str):
parser = reqparse.RequestParser()
parser.add_argument("provider", type=str, choices=["firecrawl", "jinareader"], required=True, location="args")
parser.add_argument(
"provider", type=str, choices=["firecrawl", "watercrawl", "jinareader"], required=True, location="args"
)
args = parser.parse_args()
# get crawl status
try:
+12
View File
@@ -103,6 +103,18 @@ class AccountInFreezeError(BaseHTTPException):
)
class EducationVerifyLimitError(BaseHTTPException):
error_code = "education_verify_limit"
description = "Rate limit exceeded"
code = 429
class EducationActivateLimitError(BaseHTTPException):
error_code = "education_activate_limit"
description = "Rate limit exceeded"
code = 429
class CompilanceRateLimitError(BaseHTTPException):
error_code = "compilance_rate_limit"
description = "Rate limit exceeded for downloading compliance report."
+83 -1
View File
@@ -15,7 +15,13 @@ from controllers.console.workspace.error import (
InvalidInvitationCodeError,
RepeatPasswordNotMatchError,
)
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_enabled,
enterprise_license_required,
only_edition_cloud,
setup_required,
)
from extensions.ext_database import db
from fields.member_fields import account_fields
from libs.helper import TimestampField, timezone
@@ -292,6 +298,79 @@ class AccountDeleteUpdateFeedbackApi(Resource):
return {"result": "success"}
class EducationVerifyApi(Resource):
verify_fields = {
"token": fields.String,
}
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@marshal_with(verify_fields)
def get(self):
account = current_user
return BillingService.EducationIdentity.verify(account.id, account.email)
class EducationApi(Resource):
status_fields = {
"result": fields.Boolean,
}
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
def post(self):
account = current_user
parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, location="json")
parser.add_argument("institution", type=str, required=True, location="json")
parser.add_argument("role", type=str, required=True, location="json")
args = parser.parse_args()
return BillingService.EducationIdentity.activate(account, args["token"], args["institution"], args["role"])
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@marshal_with(status_fields)
def get(self):
account = current_user
return BillingService.EducationIdentity.is_active(account.id)
class EducationAutoCompleteApi(Resource):
data_fields = {
"data": fields.List(fields.String),
"curr_page": fields.Integer,
"has_next": fields.Boolean,
}
@setup_required
@login_required
@account_initialization_required
@only_edition_cloud
@cloud_edition_billing_enabled
@marshal_with(data_fields)
def get(self):
parser = reqparse.RequestParser()
parser.add_argument("keywords", type=str, required=True, location="args")
parser.add_argument("page", type=int, required=False, location="args", default=0)
parser.add_argument("limit", type=int, required=False, location="args", default=20)
args = parser.parse_args()
return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"])
# Register API resources
api.add_resource(AccountInitApi, "/account/init")
api.add_resource(AccountProfileApi, "/account/profile")
@@ -305,5 +384,8 @@ api.add_resource(AccountIntegrateApi, "/account/integrates")
api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify")
api.add_resource(AccountDeleteApi, "/account/delete")
api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback")
api.add_resource(EducationVerifyApi, "/account/education/verify")
api.add_resource(EducationApi, "/account/education")
api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete")
# api.add_resource(AccountEmailApi, '/account/email')
# api.add_resource(AccountEmailVerifyApi, '/account/email-verify')
+9 -9
View File
@@ -236,7 +236,7 @@ class PluginFetchManifestApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def get(self):
tenant_id = current_user.current_tenant_id
@@ -260,7 +260,7 @@ class PluginFetchInstallTasksApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def get(self):
tenant_id = current_user.current_tenant_id
@@ -281,7 +281,7 @@ class PluginFetchInstallTaskApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def get(self, task_id: str):
tenant_id = current_user.current_tenant_id
@@ -295,7 +295,7 @@ class PluginDeleteInstallTaskApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self, task_id: str):
tenant_id = current_user.current_tenant_id
@@ -309,7 +309,7 @@ class PluginDeleteAllInstallTaskItemsApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self):
tenant_id = current_user.current_tenant_id
@@ -323,7 +323,7 @@ class PluginDeleteInstallTaskItemApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self, task_id: str, identifier: str):
tenant_id = current_user.current_tenant_id
@@ -337,7 +337,7 @@ class PluginUpgradeFromMarketplaceApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self):
tenant_id = current_user.current_tenant_id
@@ -360,7 +360,7 @@ class PluginUpgradeFromGithubApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self):
tenant_id = current_user.current_tenant_id
@@ -391,7 +391,7 @@ class PluginUninstallApi(Resource):
@setup_required
@login_required
@account_initialization_required
@plugin_permission_required(debug_required=True)
@plugin_permission_required(install_required=True)
def post(self):
req = reqparse.RequestParser()
req.add_argument("plugin_installation_id", type=str, required=True, location="json")
@@ -220,6 +220,23 @@ class WebappLogoWorkspaceApi(Resource):
return {"id": upload_file.id}, 201
class WorkspaceInfoApi(Resource):
@setup_required
@login_required
@account_initialization_required
# Change workspace name
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, location="json")
args = parser.parse_args()
tenant = Tenant.query.filter(Tenant.id == current_user.current_tenant_id).one_or_404()
tenant.name = args["name"]
db.session.commit()
return {"result": "success", "tenant": marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)}
api.add_resource(TenantListApi, "/workspaces") # GET for getting all tenants
api.add_resource(WorkspaceListApi, "/all-workspaces") # GET for getting all tenants
api.add_resource(TenantApi, "/workspaces/current", endpoint="workspaces_current") # GET for getting current tenant info
@@ -227,3 +244,4 @@ api.add_resource(TenantApi, "/info", endpoint="info") # Deprecated
api.add_resource(SwitchWorkspaceApi, "/workspaces/switch") # POST for switching tenant
api.add_resource(CustomConfigWorkspaceApi, "/workspaces/custom-config")
api.add_resource(WebappLogoWorkspaceApi, "/workspaces/custom-config/webapp-logo/upload")
api.add_resource(WorkspaceInfoApi, "/workspaces/info") # POST for changing workspace info
+11
View File
@@ -54,6 +54,17 @@ def only_edition_self_hosted(view):
return decorated
def cloud_edition_billing_enabled(view):
@wraps(view)
def decorated(*args, **kwargs):
features = FeatureService.get_features(current_user.current_tenant_id)
if not features.billing.enabled:
abort(403, "Billing feature is not enabled.")
return view(*args, **kwargs)
return decorated
def cloud_edition_billing_resource_check(resource: str):
def interceptor(view):
@wraps(view)
+2 -1
View File
@@ -6,5 +6,6 @@ bp = Blueprint("service_api", __name__, url_prefix="/v1")
api = ExternalApi(bp)
from . import index
from .app import app, audio, completion, conversation, file, message, workflow
from .app import annotation, app, audio, completion, conversation, file, message, workflow
from .dataset import dataset, document, hit_testing, metadata, segment, upload_file
from .workspace import models
@@ -0,0 +1,107 @@
from flask import request
from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden
from controllers.service_api import api
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from extensions.ext_redis import redis_client
from fields.annotation_fields import (
annotation_fields,
)
from libs.login import current_user
from models.model import App, EndUser
from services.annotation_service import AppAnnotationService
class AnnotationReplyActionApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
def post(self, app_model: App, end_user: EndUser, action):
parser = reqparse.RequestParser()
parser.add_argument("score_threshold", required=True, type=float, location="json")
parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
parser.add_argument("embedding_model_name", required=True, type=str, location="json")
args = parser.parse_args()
if action == "enable":
result = AppAnnotationService.enable_app_annotation(args, app_model.id)
elif action == "disable":
result = AppAnnotationService.disable_app_annotation(app_model.id)
else:
raise ValueError("Unsupported annotation reply action")
return result, 200
class AnnotationReplyActionStatusApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def get(self, app_model: App, end_user: EndUser, job_id, action):
job_id = str(job_id)
app_annotation_job_key = "{}_app_annotation_job_{}".format(action, str(job_id))
cache_result = redis_client.get(app_annotation_job_key)
if cache_result is None:
raise ValueError("The job does not exist.")
job_status = cache_result.decode()
error_msg = ""
if job_status == "error":
app_annotation_error_key = "{}_app_annotation_error_{}".format(action, str(job_id))
error_msg = redis_client.get(app_annotation_error_key).decode()
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
class AnnotationListApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def get(self, app_model: App, end_user: EndUser):
page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int)
keyword = request.args.get("keyword", default="", type=str)
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_model.id, page, limit, keyword)
response = {
"data": marshal(annotation_list, annotation_fields),
"has_more": len(annotation_list) == limit,
"limit": limit,
"total": total,
"page": page,
}
return response, 200
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(annotation_fields)
def post(self, app_model: App, end_user: EndUser):
parser = reqparse.RequestParser()
parser.add_argument("question", required=True, type=str, location="json")
parser.add_argument("answer", required=True, type=str, location="json")
args = parser.parse_args()
annotation = AppAnnotationService.insert_app_annotation_directly(args, app_model.id)
return annotation
class AnnotationUpdateDeleteApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(annotation_fields)
def post(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor:
raise Forbidden()
annotation_id = str(annotation_id)
parser = reqparse.RequestParser()
parser.add_argument("question", required=True, type=str, location="json")
parser.add_argument("answer", required=True, type=str, location="json")
args = parser.parse_args()
annotation = AppAnnotationService.update_app_annotation_directly(args, app_model.id, annotation_id)
return annotation
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
def delete(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor:
raise Forbidden()
annotation_id = str(annotation_id)
AppAnnotationService.delete_app_annotation(app_model.id, annotation_id)
return {"result": "success"}, 200
api.add_resource(AnnotationReplyActionApi, "/apps/annotation-reply/<string:action>")
api.add_resource(AnnotationReplyActionStatusApi, "/apps/annotation-reply/<string:action>/status/<uuid:job_id>")
api.add_resource(AnnotationListApi, "/apps/annotations")
api.add_resource(AnnotationUpdateDeleteApi, "/apps/annotations/<uuid:annotation_id>")
+11 -2
View File
@@ -1,3 +1,4 @@
import json
import logging
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
@@ -10,7 +11,7 @@ from controllers.service_api.app.error import NotChatAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.app.entities.app_invoke_entities import InvokeFrom
from fields.conversation_fields import message_file_fields
from fields.message_fields import agent_thought_fields, feedback_fields, retriever_resource_fields
from fields.message_fields import agent_thought_fields, feedback_fields
from fields.raws import FilesContainedField
from libs.helper import TimestampField, uuid_value
from models.model import ApiToken, App, AppMode, EndUser # 二开部分End - 密钥额度限制,新增ApiToken
@@ -19,6 +20,14 @@ from services.message_service import MessageService
class MessageListApi(Resource):
def get_retriever_resources(self):
try:
if self.message_metadata:
return json.loads(self.message_metadata).get("retriever_resources", [])
return []
except (json.JSONDecodeError, TypeError):
return []
message_fields = {
"id": fields.String,
"conversation_id": fields.String,
@@ -28,7 +37,7 @@ class MessageListApi(Resource):
"answer": fields.String(attribute="re_sign_file_url_answer"),
"message_files": fields.List(fields.Nested(message_file_fields)),
"feedback": fields.Nested(feedback_fields, attribute="user_feedback", allow_null=True),
"retriever_resources": fields.List(fields.Nested(retriever_resource_fields)),
"retriever_resources": get_retriever_resources,
"created_at": TimestampField,
"agent_thoughts": fields.List(fields.Nested(agent_thought_fields)),
"status": fields.String,
+6 -5
View File
@@ -27,6 +27,7 @@ from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db
from fields.workflow_app_log_fields import workflow_app_log_pagination_fields
from libs import helper
from libs.helper import TimestampField
from models.model import ApiToken, App, AppMode, EndUser # 二开部分End - 密钥额度限制,ApiToken
from models.workflow import WorkflowRun, WorkflowRunStatus
from services.app_generate_service import AppGenerateService
@@ -44,8 +45,8 @@ workflow_run_fields = {
"error": fields.String,
"total_steps": fields.Integer,
"total_tokens": fields.Integer,
"created_at": fields.DateTime,
"finished_at": fields.DateTime,
"created_at": TimestampField,
"finished_at": TimestampField,
"elapsed_time": fields.Float,
}
@@ -53,7 +54,7 @@ workflow_run_fields = {
class WorkflowRunDetailApi(Resource):
@validate_app_token
@marshal_with(workflow_run_fields)
def get(self, app_model: App, workflow_id: str, api_token: ApiToken): # 二开部分End - 密钥额度限制,新增api_token,否则上传文件会报错
def get(self, app_model: App, workflow_run_id: str, api_token: ApiToken): # 二开部分End - 密钥额度限制,新增api_token,否则上传文件会报错
"""
Get a workflow task running detail
"""
@@ -61,7 +62,7 @@ class WorkflowRunDetailApi(Resource):
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_id).first()
workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).first()
return workflow_run
@@ -166,6 +167,6 @@ class WorkflowAppLogApi(Resource):
api.add_resource(WorkflowRunApi, "/workflows/run")
api.add_resource(WorkflowRunDetailApi, "/workflows/run/<string:workflow_id>")
api.add_resource(WorkflowRunDetailApi, "/workflows/run/<string:workflow_run_id>")
api.add_resource(WorkflowTaskStopApi, "/workflows/tasks/<string:task_id>/stop")
api.add_resource(WorkflowAppLogApi, "/workflows/logs")
+149 -2
View File
@@ -1,6 +1,6 @@
from flask import request
from flask_restful import marshal, reqparse # type: ignore
from werkzeug.exceptions import NotFound
from werkzeug.exceptions import Forbidden, NotFound
import services.dataset_service
from controllers.service_api import api
@@ -12,7 +12,7 @@ from core.provider_manager import ProviderManager
from fields.dataset_fields import dataset_detail_fields
from libs.login import current_user
from models.dataset import Dataset, DatasetPermissionEnum
from services.dataset_service import DatasetService
from services.dataset_service import DatasetPermissionService, DatasetService
def _validate_name(name):
@@ -21,6 +21,12 @@ def _validate_name(name):
return name
def _validate_description_length(description):
if len(description) > 400:
raise ValueError("Description cannot exceed 400 characters.")
return description
class DatasetListApi(DatasetApiResource):
"""Resource for datasets."""
@@ -137,11 +143,151 @@ class DatasetListApi(DatasetApiResource):
class DatasetApi(DatasetApiResource):
"""Resource for dataset."""
def get(self, _, dataset_id):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
data = marshal(dataset, dataset_detail_fields)
if data.get("permission") == "partial_members":
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
data.update({"partial_member_list": part_users_list})
# check embedding setting
provider_manager = ProviderManager()
configurations = provider_manager.get_configurations(tenant_id=current_user.current_tenant_id)
embedding_models = configurations.get_models(model_type=ModelType.TEXT_EMBEDDING, only_active=True)
model_names = []
for embedding_model in embedding_models:
model_names.append(f"{embedding_model.model}:{embedding_model.provider.provider}")
if data["indexing_technique"] == "high_quality":
item_model = f"{data['embedding_model']}:{data['embedding_model_provider']}"
if item_model in model_names:
data["embedding_available"] = True
else:
data["embedding_available"] = False
else:
data["embedding_available"] = True
if data.get("permission") == "partial_members":
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
data.update({"partial_member_list": part_users_list})
return data, 200
def patch(self, _, dataset_id):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str)
if dataset is None:
raise NotFound("Dataset not found.")
parser = reqparse.RequestParser()
parser.add_argument(
"name",
nullable=False,
help="type is required. Name must be between 1 to 40 characters.",
type=_validate_name,
)
parser.add_argument("description", location="json", store_missing=False, type=_validate_description_length)
parser.add_argument(
"indexing_technique",
type=str,
location="json",
choices=Dataset.INDEXING_TECHNIQUE_LIST,
nullable=True,
help="Invalid indexing technique.",
)
parser.add_argument(
"permission",
type=str,
location="json",
choices=(DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM, DatasetPermissionEnum.PARTIAL_TEAM),
help="Invalid permission.",
)
parser.add_argument("embedding_model", type=str, location="json", help="Invalid embedding model.")
parser.add_argument(
"embedding_model_provider", type=str, location="json", help="Invalid embedding model provider."
)
parser.add_argument("retrieval_model", type=dict, location="json", help="Invalid retrieval model.")
parser.add_argument("partial_member_list", type=list, location="json", help="Invalid parent user list.")
parser.add_argument(
"external_retrieval_model",
type=dict,
required=False,
nullable=True,
location="json",
help="Invalid external retrieval model.",
)
parser.add_argument(
"external_knowledge_id",
type=str,
required=False,
nullable=True,
location="json",
help="Invalid external knowledge id.",
)
parser.add_argument(
"external_knowledge_api_id",
type=str,
required=False,
nullable=True,
location="json",
help="Invalid external knowledge api id.",
)
args = parser.parse_args()
data = request.get_json()
# check embedding model setting
if data.get("indexing_technique") == "high_quality":
DatasetService.check_embedding_model_setting(
dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model")
)
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
DatasetPermissionService.check_permission(
current_user, dataset, data.get("permission"), data.get("partial_member_list")
)
dataset = DatasetService.update_dataset(dataset_id_str, args, current_user)
if dataset is None:
raise NotFound("Dataset not found.")
result_data = marshal(dataset, dataset_detail_fields)
tenant_id = current_user.current_tenant_id
if data.get("partial_member_list") and data.get("permission") == "partial_members":
DatasetPermissionService.update_partial_member_list(
tenant_id, dataset_id_str, data.get("partial_member_list")
)
# clear partial member list when permission is only_me or all_team_members
elif (
data.get("permission") == DatasetPermissionEnum.ONLY_ME
or data.get("permission") == DatasetPermissionEnum.ALL_TEAM
):
DatasetPermissionService.clear_partial_member_list(dataset_id_str)
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
result_data.update({"partial_member_list": partial_member_list})
return result_data, 200
def delete(self, _, dataset_id):
"""
Deletes a dataset given its ID.
Args:
_: ignore
dataset_id (UUID): The ID of the dataset to be deleted.
Returns:
@@ -157,6 +303,7 @@ class DatasetApi(DatasetApiResource):
try:
if DatasetService.delete_dataset(dataset_id_str, current_user):
DatasetPermissionService.clear_partial_member_list(dataset_id_str)
return {"result": "success"}, 204
else:
raise NotFound("Dataset not found.")
@@ -341,7 +341,7 @@ class DocumentListApi(DatasetApiResource):
search = f"%{search}%"
query = query.filter(Document.name.like(search))
query = query.order_by(desc(Document.created_at))
query = query.order_by(desc(Document.created_at), desc(Document.position))
paginated_documents = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
documents = paginated_documents.items
+212 -30
View File
@@ -14,10 +14,20 @@ from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelType
from extensions.ext_database import db
from fields.segment_fields import segment_fields
from models.dataset import Dataset, DocumentSegment
from fields.segment_fields import child_chunk_fields, segment_fields
from models.dataset import Dataset
from services.dataset_service import DatasetService, DocumentService, SegmentService
from services.entities.knowledge_entities.knowledge_entities import SegmentUpdateArgs
from services.errors.chunk import (
ChildChunkDeleteIndexError,
ChildChunkIndexingError,
)
from services.errors.chunk import (
ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError,
)
from services.errors.chunk import (
ChildChunkIndexingError as ChildChunkIndexingServiceError,
)
class SegmentApi(DatasetApiResource):
@@ -71,7 +81,7 @@ class SegmentApi(DatasetApiResource):
return {"error": "Segments is required"}, 400
def get(self, tenant_id, dataset_id, document_id):
"""Create single segment."""
"""Get segments."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
@@ -110,26 +120,13 @@ class SegmentApi(DatasetApiResource):
status_list = args["status"]
keyword = args["keyword"]
query = DocumentSegment.query.filter(
DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
segments, total = SegmentService.get_segments(
document_id=document_id,
tenant_id=current_user.current_tenant_id,
status_list=args["status"],
keyword=args["keyword"],
)
if status_list:
query = query.filter(DocumentSegment.status.in_(status_list))
if keyword:
query = query.where(DocumentSegment.content.ilike(f"%{keyword}%"))
total = query.count()
query = query.order_by(DocumentSegment.position)
paginated_segments = query.paginate(
page=page,
per_page=limit,
max_per_page=100,
error_out=False,
)
segments = paginated_segments.items
response = {
"data": marshal(segments, segment_fields),
"doc_form": document.doc_form,
@@ -158,9 +155,8 @@ class DatasetSegmentApi(DatasetApiResource):
if not document:
raise NotFound("Document not found.")
# check segment
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
SegmentService.delete_segment(segment, document, dataset)
@@ -199,9 +195,7 @@ class DatasetSegmentApi(DatasetApiResource):
raise ProviderNotInitializeError(ex.description)
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
@@ -210,12 +204,200 @@ class DatasetSegmentApi(DatasetApiResource):
parser.add_argument("segment", type=dict, required=False, nullable=True, location="json")
args = parser.parse_args()
SegmentService.segment_create_args_validate(args["segment"], document)
segment = SegmentService.update_segment(SegmentUpdateArgs(**args["segment"]), segment, document, dataset)
return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200
updated_segment = SegmentService.update_segment(
SegmentUpdateArgs(**args["segment"]), segment, document, dataset
)
return {"data": marshal(updated_segment, segment_fields), "doc_form": document.doc_form}, 200
class ChildChunkApi(DatasetApiResource):
"""Resource for child chunks."""
@cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def post(self, tenant_id, dataset_id, document_id, segment_id):
"""Create child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# check embedding model setting
if dataset.indexing_technique == "high_quality":
try:
model_manager = ModelManager()
model_manager.get_model_instance(
tenant_id=current_user.current_tenant_id,
provider=dataset.embedding_model_provider,
model_type=ModelType.TEXT_EMBEDDING,
model=dataset.embedding_model,
)
except LLMBadRequestError:
raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.create_child_chunk(args.get("content"), segment, document, dataset)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
def get(self, tenant_id, dataset_id, document_id, segment_id):
"""Get child chunks."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
parser = reqparse.RequestParser()
parser.add_argument("limit", type=int, default=20, location="args")
parser.add_argument("keyword", type=str, default=None, location="args")
parser.add_argument("page", type=int, default=1, location="args")
args = parser.parse_args()
page = args["page"]
limit = min(args["limit"], 100)
keyword = args["keyword"]
child_chunks = SegmentService.get_child_chunks(segment_id, document_id, dataset_id, page, limit, keyword)
return {
"data": marshal(child_chunks.items, child_chunk_fields),
"total": child_chunks.total,
"total_pages": child_chunks.pages,
"page": page,
"limit": limit,
}, 200
class DatasetChildChunkApi(DatasetApiResource):
"""Resource for updating child chunks."""
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def delete(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Delete child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
child_chunk = SegmentService.get_child_chunk_by_id(
child_chunk_id=child_chunk_id, tenant_id=current_user.current_tenant_id
)
if not child_chunk:
raise NotFound("Child chunk not found.")
try:
SegmentService.delete_child_chunk(child_chunk, dataset)
except ChildChunkDeleteIndexServiceError as e:
raise ChildChunkDeleteIndexError(str(e))
return {"result": "success"}, 200
@cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def patch(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Update child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# get document
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# get segment
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# get child chunk
child_chunk = SegmentService.get_child_chunk_by_id(
child_chunk_id=child_chunk_id, tenant_id=current_user.current_tenant_id
)
if not child_chunk:
raise NotFound("Child chunk not found.")
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.update_child_chunk(
args.get("content"), child_chunk, segment, document, dataset
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
api.add_resource(SegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments")
api.add_resource(
DatasetSegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>"
)
api.add_resource(
ChildChunkApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks"
)
api.add_resource(
DatasetChildChunkApi,
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks/<uuid:child_chunk_id>",
)
@@ -0,0 +1,21 @@
from flask_login import current_user # type: ignore
from flask_restful import Resource # type: ignore
from controllers.service_api import api
from controllers.service_api.wraps import validate_dataset_token
from core.model_runtime.utils.encoders import jsonable_encoder
from services.model_provider_service import ModelProviderService
class ModelProviderAvailableModelApi(Resource):
@validate_dataset_token
def get(self, _, model_type):
tenant_id = current_user.current_tenant_id
model_provider_service = ModelProviderService()
models = model_provider_service.get_models_by_model_type(tenant_id=tenant_id, model_type=model_type)
return jsonable_encoder({"data": models})
api.add_resource(ModelProviderAvailableModelApi, "/workspaces/current/models/model-types/<string:model_type>")
+23 -13
View File
@@ -72,22 +72,32 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
if tenant.status == TenantStatus.ARCHIVE:
raise Forbidden("The workspace's status is archived.")
# ---------------------二开部分Begin 额度限制,API调用计费 ---------------------
tenantAccountJoin = (
db.session.query(TenantAccountJoin)
.filter(
TenantAccountJoin.tenant_id == app_model.tenant_id,
TenantAccountJoin.role == TenantAccountRole.OWNER,
)
.first()
)
if not tenantAccountJoin:
raise Forbidden("The workspace has not owner")
tenant_account_join = (
db.session.query(Tenant, TenantAccountJoin)
.filter(Tenant.id == api_token.tenant_id)
.filter(TenantAccountJoin.tenant_id == Tenant.id)
.filter(TenantAccountJoin.role.in_(["owner"]))
.filter(Tenant.status == TenantStatus.NORMAL)
.one_or_none()
) # TODO: only owner information is required, so only one is returned.
if tenant_account_join:
tenant, ta = tenant_account_join
account = db.session.query(Account).filter(Account.id == ta.account_id).first()
# Login admin
if account:
account.current_tenant = tenant
current_app.login_manager._update_request_context_with_user(account) # type: ignore
user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore
else:
raise Unauthorized("Tenant owner account does not exist.")
else:
raise Unauthorized("Tenant does not exist.")
# ---------------------二开部分Begin 额度限制,API调用计费 ---------------------
# TODO 需要写入缓存,读缓存
account_money = (
db.session.query(AccountMoneyExtend)
.filter(AccountMoneyExtend.account_id == tenantAccountJoin.account_id)
.filter(AccountMoneyExtend.account_id == ta.account_id)
.first()
)
if account_money and account_money.used_quota >= account_money.total_quota:
@@ -137,7 +147,7 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
# ---------------------二开部分Begin 额度限制,API调用计费 ---------------------
if kwargs.get("end_user"):
create_or_update_end_user_account_join_extend(
kwargs["end_user"].id, tenantAccountJoin.account_id, app_model.id
kwargs["end_user"].id, ta.account_id, app_model.id
)
# ---------------------二开部分End 额度限制,API调用计费 ---------------------
+28 -9
View File
@@ -19,6 +19,8 @@ class PassportResource(Resource):
def get(self):
system_features = FeatureService.get_system_features()
app_code = request.headers.get("X-App-Code")
user_id = request.args.get("user_id")
if app_code is None:
raise Unauthorized("X-App-Code header is missing.")
@@ -36,16 +38,33 @@ class PassportResource(Resource):
if not app_model or app_model.status != "normal" or not app_model.enable_site:
raise NotFound()
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=True,
session_id=generate_session_id(),
)
if user_id:
end_user = (
db.session.query(EndUser).filter(EndUser.app_id == app_model.id, EndUser.session_id == user_id).first()
)
db.session.add(end_user)
db.session.commit()
if end_user:
pass
else:
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=True,
session_id=user_id,
)
db.session.add(end_user)
db.session.commit()
else:
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=True,
session_id=generate_session_id(),
)
db.session.add(end_user)
db.session.commit()
payload = {
"iss": site.app_id,
+1 -1
View File
@@ -332,7 +332,7 @@ class BaseAgentRunner(AppRunner):
agent_thought = updated_agent_thought
if thought:
agent_thought.thought = thought
agent_thought.thought += thought
if tool_name:
agent_thought.tool = tool_name
@@ -12,39 +12,45 @@ class CotAgentOutputParser:
def handle_react_stream_output(
cls, llm_response: Generator[LLMResultChunk, None, None], usage_dict: dict
) -> Generator[Union[str, AgentScratchpadUnit.Action], None, None]:
def parse_action(json_str):
try:
action = json.loads(json_str, strict=False)
action_name = None
action_input = None
def parse_action(action) -> Union[str, AgentScratchpadUnit.Action]:
action_name = None
action_input = None
if isinstance(action, str):
try:
action = json.loads(action, strict=False)
except json.JSONDecodeError:
return action or ""
# cohere always returns a list
if isinstance(action, list) and len(action) == 1:
action = action[0]
# cohere always returns a list
if isinstance(action, list) and len(action) == 1:
action = action[0]
for key, value in action.items():
if "input" in key.lower():
action_input = value
else:
action_name = value
if action_name is not None and action_input is not None:
return AgentScratchpadUnit.Action(
action_name=action_name,
action_input=action_input,
)
for key, value in action.items():
if "input" in key.lower():
action_input = value
else:
return json_str or ""
except:
return json_str or ""
action_name = value
def extra_json_from_code_block(code_block) -> Generator[Union[str, AgentScratchpadUnit.Action], None, None]:
code_blocks = re.findall(r"```(.*?)```", code_block, re.DOTALL)
if not code_blocks:
return
for block in code_blocks:
json_text = re.sub(r"^[a-zA-Z]+\n", "", block.strip(), flags=re.MULTILINE)
yield parse_action(json_text)
if action_name is not None and action_input is not None:
return AgentScratchpadUnit.Action(
action_name=action_name,
action_input=action_input,
)
else:
return json.dumps(action)
def extra_json_from_code_block(code_block) -> list[Union[list, dict]]:
blocks = re.findall(r"```[json]*\s*([\[{].*[]}])\s*```", code_block, re.DOTALL | re.IGNORECASE)
if not blocks:
return []
try:
json_blocks = []
for block in blocks:
json_text = re.sub(r"^[a-zA-Z]+\n", "", block.strip(), flags=re.MULTILINE)
json_blocks.append(json.loads(json_text, strict=False))
return json_blocks
except:
return []
code_block_cache = ""
code_block_delimiter_count = 0
@@ -78,7 +84,7 @@ class CotAgentOutputParser:
delta = response_content[index : index + steps]
yield_delta = False
if delta == "`":
if not in_json and delta == "`":
last_character = delta
code_block_cache += delta
code_block_delimiter_count += 1
@@ -159,8 +165,14 @@ class CotAgentOutputParser:
if code_block_delimiter_count == 3:
if in_code_block:
last_character = delta
yield from extra_json_from_code_block(code_block_cache)
code_block_cache = ""
action_json_list = extra_json_from_code_block(code_block_cache)
if action_json_list:
for action_json in action_json_list:
yield parse_action(action_json)
code_block_cache = ""
else:
index += steps
continue
in_code_block = not in_code_block
code_block_delimiter_count = 0
+9
View File
@@ -70,11 +70,20 @@ class AgentStrategyIdentity(ToolIdentity):
pass
class AgentFeature(enum.StrEnum):
"""
Agent Feature, used to describe the features of the agent strategy.
"""
HISTORY_MESSAGES = "history-messages"
class AgentStrategyEntity(BaseModel):
identity: AgentStrategyIdentity
parameters: list[AgentStrategyParameter] = Field(default_factory=list)
description: I18nObject = Field(..., description="The description of the agent strategy")
output_schema: Optional[dict] = None
features: Optional[list[AgentFeature]] = None
# pydantic configs
model_config = ConfigDict(protected_namespaces=())
@@ -16,7 +16,6 @@ class ModelConfigConverter:
"""
Convert app model config dict to entity.
:param app_config: app config
:param skip_check: skip check
:raises ProviderTokenNotInitError: provider token not init error
:return: app orchestration config entity
"""
@@ -91,7 +91,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
"""
if not args.get("query"):
raise ValueError("query is required")
@@ -191,10 +191,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is streamed
"""
if not node_id:
raise ValueError("node_id is required")
@@ -248,10 +248,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
"""
if not node_id:
raise ValueError("node_id is required")
@@ -81,7 +81,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
"""
if not streaming:
raise ValueError("Agent Chat App does not support blocking mode")
+1
View File
@@ -157,6 +157,7 @@ class AppRunner:
:param files: files
:param query: query
:param memory: memory
:param image_detail_config: the image quality config
:return:
"""
# get prompt without memory and context
+1 -1
View File
@@ -77,7 +77,7 @@ class ChatAppGenerator(MessageBasedAppGenerator):
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
"""
if not args.get("query"):
raise ValueError("query is required")
@@ -75,7 +75,7 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
"""
query = args["query"]
if not isinstance(query, str):
@@ -148,6 +148,13 @@ class MessageBasedAppGenerator(BaseAppGenerator):
# get conversation introduction
introduction = self._get_conversation_introduction(application_generate_entity)
# get conversation name
if isinstance(application_generate_entity, AdvancedChatAppGenerateEntity):
query = application_generate_entity.query or "New conversation"
else:
query = next(iter(application_generate_entity.inputs.values()), "New conversation")
conversation_name = (query[:20] + "") if len(query) > 20 else query
if not conversation:
conversation = Conversation(
app_id=app_config.app_id,
@@ -156,7 +163,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
model_id=model_id,
override_model_configs=json.dumps(override_model_configs) if override_model_configs else None,
mode=app_config.app_mode.value,
name="New conversation",
name=conversation_name,
inputs=application_generate_entity.inputs,
introduction=introduction,
system_instruction="",
+5 -5
View File
@@ -167,7 +167,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param user: account or end user
:param application_generate_entity: application generate entity
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is stream
:param workflow_thread_pool_id: workflow thread pool id
"""
# init queue manager
@@ -217,10 +217,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is streamed
"""
if not node_id:
raise ValueError("node_id is required")
@@ -273,10 +273,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param invoke_from: invoke from source
:param stream: is stream
:param streaming: is streamed
"""
if not node_id:
raise ValueError("node_id is required")
-3
View File
@@ -44,9 +44,6 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
def run(self) -> None:
"""
Run application
:param application_generate_entity: application generate entity
:param queue_manager: application queue manager
:return:
"""
app_config = self.application_generate_entity.app_config
app_config = cast(WorkflowAppConfig, app_config)
@@ -48,7 +48,7 @@ class MessageCycleManage:
def _generate_conversation_name(self, *, conversation_id: str, query: str) -> Optional[Thread]:
"""
Generate conversation name.
:param conversation: conversation
:param conversation_id: conversation id
:param query: query
:return: thread
"""
@@ -44,6 +44,7 @@ from core.app.entities.task_entities import (
WorkflowFinishStreamResponse,
WorkflowStartStreamResponse,
)
from core.app.task_pipeline.exc import WorkflowRunNotFoundError
from core.file import FILE_MODEL_IDENTITY, File
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.entities.trace_entity import TraceTaskName
@@ -69,8 +70,6 @@ from tasks.extend.update_account_money_when_workflow_node_execution_created_exte
update_account_money_when_workflow_node_execution_created_extend, # 二开部分End - 密钥额度限制
)
from .exc import WorkflowRunNotFoundError
class WorkflowCycleManage:
def __init__(
@@ -157,7 +156,7 @@ class WorkflowCycleManage:
) -> WorkflowRun:
"""
Workflow run success
:param workflow_run: workflow run
:param workflow_run_id: workflow run id
:param start_at: start time
:param total_tokens: total tokens
:param total_steps: total steps
@@ -169,7 +168,7 @@ class WorkflowCycleManage:
outputs = WorkflowEntry.handle_special_values(outputs)
workflow_run.status = WorkflowRunStatus.SUCCEEDED.value
workflow_run.status = WorkflowRunStatus.SUCCEEDED
workflow_run.outputs = json.dumps(outputs or {})
workflow_run.elapsed_time = time.perf_counter() - start_at
workflow_run.total_tokens = total_tokens
@@ -204,7 +203,7 @@ class WorkflowCycleManage:
workflow_run = self._get_workflow_run(session=session, workflow_run_id=workflow_run_id)
outputs = WorkflowEntry.handle_special_values(dict(outputs) if outputs else None)
workflow_run.status = WorkflowRunStatus.PARTIAL_SUCCESSED.value
workflow_run.status = WorkflowRunStatus.PARTIAL_SUCCEEDED.value
workflow_run.outputs = json.dumps(outputs or {})
workflow_run.elapsed_time = time.perf_counter() - start_at
workflow_run.total_tokens = total_tokens
@@ -240,7 +239,7 @@ class WorkflowCycleManage:
) -> WorkflowRun:
"""
Workflow run failed
:param workflow_run: workflow run
:param workflow_run_id: workflow run id
:param start_at: start time
:param total_tokens: total tokens
:param total_steps: total steps
+1
View File
@@ -146,6 +146,7 @@ class BasicProviderConfig(BaseModel):
BOOLEAN = CommonParameterType.BOOLEAN.value
APP_SELECTOR = CommonParameterType.APP_SELECTOR.value
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value
@classmethod
def value_of(cls, value: str) -> "ProviderConfig.Type":
+2 -4
View File
@@ -4,12 +4,10 @@ import time
from typing import Optional
from configs import dify_config
from constants import IMAGE_EXTENSIONS
from core.helper.url_signer import UrlSigner
from extensions.ext_storage import storage
IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "svg"]
IMAGE_EXTENSIONS.extend([ext.upper() for ext in IMAGE_EXTENSIONS])
class UploadFileParser:
@classmethod
@@ -38,7 +36,7 @@ class UploadFileParser:
"""
get signed url from upload file
:param upload_file: UploadFile object
:param upload_file_id: the id of UploadFile object
:return:
"""
base_url = dify_config.FILES_URL
@@ -61,6 +61,7 @@ class CodeExecutor:
Execute code
:param purview: bool # Extend global code
:param language: code language
:param preload: the preload script
:param code: code
:return:
"""
+1 -1
View File
@@ -53,7 +53,7 @@ def pin_position_map(original_position_map: dict[str, int], pin_list: list[str])
"""
Pin the items in the pin list to the beginning of the position map.
Overall logic: exclude > include > pin
:param position_map: the position map to be sorted and filtered
:param original_position_map: the position map to be sorted and filtered
:param pin_list: the list of pins to be put at the beginning
:return: the sorted position map
"""
+6 -2
View File
@@ -56,8 +56,12 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
response = client.request(method=method, url=url, **kwargs)
elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL:
proxy_mounts = {
"http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL),
"https://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTPS_URL),
"http://": httpx.HTTPTransport(
proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY
),
"https://": httpx.HTTPTransport(
proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY
),
}
with httpx.Client(mounts=proxy_mounts, verify=HTTP_REQUEST_NODE_SSL_VERIFY) as client:
response = client.request(method=method, url=url, **kwargs)
+1 -6
View File
@@ -38,12 +38,7 @@ class ToolParameterCache:
return None
def set(self, parameters: dict) -> None:
"""
Cache model provider credentials.
:param credentials: provider credentials
:return:
"""
"""Cache model provider credentials."""
redis_client.setex(self.cache_key, 86400, json.dumps(parameters))
def delete(self) -> None:
+1 -1
View File
@@ -187,7 +187,7 @@ class IndexingRunner:
},
)
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = document_segment.child_chunks
child_chunks = document_segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+1 -1
View File
@@ -1,6 +1,6 @@
# Written by YORKI MINAKO🤡, Edited by Xiaoyi
CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is.
Notice: the language type user use could be diverse, which can be English, Chinese, Español, Arabic, Japanese, French, and etc.
Notice: the language type user use could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
MAKE SURE your output is the SAME language as the user's input!
Your output is restricted only to: (Input language) Intention + Subject(short as possible)
Your output MUST be a valid JSON.
@@ -38,7 +38,6 @@ class TTSModel(AIModel):
:param credentials: model credentials
:param voice: model timbre
:param content_text: text content to be translated
:param streaming: output is streaming
:param user: unique user id
:return: translated audio file
"""
@@ -1,170 +0,0 @@
from collections.abc import Mapping
from typing import Optional
import openai
from httpx import Timeout
from openai import OpenAI
from openai.types import ModerationCreateResponse
from core.model_runtime.entities.model_entities import ModelPropertyKey
from core.model_runtime.errors.invoke import (
InvokeAuthorizationError,
InvokeBadRequestError,
InvokeConnectionError,
InvokeError,
InvokeRateLimitError,
InvokeServerUnavailableError,
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.moderation_model import ModerationModel
class OpenAIModerationModel(ModerationModel):
"""
Model class for OpenAI text moderation model.
"""
def _invoke(self, model: str, credentials: dict, text: str, user: Optional[str] = None) -> bool:
"""
Invoke moderation model
:param model: model name
:param credentials: model credentials
:param text: text to moderate
:param user: unique user id
:return: false if text is safe, true otherwise
"""
# transform credentials to kwargs for model instance
credentials_kwargs = self._to_credential_kwargs(credentials)
# init model client
client = OpenAI(**credentials_kwargs)
# chars per chunk
length = self._get_max_characters_per_chunk(model, credentials)
text_chunks = [text[i : i + length] for i in range(0, len(text), length)]
max_text_chunks = self._get_max_chunks(model, credentials)
chunks = [text_chunks[i : i + max_text_chunks] for i in range(0, len(text_chunks), max_text_chunks)]
for text_chunk in chunks:
moderation_result = self._moderation_invoke(model=model, client=client, texts=text_chunk)
for result in moderation_result.results:
if result.flagged is True:
return True
return False
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate model credentials
:param model: model name
:param credentials: model credentials
:return:
"""
try:
# transform credentials to kwargs for model instance
credentials_kwargs = self._to_credential_kwargs(credentials)
client = OpenAI(**credentials_kwargs)
# call moderation model
self._moderation_invoke(
model=model,
client=client,
texts=["ping"],
)
except Exception as ex:
raise CredentialsValidateFailedError(str(ex))
def _moderation_invoke(self, model: str, client: OpenAI, texts: list[str]) -> ModerationCreateResponse:
"""
Invoke moderation model
:param model: model name
:param client: model client
:param texts: texts to moderate
:return: false if text is safe, true otherwise
"""
# call moderation model
moderation_result = client.moderations.create(model=model, input=texts)
return moderation_result
def _get_max_characters_per_chunk(self, model: str, credentials: dict) -> int:
"""
Get max characters per chunk
:param model: model name
:param credentials: model credentials
:return: max characters per chunk
"""
model_schema = self.get_model_schema(model, credentials)
if model_schema and ModelPropertyKey.MAX_CHARACTERS_PER_CHUNK in model_schema.model_properties:
max_characters_per_chunk: int = model_schema.model_properties[ModelPropertyKey.MAX_CHARACTERS_PER_CHUNK]
return max_characters_per_chunk
return 2000
def _get_max_chunks(self, model: str, credentials: dict) -> int:
"""
Get max chunks for given embedding model
:param model: model name
:param credentials: model credentials
:return: max chunks
"""
model_schema = self.get_model_schema(model, credentials)
if model_schema and ModelPropertyKey.MAX_CHUNKS in model_schema.model_properties:
max_chunks: int = model_schema.model_properties[ModelPropertyKey.MAX_CHUNKS]
return max_chunks
return 1
def _to_credential_kwargs(self, credentials: Mapping) -> dict:
"""
Transform credentials to kwargs for model instance
:param credentials:
:return:
"""
credentials_kwargs = {
"api_key": credentials["openai_api_key"],
"timeout": Timeout(315.0, read=300.0, write=10.0, connect=5.0),
"max_retries": 1,
}
if credentials.get("openai_api_base"):
openai_api_base = credentials["openai_api_base"].rstrip("/")
credentials_kwargs["base_url"] = openai_api_base + "/v1"
if "openai_organization" in credentials:
credentials_kwargs["organization"] = credentials["openai_organization"]
return credentials_kwargs
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
The key is the error type thrown to the caller
The value is the error type thrown by the model,
which needs to be converted into a unified error type for the caller.
:return: Invoke error mapping
"""
return {
InvokeConnectionError: [openai.APIConnectionError, openai.APITimeoutError],
InvokeServerUnavailableError: [openai.InternalServerError],
InvokeRateLimitError: [openai.RateLimitError],
InvokeAuthorizationError: [openai.AuthenticationError, openai.PermissionDeniedError],
InvokeBadRequestError: [
openai.BadRequestError,
openai.NotFoundError,
openai.UnprocessableEntityError,
openai.APIError,
],
}
@@ -1,22 +0,0 @@
- claude-3-haiku@20240307
- claude-3-opus@20240229
- claude-3-sonnet@20240229
- claude-3-5-sonnet-v2@20241022
- claude-3-5-sonnet@20240620
- gemini-1.0-pro-vision-001
- gemini-1.0-pro-002
- gemini-1.5-flash-001
- gemini-1.5-flash-002
- gemini-1.5-pro-001
- gemini-1.5-pro-002
- gemini-2.0-flash-001
- gemini-2.0-flash-exp
- gemini-2.0-flash-lite-preview-02-05
- gemini-2.0-flash-thinking-exp-01-21
- gemini-2.0-flash-thinking-exp-1219
- gemini-2.0-pro-exp-02-05
- gemini-exp-1114
- gemini-exp-1121
- gemini-exp-1206
- gemini-flash-experimental
- gemini-pro-experimental
@@ -1,41 +0,0 @@
model: gemini-2.0-flash-001
label:
en_US: Gemini 2.0 Flash 001
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
- document
- video
- audio
model_properties:
mode: chat
context_size: 1048576
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_output_tokens
use_template: max_tokens
default: 8192
min: 1
max: 8192
- name: json_schema
use_template: json_schema
pricing:
input: '0.00'
output: '0.00'
unit: '0.000001'
currency: USD
@@ -1,41 +0,0 @@
model: gemini-2.0-flash-lite-preview-02-05
label:
en_US: Gemini 2.0 Flash Lite Preview 0205
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
- document
- video
- audio
model_properties:
mode: chat
context_size: 1048576
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_output_tokens
use_template: max_tokens
default: 8192
min: 1
max: 8192
- name: json_schema
use_template: json_schema
pricing:
input: '0.00'
output: '0.00'
unit: '0.000001'
currency: USD
@@ -1,39 +0,0 @@
model: gemini-2.0-flash-thinking-exp-01-21
label:
en_US: Gemini 2.0 Flash Thinking Exp 0121
model_type: llm
features:
- agent-thought
- vision
- document
- video
- audio
model_properties:
mode: chat
context_size: 32767
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_output_tokens
use_template: max_tokens
default: 8192
min: 1
max: 8192
- name: json_schema
use_template: json_schema
pricing:
input: '0.00'
output: '0.00'
unit: '0.000001'
currency: USD

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