├── backend ├── apps │ ├── __init__.py │ ├── manage │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── loonflow_filter.py │ │ ├── urls.py │ │ └── models.py │ ├── util │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── views.py │ │ ├── apps.py │ │ └── models.py │ ├── ticket │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── apps.py │ │ └── urls.py │ ├── workflow │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── apps.py │ │ └── urls.py │ ├── account │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── apps.py │ │ └── urls.py │ ├── loon_model_base_admin.py │ ├── archived_model.py │ ├── homepage_view.py │ └── loon_base_view.py ├── service │ ├── __init__.py │ ├── hook │ │ ├── __init__.py │ │ └── hook_base_service.py │ ├── util │ │ ├── __init__.py │ │ ├── link_service.py │ │ ├── archive_service.py │ │ └── encrypt_service.py │ ├── account │ │ ├── __init__.py │ │ ├── account_auth_service.py │ │ └── account_tenant_service.py │ ├── common │ │ ├── __init__.py │ │ ├── log_service.py │ │ └── schema_valid_service.py │ ├── exception │ │ ├── __init__.py │ │ └── custom_common_exception.py │ ├── manage │ │ ├── __init__.py │ │ ├── overview_service.py │ │ └── common_config_service.py │ ├── permission │ │ ├── __init__.py │ │ └── user_permission.py │ ├── ticket │ │ └── __init__.py │ ├── workflow │ │ └── __init__.py │ ├── base_service.py │ ├── csrf_service.py │ ├── format_response.py │ └── redis_pool.py ├── settings │ ├── __init__.py │ ├── test.py │ └── test.py.sample ├── tests │ ├── __init__.py │ ├── test_models │ │ ├── __init__.py │ │ └── test_account_model.py │ ├── test_views │ │ ├── __init__.py │ │ ├── test_account_view.py │ │ └── test_workflow_view.py │ ├── test_services │ │ └── __init__.py │ ├── set_test_env.sh │ ├── fixtures │ │ └── workflows.json │ └── base.py ├── requirements │ ├── dev.txt │ ├── pro.txt │ ├── test.txt │ └── common.txt ├── static │ └── images │ │ └── tenant0001.png ├── loonflow │ ├── __init__.py │ ├── contexts.py │ ├── wsgi.py │ └── urls.py ├── .coveragerc ├── manage_test.py └── manage.py ├── docker_compose_deploy ├── __init__.py ├── .env ├── loonflow-ui │ ├── Dockerfile │ └── nginx.conf └── loonflow-backend │ └── Dockerfile ├── frontend ├── src │ ├── types │ │ ├── draft-js.d.ts │ │ ├── readme.md │ │ ├── common.ts │ │ ├── notification.ts │ │ ├── role.ts │ │ ├── dept.ts │ │ ├── tenant.ts │ │ ├── user.ts │ │ └── application.ts │ ├── react-app-env.d.ts │ ├── components │ │ ├── Setting │ │ │ ├── Application │ │ │ │ └── index.tsx │ │ │ └── Notification │ │ │ │ └── index.tsx │ │ ├── commonComponents │ │ │ ├── Snackbar │ │ │ │ ├── index.tsx │ │ │ │ ├── GlobalSnackbar.tsx │ │ │ │ └── SnackbarProvider.tsx │ │ │ └── DynamicLocalizationProvider.tsx │ │ ├── Ticket │ │ │ ├── AllTicket │ │ │ │ └── index.tsx │ │ │ ├── OwnerTicket │ │ │ │ └── index.tsx │ │ │ ├── ViewTicket │ │ │ │ └── index.tsx │ │ │ ├── DutyTicket │ │ │ │ └── index.tsx │ │ │ ├── RelationTicket │ │ │ │ └── index.tsx │ │ │ ├── InterveneTicket │ │ │ │ └── index.tsx │ │ │ ├── TicketDetailPage │ │ │ │ └── index.tsx │ │ │ └── NewTicketPage │ │ │ │ └── index.tsx │ │ ├── Workflow │ │ │ └── WorkflowDetail │ │ │ │ ├── WorkflowValidation │ │ │ │ ├── types.ts │ │ │ │ ├── i18n.ts │ │ │ │ ├── edgeConditionValidation.ts │ │ │ │ ├── normalNodeValidation.ts │ │ │ │ ├── formValidation.ts │ │ │ │ ├── timerNodeValidation.ts │ │ │ │ ├── connectivityValidation.ts │ │ │ │ └── exclusiveNodeValidation.ts │ │ │ │ └── checkWorkflowCompatibility.ts │ │ ├── formFields │ │ │ ├── WorkflowInfoField.tsx │ │ │ ├── TicketNodesField.tsx │ │ │ ├── TicketCurrentAssigneeInfosField.tsx │ │ │ ├── TicketCreateAtField.tsx │ │ │ ├── TicketActStateField.tsx │ │ │ ├── TicketCreatorField.tsx │ │ │ ├── types.ts │ │ │ ├── TextField.tsx │ │ │ └── TextAreaField.tsx │ │ ├── home │ │ │ └── HomePage.tsx │ │ ├── home2 │ │ │ └── HomePage.tsx │ │ └── Organization │ │ │ └── UserDept │ │ │ └── index.tsx │ ├── loonflow.svg │ ├── Loonflow_logo.png │ ├── i18n │ │ └── locales │ │ │ ├── zh-CN │ │ │ ├── department.json │ │ │ ├── signIn.json │ │ │ ├── menu.json │ │ │ ├── layout.json │ │ │ ├── role.json │ │ │ ├── user.json │ │ │ ├── setting.json │ │ │ └── ticket.json │ │ │ └── en-US │ │ │ ├── department.json │ │ │ ├── signIn.json │ │ │ ├── menu.json │ │ │ ├── layout.json │ │ │ ├── role.json │ │ │ ├── user.json │ │ │ ├── setting.json │ │ │ └── ticket.json │ ├── setupTests.ts │ ├── App.test.tsx │ ├── services │ │ ├── authService.ts │ │ ├── tenant.ts │ │ ├── API.d.ts │ │ ├── api.ts │ │ ├── notification.ts │ │ └── role.ts │ ├── index.css │ ├── hooks │ │ └── useSnackbar.ts │ ├── reportWebVitals.ts │ ├── utils │ │ ├── PrivateRoute.tsx │ │ ├── jwt.ts │ │ └── cookie.ts │ ├── App.css │ ├── store │ │ ├── tenantSlice.ts │ │ ├── index.ts │ │ └── authSlice.ts │ └── index.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── loonflow_logo.png │ ├── loonflow_logo1.png │ ├── manifest.json │ └── index.html ├── .yarn │ └── install-state.gz ├── .env ├── .yarnrc.yml ├── craco.config.js ├── tsc-fast.js └── tsconfig.json ├── static └── images │ └── donation_code.png ├── sphinx_docs ├── source │ ├── images │ │ ├── tenant_info.png │ │ ├── allow_withdraw.png │ │ ├── donation_code.png │ │ ├── new_workflow.png │ │ ├── title_property.png │ │ ├── category_tickets.png │ │ ├── fill_ticket_form.png │ │ ├── release_workflow.png │ │ ├── role_management.png │ │ ├── select_workflow.png │ │ ├── workflow_diagram.png │ │ ├── ticket_detail_page.png │ │ ├── voluntary_pick_up.png │ │ ├── use_the_new_workflow.png │ │ ├── user_dept_management.png │ │ ├── application_management.png │ │ ├── fill_workflow_basic_info.png │ │ ├── notification_management.png │ │ ├── workbench_duty_tickets.png │ │ ├── fill_workflow_form_design.png │ │ ├── workflow_version_management.png │ │ ├── fill_workflow_process_design01.png │ │ ├── fill_workflow_process_design02.png │ │ ├── fill_workflow_process_design03.png │ │ └── start_node_title_auto_generate.png │ ├── api_reference │ │ └── postman.rst │ ├── others │ │ └── release_notes │ │ │ ├── r1.x │ │ │ ├── index.rst │ │ │ └── r1.0.0.rst │ │ │ ├── r2.x │ │ │ ├── index.rst │ │ │ └── r2.0.0.rst │ │ │ ├── r3.x │ │ │ ├── index.rst │ │ │ ├── r3.0.1+.rst │ │ │ └── r3.0.0.rst │ │ │ ├── r0.x │ │ │ └── index.rst │ │ │ └── index.rst │ ├── user_guide │ │ ├── organization │ │ │ ├── index.rst │ │ │ ├── role.rst │ │ │ └── user_dept.rst │ │ ├── setting │ │ │ ├── index.rst │ │ │ ├── application.rst │ │ │ ├── tenant.rst │ │ │ └── notification.rst │ │ ├── workflow │ │ │ ├── index.rst │ │ │ └── workflow_version.rst │ │ └── ticket │ │ │ ├── index.rst │ │ │ ├── submit_ticket.rst │ │ │ ├── handle_ticket.rst │ │ │ └── query_ticket.rst │ ├── core_concepts │ │ └── ticket_system_fundamentals │ │ │ └── index.rst │ ├── introduction │ │ ├── welcome │ │ │ ├── index.rst │ │ │ ├── about_loonflow.rst │ │ │ └── getting_help.rst │ │ └── who_need_read.rst │ ├── installtion_deployment │ │ ├── installation_guide │ │ │ └── index.rst │ │ └── system_requirements │ │ │ ├── index.rst │ │ │ └── os_environment_requirements.rst │ ├── locale │ │ └── zh_CN │ │ │ └── LC_MESSAGES │ │ │ ├── user_guide │ │ │ ├── setting │ │ │ │ ├── index.po │ │ │ │ ├── application.po │ │ │ │ └── tenant.po │ │ │ ├── ticket │ │ │ │ └── index.po │ │ │ ├── workflow │ │ │ │ ├── index.po │ │ │ │ └── workflow_version.po │ │ │ └── organization │ │ │ │ ├── index.po │ │ │ │ └── role.po │ │ │ ├── introduction │ │ │ ├── welcome │ │ │ │ ├── index.po │ │ │ │ ├── about_loonflow.po │ │ │ │ └── getting_help.po │ │ │ └── who_need_read.po │ │ │ ├── others │ │ │ └── release_notes │ │ │ │ ├── r1.x │ │ │ │ └── index.po │ │ │ │ ├── r2.x │ │ │ │ ├── index.po │ │ │ │ └── r2.0.0.po │ │ │ │ ├── r3.x │ │ │ │ └── index.po │ │ │ │ ├── index.po │ │ │ │ └── r0.x │ │ │ │ └── index.po │ │ │ ├── installtion_deployment │ │ │ ├── installation_guide │ │ │ │ └── index.po │ │ │ └── system_requirements │ │ │ │ └── index.po │ │ │ ├── core_concepts │ │ │ └── ticket_system_fundamentals │ │ │ │ └── index.po │ │ │ ├── api_reference.po │ │ │ ├── api_reference │ │ │ └── postman.po │ │ │ └── index.po │ └── index.rst ├── README.md ├── requirements.txt ├── make.bat ├── Makefile └── .readthedocs.yaml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.yaml │ └── feature_request.yaml ├── workflows │ ├── unit_test.yml.disabled │ └── image_build_backend.yml └── ISSUE_TEMPLATE.md ├── Roadmap_zh.md └── .gitignore /backend/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/manage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/hook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker_compose_deploy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/exception/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/manage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/permission/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/ticket/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/service/workflow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/test_models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/test_views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/manage/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/ticket/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/util/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/test_services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/manage/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/apps/workflow/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/types/draft-js.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'draft-js'; -------------------------------------------------------------------------------- /backend/requirements/dev.txt: -------------------------------------------------------------------------------- 1 | #requrements/dev.txt 2 | -r common.txt 3 | -------------------------------------------------------------------------------- /backend/service/base_service.py: -------------------------------------------------------------------------------- 1 | class BaseService(object): 2 | pass 3 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /backend/apps/ticket/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.ticket.apps.TicketConfig' 2 | -------------------------------------------------------------------------------- /backend/requirements/pro.txt: -------------------------------------------------------------------------------- 1 | #requrements/pro.txt 2 | -r common.txt 3 | gunicorn==22.0.0 -------------------------------------------------------------------------------- /backend/apps/account/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.account.apps.AccountConfig' 2 | -------------------------------------------------------------------------------- /backend/apps/util/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/apps/account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/apps/ticket/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/apps/util/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/apps/util/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /backend/apps/workflow/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.workflow.apps.WorkflowConfig' 2 | 3 | -------------------------------------------------------------------------------- /backend/apps/workflow/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/apps/account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.workflow.apps.WorkflowConfig' 2 | -------------------------------------------------------------------------------- /backend/requirements/test.txt: -------------------------------------------------------------------------------- 1 | #requrements/test.txt 2 | -r common.txt 3 | mock==4.0.2 4 | coverage==5.2.1 -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/components/Setting/Application/index.tsx: -------------------------------------------------------------------------------- 1 | export { ApplicationList } from './ApplicationList'; -------------------------------------------------------------------------------- /frontend/src/loonflow.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/src/loonflow.svg -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/.yarn/install-state.gz -------------------------------------------------------------------------------- /frontend/src/Loonflow_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/src/Loonflow_logo.png -------------------------------------------------------------------------------- /frontend/src/components/Setting/Notification/index.tsx: -------------------------------------------------------------------------------- 1 | export { NotificationList } from './NotificationList'; 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/components/commonComponents/Snackbar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as TopSnackbar } from './GlobalSnackbar'; -------------------------------------------------------------------------------- /static/images/donation_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/static/images/donation_code.png -------------------------------------------------------------------------------- /frontend/public/loonflow_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/loonflow_logo.png -------------------------------------------------------------------------------- /frontend/public/loonflow_logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/frontend/public/loonflow_logo1.png -------------------------------------------------------------------------------- /backend/static/images/tenant0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/backend/static/images/tenant0001.png -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | # 禁用源码映射,减少构建内存使用 2 | GENERATE_SOURCEMAP=false 3 | 4 | # 可选:提高 Node.js 内存限制 5 | NODE_OPTIONS=--max_old_space_size=6144 6 | -------------------------------------------------------------------------------- /sphinx_docs/source/images/tenant_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/tenant_info.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/allow_withdraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/allow_withdraw.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/donation_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/donation_code.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/new_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/new_workflow.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/title_property.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/title_property.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/category_tickets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/category_tickets.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_ticket_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_ticket_form.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/release_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/release_workflow.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/role_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/role_management.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/select_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/select_workflow.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/workflow_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workflow_diagram.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/ticket_detail_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/ticket_detail_page.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/voluntary_pick_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/voluntary_pick_up.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/use_the_new_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/use_the_new_workflow.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/user_dept_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/user_dept_management.png -------------------------------------------------------------------------------- /backend/loonflow/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from tasks import app as celery_app 4 | 5 | __all__ = ['celery_app'] -------------------------------------------------------------------------------- /sphinx_docs/source/api_reference/postman.rst: -------------------------------------------------------------------------------- 1 | Postman Collection 2 | ==================== 3 | 4 | 5 | https://documenter.getpostman.com/view/15031929/2sB3WyJbap -------------------------------------------------------------------------------- /sphinx_docs/source/images/application_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/application_management.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_workflow_basic_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_basic_info.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/notification_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/notification_management.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/workbench_duty_tickets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workbench_duty_tickets.png -------------------------------------------------------------------------------- /backend/apps/ticket/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TicketConfig(AppConfig): 5 | name = 'apps.ticket' 6 | verbose_name = '工单' 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_workflow_form_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_form_design.png -------------------------------------------------------------------------------- /backend/apps/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = 'apps.account' 6 | verbose_name = '账户' 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/images/workflow_version_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/workflow_version_management.png -------------------------------------------------------------------------------- /backend/apps/workflow/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WorkflowConfig(AppConfig): 5 | name = 'apps.workflow' 6 | verbose_name = '工作流' 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_workflow_process_design01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design01.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_workflow_process_design02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design02.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/fill_workflow_process_design03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/fill_workflow_process_design03.png -------------------------------------------------------------------------------- /sphinx_docs/source/images/start_node_title_auto_generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackholll/loonflow/HEAD/sphinx_docs/source/images/start_node_title_auto_generate.png -------------------------------------------------------------------------------- /frontend/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | packageExtensions: 4 | "@types/node@*": 5 | peerDependenciesMeta: 6 | typescript: 7 | optional: true 8 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r1.x/index.rst: -------------------------------------------------------------------------------- 1 | r1.x Release Notes 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | r1.0.0 8 | r1.0.1+ 9 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r2.x/index.rst: -------------------------------------------------------------------------------- 1 | r2.x Release Notes 2 | ===================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | r2.0.0 8 | r2.0.1+ 9 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r3.x/index.rst: -------------------------------------------------------------------------------- 1 | r3.x Release Notes 2 | ===================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | r3.0.0 8 | r3.0.1+ 9 | -------------------------------------------------------------------------------- /backend/apps/util/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.util' 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/organization/index.rst: -------------------------------------------------------------------------------- 1 | 3. Organization Management 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | user_dept 8 | role 9 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/setting/index.rst: -------------------------------------------------------------------------------- 1 | 4. Setting Management 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | application 8 | notification 9 | tenant -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/workflow/index.rst: -------------------------------------------------------------------------------- 1 | 2. Workflow Management 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | new_update_workflow 8 | workflow_version -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: blackholllcn 4 | custom: ['https://github.com/blackholll/loonflow/blob/master/README_zh.md#%E6%AC%A2%E8%BF%8E%E6%8D%90%E5%8A%A9'] -------------------------------------------------------------------------------- /backend/loonflow/contexts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 全局变量、常量 3 | # 4 | from django.conf import settings 5 | 6 | 7 | def global_variables(request): 8 | return {'VERSION': settings.VERSION} 9 | -------------------------------------------------------------------------------- /frontend/src/components/Ticket/AllTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | function AllTicket() { 4 | return 5 | } 6 | 7 | export default AllTicket; 8 | -------------------------------------------------------------------------------- /frontend/src/components/Ticket/OwnerTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | function OwerTicket() { 4 | return 5 | } 6 | 7 | export default OwerTicket; -------------------------------------------------------------------------------- /frontend/src/components/Ticket/ViewTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | function ViewTicket() { 4 | return 5 | } 6 | 7 | export default ViewTicket; -------------------------------------------------------------------------------- /frontend/src/components/Ticket/DutyTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | 4 | function DutyTicket() { 5 | return 6 | } 7 | 8 | export default DutyTicket; -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/ticket/index.rst: -------------------------------------------------------------------------------- 1 | 1. Ticket Management 2 | ===================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | submit_ticket 8 | query_ticket 9 | handle_ticket 10 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/department.json: -------------------------------------------------------------------------------- 1 | { 2 | "parentDepartment": "上级部门", 3 | "parentDepartmentHelpText": "请在左侧部门树中选择上级部门", 4 | "departmentLeader": "部门负责人", 5 | "departmentApprover": "部门审批人" 6 | } -------------------------------------------------------------------------------- /sphinx_docs/source/core_concepts/ticket_system_fundamentals/index.rst: -------------------------------------------------------------------------------- 1 | 1. Ticketing System Fundamentals 2 | =================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | ticket_workflow 8 | -------------------------------------------------------------------------------- /frontend/src/components/Ticket/RelationTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | function RelationTicket() { 4 | return 5 | } 6 | 7 | export default RelationTicket; -------------------------------------------------------------------------------- /backend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = 4 | apps/* 5 | services/* 6 | omit = 7 | apps/manage/* 8 | 9 | [report] 10 | skip_covered = True 11 | 12 | [html] 13 | directory = coverage_html_report -------------------------------------------------------------------------------- /frontend/src/components/Ticket/InterveneTicket/index.tsx: -------------------------------------------------------------------------------- 1 | import TicketList from '../TicketList'; 2 | 3 | function InterveneTicket() { 4 | return 5 | } 6 | 7 | export default InterveneTicket; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - about: 'Use the Loonflow Community Discord for questions & discussions' 5 | name: ❓ Questions 6 | url: 'https://discord.gg/dh8vBG6J' 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/introduction/welcome/index.rst: -------------------------------------------------------------------------------- 1 | 1. Welcome to LoonFlow 2 | ============================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | about_document 8 | about_loonflow 9 | getting_help 10 | 11 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r0.x/index.rst: -------------------------------------------------------------------------------- 1 | r0.x Release Notes 2 | ===================== 3 | 4 | For detailed release notes of v0.x versions, please refer to GitHub Releases: 5 | 6 | https://github.com/blackholll/loonflow/releases 7 | -------------------------------------------------------------------------------- /sphinx_docs/source/installtion_deployment/installation_guide/index.rst: -------------------------------------------------------------------------------- 1 | 2. Installation Guide 2 | ============================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | developer_setup 8 | deployment_with_docker_compose 9 | 10 | -------------------------------------------------------------------------------- /sphinx_docs/source/installtion_deployment/system_requirements/index.rst: -------------------------------------------------------------------------------- 1 | 1. System Requirements 2 | ============================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | server_hardware_requirements 8 | os_environment_requirements 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/index.rst: -------------------------------------------------------------------------------- 1 | This document contains release notes for all versions of Loonflow. 2 | 3 | .. toctree:: 4 | :maxdepth: 3 5 | :caption: Version History 6 | 7 | r3.x/index 8 | r2.x/index 9 | r1.x/index 10 | r0.x/index 11 | -------------------------------------------------------------------------------- /backend/service/csrf_service.py: -------------------------------------------------------------------------------- 1 | from django.utils.deprecation import MiddlewareMixin 2 | 3 | 4 | class DisableCSRF(MiddlewareMixin): 5 | def process_request(self, request): 6 | if request.path.startswith('/api/'): 7 | setattr(request, '_dont_enforce_csrf_checks', True) 8 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/department.json: -------------------------------------------------------------------------------- 1 | { 2 | "parentDepartment": "Parent Department", 3 | "parentDepartmentHelpText": "please select parent department on the left parent tree", 4 | "departmentLeader": "Department Leader", 5 | "departmentApprover": "Department Approver" 6 | } -------------------------------------------------------------------------------- /backend/service/exception/custom_common_exception.py: -------------------------------------------------------------------------------- 1 | class CustomCommonException(Exception): 2 | """ 3 | custom common exception. this type exception's message will display to user 4 | """ 5 | def __init__(self, message=""): 6 | self.message = message 7 | super().__init__(self.message) 8 | -------------------------------------------------------------------------------- /backend/apps/util/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from apps.loon_base_model import BaseModel 3 | 4 | 5 | # Create your models here. 6 | class Archive(BaseModel): 7 | """archived record""" 8 | model_name = models.CharField("model name", max_length=100) 9 | data = models.JSONField("archived data") 10 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Workflow validation result 3 | */ 4 | export interface WorkflowValidationResult { 5 | /** List of detected problems */ 6 | problems: string[]; 7 | /** Whether there are any problems */ 8 | hasProblems: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/services/authService.ts: -------------------------------------------------------------------------------- 1 | import apiClient from './api'; 2 | 3 | export const login = async (email: string, password: string) => { 4 | try { 5 | const response = await apiClient.post('/api/v1.0/login', { email, password }); 6 | return response.data; 7 | } catch (error) { 8 | throw error; 9 | } 10 | }; -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/signIn.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Loonflow", 3 | "description": "企业级统一工作流解决方案", 4 | "emailLabel": "电子邮件地址", 5 | "passwordLabel": "密码", 6 | "rememberMe": "记住我", 7 | "signInButton": "登录", 8 | "forgotPassword": "忘记密码?", 9 | "noAccount": "还没有账号?注册", 10 | "welcomeBack": "欢迎回来!请登录继续。", 11 | "welcomeMessage": "欢迎!" 12 | } -------------------------------------------------------------------------------- /frontend/src/types/readme.md: -------------------------------------------------------------------------------- 1 | # about 2 | This folder is for all interface definations 3 | 4 | # rule 5 | - use interface instend of type, except interface can not satisfy requirement 6 | - all startswith 'I' 7 | - if the interface is for api response, name should be contain res 8 | - if the interface is for api request, name should be contain req 9 | - 10 | -------------------------------------------------------------------------------- /sphinx_docs/source/introduction/welcome/about_loonflow.rst: -------------------------------------------------------------------------------- 1 | 1.2 About Loonflow 2 | ============================ 3 | 4 | Loonflow is an open-source process automation platform developed based on Django. After multiple versions of iteration and refactoring, we proudly present Loonflow 3.0 - a version that achieves a qualitative leap in visualization, flexibility, and scalability. -------------------------------------------------------------------------------- /backend/apps/manage/templatetags/loonflow_filter.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter() 8 | def add_version(_input): 9 | """ 10 | 加上版本信息?v=xxx,主要是为了避免静态文件缓存 11 | :param _input: 12 | :return: 13 | """ 14 | return '?v=%s' % settings.STATIC_FILES_VERSION 15 | -------------------------------------------------------------------------------- /backend/service/format_response.py: -------------------------------------------------------------------------------- 1 | # import json 2 | from django.http import JsonResponse 3 | 4 | from django.http import HttpResponse 5 | 6 | 7 | def api_response(code, msg='', data=''): 8 | """ 9 | 格式化返回 10 | :param code: 11 | :param msg: 12 | :param data: 13 | :return: 14 | """ 15 | return JsonResponse(dict(code=code, data=data, msg=msg)) 16 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/WorkflowInfoField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | 3 | interface WorkflowInfoFieldProps { 4 | value: any; 5 | } 6 | 7 | export default function WorkflowInfoField({ value }: WorkflowInfoFieldProps) { 8 | return ( 9 | {value?.name ? value.name : ''} 10 | ) 11 | } -------------------------------------------------------------------------------- /backend/apps/loon_model_base_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | class ModelBaseAdmin(admin.ModelAdmin): 5 | list_display = ('creator', 'is_deleted', 'gmt_created', 'gmt_modified') 6 | readonly_fields = ['creator'] 7 | 8 | def save_model(self, request, obj, form, change): 9 | if not obj.creator: 10 | obj.creator = request.user.username 11 | obj.save() 12 | -------------------------------------------------------------------------------- /sphinx_docs/source/introduction/who_need_read.rst: -------------------------------------------------------------------------------- 1 | 2. Who Need to Read This Guide 2 | ================================ 3 | 4 | This guide is intended for: 5 | 6 | - **End Users** - Who submit and handle tickets in their daily work 7 | - **Admins** - Who are responsible for deploying, customizing, and maintaining the LoonFlow instance 8 | - **Workflow Admins** - Who are responsible for configuring workflows, Intervene tickets 9 | -------------------------------------------------------------------------------- /frontend/src/components/home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 | Welcome to the Home Page 8 | 9 | 10 | This is the home page content. 11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /backend/service/manage/overview_service.py: -------------------------------------------------------------------------------- 1 | from service.base_service import BaseService 2 | 3 | 4 | class OverviewService(BaseService): 5 | """ 6 | 总览 7 | """ 8 | def __init__(self): 9 | pass 10 | 11 | def get_new_ticket_type_count_statistics_info(self, start_time: str, end_time: str)->tuple: 12 | """ 13 | 获取每种类型工单创建数量统计数据 14 | :return: 15 | """ 16 | return '', '' 17 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TicketNodesField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | 3 | interface TicketNodesFieldProps { 4 | value: any | null; 5 | } 6 | 7 | export default function TicketNodesField({ value }: TicketNodesFieldProps) { 8 | return ( 9 | {value ? value.map((item: any) => item.name).join(',') : ''} 10 | ) 11 | } -------------------------------------------------------------------------------- /frontend/src/components/home2/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | 3 | export default function Home2() { 4 | return ( 5 |
6 | 7 | Welcome to the Home Page222222 8 | 9 | 10 | This is the home page content.2222222222 11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /docker_compose_deploy/.env: -------------------------------------------------------------------------------- 1 | # please change below info, especially REDIS_PASSWORD and POSTGRES_PASSWORD and ADMIN_PASSWORD 2 | REDIS_PASSWORD=loonflow123 3 | REDIS_PORT=6381 4 | POSTGRES_DB=loonflow 5 | POSTGRES_USER=loonflow 6 | POSTGRES_PASSWORD=rALnSWb!G^X4 7 | POSTGRES_PORT=5433 8 | PG_DATA_VOLUME=loonflow-pg-data 9 | 10 | TENANT_NAME=loonapp 11 | TENANT_DOMAIN=loonapp.com 12 | ADMIN_NAME=admin 13 | ADMIN_EMAIL=admin@loonapp.com 14 | ADMIN_PASSWORD=123456 15 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/organization/role.rst: -------------------------------------------------------------------------------- 1 | Role Management 2 | ================== 3 | 4 | Loonflow supports managing roles so you can create, edit, delete, and maintain role members. Roles are primarily used when configuring workflows to specify handlers, allowing you to assign a role as the processor for a workflow node. 5 | 6 | .. figure:: ../../images/role_management.png 7 | :width: 100% 8 | :align: center 9 | :alt: role management 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/hooks/useSnackbar.ts: -------------------------------------------------------------------------------- 1 | // src/hooks/useSnackbar.ts 2 | import { useContext } from 'react'; 3 | import { SnackbarContext } from '../components/commonComponents/Snackbar/SnackbarProvider'; 4 | 5 | const useSnackbar = () => { 6 | const context = useContext(SnackbarContext); 7 | if (context === undefined) { 8 | throw new Error('useSnackbar must be used within a SnackbarProvider'); 9 | } 10 | return context; 11 | }; 12 | 13 | export default useSnackbar; -------------------------------------------------------------------------------- /backend/apps/archived_model.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from apps.loon_base_model import BaseModel 3 | from django.utils.translation import gettext_lazy as _ 4 | from django.contrib.postgres.fields import JSONField 5 | 6 | 7 | class Archived(BaseModel): 8 | """archived record""" 9 | model_name = models.CharField("model name", max_length=100) 10 | data = JSONField(_('archived data')) 11 | 12 | class Meta: 13 | app_label = 'archived' 14 | 15 | -------------------------------------------------------------------------------- /backend/loonflow/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for loonflow project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.config") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/service/redis_pool.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from django.conf import settings 3 | 4 | 5 | redis_host = settings.REDIS_HOST 6 | redis_db = settings.REDIS_DB 7 | redis_port = settings.REDIS_PORT 8 | redis_password = settings.REDIS_PASSWORD 9 | if redis_password: 10 | POOL = redis.ConnectionPool(host=redis_host, port=redis_port, password=redis_password, max_connections=1000) 11 | else: 12 | POOL = redis.ConnectionPool(host=redis_host, port=redis_port, max_connections=1000) 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TicketCurrentAssigneeInfosField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | 3 | interface TicketCurrentAssigneeInfosFieldProps { 4 | value: any; 5 | } 6 | 7 | export default function TicketCurrentAssigneeInfosField({ value }: TicketCurrentAssigneeInfosFieldProps) { 8 | return ( 9 | {value ? value.map((item: any) => item.assignee).join(',') : ''} 10 | ) 11 | } -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/signIn.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Loonflow", 3 | "description": "Unified Workflow Solutions for the Enterprise", 4 | "emailLabel": "Email Address", 5 | "passwordLabel": "Password", 6 | "rememberMe": "Remember me", 7 | "signInButton": "Sign In", 8 | "forgotPassword": "Forgot password?", 9 | "noAccount": "Don't have an account? Sign Up", 10 | "welcomeBack": "Welcome back! Please sign in to continue.", 11 | "welcomeMessage": "Welcome to our app!" 12 | } -------------------------------------------------------------------------------- /frontend/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export interface IApiResponse { 2 | code: 0; 3 | data: T; 4 | msg: string; 5 | } 6 | 7 | export interface IApiErrResponse { 8 | code: -1; 9 | msg: string; 10 | } 11 | 12 | export interface ISimpleEntity { 13 | id: string, 14 | name: string 15 | } 16 | 17 | export interface ISimpleQueryParam { 18 | page?: number; 19 | perPage?: number; 20 | searchKey?: string 21 | 22 | } 23 | 24 | export interface ILabel { 25 | [key: string]: any 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TicketCreateAtField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import { formatDate } from '../../utils/dateFormat'; 3 | 4 | 5 | 6 | interface TicketCreatorFieldProps { 7 | value: string | null; 8 | } 9 | 10 | export default function TicketCreatedAtField({ value }: TicketCreatorFieldProps) { 11 | return ( 12 | {value ? formatDate(value) : ''} 13 | ) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /backend/apps/manage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from apps.manage.views import index, CommonConfigView, NotificationView, SimpleNotificationView, NotificationDetailView 3 | 4 | urlpatterns = [ 5 | path("", index), 6 | path("/common", CommonConfigView.as_view()), 7 | path("/notifications", NotificationView.as_view()), 8 | path("/simple_notifications", SimpleNotificationView.as_view()), 9 | path("/notifications/", NotificationDetailView.as_view()) 10 | ] 11 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TicketActStateField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | interface TicketActStateFieldProps { 5 | value: string; 6 | } 7 | 8 | export default function TicketActStateField({ value }: TicketActStateFieldProps) { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 | {t(`ticket.state.${value}`)} 13 | ) 14 | } -------------------------------------------------------------------------------- /frontend/src/components/formFields/TicketCreatorField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography, Tooltip } from '@mui/material'; 2 | 3 | 4 | interface TicketCreatorFieldProps { 5 | value: any | null; 6 | } 7 | 8 | export default function TicketCreatorField({ value }: TicketCreatorFieldProps) { 9 | return ( 10 | {value ? `${value.name}(${value.alias})` : ''} 11 | ) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /backend/apps/homepage_view.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.views import View 3 | 4 | 5 | class HomepageView(View): 6 | def get(self, request, *args, **kwargs): 7 | """ 8 | 首页 9 | :param request: 10 | :param args: 11 | :param kwargs: 12 | :return: 13 | """ 14 | return HttpResponse('') -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench": "工作台", 3 | "ticketManagement": "工单管理", 4 | "ticketDuty": "我的待办", 5 | "ticketOwner": "我的申请", 6 | "ticketRelation": "我的关联", 7 | "ticketView": "工单查看", 8 | "ticketIntervene": "工单干预", 9 | "ticketAll": "所有工单", 10 | "workflowManagement": "工作流管理", 11 | "organization": "组织架构", 12 | "userAndDept": "用户及部门", 13 | "role": "角色", 14 | "setting": "系统设置", 15 | "tenant": "租户管理", 16 | "application": "应用管理", 17 | "notification": "通知管理", 18 | "signin": "登录" 19 | } -------------------------------------------------------------------------------- /frontend/src/types/notification.ts: -------------------------------------------------------------------------------- 1 | import { IApiResponse } from './common'; 2 | 3 | 4 | export interface INotificationResEntity { 5 | id: string; 6 | label: string; 7 | createdAt: string; 8 | updatedAt: string; 9 | name: string; 10 | description: string; 11 | type: string; 12 | extra: any; 13 | tenantId: string; 14 | } 15 | 16 | export interface INotificationListResData { 17 | applicationInfoList: INotificationResEntity[] 18 | } 19 | 20 | export interface INotificationListRes extends IApiResponse { } -------------------------------------------------------------------------------- /backend/service/util/link_service.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, data): 3 | self.data = data 4 | self.next = None 5 | 6 | 7 | class LinkedList: 8 | def __int__(self): 9 | self.head = None 10 | 11 | def append(self, data): 12 | new_node = Node(data) 13 | if not self.head: 14 | self.head = new_node 15 | else: 16 | current = self.head 17 | while current.next: 18 | current = current.next 19 | current.next = new_node 20 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "userInfo": "用户信息", 3 | "updateProfileSuccess": "更新用户信息成功", 4 | "updateProfileFail": "更新用户信息失败", 5 | "name": "姓名", 6 | "changePassword": "修改密码", 7 | "currentPassword": "当前密码", 8 | "newPassword": "新密码", 9 | "confirmPassword": "确认新密码", 10 | "changePasswordSuccess": "密码修改成功", 11 | "changePasswordFail": "密码修改失败", 12 | "passwordsNotMatch": "两次输入的新密码不一致", 13 | "currentPasswordRequired": "请输入当前密码", 14 | "newPasswordRequired": "请输入新密码", 15 | "confirmPasswordRequired": "请确认新密码" 16 | } -------------------------------------------------------------------------------- /frontend/src/types/role.ts: -------------------------------------------------------------------------------- 1 | import { IApiResponse } from "./common"; 2 | 3 | export interface IRoleResEntity { 4 | id: string; 5 | label: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | name: string; 9 | description: string; 10 | tenantId: string; 11 | } 12 | 13 | export interface ISimpleRole { 14 | id: string; 15 | name: string; 16 | } 17 | 18 | export interface IRoleListResData { 19 | roleList: IRoleResEntity[] 20 | } 21 | 22 | export interface IRoleListRes extends IApiResponse { } -------------------------------------------------------------------------------- /frontend/src/components/Ticket/TicketDetailPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import TicketDetail from '../TicketDetail'; 4 | 5 | function TicketDetailPage() { 6 | const { ticketId } = useParams(); 7 | const [refreshToken, setRefreshToken] = useState(0); 8 | console.log('ticketId', ticketId) 9 | 10 | 11 | return { setRefreshToken(refreshToken + 1) }} /> 12 | } 13 | 14 | export default TicketDetailPage; -------------------------------------------------------------------------------- /sphinx_docs/source/introduction/welcome/getting_help.rst: -------------------------------------------------------------------------------- 1 | 1.3 Get Help & Community Support 2 | ================================= 3 | 4 | - 📝 GitHub Issues - Submit bug reports and feature requests. https://github.com/blackholll/loonflow/issues 5 | - 💬 Discussion Forum Discord. https://discord.gg/WuppaG638k 6 | - 📧 Commercial Support & Customization: For enterprise-level deep customization, technical training, or deployment support needs, please contact me at [blackholll@163.com;blackholll.cn@gmail.com]. 7 | - 💰 Member Benefits. See https://patreon.com/blackholllcn for more details. -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "roleList": "角色列表", 3 | "addRoleSuccess": "添加角色成功", 4 | "addRoleFail": "添加角色失败", 5 | "updateRoleSuccess": "更新角色成功", 6 | "updateRoleFail": "更新角色失败", 7 | "deleteRoleSuccess": "删除角色成功", 8 | "deleteRoleFail": "删除角色失败", 9 | "roleName": "角色名称", 10 | "roleDescription": "角色描述", 11 | "rolePermission": "角色权限", 12 | "roleMember": "角色成员", 13 | "deleteMemberConfirm": "确认删除成员", 14 | "deleteMemberConfirmMsg": "确定要从角色中删除该成员吗?删除后该成员将失去此角色的所有权限。", 15 | "deleteMemberSuccess": "删除成员成功", 16 | "deleteMemberFail": "删除成员失败" 17 | } -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "leader": "负责人", 3 | "changeParentDept": "修改父部门", 4 | "passwordNotMatch": "两次输入的密码不一致", 5 | "resetPassword": "重置密码", 6 | "confirmResetPassword": "确认重置密码", 7 | "confirmResetPasswordMsg": "密码将被重置为'123456', 请尽快修改密码", 8 | "resetPasswordSuccess": "重置密码成功", 9 | "resetPasswordFail": "重置密码失败", 10 | "deleteUserSuccess": "删除用户成功", 11 | "deleteUserFail": "删除用户失败", 12 | "admin": "管理员", 13 | "workflowAdmin": "工作流管理员", 14 | "normal": "普通用户", 15 | "userDeptHelpText": "用户所属部门,支持设置多个部门,其中第一个部门将作为用户的主要部门,审批时将以主要部门作为用户部门来获取部门审批人" 16 | } -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r2.x/r2.0.0.rst: -------------------------------------------------------------------------------- 1 | r2.0.0 2 | --------- 3 | 4 | - Built-in ticket creation, viewing, processing, and management interface (major change in this release) 5 | - Support for users belonging to multiple departments simultaneously (significant change in this release) 6 | - flowlog API supports specifying order or reverse order 7 | - Workflow configuration interface supports viewing daily new ticket statistics 8 | - Ticket details support administrator intervention 9 | - Support for users to change their own passwords 10 | - Various other optimizations 11 | -------------------------------------------------------------------------------- /backend/service/account/account_auth_service.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend 2 | from django.contrib.auth import get_user_model 3 | 4 | 5 | class EmailBackend(ModelBackend): 6 | def authenticate(self, request, email=None, password=None, **kwargs): 7 | UserModel = get_user_model() 8 | try: 9 | user = UserModel.objects.get(email=email) 10 | except UserModel.DoesNotExist: 11 | return None 12 | else: 13 | if user.check_password(password): 14 | return user 15 | return None 16 | -------------------------------------------------------------------------------- /backend/service/common/log_service.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import traceback 4 | 5 | logger = logging.getLogger('django') 6 | 7 | 8 | def auto_log(func): 9 | """ 10 | auto write log decorator 11 | :param func: 12 | :return: 13 | """ 14 | @functools.wraps(func) 15 | def _deco(*args, **kwargs): 16 | try: 17 | real_func = func(*args, **kwargs) 18 | return real_func 19 | except Exception as e: 20 | logger.error(traceback.format_exc()) 21 | return False, e.__str__() 22 | return _deco 23 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/utils/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { useSelector } from 'react-redux'; 4 | import { RootState } from '../store'; 5 | 6 | interface PrivateRouteProps { 7 | element: React.ReactElement | null; 8 | } 9 | 10 | const PrivateRoute: React.FC = ({ element }) => { 11 | const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated); 12 | console.log('routeisAuthenticated:', isAuthenticated); 13 | return isAuthenticated ? element : ; 14 | }; 15 | 16 | export default PrivateRoute; 17 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench": "Workbench", 3 | "ticketManagement": "Ticket Management", 4 | "ticketDuty": "My Duty", 5 | "ticketOwner": "My Owner", 6 | "ticketRelation": "My Relation", 7 | "ticketView": "My View", 8 | "ticketIntervene": "My Intervene", 9 | "ticketAll": "All tickets", 10 | "workflowManagement": "Workflow Management", 11 | "organization": "Organization", 12 | "userAndDept": "User & Dept", 13 | "role": "Role", 14 | "setting": "Setting", 15 | "tenant": "Tenant", 16 | "application": "Application", 17 | "notification": "Notification", 18 | "signin": "Sign In" 19 | } -------------------------------------------------------------------------------- /docker_compose_deploy/loonflow-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.18.0-slim AS builder 2 | 3 | RUN corepack enable && corepack prepare yarn@4.4.1 --activate 4 | 5 | COPY ./frontend /app/loonflow/ 6 | WORKDIR /app/loonflow 7 | ENV BROWSERSLIST_IGNORE_OLD_DATA=true 8 | 9 | RUN yarn install --immutable && yarn build 10 | 11 | 12 | FROM nginx:1.24.0-alpine 13 | LABEL maintainer=blackholll@163.com 14 | 15 | COPY docker_compose_deploy/loonflow-ui/nginx.conf /etc/nginx/nginx.conf 16 | 17 | COPY --from=builder /app/loonflow/build/ /usr/share/nginx/html/ 18 | 19 | RUN mkdir /var/log/loonflow/ 20 | EXPOSE 80 21 | 22 | CMD ["nginx", "-g", "daemon off;"] 23 | -------------------------------------------------------------------------------- /frontend/src/types/dept.ts: -------------------------------------------------------------------------------- 1 | import { ISimpleUser } from "./user"; 2 | 3 | export interface ISimpleDept { 4 | id: string; 5 | name: string; 6 | children: ISimpleDept[]; 7 | } 8 | 9 | export interface ISimpleDeptPath { 10 | id: string; 11 | name: string; 12 | path: string; 13 | } 14 | 15 | export interface ISimpleParentDept { 16 | id: string; 17 | name: string; 18 | } 19 | 20 | export interface IDept { 21 | id: string; 22 | name: string; 23 | label: any; 24 | creatorInfo: ISimpleUser, 25 | leaderInfo: ISimpleUser, 26 | approverInfoList: ISimpleUser[], 27 | children: IDept, 28 | parentDeptInfo: ISimpleParentDept, 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /sphinx_docs/README.md: -------------------------------------------------------------------------------- 1 | # loonflow document 2 | This is mult-language document, default language is English. 3 | 4 | ## quick start 5 | 6 | ### 1. install dependencies 7 | ```bash 8 | cd sphinx_doc 9 | pip install -r requirements.txt 10 | ``` 11 | 12 | ### 2. build html 13 | ```bash 14 | 15 | # get latest pot files 16 | make gettext 17 | 18 | # gen po files 19 | make update-translations 20 | 21 | # update po files 22 | 23 | # build English document 24 | make html 25 | # build Chinese document 26 | make html-zh 27 | 28 | # review document 29 | # English: open _build/html/index.html in browser 30 | # Chinese: open _build/html-zh/index.html in browser 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /sphinx_docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==1.0.0 2 | babel==2.17.0 3 | certifi==2025.11.12 4 | charset-normalizer==3.4.4 5 | click==8.3.1 6 | docutils==0.21.2 7 | idna==3.11 8 | imagesize==1.4.1 9 | jieba==0.42.1 10 | Jinja2==3.1.6 11 | MarkupSafe==3.0.3 12 | packaging==25.0 13 | Pygments==2.19.2 14 | requests==2.32.5 15 | roman-numerals-py==3.1.0 16 | snowballstemmer==3.0.1 17 | Sphinx==8.2.3 18 | sphinx-intl==2.3.2 19 | sphinx-rtd-theme==3.0.2 20 | sphinxcontrib-applehelp==2.0.0 21 | sphinxcontrib-devhelp==2.0.0 22 | sphinxcontrib-htmlhelp==2.1.0 23 | sphinxcontrib-jquery==4.1 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==2.0.0 26 | sphinxcontrib-serializinghtml==2.0.0 27 | urllib3==2.5.0 -------------------------------------------------------------------------------- /backend/service/common/schema_valid_service.py: -------------------------------------------------------------------------------- 1 | from service.base_service import BaseService 2 | from schema import SchemaUnexpectedTypeError 3 | 4 | 5 | class SchemaValidService(BaseService): 6 | 7 | @staticmethod 8 | def parse_integer_list(value): 9 | print("11111") 10 | if value is None: 11 | return 12 | else: 13 | try: 14 | return [int(item if item != "" else 0) for item in value] 15 | except Exception: 16 | raise MyCustomError("11111") 17 | 18 | @staticmethod 19 | def parse_str_list(value): 20 | return [str(item) for item in value] 21 | 22 | 23 | 24 | class MyCustomError(Exception): 25 | pass -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/setting/application.rst: -------------------------------------------------------------------------------- 1 | Application Management 2 | ====================== 3 | 4 | Applications are used to authenticate API calls. After creating an application, a token is generated and should be used to sign each request. The signing algorithm is documented in the Postman collection: https://documenter.getpostman.com/view/15031929/2sB3WyJbap 5 | 6 | .. figure:: ../../images/application_management.png 7 | :width: 100% 8 | :align: center 9 | :alt: Application management page 10 | 11 | 12 | Application types 13 | ------------------- 14 | - admin: Administrators can call all APIs. 15 | - workflow_admin: Workflow administrators can call workflow/ticket APIs and common APIs. 16 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/types.ts: -------------------------------------------------------------------------------- 1 | import { IFormField } from '../../types/workflowDesign'; 2 | 3 | // 基础字段组件属性接口 4 | export interface BaseFieldProps { 5 | field: IFormField; 6 | value?: any; 7 | onChange?: (value: any) => void; 8 | disabled?: boolean; 9 | readOnly?: boolean; 10 | required?: boolean; 11 | error?: boolean; 12 | helperText?: string; 13 | size?: 'small' | 'medium' | 'large'; 14 | variant?: 'outlined' | 'filled' | 'standard'; 15 | fullWidth?: boolean; 16 | } 17 | 18 | 19 | // 字段组件配置 20 | export interface FieldComponentConfig { 21 | // mode: FieldRenderMode; 22 | showLabel?: boolean; 23 | showDescription?: boolean; 24 | showValidation?: boolean; 25 | } -------------------------------------------------------------------------------- /frontend/src/services/tenant.ts: -------------------------------------------------------------------------------- 1 | import apiClient from "./api"; 2 | import {ITenantDetailRes} from "../types/tenant"; 3 | 4 | export const getTenantDetail = async (tenantId: string): Promise => { 5 | try { 6 | const response = await apiClient.get(`/api/v1.0/accounts/tenants/${tenantId}`); 7 | return response.data; 8 | } catch (error) { 9 | throw error; 10 | } 11 | } 12 | 13 | export const getTenantByDomain = async (domain: string): Promise => { 14 | try { 15 | const response = await apiClient.get('/api/v1.0/accounts/tenants/by_domain', { 16 | params: { domain } 17 | }); 18 | return response.data; 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; -------------------------------------------------------------------------------- /backend/apps/manage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from apps.loon_base_model import BaseCommonModel 3 | 4 | 5 | class Notification(BaseCommonModel): 6 | """ 7 | notice config, need encrypt 8 | """ 9 | NOTICE_TYPE_CHOICE = [ 10 | ("dingtalk", "dingtalk"), 11 | ("wecom", "wecom"), 12 | ("feishu", "feishu"), 13 | ("hook", "hook") 14 | ] 15 | name = models.CharField("name", max_length=50, null=False, default="") 16 | description = models.CharField("description", max_length=200, null=False, default="") 17 | type = models.CharField("type", max_length=50, null=False, default="") 18 | extra = models.JSONField("extra", max_length=1000, null=False) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "userInfo": "User Info", 3 | "updateProfileSuccess": "Update Profile Success", 4 | "updateProfileFail": "Update Profile Fail", 5 | "name": "Name", 6 | "changePassword": "Change Password", 7 | "currentPassword": "Current Password", 8 | "newPassword": "New Password", 9 | "confirmPassword": "Confirm New Password", 10 | "changePasswordSuccess": "Password changed successfully", 11 | "changePasswordFail": "Failed to change password", 12 | "passwordsNotMatch": "New passwords do not match", 13 | "currentPasswordRequired": "Please enter current password", 14 | "newPasswordRequired": "Please enter new password", 15 | "confirmPasswordRequired": "Please confirm new password" 16 | } -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "tenant": { 3 | "tenantInfo": "租户信息", 4 | "tenantDetail": "租户详情", 5 | "name": "名称", 6 | "domain": "域名", 7 | "workflowLimit": "工作流限制", 8 | "ticketLimit": "工单限制", 9 | "unLimited": "无限制", 10 | "tenantId": "租户ID" 11 | }, 12 | "application": { 13 | "appTypeDescription": "管理员可以调用所有api,工作流管理员只能调用工单和工作流相关api和一些通用api", 14 | "applicationList": "应用列表", 15 | "searchWithKeyword": "关键字搜索", 16 | "applicationDetial": "应用详情", 17 | "newApplication": "新建应用", 18 | "tokenCopied": "Token已复制" 19 | }, 20 | "notification": { 21 | "notificationList": "通知列表", 22 | "notificationDetial": "通知详情", 23 | "newNotification": "新建通知", 24 | "notificationTypeDescription": "通知类型说明" 25 | } 26 | } -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "roleList": "Role List", 3 | "addRoleSuccess": "Add Role Success", 4 | "addRoleFail": "Add Role Fail", 5 | "updateRoleSuccess": "Update Role Success", 6 | "updateRoleFail": "Update Role Fail", 7 | "deleteRoleSuccess": "Delete Role Success", 8 | "deleteRoleFail": "Delete Role Fail", 9 | "roleName": "Role Name", 10 | "roleDescription": "Role Description", 11 | "rolePermission": "Role Permission", 12 | "roleMember": "Role Member", 13 | "deleteMemberConfirm": "Confirm Delete Member", 14 | "deleteMemberConfirmMsg": "Are you sure you want to remove this member from the role? The member will lose all permissions associated with this role.", 15 | "deleteMemberSuccess": "Delete Member Success", 16 | "deleteMemberFail": "Delete Member Fail" 17 | } -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/workflow/workflow_version.rst: -------------------------------------------------------------------------------- 1 | Workflow Version Management 2 | =========================== 3 | 4 | Loonflow supports multiple workflow versions. Each workflow can have the 5 | following version types: 6 | 7 | - **default**: The active version used when a user selects the workflow in 8 | Workbench to create a new ticket. 9 | - **candidate**: A draft version for validation. Administrators can switch the 10 | version type or create test tickets with the candidate version. 11 | - **archived**: A retired version. New tickets cannot be created with it, but 12 | existing tickets created in the past can still be processed. 13 | 14 | .. figure:: ../../images/workflow_version_management.png 15 | :width: 100% 16 | :align: center 17 | :alt: workflow version management 18 | -------------------------------------------------------------------------------- /backend/requirements/common.txt: -------------------------------------------------------------------------------- 1 | #requrements/common.txt 2 | #python=3.12.x , you must use python3.12.x 3 | amqp==5.3.1 4 | asgiref==3.10.0 5 | billiard==4.2.2 6 | celery==5.5.3 7 | certifi==2025.10.5 8 | cffi==2.0.0 9 | charset-normalizer==3.4.3 10 | click==8.3.0 11 | click-didyoumean==0.3.1 12 | click-plugins==1.1.1.2 13 | click-repl==0.3.0 14 | cryptography==46.0.2 15 | Django==5.2.7 16 | idna==3.10 17 | kombu==5.5.4 18 | packaging==25.0 19 | prompt_toolkit==3.0.52 20 | psycopg==3.2.10 21 | psycopg-binary==3.2.10 22 | pycparser==2.23 23 | PyJWT==2.10.1 24 | python-dateutil==2.9.0.post0 25 | redis==6.4.0 26 | requests==2.32.5 27 | schema==0.7.7 28 | simplejson==3.20.2 29 | six==1.17.0 30 | sqlparse==0.5.3 31 | typing_extensions==4.15.0 32 | tzdata==2025.2 33 | urllib3==2.5.0 34 | vine==5.1.0 35 | wcwidth==0.2.14 36 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/ticket/submit_ticket.rst: -------------------------------------------------------------------------------- 1 | submit ticket 2 | =============== 3 | 4 | 5 | This guide is intended for users who want to submit a ticket. 6 | 7 | 1. Login to LoonFlow 8 | 2. Clieck "Workbench" in the left sidebar 9 | 3. Select a workflow from the dropdown menu 10 | 11 | .. figure:: ../../images/select_workflow.png 12 | :width: 100% 13 | :align: center 14 | :alt: Select Workflow 15 | 16 | 4. Click the "New Ticket" button 17 | 5. Fill in the ticket information 18 | 19 | .. figure:: ../../images/fill_ticket_form.png 20 | :width: 100% 21 | :align: center 22 | :alt: Fill Ticket Info 23 | 24 | 6. Click corresponding button listed in the bottom of the dialogpage to submit the ticket. 25 | 7. The ticket will be submitted and the next handle user will receive a notification. 26 | -------------------------------------------------------------------------------- /frontend/src/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | 2 | export const parseJwt = (token: string): any => { 3 | try { 4 | const base64Url = token.split('.')[1]; 5 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 6 | const jsonPayload = decodeURIComponent( 7 | atob(base64) 8 | .split('') 9 | .map(function (c) { 10 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 11 | }) 12 | .join('') 13 | ); 14 | 15 | return JSON.parse(jsonPayload); 16 | } catch (e) { 17 | return null; 18 | } 19 | }; 20 | 21 | export const getJwtExpiration = (token: string): Date | null => { 22 | const decoded = parseJwt(token); 23 | if (!decoded || !decoded.exp) { 24 | return null; 25 | } 26 | return new Date(decoded.exp * 1000); // JWT exp is in seconds, convert to milliseconds 27 | }; -------------------------------------------------------------------------------- /frontend/src/types/tenant.ts: -------------------------------------------------------------------------------- 1 | import { IApiResponse } from './common'; 2 | 3 | 4 | export interface ITenantDetailResEntity { 5 | id: string; 6 | label: string; 7 | createdAt: string; 8 | updatedAt: string; 9 | name: string; 10 | domain: string; 11 | icon: string; 12 | workflowLimit: number; 13 | ticketLimit: number; 14 | isActive: boolean; 15 | } 16 | 17 | export interface ITenantBasicInfo { 18 | id: string; 19 | name: string; 20 | domain: string; 21 | icon: string 22 | } 23 | 24 | export interface ITenantDetailResData { 25 | tenantInfo: ITenantDetailResEntity 26 | } 27 | 28 | export interface ITenantBasicInfoResData { 29 | tenantInfo: ITenantBasicInfo 30 | } 31 | 32 | 33 | export interface ITenantDetailRes extends IApiResponse { } 34 | 35 | export interface ITenantBasicInfoRes extends IApiResponse { } -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/setting/index.rst:2 22 | msgid "4. Setting Management" 23 | msgstr "4. 配置管理" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/ticket/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/ticket/index.rst:2 22 | msgid "1. Ticket Management" 23 | msgstr "1. 工单管理" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/workflow/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/workflow/index.rst:2 22 | msgid "2. Workflow Management" 23 | msgstr "2. 流程管理" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/introduction/welcome/index.rst:2 22 | msgid "1. Welcome to LoonFlow" 23 | msgstr "1. 欢迎使用 LoonFlow" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r1.x/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/r1.x/index.rst:2 22 | msgid "r1.x Release Notes" 23 | msgstr "r1.x 发布说明" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r2.x/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/r2.x/index.rst:2 22 | msgid "r2.x Release Notes" 23 | msgstr "r2.x 发布说明" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r3.x/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/r3.x/index.rst:2 22 | msgid "r3.x Release Notes" 23 | msgstr "r3.x 发布说明" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/organization/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/organization/index.rst:2 22 | msgid "3. Organization Management" 23 | msgstr "3. 组织管理" 24 | 25 | -------------------------------------------------------------------------------- /backend/manage_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.test") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/setting/tenant.rst: -------------------------------------------------------------------------------- 1 | Tenant Management 2 | ==================== 3 | 4 | Loonflow supports multi-tenant. Most users only need a single tenant; if you require multi-tenant capabilities, please contact blackholll (blackholll@163.com; blackholll_cn@gmail.com) for licensing. In multi-tenant mode all data—users, tickets, workflows, notifications, applications, roles, departments, and more—are fully isolated. Typical scenarios include group enterprises or SaaS deployments where multiple independent organizations share one platform. 5 | 6 | .. figure:: ../../images/tenant_info.png 7 | :width: 100% 8 | :align: center 9 | :alt: tenant info 10 | 11 | Future roadmap 12 | -------------- 13 | Planned: cross-tenant ticket collaboration, allowing tickets to flow between tenants. Example scenarios: inter-company ticket handling, external customer cases, supplier tickets, and reseller/partner support. -------------------------------------------------------------------------------- /.github/workflows/unit_test.yml.disabled: -------------------------------------------------------------------------------- 1 | name: Unit test CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - r** 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 4 14 | matrix: 15 | python-version: [ "3.12" ] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install Dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -r requirements/test.txt 26 | - name: Create Database 27 | run: | 28 | python manage_test.py makemigrations 29 | python manage_test.py migrate 30 | - name: Run Unit Tests 31 | run: | 32 | coverage run manage_test.py test 33 | coverage report 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "leader": "Leader", 3 | "changeParentDept": "Change Parent Dept", 4 | "passwordNotMatch": "The two passwords entered are not the same", 5 | "resetPassword": "Reset Password", 6 | "confirmResetPassword": "Confirm Reset Password", 7 | "confirmResetPasswordMsg": "Password will be reset to '123456', please change it as soon as possible", 8 | "resetPasswordSuccess": "Reset Password Success", 9 | "resetPasswordFail": "Reset Password Fail", 10 | "deleteUserSuccess": "Delete User Success", 11 | "deleteUserFail": "Delete User Fail", 12 | "admin": "Admin", 13 | "workflowAdmin": "Workflow Admin", 14 | "normal": "Normal", 15 | "userDeptHelpText": "User belongs to departments, support setting multiple departments, the first department will be used as the main department, and the department approvers will be obtained based on the main department when approving" 16 | } -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/installtion_deployment/installation_guide/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/installtion_deployment/installation_guide/index.rst:2 22 | msgid "2. Installation Guide" 23 | msgstr "2. 安装指南" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/installtion_deployment/system_requirements/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/installtion_deployment/system_requirements/index.rst:2 22 | msgid "1. System Requirements" 23 | msgstr "1. 系统要求" 24 | 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/setting/notification.rst: -------------------------------------------------------------------------------- 1 | Notification Management 2 | ========================= 3 | 4 | Loonflow lets you add, edit, and delete notifications. Multiple delivery 5 | channels are available so you can wire notifications into your own systems. 6 | 7 | .. figure:: ../../images/notification_management.png 8 | :width: 100% 9 | :align: center 10 | :alt: notification management 11 | 12 | 13 | Notification Types 14 | ------------------- 15 | 16 | - hook: Triggers a POST request to your notification endpoint with the current 17 | ticket data in request body. The token is used for signing, following the same 18 | approach as application tokens. 19 | - teams (planned): Sends to a Teams bot. 20 | - email (planned): Sends email; requires mail server configuration. 21 | - dingtalk (planned): Sends via DingTalk bot. 22 | - wecom (planned): Sends via WeCom bot. 23 | - feishu (planned): Sends via Feishu bot. 24 | -------------------------------------------------------------------------------- /frontend/src/store/tenantSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { ITenantBasicInfo } from '../types/tenant'; 3 | 4 | interface TenantState { 5 | tenantInfo: ITenantBasicInfo | null; 6 | lastUpdateTime: number | null; 7 | } 8 | 9 | const initialState: TenantState = { 10 | tenantInfo: null, 11 | lastUpdateTime: null, 12 | }; 13 | 14 | const tenantSlice = createSlice({ 15 | name: 'tenant', 16 | initialState, 17 | reducers: { 18 | setTenantBasicInfo: (state, action: PayloadAction) => { 19 | state.tenantInfo = action.payload; 20 | state.lastUpdateTime = Date.now(); 21 | }, 22 | clearTenantInfo: (state) => { 23 | state.tenantInfo = null; 24 | state.lastUpdateTime = null; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { setTenantBasicInfo, clearTenantInfo } = tenantSlice.actions; 30 | export const tenantReducer = tenantSlice.reducer; -------------------------------------------------------------------------------- /backend/apps/ticket/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from apps.ticket.views import TicketListView,TicketDetailFormView, TicketDetailActionsView, TicketHandleView, TicketFlowHistoryView, TicketCurrentNodeInfosView, TicketDetailAdminActionsView, TicketMockExternalAssigneeView 3 | 4 | urlpatterns = [ 5 | path('', TicketListView.as_view()), 6 | path('//ticket_detail_form', TicketDetailFormView.as_view()), 7 | path('//ticket_detail_actions', TicketDetailActionsView.as_view()), 8 | path('//ticket_detail_admin_actions', TicketDetailAdminActionsView.as_view()), 9 | path('//handle', TicketHandleView.as_view()), 10 | path('//ticket_flow_history', TicketFlowHistoryView.as_view()), 11 | path('//current_node_infos', TicketCurrentNodeInfosView.as_view()), 12 | path('/mock_external_assignee', TicketMockExternalAssigneeView.as_view()), 13 | ] 14 | -------------------------------------------------------------------------------- /frontend/src/components/commonComponents/Snackbar/GlobalSnackbar.tsx: -------------------------------------------------------------------------------- 1 | // src/components/Snackbar/GlobalSnackbar.tsx 2 | import { Alert, Snackbar } from '@mui/material'; 3 | import React from 'react'; 4 | 5 | interface GlobalSnackbarProps { 6 | open: boolean; 7 | onClose: (event: React.SyntheticEvent | Event, reason?: string) => void; 8 | message: string | React.ReactNode; 9 | severity: 'error' | 'warning' | 'info' | 'success'; 10 | } 11 | 12 | const GlobalSnackbar: React.FC = ({ open, onClose, message, severity }) => { 13 | return ( 14 | 23 | 24 | {message} 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default GlobalSnackbar; -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { persistStore, persistReducer } from 'redux-persist'; 3 | import storage from 'redux-persist/lib/storage'; 4 | import { authReducer, loginState, logoutState } from './authSlice'; 5 | import { tenantReducer, setTenantBasicInfo, clearTenantInfo } from './tenantSlice'; 6 | 7 | const persistConfig = { 8 | key: 'root', 9 | storage, 10 | }; 11 | 12 | const persistedAuthReducer = persistReducer(persistConfig, authReducer); 13 | const persistedTenantReducer = persistReducer(persistConfig, tenantReducer); 14 | 15 | const store = configureStore({ 16 | reducer: { 17 | auth: persistedAuthReducer, 18 | tenant: persistedTenantReducer, 19 | }, 20 | }); 21 | export const persistor = persistStore(store); 22 | 23 | export type RootState = ReturnType; 24 | 25 | export { store, loginState, logoutState, setTenantBasicInfo, clearTenantInfo }; 26 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/core_concepts/ticket_system_fundamentals/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: loonflow \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language: zh_CN\n" 15 | "Language-Team: zh_CN \n" 16 | "Plural-Forms: nplurals=1; plural=0;\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=utf-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Generated-By: Babel 2.17.0\n" 21 | 22 | #: ../../source/core_concepts/ticket_system_fundamentals/index.rst:2 23 | msgid "1. Ticketing System Fundamentals" 24 | msgstr "工单系统基础概念" 25 | 26 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import warnings 5 | warnings.filterwarnings("ignore") 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.config") 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError: 11 | # The above import may fail for some other reason. Ensure that the 12 | # issue is really that Django is missing to avoid masking other 13 | # exceptions on Python 2. 14 | try: 15 | import django 16 | except ImportError: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) 22 | raise 23 | execute_from_command_line(sys.argv) 24 | -------------------------------------------------------------------------------- /frontend/src/types/user.ts: -------------------------------------------------------------------------------- 1 | import { IApiResponse } from './common'; 2 | 3 | export interface IUser { 4 | id: string; 5 | name: string; 6 | type: string; 7 | alias: string; 8 | email: string; 9 | tenantId: string; 10 | phone: string; 11 | avatar?: string; 12 | lang?: string; 13 | isActive: boolean; 14 | deptInfoList: any[]; // 使用any避免循环依赖,实际使用时应该导入ISimpleDept 15 | } 16 | 17 | export interface IUserListResData { 18 | page: number; 19 | perPage: number; 20 | total: number; 21 | userInfoList: IUser[] 22 | } 23 | 24 | export interface IUserSimpleListResData { 25 | page: number; 26 | perPage: number; 27 | total: number; 28 | userInfoList: ISimpleUser[] 29 | } 30 | 31 | export interface ISimpleUser { 32 | id: string; 33 | name: string; 34 | alias: string; 35 | email: string; 36 | } 37 | 38 | export interface ISimpleUserListRes extends IApiResponse { } 39 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from '../../../../i18n'; 2 | 3 | /** 4 | * Get translated validation message 5 | * @param key The translation key 6 | * @param params Optional parameters for interpolation 7 | * @returns Translated message 8 | */ 9 | export const t = (key: string, params?: Record): string => { 10 | return i18n.t(key, params) as string; 11 | }; 12 | 13 | /** 14 | * Get translated validation message for workflow validation 15 | * @param category The validation category (basic, connectivity, etc.) 16 | * @param messageKey The specific message key 17 | * @param params Optional parameters for interpolation 18 | * @returns Translated message 19 | */ 20 | export const getValidationMessage = ( 21 | category: string, 22 | messageKey: string, 23 | params?: Record 24 | ): string => { 25 | return t(`workflowValidation.${category}.${messageKey}`, params); 26 | }; 27 | -------------------------------------------------------------------------------- /sphinx_docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /backend/apps/workflow/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.workflow.views import WorkflowView, WorkflowInitNodeView, WorkflowVersionsView, WorkflowDetailView, WorkflowTicketCreationFormView, WorkflowTicketCreationActionsView, WorkflowProcessSingleSchemaView, WorkflowVersionDetailView 4 | 5 | urlpatterns = [ 6 | path('', WorkflowView.as_view()), 7 | path('/', WorkflowDetailView.as_view()), 8 | path('//init_node', WorkflowInitNodeView.as_view()), 9 | path('//versions', WorkflowVersionsView.as_view()), 10 | path('//versions/', WorkflowVersionDetailView.as_view()), 11 | path('//ticket_creation_form', WorkflowTicketCreationFormView.as_view()), 12 | path('//ticket_creation_actions', WorkflowTicketCreationActionsView.as_view()), 13 | path('//process_single_schema', WorkflowProcessSingleSchemaView.as_view()), 14 | ] -------------------------------------------------------------------------------- /frontend/src/components/Organization/UserDept/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tab, Tabs } from '@mui/material'; 2 | import { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import Dept from './Dept'; 5 | import User from './User'; 6 | 7 | 8 | 9 | function UserDept() { 10 | const [activeTab, setActiveTab] = useState('users'); 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 |
15 | setActiveTab(newValue)} 18 | > 19 | 20 | 21 | 22 | 23 | {activeTab === 'users' ? ( 24 | 25 | ) : ( 26 |
27 | )} 28 |
29 | ); 30 | } 31 | 32 | export default UserDept; -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "tenant": { 3 | "name": "Name", 4 | "domain": "Domain", 5 | "workflowLimit": "Workflow Limit", 6 | "ticketLimit": "Ticket Limit", 7 | "unLimited": "Unlimited", 8 | "tenantInfo": "Tenant Info", 9 | "tenantDetail": "Tenant Detail", 10 | "tenantId": "Tenant Id" 11 | }, 12 | "application": { 13 | "appTypeDescription": "Admin can call all apis, workflow_admin can only call ticket and workflow related apis and some common apis", 14 | "applicationList": "Application List", 15 | "searchWithKeyword": "Search With Keyword", 16 | "applicationDetial": "Application", 17 | "newApplication": "New Application", 18 | "tokenCopied": "Token Copied" 19 | }, 20 | "notification": { 21 | "notificationList": "Notification", 22 | "notificationDetial": "Notification Detail", 23 | "newNotification": "New Notification", 24 | "notificationTypeDescription": "Notification Type Description" 25 | } 26 | } -------------------------------------------------------------------------------- /sphinx_docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | # Use python -m sphinx to ensure we use the current Python environment 8 | SPHINXBUILD ?= python -m sphinx 9 | SOURCEDIR = source 10 | BUILDDIR = build 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | # gen po files 24 | update-translations: 25 | sphinx-intl update -p build/gettext -l zh_CN 26 | 27 | html-zh: 28 | sphinx-build -b html -D language=zh_CN source build/html/zh_CN -------------------------------------------------------------------------------- /docker_compose_deploy/loonflow-backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.12-slim AS builder 2 | WORKDIR /app 3 | 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | build-essential \ 7 | python3-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | COPY ./backend/requirements/common.txt . 11 | COPY ./backend/requirements/pro.txt . 12 | 13 | RUN pip install --user --no-warn-script-location -r pro.txt 14 | 15 | 16 | FROM python:3.12.12-slim 17 | LABEL maintainer=blackholll@163.com 18 | 19 | WORKDIR /app/loonflow 20 | 21 | ENV PYTHONUNBUFFERED=1 \ 22 | PATH=/root/.local/bin:$PATH 23 | 24 | 25 | COPY --from=builder /root/.local /root/.local 26 | 27 | COPY ./backend /app/loonflow 28 | 29 | RUN cp settings/pro.py.sample settings/config.py && \ 30 | sed -i "/HOMEPATH = os.environ/c\ HOMEPATH = '/var/log/loonflow'" /app/loonflow/settings/common.py 31 | 32 | RUN mkdir -p /var/log/loonflow 33 | 34 | # execution permisson 35 | # RUN chmod +x /app/loonflow/manage.py 36 | 37 | EXPOSE 8000 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/types/application.ts: -------------------------------------------------------------------------------- 1 | import { IApiResponse } from './common'; 2 | 3 | // 定义 label 的类型为任意 key-value 对的字典对象 4 | type LabelDict = Record; 5 | 6 | export interface IApplicationResEntity { 7 | id: string; 8 | label: LabelDict; // 修改为字典对象类型 9 | createdAt: string; 10 | updatedAt: string; 11 | name: string; 12 | description: string; 13 | type: string; 14 | token: string; 15 | tenantId: string; 16 | } 17 | 18 | export interface ISimpleApplicationResEntity { 19 | id: string; 20 | name: string; 21 | description: string; 22 | type: string; 23 | tenantId?: string; 24 | } 25 | export interface IApplicationListResData { 26 | applicationInfoList: IApplicationResEntity[] 27 | } 28 | 29 | export interface ISimpleApplicationResData { 30 | applicationInfoList: ISimpleApplicationResEntity[] 31 | } 32 | 33 | export interface IApplicationListRes extends IApiResponse { } 34 | export interface ISimpleApplicationListRes extends IApiResponse { } 35 | -------------------------------------------------------------------------------- /sphinx_docs/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation with Sphinx 14 | # Note: This configuration file is in sphinx_docs/ directory 15 | sphinx: 16 | configuration: sphinx_docs/source/conf.py 17 | builder: html 18 | 19 | # Configure the languages supported 20 | # English is the default, Chinese (Simplified) is also supported 21 | formats: 22 | - htmlzip 23 | - pdf 24 | 25 | # Optionally, but recommended, 26 | # declare the Python requirements required to build your documentation 27 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 28 | python: 29 | install: 30 | - requirements: sphinx_docs/requirements.txt 31 | 32 | # Additional build options 33 | submodules: 34 | recursive: false 35 | 36 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r3.x/r3.0.1+.rst: -------------------------------------------------------------------------------- 1 | r3.0.1+ 2 | ================== 3 | 4 | --------- 5 | r3.0.1 6 | --------- 7 | - Docker Compose: custom Redis/Postgres ports, drop version key, ARM build, pinned tags. 8 | - User-facing: reset personal password; choose workflow version on create; English publish hint; flow preview node text; default language follows browser. 9 | - Frontend: remove unused imports; upgrade yarn 4 / Node 22 / TS 5; incremental `yarn tsc`; type fixes; required label handled. 10 | - I18n: numbers/dates/components/templates covered; i18-ally config; default language follows browser. 11 | - Quality (workflow/forms): workflow version type editable; ticket field edits fixed; edge required check; user component drag fixed; rule editor i18n; notification editing fixed. 12 | - Quality (templates/dates): date/time placeholders fixed; title template parsing fixed (creator, select display, editing). 13 | - Ops: stdout logging restored; add initial user on deploy. 14 | - Docs: issue templates refreshed; user/API docs. -------------------------------------------------------------------------------- /backend/service/manage/common_config_service.py: -------------------------------------------------------------------------------- 1 | from apps.account.models import Tenant, User 2 | from service.base_service import BaseService 3 | 4 | 5 | class CommonConfigService(BaseService): 6 | def __init__(self): 7 | pass 8 | 9 | @classmethod 10 | def get_common_config(cls, tenant_id, user_id): 11 | """ 12 | get common config 13 | :param tenant_id: 14 | :param user_id: 15 | :return: 16 | """ 17 | result = dict() 18 | tenant_record = Tenant.objects.get(id=tenant_id) 19 | result["tenant_id"] = tenant_record.id 20 | result["tenant_name"] = tenant_record.name 21 | result["tenant_icon"] = tenant_record.icon 22 | result["tenant_domain"] = tenant_record.domain 23 | result["lang"] = tenant_record.lang 24 | if user_id: 25 | user_record = User.objects.ge(id=user_id) 26 | result["lang"] = user_record.lang 27 | return result 28 | 29 | 30 | common_config_service_ins = CommonConfigService() 31 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/index.rst:3 22 | msgid "Version History" 23 | msgstr "版本历史" 24 | 25 | #: ../../source/others/release_notes/index.rst:1 26 | msgid "This document contains release notes for all versions of Loonflow." 27 | msgstr "本文档包含 Loonflow 所有版本的发布说明。" 28 | 29 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/api_reference.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 18:56+0800\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/api_reference/postman.rst:2 22 | msgid "Postman Collection" 23 | msgstr "Postman 集合" 24 | 25 | #: ../../source/api_reference/postman.rst:5 26 | msgid "https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 27 | msgstr "https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 28 | 29 | -------------------------------------------------------------------------------- /Roadmap_zh.md: -------------------------------------------------------------------------------- 1 | # 🗺️ 项目路线图 2 | 3 | ## 🎯 3.1.0 版本 (2026年2-3月) 4 | ### 核心功能增强 5 | - 📝 **表单字段扩展** - 新增文件上传、外部数据源、富文本编辑器字段类型 6 | - ⏰ **流程超时控制** - 支持节点超时自动流转,提升流程自动化程度 7 | - 🔍 **全文检索** - 实现工单和流程的全文搜索功能 8 | - 📋 **工作流分类** - 支持工作流分类管理,便于组织和管理 9 | 10 | ### 企业集成 11 | - 🔐 **OAuth认证** - 集成企业微信、飞书、钉钉、Azure等主流企业认证 12 | - 📢 **消息通知** - 支持企业微信、钉钉、飞书、Teams等平台的消息推送 13 | - 🔗 **子工单生成** - 支持配置规则自动生成子工单,实现复杂业务流程 14 | 15 | ### 权限与安全 16 | - 🛡️ **权限精细化** - 字段级权限控制,支持隐藏、脱敏、处理等操作 17 | - 👥 **创建权限控制** - 工作流创建权限的精细化配置 18 | - 📊 **Hook事件记录** - 完整的Hook事件查询和审计功能 19 | 20 | ### 开发与文档 21 | - 📚 **ReadTheDocs文档** - 完整的在线文档系统 22 | - 🧪 **单元测试** - 前后端完整的单元测试覆盖 23 | - 🌐 **后端国际化** - 后端API的国际化支持 24 | - 🐛 **Bug修复** - 持续的问题修复和小功能优化 25 | 26 | ## 中期规划 (2026年5月-6月) 27 | - 🚀 **性能优化** - 优化大数据量场景下的系统性能 28 | - 🔌 **插件生态** - 丰富官方插件库,支持更多业务场景 29 | - 📱 **移动端适配** - 优化移动设备上的使用体验 30 | - 🌐 **更多语言国际化** - 完善多国国际化** - 完整的多语言界面支持 31 | 32 | ## 长期愿景 (2026年下半年) 33 | - 🤖 **AI集成** - 集成AI能力,提供智能化的流程建议和自动化,智能分析工单数据,基于工单提供知识问答 34 | - 🔗 **生态集成** - 与更多主流企业系统深度集成 35 | - 📊 **数据分析** - 提供流程数据分析和优化建议 36 | - 🏢 **企业级功能** - 增强企业级部署和管理能力 -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/api_reference/postman.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/api_reference/postman.rst:2 22 | msgid "Postman Collection" 23 | msgstr "Postman 集合" 24 | 25 | #: ../../source/api_reference/postman.rst:5 26 | msgid "https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 27 | msgstr "https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 28 | 29 | -------------------------------------------------------------------------------- /frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | 3 | module.exports = { 4 | webpack: { 5 | configure: (webpackConfig) => { 6 | // 找到并移除 ForkTsCheckerWebpackPlugin 7 | webpackConfig.plugins = webpackConfig.plugins.filter( 8 | plugin => !(plugin instanceof ForkTsCheckerWebpackPlugin) 9 | ); 10 | 11 | // 可选:如果你想保留类型检查但提高内存限制,可以这样配置: 12 | // webpackConfig.plugins = webpackConfig.plugins.map(plugin => { 13 | // if (plugin instanceof ForkTsCheckerWebpackPlugin) { 14 | // return new ForkTsCheckerWebpackPlugin({ 15 | // ...plugin.options, 16 | // typescript: { 17 | // ...plugin.options.typescript, 18 | // memoryLimit: 8192, // 8GB 内存限制 19 | // }, 20 | // }); 21 | // } 22 | // return plugin; 23 | // }); 24 | 25 | return webpackConfig; 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/tsc-fast.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // 快速TypeScript编译脚本 4 | const { execSync } = require('child_process'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | console.log('🚀 开始快速TypeScript编译...'); 9 | 10 | // 清理缓存 11 | try { 12 | if (fs.existsSync('tsconfig.tsbuildinfo')) { 13 | fs.unlinkSync('tsconfig.tsbuildinfo'); 14 | console.log('✅ 清理了编译缓存'); 15 | } 16 | } catch (error) { 17 | console.log('⚠️ 清理缓存失败:', error.message); 18 | } 19 | 20 | // 设置环境变量优化 21 | process.env.NODE_OPTIONS = '--max-old-space-size=8192'; 22 | 23 | try { 24 | console.log('📦 开始编译...'); 25 | const startTime = Date.now(); 26 | 27 | // 使用优化的编译参数 28 | execSync('npx tsc --incremental --skipLibCheck --noEmit', { 29 | stdio: 'inherit', 30 | cwd: process.cwd() 31 | }); 32 | 33 | const endTime = Date.now(); 34 | const duration = (endTime - startTime) / 1000; 35 | 36 | console.log(`✅ 编译完成!耗时: ${duration.toFixed(2)}秒`); 37 | 38 | } catch (error) { 39 | console.error('❌ 编译失败:', error.message); 40 | process.exit(1); 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE and Editor Files 2 | .idea/ 3 | .vscode/ 4 | *.swp 5 | 6 | # Python 7 | *.pyc 8 | __pycache__/ 9 | venv/ 10 | .coverage 11 | coverage_html_report/ 12 | 13 | # Node.js and Frontend 14 | node_modules/ 15 | frontend/node_modules/ 16 | /.pnp 17 | .pnp.js 18 | /coverage 19 | /build 20 | frontend/build/ 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Environment Files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | # Build and Documentation 32 | sphinx_docs/build/ 33 | sphinx_docs/source/_build/ 34 | 35 | # Media and User Uploads 36 | media/flowchart/*.* 37 | media/workflow_script/*.* 38 | media/ticket_file/*.* 39 | media/notice_script/*.* 40 | !media/workflow_script/demo_script.py 41 | !media/notice_script/demo_notice_script.py 42 | 43 | # Database and Configuration 44 | tests/testloonflow.sql 45 | settings/config.py 46 | loonflow_test.db 47 | docker_compose_deploy/loonflow_only/dbdata/* 48 | docker_compose_deploy/loonflow_shutongflow/dbdata/* 49 | 50 | # System Files 51 | .DS_Store 52 | clock_flag.txt 53 | 54 | *.mo -------------------------------------------------------------------------------- /backend/service/hook/hook_base_service.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import requests 3 | from django.conf import settings 4 | from service.base_service import BaseService 5 | from service.common.common_service import common_service_ins 6 | from service.exception.custom_common_exception import CustomCommonException 7 | 8 | 9 | class HookBaseService(BaseService): 10 | @classmethod 11 | def call_hook(cls, hook_url: str, token: str, request_data_dict: dict) -> dict: 12 | hook_url_forbidden_list = settings.HOOK_HOST_FORBIDDEN 13 | for url in hook_url_forbidden_list: 14 | if fnmatch.fnmatch(hook_url, url): 15 | raise CustomCommonException("hook url is blocked, please contact system administrator to change HOOK_HOST_FORBIDDEN configure") 16 | signature, timestamp = common_service_ins.gen_signature_by_token(token) 17 | headers = dict(signature=signature, timestamp=timestamp) 18 | result = requests.post(hook_url, headers=headers, json=request_data_dict).json() 19 | return result 20 | 21 | 22 | hook_base_service_ins = HookBaseService() 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/organization/user_dept.rst: -------------------------------------------------------------------------------- 1 | User and Department Management 2 | ============================== 3 | 4 | Loonflow provides built-in user and department management. You can add, edit, and delete users or departments in the UI, and you can also synchronize them in bulk through the API. 5 | 6 | .. figure:: ../../images/user_dept_management.png 7 | :width: 100% 8 | :align: center 9 | :alt: user and department management 10 | 11 | How to manage 12 | ------------- 13 | - UI maintenance: manually create or adjust users and departments on the “Organization Management” page—best for small-scale or ad-hoc changes. 14 | - API sync: call the organization management APIs to maintain users and departments in bulk. Populate the ``label`` field with an external system’s unique identifier (e.g., LDAP, AD) to simplify mapping and reconciliation later. 15 | 16 | Coming soon 17 | ----------- 18 | Support for Azure AD, Google, DingTalk, and WeCom (WeChat Work) OAuth logins is on the way. After enabling, users and departments can be pulled from external identity sources without manual upkeep in the UI. 19 | -------------------------------------------------------------------------------- /backend/tests/set_test_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 从开发环境导出数据,并导入到测试环境 4 | 5 | 6 | echo " " 7 | echo " " 8 | echo "########### 开始从开发环境导出数据 ###########" 9 | /usr/local/mysql/bin/mysqldump -uloonflownew loonflownew > testloonflow.sql 10 | echo " " 11 | echo " " 12 | echo "########### 导出数据完成 ###########" 13 | 14 | echo " " 15 | echo " " 16 | echo "########### 开始导入数据到测试环境,输入测试环境数据库密码 ###########" 17 | /usr/local/mysql/bin/mysql -uloonflownew -h127.0.0.1 -p test_loonflownew < "testloonflow.sql" 18 | echo " " 19 | echo " " 20 | 21 | echo "########### 完成数据初始化 ###########" 22 | 23 | echo " " 24 | echo " " 25 | 26 | echo "你现在可以在上级目录执行python manage.py test tests/ --keepdb开始单元测试了" 27 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 28 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 29 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/checkWorkflowCompatibility.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from "../../../types/workflow"; 2 | 3 | function checkWorkflowCompatibility(workflowDetailInfo: IWorkflowFullDefinition, workflowSourceDetailInfo: IWorkflowFullDefinition | null): Promise<{ isCompatible: boolean, messages: string[] }> { 4 | const messages: string[] = []; 5 | if (!workflowSourceDetailInfo) { 6 | return Promise.resolve({ isCompatible: true, messages }); 7 | } 8 | // situaltions that not compatibility with old version: 1.node deleted 9 | const source_node = workflowSourceDetailInfo.processSchema.nodeInfoList; 10 | const detail_node = workflowDetailInfo.processSchema.nodeInfoList; 11 | for (const node of source_node) { 12 | if (!detail_node.find((detail_node) => detail_node.id === node.id)) { 13 | messages.push(`node(${node.name}) deleted`) 14 | return Promise.resolve({ isCompatible: false, messages }); 15 | } 16 | } 17 | return Promise.resolve({ isCompatible: true, messages }); 18 | } 19 | 20 | export default checkWorkflowCompatibility; -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/edgeConditionValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate condition edges must have conditionGroups 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of edge condition validation problems 8 | */ 9 | export const validateEdgeConditions = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | // Check condition edges must have conditionGroups 13 | for (const edge of workflowData.processSchema.edgeInfoList) { 14 | if (edge.type === 'condition') { 15 | if (!edge.props.conditionGroups || 16 | !Array.isArray(edge.props.conditionGroups) || 17 | edge.props.conditionGroups.length === 0) { 18 | problems.push(getValidationMessage('edge', 'conditionGroupRequired', { 19 | edgeName: edge.name 20 | })); 21 | } 22 | } 23 | } 24 | 25 | return problems; 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { PersistGate } from 'redux-persist/integration/react'; 5 | import App from './App'; 6 | import DynamicLocalizationProvider from './components/commonComponents/DynamicLocalizationProvider'; 7 | import './i18n/index'; 8 | import './index.css'; 9 | import reportWebVitals from './reportWebVitals'; 10 | import { persistor, store } from './store'; 11 | 12 | 13 | const root = ReactDOM.createRoot( 14 | document.getElementById('root') as HTMLElement 15 | ); 16 | root.render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | // If you want to start measuring performance in your app, pass a function 30 | // to log results (for example: reportWebVitals(console.log)) 31 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 32 | reportWebVitals(); 33 | -------------------------------------------------------------------------------- /docker_compose_deploy/loonflow-ui/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 768; 7 | } 8 | 9 | http { 10 | sendfile on; 11 | tcp_nopush on; 12 | tcp_nodelay on; 13 | keepalive_timeout 65; 14 | types_hash_max_size 2048; 15 | client_max_body_size 1000M; 16 | 17 | include mime.types; 18 | default_type application/octet-stream; 19 | 20 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 21 | ssl_prefer_server_ciphers on; 22 | 23 | access_log /var/log/nginx/access.log; 24 | error_log /var/log/nginx/error.log; 25 | 26 | gzip on; 27 | gzip_disable "msie6"; 28 | 29 | server { 30 | listen 80; 31 | 32 | access_log /var/log/loonflow/nginx_access.log; 33 | error_log /var/log/loonflow/nginx_error.log; 34 | 35 | root /usr/share/nginx/html; 36 | index index.html; 37 | 38 | location = / { 39 | try_files /index.html =404; 40 | } 41 | 42 | location / { 43 | try_files $uri $uri/ /index.html; 44 | } 45 | 46 | location /api { 47 | proxy_pass http://loonflow-backend:8000; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /backend/apps/util/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2025-10-07 05:49 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Archive', 17 | fields=[ 18 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 19 | ('label', models.JSONField(blank=True, default=dict, verbose_name='label')), 20 | ('creator_id', models.UUIDField(default=uuid.uuid4, editable=False, null=True, verbose_name='creator_id')), 21 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created_at')), 22 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated_at')), 23 | ('model_name', models.CharField(max_length=100, verbose_name='model name')), 24 | ('data', models.JSONField(verbose_name='archived data')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /.github/workflows/image_build_backend.yml: -------------------------------------------------------------------------------- 1 | name: Build Backend Image 2 | on: 3 | push: 4 | tags: 5 | - 'r**' 6 | # branches: 7 | # - v3.0.1 8 | jobs: 9 | build_task: 10 | name: Build Task 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v4 16 | - 17 | name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | - 20 | name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | - 23 | name: Login to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | - 29 | name: Build and push 30 | uses: docker/build-push-action@v5 31 | with: 32 | context: . 33 | file: docker_compose_deploy/loonflow-backend/Dockerfile 34 | platforms: linux/amd64,linux/arm64 35 | cache-from: type=gha 36 | cache-to: type=gha,mode=max 37 | push: true 38 | # tags: blackholll/loonflow-backend:latest,blackholll/loonflow-backend:${{ github.ref_name }} 39 | tags: blackholll/loonflow-backend:${{ github.ref_name }} 40 | -------------------------------------------------------------------------------- /frontend/src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | export const setCookie = (name: string, value: string, options: any = {}) => { 2 | options = { 3 | path: '/', 4 | ...options, 5 | }; 6 | 7 | if (options.expires instanceof Date) { 8 | options.expires = options.expires.toUTCString(); 9 | } 10 | 11 | let updatedCookie = `${name}=${value}`; 12 | 13 | for (let optionKey in options) { 14 | if (options.hasOwnProperty(optionKey)) { 15 | const optionValue = options[optionKey]; 16 | if (optionValue !== undefined && optionValue !== '') { 17 | updatedCookie += `; ${optionKey}`; 18 | if (optionValue !== true) { 19 | updatedCookie += `=${optionValue}`; 20 | } 21 | } 22 | } 23 | } 24 | 25 | document.cookie = updatedCookie; 26 | }; 27 | 28 | export const getCookie = (name: string): string | undefined => { 29 | const matches = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1')}=([^;]*)`)); 30 | return matches ? decodeURIComponent(matches[1]) : undefined; 31 | }; 32 | 33 | export const removeCookie = (name: string, options: any = {}) => { 34 | setCookie(name, '', { 35 | expires: -1, 36 | ...options, 37 | }); 38 | }; -------------------------------------------------------------------------------- /frontend/src/i18n/locales/zh-CN/ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "approveState": "审批状态", 3 | "keyword": "关键字", 4 | "creator": "创建人", 5 | "createDateStart": "创建起始日期", 6 | "createDateEnd": "创建结束日期", 7 | "ticketType": "工单类型", 8 | "ticketStatus": "工单状态", 9 | "ticketTitle": "工单标题", 10 | "ticketCreator": "工单创建人", 11 | "ticketCreateTime": "工单创建时间", 12 | "allTickets": "所有工单", 13 | "owerTickets": "我的工单", 14 | "viewTickets": "查看工单", 15 | "dutyTickets": "待办工单", 16 | "relationTickets": "关联工单", 17 | "interveneTickets": "干预工单", 18 | "adminActions": "管理员操作", 19 | "selectAssignee": "选择处理人", 20 | "operationRecord": "操作记录", 21 | "processor": "操作人", 22 | "actionType": "操作类型", 23 | "comment": "留言", 24 | "operationTime": "操作时间", 25 | "actionName": { 26 | "force_forward": "强制转发", 27 | "force_close": "强制关闭", 28 | "force_alter_node": "强制变更节点", 29 | "add_comment": "添加留言", 30 | "forward": "转发", 31 | "consult": "加签", 32 | "consult_submit": "加签完成", 33 | "accept": "接受", 34 | "withdraw": "撤回" 35 | }, 36 | "state": { 37 | "in-draft": "草稿中", 38 | "on-going": "进行中", 39 | "rejected": "被拒绝", 40 | "withdrawn": "被撤回", 41 | "finished": "已完成", 42 | "closed": "已关闭" 43 | }, 44 | "ticketCreated": "工单已创建", 45 | "ticketHandled": "工单已处理" 46 | } 47 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r0.x/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/r0.x/index.rst:2 22 | msgid "r0.x Release Notes" 23 | msgstr "r0.x 发布说明" 24 | 25 | #: ../../source/others/release_notes/r0.x/index.rst:4 26 | msgid "" 27 | "For detailed release notes of v0.x versions, please refer to GitHub " 28 | "Releases:" 29 | msgstr "v0.x 版本的详细发布说明请参考 GitHub Releases:" 30 | 31 | #: ../../source/others/release_notes/r0.x/index.rst:6 32 | msgid "https://github.com/blackholll/loonflow/releases" 33 | msgstr "https://github.com/blackholll/loonflow/releases" 34 | 35 | -------------------------------------------------------------------------------- /sphinx_docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. loonflow documentation master file, created by 2 | sphinx-quickstart on Sat Mar 7 12:17:00 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to loonflow's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | :caption: Introduction 12 | 13 | introduction/welcome/index 14 | introduction/who_need_read 15 | 16 | .. toctree:: 17 | :maxdepth: 3 18 | :caption: Installation & Deployment 19 | 20 | installtion_deployment/system_requirements/index 21 | installtion_deployment/installation_guide/index 22 | 23 | 24 | .. toctree:: 25 | :maxdepth: 3 26 | :caption: Core Concepts 27 | 28 | core_concepts/ticket_system_fundamentals/index 29 | 30 | 31 | .. toctree:: 32 | :maxdepth: 3 33 | :caption: User Guide 34 | 35 | user_guide/ticket/index 36 | user_guide/workflow/index 37 | user_guide/organization/index 38 | user_guide/setting/index 39 | 40 | .. toctree:: 41 | :maxdepth: 3 42 | :caption: API Reference 43 | 44 | api_reference/postman 45 | 46 | .. toctree:: 47 | :maxdepth: 3 48 | :caption: Release Notes 49 | 50 | others/release_notes/index 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": [ 6 | "./src/*" 7 | ], 8 | }, 9 | "target": "ES2022", 10 | "lib": [ 11 | "dom", 12 | "dom.iterable", 13 | "esnext" 14 | ], 15 | "allowJs": true, 16 | // "incremental": true, 17 | "skipLibCheck": true, 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true, 20 | "strict": false, 21 | "forceConsistentCasingInFileNames": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "module": "esnext", 24 | "moduleResolution": "node", 25 | "resolveJsonModule": true, 26 | "isolatedModules": true, 27 | "noEmit": true, 28 | "jsx": "react-jsx", 29 | "noUnusedLocals": false, 30 | "noUnusedParameters": false, 31 | "assumeChangesOnlyAffectDirectDependencies": true, 32 | "disableSourceOfProjectReferenceRedirect": true, 33 | "disableSolutionSearching": true, 34 | "disableReferencedProjectLoad": true, 35 | "noImplicitAny": false, 36 | "noImplicitReturns": false, 37 | "noImplicitThis": false, 38 | "allowImportingTsExtensions": false, 39 | "noEmit": true, 40 | "verbatimModuleSyntax": false 41 | }, 42 | "include": [ 43 | "src", 44 | "**/*.json" 45 | ], 46 | } -------------------------------------------------------------------------------- /frontend/src/services/API.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | export interface CurrentUser { 3 | id: number; 4 | username: string; 5 | phone: string; 6 | isActive: boolean; 7 | typeId: number; 8 | email: string; 9 | } 10 | 11 | export interface CommonResponse { 12 | code: number; 13 | data: any; 14 | msg: string; 15 | } 16 | 17 | export interface LoginStateType { 18 | code?: number; 19 | data?: any; 20 | type?: string; 21 | } 22 | 23 | export interface NoticeIconData { 24 | id: string; 25 | key: string; 26 | avatar: string; 27 | title: string; 28 | datetime: string; 29 | type: string; 30 | read?: boolean; 31 | description: string; 32 | clickClose?: boolean; 33 | extra: any; 34 | status: string; 35 | } 36 | 37 | export interface TicketListData { 38 | code: number; 39 | data: any; 40 | msg: string; 41 | } 42 | 43 | export interface WorkflowListData { 44 | code: number; 45 | data: any; 46 | msg: string; 47 | } 48 | 49 | export interface WorkflowInitStateData { 50 | code: number; 51 | data: any; 52 | msg: string; 53 | } 54 | 55 | export interface queryUserSimpleData { 56 | code: number; 57 | data: any; 58 | msg: string; 59 | } 60 | 61 | 62 | 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /frontend/src/store/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import camelcaseKeys from 'camelcase-keys'; 3 | import { jwtDecode } from 'jwt-decode'; 4 | 5 | 6 | interface AuthState { 7 | isAuthenticated: boolean; 8 | user: null | User 9 | } 10 | 11 | interface User { 12 | name: string, 13 | alias: string, 14 | email: string, 15 | type: string, 16 | tenantId: string, 17 | } 18 | 19 | const initialState: AuthState = { 20 | isAuthenticated: false, 21 | user: null 22 | }; 23 | 24 | export const authSlice = createSlice({ 25 | name: 'auth', 26 | initialState, 27 | reducers: { 28 | loginState: (state, action: PayloadAction) => { 29 | console.log('Before:', state); 30 | state.isAuthenticated = true; 31 | const decoded: any = jwtDecode(action.payload); 32 | state.user = camelcaseKeys(decoded.data, { deep: true }) as User; 33 | console.log('state111111:', state); 34 | console.log('statestatestatestatestatestate'); 35 | console.log('After:', state); // 打印状态 36 | }, 37 | logoutState: (state) => { 38 | state.isAuthenticated = false; 39 | state.user = null; 40 | }, 41 | }, 42 | }); 43 | 44 | export const { loginState, logoutState } = authSlice.actions; 45 | 46 | export const authReducer = authSlice.reducer; -------------------------------------------------------------------------------- /backend/tests/test_views/test_account_view.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.test import TestCase 3 | from django.test.client import Client 4 | 5 | 6 | class TestAccountView(TestCase): 7 | fixtures = ['accounts.json', 'workflows.json'] 8 | 9 | def test_get_user_list_without_login(self): 10 | """ 11 | get user list without login 12 | :return: 13 | """ 14 | c = Client() 15 | response_content = c.get('/api/v1.0/accounts/users').content 16 | response_content_dict = json.loads(str(response_content, encoding='utf-8')) 17 | self.assertEqual(response_content_dict.get('code'), -1) 18 | 19 | def test_get_user_list_with_login(self): 20 | """ 21 | get user list with login 22 | :return: 23 | """ 24 | c = Client() 25 | # login 26 | login_content = c.post('/api/v1.0/login', json.dumps(dict(email='blackholll@163.com', password='123456')),content_type="application/json") 27 | login_response_dict = json.loads(login_content.content) 28 | jwt = login_response_dict.get("data").get("jwt") 29 | c.cookies.load(dict(jwt=jwt)) 30 | response_content = c.get('/api/v1.0/accounts/users').content 31 | response_content_dict = json.loads(str(response_content, encoding='utf-8')) 32 | self.assertEqual(response_content_dict.get('code'), 0) 33 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, TextField as MuiTextField } from '@mui/material'; 2 | import React from 'react'; 3 | import ViewField from './ViewField'; 4 | 5 | interface TextFieldProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | mode: 'view' | 'edit'; 9 | props: any; 10 | } 11 | 12 | function TextField({ 13 | value = '', 14 | onChange, 15 | mode, 16 | props, 17 | }: TextFieldProps) { 18 | 19 | const handleChange = (event: React.ChangeEvent) => { 20 | if (onChange) { 21 | onChange(event.target.value); 22 | } 23 | }; 24 | 25 | // view mode only show value 26 | if (mode === 'view') { 27 | return ( 28 | 29 | ); 30 | } 31 | 32 | // edit mode, support edit 33 | return ( 34 | 35 | 44 | 45 | ); 46 | 47 | }; 48 | 49 | export default TextField; -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/about_loonflow.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/introduction/welcome/about_loonflow.rst:2 22 | msgid "1.2 About Loonflow" 23 | msgstr "1.2 关于 Loonflow" 24 | 25 | #: ../../source/introduction/welcome/about_loonflow.rst:4 26 | msgid "" 27 | "Loonflow is an open-source process automation platform developed based on" 28 | " Django. After multiple versions of iteration and refactoring, we proudly" 29 | " present Loonflow 3.0 - a version that achieves a qualitative leap in " 30 | "visualization, flexibility, and scalability." 31 | msgstr "Loonflow 是基于 Django 开发的开源流程自动化平台。经过多次迭代与重构,我们自豪地推出 Loonflow 3.0——在可视化、灵活性和可扩展性上实现了质的飞跃。" 32 | 33 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/index.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 18:56+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/index.rst:9 22 | msgid "Introduction" 23 | msgstr "引言" 24 | 25 | #: ../../source/index.rst:16 26 | msgid "Installation & Deployment" 27 | msgstr "安装与部署" 28 | 29 | #: ../../source/index.rst:24 30 | msgid "Core Concepts" 31 | msgstr "核心概念" 32 | 33 | #: ../../source/index.rst:31 34 | msgid "User Guide" 35 | msgstr "用户指南" 36 | 37 | #: ../../source/index.rst:40 38 | msgid "API Reference" 39 | msgstr "API 参考" 40 | 41 | #: ../../source/index.rst:46 42 | msgid "Release Notes" 43 | msgstr "发行说明" 44 | 45 | #: ../../source/index.rst:7 46 | msgid "Welcome to loonflow's documentation!" 47 | msgstr "欢迎阅读 loonflow 文档!" 48 | 49 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/organization/role.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/organization/role.rst:2 22 | msgid "Role Management" 23 | msgstr "角色管理" 24 | 25 | #: ../../source/user_guide/organization/role.rst:4 26 | msgid "" 27 | "Loonflow supports managing roles so you can create, edit, delete, and " 28 | "maintain role members. Roles are primarily used when configuring " 29 | "workflows to specify handlers, allowing you to assign a role as the " 30 | "processor for a workflow node." 31 | msgstr "Loonflow 支持角色管理,可创建、编辑、删除并维护角色成员。配置流程时可将角色指定为节点处理人。" 32 | 33 | #: ../../source/user_guide/organization/role.rst:6 34 | msgid "role management" 35 | msgstr "角色管理页面" 36 | -------------------------------------------------------------------------------- /frontend/src/components/commonComponents/DynamicLocalizationProvider.tsx: -------------------------------------------------------------------------------- 1 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 2 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 3 | import dayjs from 'dayjs'; 4 | import 'dayjs/locale/en'; 5 | import 'dayjs/locale/zh-cn'; 6 | import React from 'react'; 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | interface DynamicLocalizationProviderProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | function DynamicLocalizationProvider({ children }: DynamicLocalizationProviderProps) { 14 | const { i18n } = useTranslation(); 15 | 16 | // 根据当前语言设置 dayjs locale 17 | const getDayjsLocale = React.useCallback(() => { 18 | switch (i18n.language) { 19 | case 'zh-CN': 20 | return 'zh-cn'; 21 | case 'en-US': 22 | default: 23 | return 'en'; 24 | } 25 | }, [i18n.language]); 26 | 27 | // 设置 dayjs locale 28 | React.useEffect(() => { 29 | const locale = getDayjsLocale(); 30 | dayjs.locale(locale); 31 | }, [getDayjsLocale]); 32 | 33 | return ( 34 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | export default DynamicLocalizationProvider; 44 | -------------------------------------------------------------------------------- /backend/tests/fixtures/workflows.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "workflow.workflow", 4 | "pk": 1, 5 | "fields": { 6 | "creator_id": 1, 7 | "created_at": "2018-04-23T20:49:32.229+08:00", 8 | "updated_at": "2018-10-22T08:05:15.574+08:00", 9 | "name": "\u8bf7\u5047\u7533\u8bf7", 10 | "label":"", 11 | "description": "\u8bf7\u5047\u7533\u8bf7", 12 | "notices": "2", 13 | "limit_expression": "{}", 14 | "display_form_str": "[\"sn\", \"title\", \"leave_start\", \"leave_end\", \"leave_days\", \"leave_proxy\", \"leave_type\", \"creator\", \"created_at\", \"leave_reason\"]", 15 | "title_template": "\u4f60\u6709\u4e00\u4e2a\u5f85\u529e\u5de5\u5355:{title}23", 16 | "content_template": "\u6807\u9898:{title}, \u521b\u5efa\u65f6\u95f4:{created_at}333" 17 | } 18 | }, 19 | { 20 | "model": "workflow.workflow", 21 | "pk": 2, 22 | "fields": { 23 | "creator_id": 1, 24 | "created_at": "2018-05-06T12:32:36.690+08:00", 25 | "updated_at": "2018-11-05T23:32:57.667+08:00", 26 | "name": "vpn\u7533\u8bf7", 27 | "label":"", 28 | "description": "vpn\u6743\u9650\u7533\u8bf7", 29 | "notices": "", 30 | "limit_expression": "{}", 31 | "display_form_str": "[\"sn\", \"title\", \"model\", \"created_at\",\"participant.participant_alias\",\"vpn_reason\"]", 32 | "title_template": "", 33 | "content_template": "" 34 | } 35 | } 36 | 37 | ] -------------------------------------------------------------------------------- /frontend/src/components/Ticket/NewTicketPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from '@mui/material'; 2 | import { useEffect, useMemo } from 'react'; 3 | import { useNavigate, useSearchParams } from 'react-router-dom'; 4 | import useSnackbar from '../../../hooks/useSnackbar'; 5 | import TicketDetail from '../TicketDetail'; 6 | 7 | function NewTicketPage() { 8 | const [searchParams] = useSearchParams(); 9 | const workflowId = useMemo(() => searchParams.get('workflow_id') || '', [searchParams]); 10 | const workflowVersionName = useMemo(() => searchParams.get('version_name') || undefined, [searchParams]); 11 | const { showMessage } = useSnackbar(); 12 | const navigate = useNavigate(); 13 | 14 | useEffect(() => { 15 | if (!workflowId) { 16 | showMessage('缺少 workflow_id 参数,无法创建工单', 'error'); 17 | } 18 | }, [workflowId, showMessage]); 19 | 20 | const handleTicketCreated = (ticketId: string) => { 21 | showMessage('工单创建成功', 'success'); 22 | navigate(`/ticket/${ticketId}`); 23 | }; 24 | 25 | if (!workflowId) { 26 | return ( 27 | 28 | 缺少 workflow_id 参数,无法创建工单。 29 | 30 | ); 31 | } 32 | 33 | return ( 34 | 39 | ); 40 | } 41 | 42 | export default NewTicketPage; 43 | -------------------------------------------------------------------------------- /frontend/src/components/formFields/TextAreaField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField as MuiTextField, FormControl } from '@mui/material'; 3 | import ViewField from './ViewField'; 4 | 5 | interface TextAreaFieldProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | mode: 'view' | 'edit'; 9 | props: any; 10 | } 11 | 12 | function TextAreaField({ 13 | value = '', 14 | onChange, 15 | mode, 16 | props, 17 | }: TextAreaFieldProps) { 18 | 19 | const handleChange = (event: React.ChangeEvent) => { 20 | if (onChange) { 21 | onChange(event.target.value); 22 | } 23 | }; 24 | 25 | // view mode only show value 26 | if (mode === 'view') { 27 | return ( 28 | 29 | ); 30 | } 31 | 32 | // edit mode, support edit 33 | return ( 34 | 35 | 46 | 47 | ); 48 | 49 | }; 50 | 51 | export default TextAreaField; -------------------------------------------------------------------------------- /frontend/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import camelcaseKeys from 'camelcase-keys'; 3 | import snakecaseKeys from 'snakecase-keys'; 4 | import { getCookie, removeCookie } from '../utils/cookie'; 5 | 6 | 7 | const apiClient = axios.create({ 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | 13 | apiClient.interceptors.request.use( 14 | (config) => { 15 | const token = getCookie('jwtToken'); 16 | if (token && !config.url?.includes('/api/v1.0/login')) { 17 | config.headers.Authorization = `Bearer ${token}`; 18 | } 19 | if (config.data && typeof config.data === 'object') { 20 | config.data = snakecaseKeys(config.data, { deep: true }); 21 | } 22 | if (config.params && typeof config.params === 'object') { 23 | config.params = snakecaseKeys(config.params, { deep: true }); 24 | } 25 | return config; 26 | }, 27 | (error) => { 28 | return Promise.reject(error); 29 | } 30 | ); 31 | 32 | 33 | apiClient.interceptors.response.use( 34 | (response) => { 35 | if (response.data && typeof response.data === 'object') { 36 | response.data = camelcaseKeys(response.data, { deep: true }); 37 | } 38 | return response; 39 | }, 40 | (error) => { 41 | 42 | if (error.response && error.response.status === 401) { 43 | removeCookie('jwtToken'); 44 | window.location.href = '/signin'; 45 | } 46 | 47 | return Promise.reject(error); 48 | } 49 | ); 50 | 51 | export default apiClient; -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/normalNodeValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate normal nodes 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of normal node validation problems 8 | */ 9 | export const validateNormalNodes = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | // Check normal node must has assignee 13 | for (const node of workflowData.processSchema.nodeInfoList) { 14 | if (node.type === 'normal') { 15 | // 检查处理人类型 16 | if (!node.props.assigneeType) { 17 | problems.push(getValidationMessage('normal', 'assigneeTypeRequired', { 18 | nodeName: node.name 19 | })); 20 | } 21 | 22 | // 检查用户/部门设置 23 | if (!node.props.assignee || node.props.assignee === '') { 24 | problems.push(getValidationMessage('normal', 'assigneeRequired', { 25 | nodeName: node.name 26 | })); 27 | } 28 | 29 | // 检查分配策略 30 | if (!node.props.assignmentStrategy) { 31 | problems.push(getValidationMessage('normal', 'assignmentStrategyRequired', { 32 | nodeName: node.name 33 | })); 34 | } 35 | } 36 | } 37 | 38 | return problems; 39 | }; 40 | -------------------------------------------------------------------------------- /backend/apps/manage/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2025-10-07 05:49 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Notification', 17 | fields=[ 18 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 19 | ('label', models.JSONField(blank=True, default=dict, verbose_name='label')), 20 | ('creator_id', models.UUIDField(default=uuid.uuid4, editable=False, null=True, verbose_name='creator_id')), 21 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created_at')), 22 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated_at')), 23 | ('tenant_id', models.UUIDField(default='00000000-0000-0000-0000-000000000001', verbose_name='tenant_id')), 24 | ('name', models.CharField(default='', max_length=50, verbose_name='name')), 25 | ('description', models.CharField(default='', max_length=200, verbose_name='description')), 26 | ('type', models.CharField(default='', max_length=50, verbose_name='type')), 27 | ('extra', models.JSONField(max_length=1000, verbose_name='extra')), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /frontend/src/components/commonComponents/Snackbar/SnackbarProvider.tsx: -------------------------------------------------------------------------------- 1 | // src/components/Snackbar/SnackbarProvider.tsx 2 | import React, { createContext, ReactNode, useState } from 'react'; 3 | import GlobalSnackbar from './GlobalSnackbar'; 4 | 5 | interface SnackbarContextType { 6 | showMessage: (message: string | ReactNode, severity: 'error' | 'warning' | 'info' | 'success') => void; 7 | } 8 | 9 | export const SnackbarContext = createContext(undefined); 10 | 11 | interface SnackbarProviderProps { 12 | children: ReactNode; 13 | } 14 | 15 | const SnackbarProvider: React.FC = ({ children }) => { 16 | const [open, setOpen] = useState(false); 17 | const [message, setMessage] = useState(''); 18 | const [severity, setSeverity] = useState<'error' | 'warning' | 'info' | 'success'>('info'); 19 | 20 | const handleClose = (_event: React.SyntheticEvent | Event, reason?: string) => { 21 | if (reason === 'clickaway') { 22 | return; 23 | } 24 | setOpen(false); 25 | }; 26 | 27 | const showMessage = (msg: string | ReactNode, sev: 'error' | 'warning' | 'info' | 'success') => { 28 | setMessage(msg); 29 | setSeverity(sev); 30 | setOpen(true); 31 | }; 32 | 33 | return ( 34 | 35 | {children} 36 | 42 | 43 | ); 44 | }; 45 | 46 | export default SnackbarProvider; -------------------------------------------------------------------------------- /frontend/src/i18n/locales/en-US/ticket.json: -------------------------------------------------------------------------------- 1 | { 2 | "approveState": "Approve State", 3 | "keyword": "Search with keyword", 4 | "creator": "Creator", 5 | "createDateStart": "Create Date Start", 6 | "createDateEnd": "Create Date End", 7 | "ticketType": "Ticekt Type", 8 | "ticketStatus": "Ticket Status", 9 | "ticketTitle": "Ticket Title", 10 | "ticketCreator": "Ticket Creator", 11 | "ticketCreateTime": "Ticket Create Time", 12 | "allTickets": "All Tickets", 13 | "owerTickets": "Owner Tickets", 14 | "viewTickets": "View Tickets", 15 | "dutyTickets": "Duty Tickets", 16 | "relationTickets": "Relation Tickets", 17 | "interveneTickets": "Intervene Tickets", 18 | "adminActions": "Admin Actions", 19 | "selectAssignee": "Select Assignee", 20 | "operationRecord": "Operation Record", 21 | "processor": "processor", 22 | "actionType": "action type", 23 | "comment": "comment", 24 | "operationTime": "operation time", 25 | "actionName": { 26 | "force_forward": "force forward", 27 | "force_close": "force close", 28 | "force_alter_node": "force alter node", 29 | "add_comment": "add comment", 30 | "forward": "forward", 31 | "consult": "consult", 32 | "consult_submit": "consult submit", 33 | "accept": "accept", 34 | "withdraw": "withdraw" 35 | }, 36 | "state": { 37 | "in-draft": "Draft", 38 | "on-going": "In Progress", 39 | "rejected": "Rejected", 40 | "withdrawn": "Withdrawn", 41 | "finished": "Finished", 42 | "closed": "Closed" 43 | }, 44 | "ticketCreated": "Ticket Created", 45 | "ticketHandled": "ticket handled" 46 | } 47 | -------------------------------------------------------------------------------- /backend/service/util/archive_service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from django.forms.models import model_to_dict 5 | 6 | from service.base_service import BaseService 7 | from apps.util.models import Archive 8 | from django.core.serializers.json import DjangoJSONEncoder 9 | 10 | 11 | class ArchiveService(BaseService): 12 | """ 13 | archive service 14 | """ 15 | @classmethod 16 | def archive_record(cls, model_name, record, operator_id): 17 | """ 18 | archive record 19 | :param model_name: 20 | :param record: 21 | :param operator_id: 22 | :return: 23 | """ 24 | data = json.dumps(record.get_dict(), cls=DjangoJSONEncoder) 25 | archived_obj = Archive(data=data, model_name=model_name, creator_id=operator_id) 26 | archived_obj.save() 27 | record.delete() 28 | return True, "" 29 | 30 | @classmethod 31 | def archive_record_list(cls, model_name, record_queryset, operator_id): 32 | """ 33 | archive record list 34 | :param model_name: 35 | :param record_queryset: 36 | :param operator_id: 37 | :return: 38 | """ 39 | archive_list = [] 40 | for record in record_queryset: 41 | data = json.dumps(record.get_dict(), cls=DjangoJSONEncoder) 42 | archive_list.append(Archive(data=data, model_name=model_name, creator_id=operator_id)) 43 | Archive.objects.bulk_create(archive_list) 44 | 45 | record_queryset.delete() 46 | return True, "" 47 | 48 | 49 | archive_service_ins = ArchiveService() 50 | -------------------------------------------------------------------------------- /backend/loonflow/urls.py: -------------------------------------------------------------------------------- 1 | """loonflow URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | # from django.conf.urls import url, include 17 | from django.conf.urls.static import static 18 | from django.conf import settings 19 | from django.urls import path, include 20 | from apps.account.views import JwtLoginView 21 | from apps.manage.views import CommonConfigView 22 | from apps.workflow.views import WorkflowSimpleView 23 | 24 | urlpatterns = [ 25 | path('', include('apps.manage.urls')), 26 | path('manage', include('apps.manage.urls')), 27 | path('api/v1.0/login', JwtLoginView.as_view()), 28 | path('api/v1.0/accounts', include('apps.account.urls')), 29 | path('api/v1.0/tickets', include('apps.ticket.urls')), 30 | path('api/v1.0/workflows', include('apps.workflow.urls')), 31 | path('api/v1.0/simple_workflows', WorkflowSimpleView.as_view()), 32 | path('api/v1.0/manage', include('apps.manage.urls')), 33 | 34 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 35 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/who_need_read.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/introduction/who_need_read.rst:2 22 | msgid "2. Who Need to Read This Guide" 23 | msgstr "2. 适用读者" 24 | 25 | #: ../../source/introduction/who_need_read.rst:4 26 | msgid "This guide is intended for:" 27 | msgstr "本指南适用于:" 28 | 29 | #: ../../source/introduction/who_need_read.rst:6 30 | msgid "**End Users** - Who submit and handle tickets in their daily work" 31 | msgstr "**终端用户** —— 日常提交和处理工单的用户" 32 | 33 | #: ../../source/introduction/who_need_read.rst:7 34 | msgid "" 35 | "**Admins** - Who are responsible for deploying, customizing, and " 36 | "maintaining the LoonFlow instance" 37 | msgstr "**管理员** —— 负责部署、定制和维护 LoonFlow 实例" 38 | 39 | #: ../../source/introduction/who_need_read.rst:8 40 | msgid "" 41 | "**Workflow Admins** - Who are responsible for configuring workflows, " 42 | "Intervene tickets" 43 | msgstr "**工作流管理员** —— 负责配置流程并可干预工单" 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: '🪲 Report a bug' 2 | description: 'Report a bug that you have encountered' 3 | labels: 4 | - bug 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please use English to describe your issue, otherwise the issue will be force closed. Search with keyword before reporting a new bug to avoid duplicates. Thank you! 10 | - type: textarea 11 | id: description 12 | validations: 13 | required: true 14 | attributes: 15 | label: '📝 Description & Context' 16 | placeholder: | 17 | Describe the observed behavior that you believe to be incorrect, as well as any additional context that might be useful in understanding the bug. 18 | - type: textarea 19 | id: expected-behavior 20 | validations: 21 | required: true 22 | attributes: 23 | label: '👍 Expected Behavior' 24 | placeholder: | 25 | Describe the behavior or result that you expected instead of the observed one. Please provide screenshots if needed 26 | - type: input 27 | id: version 28 | validations: 29 | required: true 30 | attributes: 31 | label: Version 32 | description: please use the released version, name is startswith 'r'. eg. r3.0.0, r.2.0.18 etc. 33 | - type: input 34 | id: os 35 | attributes: 36 | label: os 37 | description: if you think this is os related issue, please provide the os info. 38 | - type: input 39 | id: browser 40 | attributes: 41 | label: browser 42 | description: if you think this is browser related issue, please provide the browser and version info 43 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/ticket/handle_ticket.rst: -------------------------------------------------------------------------------- 1 | Handle Tickets 2 | ================ 3 | 4 | After signing in to Loonflow, open your pending tickets from the left sidebar via **Workbench** or through **Ticket Management → My Duty**. Click **Detail** in the **Action** column to view a ticket. The detail page contains three main areas: 5 | 6 | .. figure:: ../../images/ticket_detail_page.png 7 | :width: 100% 8 | :align: center 9 | :alt: ticket detail page 10 | 11 | 12 | .. figure:: ../../images/workflow_diagram.png 13 | :width: 100% 14 | :align: center 15 | :alt: workflow diagram 16 | 17 | Ticket Detail 18 | -------------- 19 | Shows all visible or editable fields for the ticket. Actions available to you appear under the fields and fall into four categories: 20 | 21 | - edge actions: Actions defined on the outgoing edges of the current workflow node. 22 | - viewer actions: Available to anyone who can view the ticket, e.g., `ADD COMMENT`. 23 | - handler actions: Available to users permitted to handle the ticket, e.g., `FORWARD`, `CONSULT`, `CONSULT_SUBMIT`. 24 | - special actions: Context-specific options such as `CONSULT_SUBMIT` (for assignees during a consultation) or `WITHDRAW` (when withdrawal is allowed on the current node and you are the ticket creator). 25 | 26 | Admin Actions 27 | -------------- 28 | Visible when you are an administrator, the workflow manager for the ticket, or the ticket’s dispatcher. Options include `FORCE_FORWARD` and `FORCE_CLOSE`. 29 | 30 | Operation Record 31 | ----------------- 32 | Displays the ticket’s activity history, including actor, time, action type, and details of each operation. -------------------------------------------------------------------------------- /frontend/src/services/notification.ts: -------------------------------------------------------------------------------- 1 | import apiClient from './api'; 2 | 3 | 4 | interface INotificationNew { 5 | name: string, 6 | description: string, 7 | type: string, 8 | extra: string 9 | } 10 | 11 | export const getNotificationList = async (searchValue: string, page: number, perPage: number) => { 12 | const response = await apiClient.get('/api/v1.0/manage/notifications', { params: { search_value: searchValue, page: page + 1, per_page: perPage } }); 13 | return response.data; 14 | }; 15 | 16 | export const getSimpleNotificationList = async (searchValue: string, page: number, perPage: number) => { 17 | const response = await apiClient.get('/api/v1.0/manage/simple_notifications', { params: { search_value: searchValue, page, per_page: perPage } }); 18 | return response.data; 19 | }; 20 | 21 | export const delNotification = async (notificationId: string) => { 22 | const response = await apiClient.delete(`/api/v1.0/manage/notifications/${notificationId}`); 23 | return response.data; 24 | }; 25 | export const getNotificationDetail = async (notificationID: string) => { 26 | const response = await apiClient.get(`/api/v1.0/manage/notifications/${notificationID}`); 27 | return response.data 28 | }; 29 | 30 | export const addNotification = async (params: INotificationNew) => { 31 | const response = await apiClient.post('/api/v1.0/manage/notifications', params); 32 | return response.data; 33 | } 34 | 35 | export const updateNotification = async (notification_id: string, params: INotificationNew) => { 36 | const response = await apiClient.patch(`/api/v1.0/manage/notifications/${notification_id}`, params); 37 | return response.data; 38 | } -------------------------------------------------------------------------------- /sphinx_docs/source/installtion_deployment/system_requirements/os_environment_requirements.rst: -------------------------------------------------------------------------------- 1 | OS Environment Requirements 2 | ==================================== 3 | 4 | This section describes the operating system requirements for running **LoonFlow**. 5 | 6 | Linux (Recommended) 7 | -------------------- 8 | 9 | **LoonFlow is recommended to run on Linux operating systems, which is the preferred choice for production environments.** 10 | 11 | Supported All major Linux distributions: 12 | 13 | .. note:: 14 | Linux operating system is strongly recommended for production environments to ensure system stability and performance. 15 | 16 | Windows (Development Only) 17 | -------------------------- 18 | 19 | **Windows operating system is not recommended for running LoonFlow in production environments.** 20 | 21 | Since LoonFlow's asynchronous tasks are implemented using Celery, there may be compatibility issues on Windows systems, causing asynchronous tasks to fail to work properly. Here is some compatibility resoves https://stackoverflow.com/questions/37255548/how-to-run-celery-on-windows: 22 | 23 | .. warning:: 24 | you can use Windows as a development environment, but may encounter issues such as asynchronous task execution exceptions. 25 | 26 | macOS (Development Only) 27 | ------------------------ 28 | 29 | **macOS can be used as a development environment, but is not recommended for production environments.** 30 | 31 | Supported common macOS versions. 32 | 33 | .. note:: 34 | macOS is suitable for local development and testing, but is not recommended for production environment deployment. Linux operating system should be used for production environments. -------------------------------------------------------------------------------- /backend/tests/test_views/test_workflow_view.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.test import TestCase 3 | from tests.base import LoonflowApiCall 4 | 5 | 6 | class TestWorkflowView(TestCase): 7 | fixtures = ['accounts.json', 'workflows.json'] 8 | 9 | def test_get_workflow_list_without_arg(self): 10 | """ 11 | 获取工作流列表 12 | :return: 13 | """ 14 | url = '/api/v1.0/workflows' 15 | response_content_dict = LoonflowApiCall().api_call('get', url) 16 | self.assertEqual(response_content_dict.get('code'), 0) 17 | self.assertGreater(len(response_content_dict.get('data')), 1) 18 | 19 | def test_get_workflow_init_state(self): 20 | """ 21 | 获取工作流初始状态 22 | :return: 23 | """ 24 | workflow_id = 1 25 | url = '/api/v1.0/workflows/{}/init_state'.format(workflow_id) 26 | response_content_dict = LoonflowApiCall().api_call('get', url) 27 | self.assertEqual(response_content_dict.get('code'), 0) 28 | 29 | def test_get_workflow_states(self): 30 | """ 31 | 获取工作流状态列表 32 | :return: 33 | """ 34 | workflow_id = 1 35 | url = '/api/v1.0/workflows/{}/states'.format(workflow_id) 36 | response_content_dict = LoonflowApiCall().api_call('get', url) 37 | self.assertEqual(response_content_dict.get('code'), 0) 38 | 39 | def test_get_workflow_state_detail(self): 40 | """ 41 | 获取状态详情 42 | :return: 43 | """ 44 | state_id = 3 45 | url = '/api/v1.0/workflows/states/{}'.format(state_id) 46 | response_content_dict = LoonflowApiCall().api_call('get', url) 47 | self.assertEqual(response_content_dict.get('code'), 0) 48 | -------------------------------------------------------------------------------- /backend/settings/test.py: -------------------------------------------------------------------------------- 1 | from settings.common import * 2 | 3 | # for multi computer room deploy and use separate redis server 4 | DEPLOY_ZONE = '' 5 | 6 | MIDDLEWARE = [ 7 | 'service.csrf_service.DisableCSRF', 8 | 'django.middleware.security.SecurityMiddleware', 9 | 'django.contrib.sessions.middleware.SessionMiddleware', 10 | 'django.middleware.common.CommonMiddleware', 11 | 'django.middleware.csrf.CsrfViewMiddleware', 12 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 13 | 'service.permission.app_permission.AppPermissionCheck', 14 | 'django.contrib.messages.middleware.MessageMiddleware', 15 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 16 | ] 17 | 18 | 19 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 20 | MEDIA_URL = '/media/' 21 | 22 | # Database 23 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 28 | 'NAME': 'loonflow_test.db', # Or path to database file if using sqlite3. 29 | 'TEST_CHARSET': 'utf8', 30 | 'TEST_COLLATION': 'utf8_general_ci', 31 | } 32 | } 33 | 34 | REDIS_HOST = '127.0.0.1' 35 | REDIS_PORT = 6379 36 | REDIS_DB = 0 37 | REDIS_PASSWORD = '' 38 | 39 | if REDIS_PASSWORD: 40 | CELERY_BROKER_URL = 'redis://:{}@{}:{}/{}'.format(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_DB) 41 | else: 42 | CELERY_BROKER_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_DB) 43 | 44 | FIXTURE_DIRS = ('tests/fixtures/',) 45 | 46 | ENCRYPTION_KEY = '2VLMQOroSgJUC68n30X9VzFkUPzN0oYpprGlwy/ffmk=' # you can generate your key, refer to document -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/formValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate form schema 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of form validation problems 8 | */ 9 | export const validateFormSchema = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | // Check if form design is empty 13 | if (workflowData.formSchema.componentInfoList.length === 0) { 14 | problems.push(getValidationMessage('form', 'formDesignEmpty')); 15 | } 16 | 17 | // Check if every row component has children 18 | for (const component of workflowData.formSchema.componentInfoList) { 19 | if (component.type === 'row') { 20 | if (component.children.length === 0) { 21 | problems.push(getValidationMessage('form', 'rowComponentNoChildren')); 22 | } 23 | } 24 | } 25 | 26 | // Rule: title component MUST exist exactly once 27 | // Flatten components to count title components 28 | const allComponents = workflowData.formSchema.componentInfoList.flatMap((component: any) => { 29 | if (component.type === 'row') { 30 | return component.children || []; 31 | } 32 | return [component]; 33 | }); 34 | 35 | const titleComponents = allComponents.filter((c: any) => c.type === 'title'); 36 | if (titleComponents.length !== 1) { 37 | problems.push(getValidationMessage('form', 'titleComponentCountError', { 38 | count: titleComponents.length 39 | })); 40 | } 41 | 42 | return problems; 43 | }; 44 | -------------------------------------------------------------------------------- /backend/service/util/encrypt_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | from django.conf import settings 4 | from service.base_service import BaseService 5 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 6 | from cryptography.hazmat.backends import default_backend 7 | from cryptography.hazmat.primitives import padding 8 | 9 | 10 | class EncryptService(BaseService): 11 | @classmethod 12 | def encrypt(cls, data:str)->str: 13 | """ 14 | AES encrypt 15 | """ 16 | encrypt_key = settings.ENCRYPTION_KEY 17 | iv = os.urandom(16) 18 | padder = padding.PKCS7(128).padder() 19 | padded_data = padder.update(data.encode()) + padder.finalize() 20 | cipher = Cipher(algorithms.AES(base64.b64decode(encrypt_key)), modes.CBC(iv), backend=default_backend()) 21 | encryptor = cipher.encryptor() 22 | encrypted_data = encryptor.update(padded_data) + encryptor.finalize() 23 | return base64.b64encode(iv).decode('utf-8') +',' + base64.b64encode(encrypted_data).decode('utf-8') 24 | 25 | @classmethod 26 | def decrypt(cls, data:str)->str: 27 | encrypt_key = settings.ENCRYPTION_KEY 28 | iv = base64.b64decode(data.split(',')[0]) 29 | encrypted_data = base64.b64decode(data.split(',')[1]) 30 | cipher = Cipher(algorithms.AES(base64.b64decode(encrypt_key)), modes.CBC(iv), backend=default_backend()) 31 | decryptor = cipher.decryptor() 32 | decrypted_padded_data = decryptor.update(encrypted_data) + decryptor.finalize() 33 | unpadder = padding.PKCS7(128).unpadder() 34 | data = unpadder.update(decrypted_padded_data) + unpadder.finalize() 35 | return data.decode() 36 | 37 | 38 | encrypt_service_ins = EncryptService() -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue Report 3 | about: Report LoonFlow issues, feature requests, or questions 4 | title: '' 5 | labels: ['bug', 'question', 'enhancement'] 6 | assignees: '' 7 | --- 8 | 9 | ## ⚠️ IMPORTANT: Language Requirement 10 | **This issue MUST be written in English. Issues written in other languages will be closed immediately without review.** 11 | 12 | ## Issue Type 13 | 14 | - [ ] Bug Report 15 | - [ ] Feature Request 16 | - [ ] Question/Help 17 | - [ ] Other 18 | 19 | ## LoonFlow Version 20 | 21 | 22 | ## Description 23 | 24 | 25 | ## System Logs Check 26 | 27 | - [ ] Checked system logs 28 | - [ ] Not checked system logs 29 | - [ ] Not applicable 30 | 31 | ## Historical Issues Search 32 | 33 | - [ ] Searched historical issues 34 | - [ ] Not searched historical issues 35 | 36 | ## Attempted Solutions 37 | 38 | 39 | ## Environment Information 40 | - Operating System: 41 | - Python Version: 42 | - Django Version: 43 | - Database Type: 44 | - Browser Type and Version: 45 | 46 | ## Steps to Reproduce (Bug Reports Only) 47 | 1. 48 | 2. 49 | 3. 50 | 51 | ## Expected Behavior 52 | 53 | 54 | ## Actual Behavior 55 | 56 | 57 | ## Additional Information 58 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r1.x/r1.0.0.rst: -------------------------------------------------------------------------------- 1 | r1.0.0 2 | --------- 3 | 4 | - Upgraded to Python 3.6 5 | - Unified configuration file to config.py 6 | - New API: Withdraw ticket 7 | - Ticket details API added return of current state detailed information 8 | - Allow ticket creator to close ticket directly in initial state 9 | - Ticket list API performance optimization 10 | - flowstep API added return of current state information, and records sorted by state order id 11 | - Ticket list query API added support for query conditions: draft, in progress, withdrawn, returned, completed 12 | - Custom notifications changed from script to hook method 13 | - Admin panel homepage added ticket quantity classification statistics 14 | - Admin panel displays current detailed version number 15 | - Admin panel supports user, department, and role editing 16 | - Admin panel hides handler input box information for initial and end states when configuring states 17 | - Admin panel supports ticket intervention: directly close, transfer, modify ticket state, delete 18 | - Support setting multiple departments when state participant type is department 19 | - Transition operation supports target state as initial state: no longer need to configure an intermediate state like "Creator Editing" 20 | - Workflow state hook supports configuring additional parameter information 21 | - Admin panel permission control refined: divided into super administrator and workflow administrator 22 | - Using readthedoc to manage project documentation 23 | - Static files moved from CDN to local to avoid inability to use normally when internal network deployment has no external network access 24 | - Code structure and internal logic optimization (removed redundant code, singleton pattern to reduce memory usage, database operation statement optimization, type hints, view parameter strong validation, etc.) 25 | -------------------------------------------------------------------------------- /sphinx_docs/source/user_guide/ticket/query_ticket.rst: -------------------------------------------------------------------------------- 1 | Query tickets 2 | ============= 3 | 4 | After signing in to Loonflow you can browse different ticket lists. The workbench shows your pending tasks. Under the “Ticket management” menu you will find: 5 | 6 | - `My Duty`: tickets currently assigned to you 7 | - `My Owner`: tickets you created 8 | - `My Relation`: tickets you are related to (created by you, previously assigned to you, handled by you, commented by you, or force‑intervened by you) 9 | - `My View`: tickets in workflows where you have view permission 10 | - `My Intervene`: tickets whose workflows list you as Dispatcher 11 | - `All Tickets`: every ticket; only administrators can view and intervene through this entry 12 | 13 | .. figure:: ../../images/category_tickets.png 14 | :width: 100% 15 | :align: center 16 | :alt: Category Tickets 17 | 18 | Each ticket list provides filters above the table: `Keyword`, `Creator`, `Created time range`, and `Workflow` (ticket type). `Keyword` currently supports fuzzy search on the ticket title only. To enable searching by custom fields, set the title field to `Auto Generate` in the workflow form designer and include the desired fields in the title template. The system will then auto-generate titles containing those field values, letting you find tickets via those custom fields. 19 | 20 | .. figure:: ../../images/title_property.png 21 | :width: 100% 22 | :align: center 23 | :alt: title property 24 | 25 | .. note:: 26 | if you set title property to "Auto Generate", the system will auto-generate a title for the ticket based on the custom fields. so you must set the title field's permission to "hidden" for the start node. 27 | 28 | 29 | .. figure:: ../../images/start_node_title_auto_generate.png 30 | :width: 100% 31 | :align: center 32 | :alt: start node properly if title field is auto_generate -------------------------------------------------------------------------------- /backend/apps/loon_base_view.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import simplejson 3 | from django.views import View 4 | from schema import SchemaError 5 | 6 | from service.format_response import api_response 7 | 8 | logger = logging.getLogger('django') 9 | 10 | 11 | class BaseView(View): 12 | """ 13 | base view for params validate 14 | """ 15 | def dispatch(self, request, *args, **kwargs): 16 | # Try to dispatch to the right method; if a method doesn't exist, 17 | # defer to the error handler. Also defer to the error handler if the 18 | # request method isn't on the approved list. 19 | if request.method.lower() in self.http_method_names: 20 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 21 | else: 22 | handler = self.http_method_not_allowed 23 | request_method = request.method.lower() 24 | meth_schema = getattr(self, request.method.lower() + '_schema', None) 25 | if meth_schema: 26 | if request_method in ['post', 'patch', 'put', 'delete']: 27 | json_dict = simplejson.loads(request.body) 28 | else: 29 | request_data_dict = dict(request.GET) 30 | json_dict = dict() 31 | for key, value in request_data_dict.items(): 32 | json_dict[key] = ','.join(value) 33 | 34 | try: 35 | meth_schema.validate(json_dict) 36 | except SchemaError as Se: 37 | logger.error('111111') 38 | logger.error(Se) 39 | return api_response(-1, 'Request data is invalid:{}'.format(str(Se)), {}) 40 | except Exception as e: 41 | logger.error('2222') 42 | logger.error(e) 43 | return api_response(-1, 'Internal Server Error', {}) 44 | return handler(request, *args, **kwargs) 45 | -------------------------------------------------------------------------------- /backend/settings/test.py.sample: -------------------------------------------------------------------------------- 1 | from settings.common import * 2 | 3 | # for multi computer room deploy and use separate redis server 4 | DEPLOY_ZONE = '' 5 | 6 | MIDDLEWARE = [ 7 | 'service.csrf_service.DisableCSRF', 8 | 'django.middleware.security.SecurityMiddleware', 9 | 'django.contrib.sessions.middleware.SessionMiddleware', 10 | 'django.middleware.common.CommonMiddleware', 11 | 'django.middleware.csrf.CsrfViewMiddleware', 12 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 13 | 'service.permission.app_permission.AppPermissionCheck', 14 | 'django.contrib.messages.middleware.MessageMiddleware', 15 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 16 | ] 17 | 18 | 19 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 20 | MEDIA_URL = '/media/' 21 | 22 | # Database 23 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 24 | 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 28 | 'NAME': 'loonflow_test.db', # Or path to database file if using sqlite3. 29 | 'TEST_CHARSET': 'utf8', 30 | 'TEST_COLLATION': 'utf8_general_ci', 31 | } 32 | } 33 | 34 | REDIS_HOST = '127.0.0.1' 35 | REDIS_PORT = 6379 36 | REDIS_DB = 0 37 | REDIS_PASSWORD = '' 38 | 39 | if REDIS_PASSWORD: 40 | CELERY_BROKER_URL = 'redis://:{}@{}:{}/{}'.format(REDIS_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_DB) 41 | else: 42 | CELERY_BROKER_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_DB) 43 | 44 | FIXTURE_DIRS = ('tests/fixtures/',) 45 | 46 | HOOK_HOST_ALLOWED = [] # host list that allow used as hook url, such as ['192,168.1.12', 'www.baidu.com'], if no this setting key means allow 47 | 48 | JWT_SALT = 'aUApFqfQjyYVAPo8' 49 | ENCRYPTION_KEY = '2VLMQOroSgJUC68n30X9VzFkUPzN0oYpprGlwy/ffmk=' # you can generate your key, refer to document -------------------------------------------------------------------------------- /backend/service/account/account_tenant_service.py: -------------------------------------------------------------------------------- 1 | from apps.account.models import Tenant 2 | from service.base_service import BaseService 3 | from service.exception.custom_common_exception import CustomCommonException 4 | 5 | 6 | class AccountTenantService(BaseService): 7 | 8 | @classmethod 9 | def get_tenant_detail(cls, tenant_id): 10 | """ 11 | get tenant detail info 12 | :param tenant_id: 13 | :return: 14 | """ 15 | try: 16 | tenant_obj = Tenant.objects.get(id=tenant_id) 17 | except Tenant.DoesNotExist as e: 18 | raise CustomCommonException("tenant is not exist or has been deleted") 19 | except: 20 | raise 21 | result = tenant_obj.get_dict() 22 | result.pop("creator_info") 23 | return result 24 | 25 | @classmethod 26 | def get_tenant_by_domain(cls, domain): 27 | """ 28 | get tenant detail info by domain 29 | :param domain: domain name 30 | :return: tenant info dict 31 | """ 32 | try: 33 | tenant_obj = Tenant.objects.get(domain=domain) 34 | except Tenant.DoesNotExist: 35 | # If tenant not found by domain, return default tenant with id '00000000-0000-0000-0000-000000000001' 36 | try: 37 | tenant_obj = Tenant.objects.get(id='00000000-0000-0000-0000-000000000001') 38 | except Tenant.DoesNotExist: 39 | raise CustomCommonException("Default tenant not found") 40 | except Exception: 41 | raise 42 | except Exception: 43 | raise 44 | result = tenant_obj.get_dict() 45 | res_result = { 46 | 'id': result['id'], 47 | 'name': result['name'], 48 | 'domain': result['domain'], 49 | 'logo_path': result['logo_path'], 50 | } 51 | return res_result 52 | 53 | 54 | account_tenant_service_ins = AccountTenantService() 55 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/application.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/setting/application.rst:2 22 | msgid "Application Management" 23 | msgstr "应用管理" 24 | 25 | #: ../../source/user_guide/setting/application.rst:4 26 | msgid "" 27 | "Applications are used to authenticate API calls. After creating an " 28 | "application, a token is generated and should be used to sign each " 29 | "request. The signing algorithm is documented in the Postman collection: " 30 | "https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 31 | msgstr "应用用于鉴权 API 调用。创建应用后会生成 token,需在每次请求中签名,签名算法见 Postman 集合:https://documenter.getpostman.com/view/15031929/2sB3WyJbap" 32 | 33 | #: ../../source/user_guide/setting/application.rst:6 34 | msgid "Application management page" 35 | msgstr "应用管理页面" 36 | 37 | #: ../../source/user_guide/setting/application.rst:13 38 | msgid "Application types" 39 | msgstr "应用类型" 40 | 41 | #: ../../source/user_guide/setting/application.rst:14 42 | msgid "admin: Administrators can call all APIs." 43 | msgstr "admin:管理员可调用全部接口" 44 | 45 | #: ../../source/user_guide/setting/application.rst:15 46 | msgid "" 47 | "workflow_admin: Workflow administrators can call workflow/ticket APIs and" 48 | " common APIs." 49 | msgstr "workflow_admin:流程管理员可调用工作流/工单及公共接口" 50 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 27 | Loonflow 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/introduction/welcome/getting_help.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/introduction/welcome/getting_help.rst:2 22 | msgid "1.3 Get Help & Community Support" 23 | msgstr "1.3 获取帮助与社区支持" 24 | 25 | #: ../../source/introduction/welcome/getting_help.rst:4 26 | msgid "" 27 | "📝 GitHub Issues - Submit bug reports and feature requests. " 28 | "https://github.com/blackholll/loonflow/issues" 29 | msgstr "📝 GitHub Issues - 提交缺陷与功能需求:https://github.com/blackholll/loonflow/issues" 30 | 31 | #: ../../source/introduction/welcome/getting_help.rst:5 32 | msgid "💬 Discussion Forum Discord. https://discord.gg/WuppaG638k" 33 | msgstr "💬 讨论论坛 Discord:https://discord.gg/WuppaG638k" 34 | 35 | #: ../../source/introduction/welcome/getting_help.rst:6 36 | msgid "" 37 | "📧 Commercial Support & Customization: For enterprise-level deep " 38 | "customization, technical training, or deployment support needs, please " 39 | "contact me at [blackholll@163.com;blackholll.cn@gmail.com]." 40 | msgstr "📧 商业支持与定制:如需企业级深度定制、技术培训或部署支持,请联系 [blackholll@163.com;blackholll.cn@gmail.com]。" 41 | 42 | #: ../../source/introduction/welcome/getting_help.rst:7 43 | msgid "💰 Member Benefits. See https://patreon.com/blackholllcn for more details." 44 | msgstr "💰 会员权益:详见 https://github.com/blackholll/loonflow/blob/master/README_zh.md" 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/timerNodeValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate timer nodes 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of timer node validation problems 8 | */ 9 | export const validateTimerNodes = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | for (const node of workflowData.processSchema.nodeInfoList) { 13 | if (node.type === 'timer') { 14 | // 获取定时器节点的入边和出边 15 | const inputEdges = workflowData.processSchema.edgeInfoList.filter( 16 | (edge) => edge.targetNodeId === node.id 17 | ); 18 | const outputEdges = workflowData.processSchema.edgeInfoList.filter( 19 | (edge) => edge.sourceNodeId === node.id 20 | ); 21 | 22 | // 检查定时器节点必须有超过一个入边 23 | if (inputEdges.length < 1) { 24 | problems.push(getValidationMessage('timer', 'needInputEdge', { 25 | nodeName: node.name, 26 | count: inputEdges.length 27 | })); 28 | } 29 | 30 | // 检查定时器节点只能有一个出边 31 | if (outputEdges.length !== 1) { 32 | problems.push(getValidationMessage('timer', 'onlyOneOutputEdge', { 33 | nodeName: node.name, 34 | count: outputEdges.length 35 | })); 36 | } 37 | 38 | // 检查定时器节点的出边目标节点不可以是自己 39 | for (const outputEdge of outputEdges) { 40 | if (outputEdge.targetNodeId === node.id) { 41 | problems.push(getValidationMessage('timer', 'outputEdgeCannotBeSelf', { 42 | nodeName: node.name 43 | })); 44 | } 45 | } 46 | } 47 | } 48 | 49 | return problems; 50 | }; 51 | -------------------------------------------------------------------------------- /backend/apps/account/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from apps.account.views import UserView, UserDetailView, RoleView, DeptView, UserRoleView, RoleUserView, \ 3 | UserResetPasswordView, RoleDetailView, \ 4 | DeptDetailView, SimpleUsersView, UserChangePasswordView, DeptPathsView,DeptPathView,\ 5 | DeptTreeView, UserProfileView, SimpleRolesView, ApplicationView, SimpleApplicationView, \ 6 | ApplicationDetailView, TenantDetailView, TenantDomainView, DeptParentDeptView, ApplicationWorkflowPermissionListView 7 | 8 | urlpatterns = [ 9 | path('/users', UserView.as_view()), 10 | path('/my_profile', UserProfileView.as_view()), 11 | path('/simple_users', SimpleUsersView.as_view()), 12 | path('/users/change_password', UserChangePasswordView.as_view()), 13 | path('/users/', UserDetailView.as_view()), 14 | path('/users//roles', UserRoleView.as_view()), 15 | path('/users//reset_password', UserResetPasswordView.as_view()), 16 | path('/roles', RoleView.as_view()), 17 | path('/roles/', RoleDetailView.as_view()), 18 | path('/simple_roles', SimpleRolesView.as_view()), 19 | path('/roles//users', RoleUserView.as_view()), 20 | path('/depts_tree', DeptTreeView.as_view()), 21 | path('/dept_paths', DeptPathsView.as_view()), 22 | path('/dept_paths/', DeptPathView.as_view()), 23 | path('/depts', DeptView.as_view()), 24 | path('/depts/', DeptDetailView.as_view()), 25 | path('/depts//parent_dept', DeptParentDeptView.as_view()), 26 | path('/applications', ApplicationView.as_view()), 27 | path('/applications/', ApplicationDetailView.as_view()), 28 | path('/applications//workflows', ApplicationWorkflowPermissionListView.as_view()), 29 | path('/simple_applications', SimpleApplicationView.as_view()), 30 | path('/tenants/by_domain', TenantDomainView.as_view()), 31 | path('/tenants/', TenantDetailView.as_view()) 32 | ] 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: '💡 Request a feature' 2 | description: 'Request a new feature or change to loonflow' 3 | labels: 4 | - enhancement 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | This template helps your propose new feature, enhancements, or other ideas for Loonflow. Please use English to describe your request, otherwise the issue will be force closed. 10 | - type: textarea 11 | id: need 12 | validations: 13 | required: true 14 | attributes: 15 | label: '🔖 Need' 16 | description: What is the rationale for this change, and who will benefit from it? 17 | placeholder: | 18 | Explain why this change is important and who will benefit from it. Provide context for the change, such as highlighting relevant past work in this area or specific use cases. 19 | - type: textarea 20 | id: proposal 21 | validations: 22 | required: true 23 | attributes: 24 | label: '📝 Proposal' 25 | description: Describe the change that you are suggesting. 26 | placeholder: | 27 | Provide sufficient detail for reviewers to give concrete feedback. 28 | 29 | Including a proposed implementation is optional, but if you do, consider adding design details such as TypeScript examples, database schema, or sequence diagrams. 30 | 31 | If the change requires particular care when being rolled out, it can be helpful to include a plan for a phased release. 32 | - id: willing-to-submit-pr 33 | type: dropdown 34 | attributes: 35 | label: Are you willing to submit a PR? 36 | description: This is absolutely not required, but we are happy to guide you in the contribution process. 37 | options: 38 | - Undecided 39 | - Yes, and I have enough information to get started 40 | - Yes, but I would like some more guidance 41 | - No, but I'm happy to collaborate on a PR with someone else 42 | - No, I don't have time to work on this right now 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/connectivityValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate node connectivity 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of connectivity validation problems 8 | */ 9 | export const validateNodeConnectivity = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | // Check if process design is empty 13 | if (workflowData.processSchema.nodeInfoList.length === 0) { 14 | problems.push(getValidationMessage('connectivity', 'processDesignEmpty')); 15 | } 16 | 17 | // Check if dissociative node exist, means start node has no output edge, end node has no input edge, common node missing input edge or output edge 18 | for (const node of workflowData.processSchema.nodeInfoList) { 19 | const outputEdgeList = workflowData.processSchema.edgeInfoList.filter((edge) => edge.sourceNodeId === node.id); 20 | const inputEdgeList = workflowData.processSchema.edgeInfoList.filter((edge) => edge.targetNodeId === node.id); 21 | 22 | if (node.type === 'start') { 23 | if (outputEdgeList.length === 0) { 24 | problems.push(getValidationMessage('connectivity', 'startNodeNoOutput')); 25 | } 26 | } 27 | 28 | if (node.type === 'end') { 29 | if (inputEdgeList.length === 0) { 30 | problems.push(getValidationMessage('connectivity', 'endNodeNoInput')); 31 | } 32 | } 33 | 34 | if (['normal', 'parallel', 'exclusive', 'timer', 'hook'].includes(node.type)) { 35 | if (outputEdgeList.length === 0 || inputEdgeList.length === 0) { 36 | problems.push(getValidationMessage('connectivity', 'nodeNoConnection', { 37 | nodeType: node.type 38 | })); 39 | } 40 | } 41 | } 42 | 43 | return problems; 44 | }; 45 | -------------------------------------------------------------------------------- /backend/tests/base.py: -------------------------------------------------------------------------------- 1 | 2 | class LoonflowApiCall(object): 3 | """ 4 | loonflow api调用 5 | """ 6 | def __init__(self, username='admin'): 7 | from service.common.common_service import common_service_ins 8 | flag, msg = common_service_ins.gen_signature('ops') 9 | if not flag: 10 | pass 11 | self.signature = msg.get('signature', '') 12 | self.timestamp = msg.get('timestamp', '') 13 | self.headers = {'HTTP_SIGNATURE': self.signature, 'HTTP_TIMESTAMP': self.timestamp, 'HTTP_APPNAME': 'ops', 'HTTP_USERNAME':username} 14 | 15 | def api_call(self, method, url, params={}): 16 | import json 17 | from django.test.client import Client 18 | c = Client() 19 | if method not in ('get', 'post', 'patch', 'delete', 'put'): 20 | return json.loads(dict(code=-1, msg='method is invalid')) 21 | if method == 'get': 22 | response_content = c.get(url, data=params, **self.headers).content 23 | elif method == 'post': 24 | # response_content = c.post(url, params, content_type='application/json', **self.headers).content 25 | response_content = c.post(url, data=json.dumps(params), content_type='application/json', **self.headers).content 26 | elif method == 'patch': 27 | response_content = c.patch(url, data=json.dumps(params), content_type='application/json', **self.headers).content 28 | elif method == 'delete': 29 | response_content = c.delete(url, data=json.dumps(params), content_type='application/json', **self.headers).content 30 | elif method == 'put': 31 | response_content = c.put(url, data=json.dumps(params), content_type='application/json', **self.headers).content 32 | if url == '/api/v1.0/accounts/app_token': 33 | print('#'*30) 34 | print(method) 35 | print(params) 36 | print(response_content) 37 | print('#' * 30) 38 | response_content_dict = json.loads(str(response_content, encoding='utf-8')) 39 | 40 | return response_content_dict 41 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/workflow/workflow_version.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/workflow/workflow_version.rst:2 22 | msgid "Workflow Version Management" 23 | msgstr "流程版本管理" 24 | 25 | #: ../../source/user_guide/workflow/workflow_version.rst:4 26 | msgid "" 27 | "Loonflow supports multiple workflow versions. Each workflow can have the " 28 | "following version types:" 29 | msgstr "Loonflow 支持多版本流程。每个流程可拥有以下版本类型:" 30 | 31 | #: ../../source/user_guide/workflow/workflow_version.rst:7 32 | msgid "" 33 | "**default**: The active version used when a user selects the workflow in " 34 | "Workbench to create a new ticket." 35 | msgstr "**default**:当前生效版本,工作台选择流程创建工单时使用。" 36 | 37 | #: ../../source/user_guide/workflow/workflow_version.rst:9 38 | msgid "" 39 | "**candidate**: A draft version for validation. Administrators can switch " 40 | "the version type or create test tickets with the candidate version." 41 | msgstr "**candidate**:候选版本,用于验证。管理员可切换版本类型或用该版本创建测试工单。" 42 | 43 | #: ../../source/user_guide/workflow/workflow_version.rst:11 44 | msgid "" 45 | "**archived**: A retired version. New tickets cannot be created with it, " 46 | "but existing tickets created in the past can still be processed." 47 | msgstr "**archived**:归档版本,不能再创建新工单,但历史工单仍可按该版本继续处理。" 48 | 49 | #: ../../source/user_guide/workflow/workflow_version.rst:14 50 | msgid "workflow version management" 51 | msgstr "流程版本管理页面" 52 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/others/release_notes/r2.x/r2.0.0.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:2 22 | msgid "r2.0.0" 23 | msgstr "r2.0.0" 24 | 25 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:4 26 | msgid "" 27 | "Built-in ticket creation, viewing, processing, and management interface " 28 | "(major change in this release)" 29 | msgstr "内置工单创建、查看、处理、管理界面(本次版本重大变更)" 30 | 31 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:5 32 | msgid "" 33 | "Support for users belonging to multiple departments simultaneously " 34 | "(significant change in this release)" 35 | msgstr "支持用户同时隶属多个部门(本次版本重大变更)" 36 | 37 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:6 38 | msgid "flowlog API supports specifying order or reverse order" 39 | msgstr "flowlog 接口支持指定正序或倒序" 40 | 41 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:7 42 | msgid "" 43 | "Workflow configuration interface supports viewing daily new ticket " 44 | "statistics" 45 | msgstr "流程配置界面支持查看每日新增工单统计" 46 | 47 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:8 48 | msgid "Ticket details support administrator intervention" 49 | msgstr "工单详情支持管理员干预" 50 | 51 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:9 52 | msgid "Support for users to change their own passwords" 53 | msgstr "支持用户修改自己的密码" 54 | 55 | #: ../../source/others/release_notes/r2.x/r2.0.0.rst:10 56 | msgid "Various other optimizations" 57 | msgstr "其他多项优化" 58 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/Workflow/WorkflowDetail/WorkflowValidation/exclusiveNodeValidation.ts: -------------------------------------------------------------------------------- 1 | import { IWorkflowFullDefinition } from '../../../../types/workflow'; 2 | import { getValidationMessage } from './i18n'; 3 | 4 | /** 5 | * Validate exclusive gateway nodes 6 | * @param workflowData Complete workflow definition data 7 | * @returns List of exclusive gateway validation problems 8 | */ 9 | export const validateExclusiveNodes = (workflowData: IWorkflowFullDefinition): string[] => { 10 | const problems: string[] = []; 11 | 12 | for (const node of workflowData.processSchema.nodeInfoList) { 13 | if (node.type === 'exclusive') { 14 | // 获取排他网关的入边和出边 15 | const inputEdges = workflowData.processSchema.edgeInfoList.filter( 16 | (edge) => edge.targetNodeId === node.id 17 | ); 18 | const outputEdges = workflowData.processSchema.edgeInfoList.filter( 19 | (edge) => edge.sourceNodeId === node.id 20 | ); 21 | 22 | // 检查排他网关必须有入边和出边 23 | if (inputEdges.length === 0) { 24 | problems.push(getValidationMessage('exclusive', 'noInputEdge', { 25 | nodeName: node.name 26 | })); 27 | } 28 | if (outputEdges.length === 0) { 29 | problems.push(getValidationMessage('exclusive', 'noOutputEdge', { 30 | nodeName: node.name 31 | })); 32 | } 33 | 34 | // 检查排他网关的起始节点不可以是结束类型的节点 35 | for (const inputEdge of inputEdges) { 36 | const sourceNode = workflowData.processSchema.nodeInfoList.find( 37 | (n) => n.id === inputEdge.sourceNodeId 38 | ); 39 | if (sourceNode && sourceNode.type === 'end') { 40 | problems.push(getValidationMessage('exclusive', 'sourceNodeCannotBeEnd', { 41 | nodeName: node.name, 42 | sourceNodeName: sourceNode.name 43 | })); 44 | } 45 | } 46 | } 47 | } 48 | 49 | return problems; 50 | }; 51 | -------------------------------------------------------------------------------- /backend/tests/test_models/test_account_model.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from apps.account.models import User, Dept, Role 3 | 4 | 5 | class TestAccountModel(TestCase): 6 | fixtures = ['accounts.json'] 7 | 8 | def test_loon_user_get_dict(self): 9 | """ 10 | 测试获取字典格式用户信息 11 | :return: 12 | """ 13 | test_user1 = User.objects.get(username='guiji') 14 | assert isinstance(test_user1.get_dict(), dict) 15 | 16 | def test_loon_user_is_staff(self): 17 | """ 18 | 测试用户staff判断 19 | :return: 20 | """ 21 | test_user1 = User.objects.get(username='guiji') 22 | assert isinstance(test_user1.is_staff, bool) 23 | 24 | def test_loon_user_get_short_name(self): 25 | """ 26 | 测试获取short_name 27 | :return: 28 | """ 29 | test_user1 = User.objects.get(username='guiji') 30 | self.assertEqual(test_user1.get_short_name(), 'guiji') 31 | 32 | def test_loon_user_get_alias_name(self): 33 | """ 34 | 测试获取用户昵称 35 | :return: 36 | """ 37 | test_user1 = User.objects.get(username='guiji') 38 | self.assertEqual(test_user1.get_alias_name(), '轨迹') 39 | 40 | def test_loon_user_dept_name(self): 41 | """ 42 | 测试获取用户部门名称 43 | :return: 44 | """ 45 | test_user1 = User.objects.get(username='admin') 46 | self.assertEqual(test_user1.dept_name, '总部,技术部') 47 | 48 | def test_loon_user_get_json(self): 49 | """ 50 | 测试获取用户json格式信息 51 | :return: 52 | """ 53 | test_user1 = User.objects.get(username='guiji') 54 | assert isinstance(test_user1.get_json(), str) 55 | 56 | def test_role_get_dict(self): 57 | """ 58 | 测试获取角色字典信息格式 59 | :return: 60 | """ 61 | role = LoonRole.objects.get(name='VPN管理员') 62 | assert isinstance(role.get_dict(), dict) 63 | 64 | def test_dept_get_dict(self): 65 | """ 66 | 测试获取部门字典信息格式 67 | :return: 68 | """ 69 | dept = LoonDept.objects.get(id=1) 70 | assert isinstance(dept.get_dict(), dict) 71 | -------------------------------------------------------------------------------- /sphinx_docs/source/locale/zh_CN/LC_MESSAGES/user_guide/setting/tenant.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) 2020-2025, blackholll 3 | # This file is distributed under the same license as the loonflow package. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: loonflow \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-12-11 20:46+0800\n" 11 | "PO-Revision-Date: 2025-12-11 00:00+0800\n" 12 | "Last-Translator: ChatGPT \n" 13 | "Language: zh_CN\n" 14 | "Language-Team: zh_CN \n" 15 | "Plural-Forms: nplurals=1; plural=0;\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: ../../source/user_guide/setting/tenant.rst:2 22 | msgid "Tenant Management" 23 | msgstr "租户管理" 24 | 25 | #: ../../source/user_guide/setting/tenant.rst:4 26 | msgid "" 27 | "Loonflow supports multi-tenant. Most users only need a single tenant; if " 28 | "you require multi-tenant capabilities, please contact blackholll " 29 | "(blackholll@163.com; blackholll_cn@gmail.com) for licensing. In multi-" 30 | "tenant mode all data—users, tickets, workflows, notifications, " 31 | "applications, roles, departments, and more—are fully isolated. Typical " 32 | "scenarios include group enterprises or SaaS deployments where multiple " 33 | "independent organizations share one platform." 34 | msgstr "Loonflow 支持多租户。大多数用户只需单租户;如需多租户能力,请联系 blackholll(blackholll@163.com;blackholll_cn@gmail.com)获取授权。多租户模式下,用户、工单、流程、通知、应用、角色、部门等数据完全隔离,适用于集团企业或多组织共用平台的 SaaS 场景。" 35 | 36 | #: ../../source/user_guide/setting/tenant.rst:6 37 | msgid "tenant info" 38 | msgstr "租户信息" 39 | 40 | #: ../../source/user_guide/setting/tenant.rst:12 41 | msgid "Future roadmap" 42 | msgstr "后续规划" 43 | 44 | #: ../../source/user_guide/setting/tenant.rst:13 45 | msgid "" 46 | "Planned: cross-tenant ticket collaboration, allowing tickets to flow " 47 | "between tenants. Example scenarios: inter-company ticket handling, " 48 | "external customer cases, supplier tickets, and reseller/partner support." 49 | msgstr "规划中:跨租户工单协同,支持工单在租户间流转,适用于跨公司工单处理、外部客户案例、供应商工单、渠道伙伴支持等场景。" 50 | -------------------------------------------------------------------------------- /sphinx_docs/source/others/release_notes/r3.x/r3.0.0.rst: -------------------------------------------------------------------------------- 1 | r3.0.0 2 | --------- 3 | 4 | 🎯 Revolutionary Visual Design 5 | 6 | - **Drag-and-Drop Process Designer**: No complex configuration needed. Complete complex business process modeling through intuitive drag-and-drop connections. Supports advanced nodes like conditional branches, parallel tasks, and hooks. 7 | - **Smart Form Designer**: Powerful visual form building tool with rich field types (text, numbers, dropdowns, personnel selection, attachments, etc.) and flexible layouts. 8 | - **Real-time Preview & Validation**: Real-time preview during process design with built-in process logic validation to prevent design errors early. 9 | - **Multi-Version Process Configuration**: You can configure multiple versions of processes and easily test and switch between versions. 10 | 11 | 12 | 🔧 Ultimate Flexibility & Extensibility 13 | 14 | - **Plugin Architecture**: We provide plugin extension capabilities for almost all key nodes (such as custom actions, permission validation, notification methods, etc.). Your unique business logic can be easily integrated like building blocks. 15 | - **Powerful API System**: Provides comprehensive and clear RESTful APIs for seamless integration with your customer service systems, CMDB, monitoring systems, CI/CD, OA, and other third-party systems. 16 | - **Highly Customizable Permission Model**: Supports fine-grained permission control based on roles, departments, or even specific business conditions to meet complex enterprise permission management needs. 17 | 18 | 💼 Enterprise-Ready Features Out of the Box 19 | 20 | - **Multi-Type Ticket Support**: Easily manage various processes including IT operations, HR approvals, financial reimbursements, customer service, etc. 21 | - **Automation & Smart Routing**: Supports conditional routing based on form data, automatic assignee assignment, and intelligent ticket flow. 22 | - **Comprehensive Audit Logs**: Complete records of every operation from ticket creation to closure, meeting compliance and audit requirements. 23 | - **Multi-Tenant Support (Optional)**: Provides data isolation capabilities for SaaS providers or large enterprise groups (requires additional authorization). 24 | -------------------------------------------------------------------------------- /backend/service/permission/user_permission.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import wraps 3 | from service.account.account_application_service import account_application_service_ins 4 | from service.account.account_base_service import account_base_service_ins 5 | from service.account.account_user_service import account_user_service_ins 6 | from service.exception.custom_common_exception import CustomCommonException 7 | from service.format_response import api_response 8 | 9 | 10 | def user_permission_check(type='', workflow_id_source='url'): 11 | """ 12 | user permission check decorator 13 | :param workflow_id_source: from url or body 14 | :param types: 15 | :return: 16 | """ 17 | def decorator(func): 18 | @wraps(func) 19 | def _deco(view_class, request, *args, **kwargs): 20 | email = request.META.get('HTTP_EMAIL') 21 | tenant_id = request.META.get('HTTP_TENANTID') 22 | app_name = request.META.get('HTTP_APPNAME') 23 | if workflow_id_source == 'url': 24 | workflow_id = kwargs.get('workflow_id') 25 | workflow_version_id = kwargs.get('workflow_version_id') 26 | elif workflow_id_source == 'body': 27 | request_data = request.body 28 | workflow_id = json.loads(request_data).get('workflow_id') 29 | workflow_version_id = json.loads(request_data).get('workflow_version_id') 30 | if app_name != 'loonflow': 31 | try: 32 | account_application_service_ins.app_type_check(tenant_id, app_name, type, workflow_id, workflow_version_id) 33 | except CustomCommonException as e: 34 | return api_response(-1, 'has no permission: {}'.format(e.message), {}) 35 | else: 36 | try: 37 | account_user_service_ins.user_type_check(email=email, tenant_id=tenant_id, type=type) 38 | except CustomCommonException as e: 39 | return api_response(-1, 'has no permission: {}'.format(e.message), {}) 40 | return func(view_class, request, *args, **kwargs) 41 | return _deco 42 | return decorator 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/services/role.ts: -------------------------------------------------------------------------------- 1 | import apiClient from './api'; 2 | 3 | export const getRoleList = async (searchValue: string, page: number, perPage: number) => { 4 | const response = await apiClient.get('/api/v1.0/accounts/roles', { params: { search_value: searchValue, page: page + 1, per_page: perPage } }); 5 | return response.data; 6 | }; 7 | export const getSimpleRoles = async (searchValue: string, roleIds: string, page: number, perPage: number) => { 8 | const response = await apiClient.get('/api/v1.0/accounts/simple_roles', { params: { search_value: searchValue, role_ids: roleIds, page: page + 1, per_page: perPage } }); 9 | return response.data; 10 | }; 11 | export const deleteRole = async (roleId: string) => { 12 | const response = await apiClient.delete(`/api/v1.0/accounts/roles/${roleId}`); 13 | return response.data; 14 | }; 15 | 16 | export const addRole = async (name: string, description: string) => { 17 | const response = await apiClient.post('/api/v1.0/accounts/roles', { name, description }); 18 | return response.data; 19 | }; 20 | export const updateRole = async (roleId: string, name: string, description: string) => { 21 | const response = await apiClient.patch(`/api/v1.0/accounts/roles/${roleId}`, { name, description }); 22 | return response.data; 23 | } 24 | 25 | export const getRoleDetail = async (roleId: string) => { 26 | const response = await apiClient.get(`/api/v1.0/accounts/roles/${roleId}`); 27 | return response.data; 28 | }; 29 | 30 | export const addRoleUser = async (roleId: string, userIds: string[]) => { 31 | const response = await apiClient.post(`/api/v1.0/accounts/roles/${roleId}/users`, { user_ids: userIds }); 32 | return response.data; 33 | } 34 | 35 | export const deleteRoleUser = async (roleId: string, userIds: string[]) => { 36 | const response = await apiClient.delete(`/api/v1.0/accounts/roles/${roleId}/users`, { data: { user_ids: userIds } }); 37 | return response.data; 38 | } 39 | 40 | export const getRoleUserList = async (roleId: string, searchValue: string, page: number, perPage: number) => { 41 | const response = await apiClient.get(`/api/v1.0/accounts/roles/${roleId}/users`, { params: { search_value: searchValue, page: page + 1, per_page: perPage } }); 42 | return response.data; 43 | } --------------------------------------------------------------------------------