diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index d3553db6584..00000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "pip"
- directory: "/"
- schedule:
- interval: "weekly"
- timezone: "Asia/Shanghai"
- day: "friday"
- target-branch: "v3"
\ No newline at end of file
diff --git a/.github/workflows/build-and-push-python-pg.yml b/.github/workflows/build-and-push-python-pg.yml
index b0d9eb4ccce..fda7b40110c 100644
--- a/.github/workflows/build-and-push-python-pg.yml
+++ b/.github/workflows/build-and-push-python-pg.yml
@@ -4,9 +4,9 @@ on:
workflow_dispatch:
inputs:
architecture:
- description: 'Architecture'
+ description: "Architecture"
required: true
- default: 'linux/amd64'
+ default: "linux/amd64"
type: choice
options:
- linux/amd64
@@ -25,15 +25,15 @@ jobs:
run: |
DOCKER_IMAGE=ghcr.io/1panel-dev/maxkb-base
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
- TAG_NAME=python3.11-pg17.10-20260525
+ TAG_NAME=python3.13-pg17.11-20260902
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
- echo ::set-output name=docker_image::${DOCKER_IMAGE}
- echo ::set-output name=version::${TAG_NAME}
- echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --no-cache \
+ echo "docker_image=${DOCKER_IMAGE}" >> $GITHUB_OUTPUT
+ echo "version=${TAG_NAME}" >> $GITHUB_OUTPUT
+ echo "buildx_args=--platform ${DOCKER_PLATFORMS} --no-cache \
--build-arg VERSION=${TAG_NAME} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${GITHUB_SHA::8} \
- ${DOCKER_IMAGE_TAGS} .
+ ${DOCKER_IMAGE_TAGS} ." >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
@@ -48,4 +48,4 @@ jobs:
password: ${{ secrets.GH_TOKEN }}
- name: Docker Buildx (build-and-push)
run: |
- docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-base
\ No newline at end of file
+ docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-base
diff --git a/.github/workflows/build-and-push-vector-model.yml b/.github/workflows/build-and-push-vector-model.yml
index cbf6a5204c9..0a7c1e5524b 100644
--- a/.github/workflows/build-and-push-vector-model.yml
+++ b/.github/workflows/build-and-push-vector-model.yml
@@ -4,13 +4,13 @@ on:
workflow_dispatch:
inputs:
dockerImageTag:
- description: 'Docker Image Tag'
- default: 'v2.0.3'
+ description: "Docker Image Tag"
+ default: "v2.0.3"
required: true
architecture:
- description: 'Architecture'
+ description: "Architecture"
required: true
- default: 'linux/amd64'
+ default: "linux/amd64"
type: choice
options:
- linux/amd64
@@ -32,13 +32,13 @@ jobs:
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
- echo ::set-output name=docker_image::${DOCKER_IMAGE}
- echo ::set-output name=version::${TAG_NAME}
- echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --no-cache \
+ echo "docker_image=${DOCKER_IMAGE}" >> $GITHUB_OUTPUT
+ echo "version=${TAG_NAME}" >> $GITHUB_OUTPUT
+ echo "buildx_args=--platform ${DOCKER_PLATFORMS} --no-cache \
--build-arg VERSION=${TAG_NAME} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${GITHUB_SHA::8} \
- ${DOCKER_IMAGE_TAGS} .
+ ${DOCKER_IMAGE_TAGS} ." >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
@@ -53,4 +53,4 @@ jobs:
password: ${{ secrets.GH_TOKEN }}
- name: Docker Buildx (build-and-push)
run: |
- docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-vector-model
\ No newline at end of file
+ docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-vector-model
diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml
index 50b882fddc0..95b2368bd46 100644
--- a/.github/workflows/build-and-push.yml
+++ b/.github/workflows/build-and-push.yml
@@ -7,7 +7,7 @@ on:
inputs:
dockerImageTag:
description: 'Image Tag'
- default: 'v2.10.0-dev'
+ default: 'v3.0.0-dev'
required: true
dockerImageTagWithLatest:
description: '是否发布latest tag(正式发版时选择,测试版本切勿选择)'
diff --git a/.github/workflows/python-format-check.yml b/.github/workflows/python-format-check.yml
new file mode 100644
index 00000000000..d6634362adb
--- /dev/null
+++ b/.github/workflows/python-format-check.yml
@@ -0,0 +1,47 @@
+name: Python Format Check
+
+on:
+ pull_request:
+ types: [opened, reopened]
+
+jobs:
+ pre-commit:
+ name: Ruff format changed files
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+
+ - name: Install pre-commit
+ run: python -m pip install pre-commit==4.6.1
+
+ - name: Check changed files
+ env:
+ BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
+ HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: |
+ if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
+ if git show-ref --verify --quiet "refs/remotes/origin/${DEFAULT_BRANCH}"; then
+ BASE_SHA=$(git merge-base "$HEAD_SHA" "origin/${DEFAULT_BRANCH}")
+ elif git cat-file -e "${HEAD_SHA}^" 2>/dev/null; then
+ BASE_SHA=$(git rev-parse "${HEAD_SHA}^")
+ else
+ pre-commit run --all-files
+ git diff --cached --exit-code
+ exit 0
+ fi
+ fi
+
+ pre-commit run --from-ref "$BASE_SHA" --to-ref "$HEAD_SHA"
+
+ # The local hook formats and stages files. A clean index proves that
+ # every changed Python file was already formatted before CI ran.
+ git diff --cached --exit-code
diff --git a/.gitignore b/.gitignore
index 91e78cc6947..32f4aaf3ec9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
# VS Code
.vscode
+.claude/
+.codex/
*.project
*.factorypath
@@ -190,4 +192,5 @@ apps/models_provider/impl/tencent_model_provider/model/stt.py
tmp/
config.yml
.SANDBOX_BANNED_HOSTS
-copilot-instructions.md
\ No newline at end of file
+copilot-instructions.md
+.ai/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000000..3e75dd19aa1
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,8 @@
+repos:
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.15.21
+ hooks:
+ - id: ruff-format
+ name: ruff format and stage
+ entry: bash -c 'ruff format "$@" && git add -- "$@"' --
+ require_serial: true
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000000..bea2e73223e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,123 @@
+# MaxKB-v3
+
+MaxKB (Max Knowledge Brain) is an open-source platform for building enterprise AI agents with RAG pipelines, agentic workflows, and MCP tool-use capabilities.
+
+## Agent Instructions
+
+- This file is the shared source of repository instructions for all coding agents.
+- For changes under `ui/`, read and follow `ui/AGENTS.md` before editing or reviewing frontend code.
+- Keep tool-specific entry files such as `CLAUDE.md` small; shared project rules belong here or in the applicable nested `AGENTS.md`.
+
+## Tech Stack
+
+**Backend:** Python 3.13, Django 6.0, Django REST Framework, LangChain 1.3, LangGraph 1.2, Celery 5.6, uv (package manager)
+
+**Frontend:** Vue 3.5, TypeScript, Vite 8, Element Plus, Pinia, Vue Router
+
+**Infrastructure:** PostgreSQL 17.10 + pgvector, Redis 7
+
+## Project Structure
+
+```
+apps/ # Django backend
+ application/ # AI agents & applications
+ knowledge/ # Knowledge base & document management
+ chat/ # Chat functionality & MCP integration
+ models_provider/ # LLM provider integrations (OpenAI, Anthropic, etc.)
+ common/ # Shared utilities, auth, caching, chunking
+ maxkb/ # Django settings, URL routing, config
+ ops/ # Celery task queue configuration
+ users/ # User management
+ tools/ # Tool/function management
+ui/ # Vue.js frontend
+ src/
+ api/ # API client services
+ views/ # Page views
+ components/ # Vue components
+ workflow/ # Workflow UI
+installer/ # Docker build scripts and startup scripts
+main.py # Application entry point
+```
+
+## Development Setup
+
+### Backend
+
+```bash
+# Install dependencies (requires Python 3.13)
+python -m uv pip install -r pyproject.toml
+
+# Run database migrations
+cd apps && python manage.py migrate
+
+# Start dev server
+python main.py dev
+# or: cd apps && python manage.py runserver 0.0.0.0:8080
+```
+
+### Frontend
+
+```bash
+cd ui
+npm install
+npm run dev # dev server with hot reload
+npm run build # production build
+npm run type-check # TypeScript check
+npm run lint # ESLint
+npm run format # Prettier
+```
+
+### Service Management
+
+```bash
+python main.py start all -d # start all services (web + celery)
+python main.py start web -d # web only
+python main.py start task -d # celery worker
+python main.py start local_model -d # local model service
+```
+
+### Docker
+
+```bash
+docker build -f installer/Dockerfile -t maxkb:latest .
+```
+
+## Configuration
+
+Key environment variables (see `.env`):
+
+| Variable | Description |
+| ------------------------------------------------------- | ---------------------- |
+| `MAXKB_DB_HOST` / `MAXKB_DB_PORT` | PostgreSQL connection |
+| `MAXKB_DB_NAME` / `MAXKB_DB_USER` / `MAXKB_DB_PASSWORD` | PostgreSQL credentials |
+| `MAXKB_REDIS_HOST` / `MAXKB_REDIS_PORT` | Redis connection |
+
+Settings files:
+
+- `apps/maxkb/conf.py` — main config manager (reads env vars)
+- `apps/maxkb/settings/base/web.py` — Django settings
+- `apps/maxkb/settings/lib.py` — Celery/Redis settings
+
+## Testing
+
+```bash
+cd apps && python manage.py test
+```
+
+## Code Style
+
+- Python: Ruff linter, line length 120 (`pyproject.toml`)
+- TypeScript/Vue: ESLint + Prettier (`ui/`)
+- Migrations in `apps/*/migrations/`
+
+## Key Concepts
+
+- **Knowledge Base**: Documents are chunked, embedded, and stored in PostgreSQL with pgvector for semantic search.
+- **Applications/Agents**: Built on top of knowledge bases; support workflow-based and RAG-based configurations.
+- **Models Provider**: Abstraction layer in `apps/models_provider/` supporting 10+ LLM providers via LangChain.
+- **MCP**: Model Context Protocol tools integrated via `langchain-mcp-adapters` in `apps/chat/mcp/`.
+- **Task Queue**: Heavy document processing runs async via Celery workers.
+
+## Internationalization
+
+Translations in `apps/locales/` (zh_CN, en_US, zh_Hant) and `ui/src/locales/`.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000000..7d0efdf7c9f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,3 @@
+@AGENTS.md
+
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7e0cfbc7770..c901d5efeb1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,6 +14,18 @@ This [development guideline](https://github.com/1Panel-dev/MaxKB/wiki/3-%E5%BC%8
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
+## Code formatting
+
+This project uses [pre-commit](https://pre-commit.com/) and Ruff to format Python code. After cloning the repository and setting up the development environment, install the Git hook once:
+
+```bash
+uv sync --group dev
+uv run pre-commit install --install-hooks
+```
+
+
+The hook runs automatically on each `git commit` and stages files changed by the formatter. The same check also runs in CI for every pull request, so all formatting changes must be committed before the pull request can be merged.
+
## Report issues
It is a great way to contribute by reporting an issue. Well-written and complete bug reports are always welcome! Please open an issue and follow the template to fill in required information.
@@ -27,4 +39,4 @@ When reporting issues, always include:
* Snapshots or log files if needed
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
-replace those parts with "REDACTED" or other strings like "****".
\ No newline at end of file
+replace those parts with "REDACTED" or other strings like "****".
diff --git a/README.md b/README.md
index 93f8e95ea70..d5177bf8e37 100644
--- a/README.md
+++ b/README.md
@@ -54,10 +54,6 @@ Access MaxKB web interface at `http://your_server_ip:8080` with default admin cr
- LLM Framework:[LangChain](https://www.langchain.com/)
- Database:[PostgreSQL + pgvector](https://www.postgresql.org/)
-## Star History
-
-[](https://star-history.com/#1Panel-dev/MaxKB&Date)
-
## License
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
diff --git a/README_CN.md b/README_CN.md
index da5db24891d..fda3ab9b65a 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -4,7 +4,7 @@
-
+
diff --git a/SECURITY.md b/SECURITY.md
index 22be037d008..88a06f8f61a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,17 +1,15 @@
# 安全说明
-如果您发现安全问题,请直接联系我们:
+如果您发现安全问题,请[提交给我们](https://github.com/1Panel-dev/MaxKB/security/advisories/new)。
-- support@fit2cloud.com
-- 400-052-0755
+谢绝只有静态代码分析,未经完整PoC验证的安全建议。
感谢您的支持!
# Security Policy
-All security bugs should be reported to the contact as below:
+Any security issue please [submit a vulnerability report](https://github.com/1Panel-dev/MaxKB/security/advisories/new).
-- support@fit2cloud.com
-- 400-052-0755
+We do not accept security recommendations based solely on static code analysis without complete PoC validation.
-Thanks for your support!
\ No newline at end of file
+Thanks for your support!
diff --git a/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py b/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py
index 914f20b524e..0b5d8af6852 100644
--- a/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py
+++ b/apps/application/chat_pipeline/step/chat_step/impl/base_chat_step.py
@@ -384,10 +384,11 @@ def _handle_mcp_request(
if tool is None or tool.is_active is False:
continue
executor = ToolExecutor()
+ init_params_default_value = {i["field"]: i.get('default_value') for i in tool.init_field_list}
if tool.init_params is not None:
- tool_init_params = json.loads(rsa_long_decrypt(tool.init_params))
+ tool_init_params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
else:
- tool_init_params = {i["field"]: i.get("default_value") for i in tool.init_field_list}
+ tool_init_params = init_params_default_value
tool_config = executor.get_tool_mcp_config(tool, tool_init_params)
mcp_servers_config[str(tool.id)] = tool_config
diff --git a/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py b/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py
index e57eebd9f65..b96c9ba2187 100644
--- a/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py
+++ b/apps/application/chat_pipeline/step/search_dataset_step/impl/base_search_dataset_step.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: base_search_dataset_step.py
- @date:2024/1/10 10:33
- @desc:
+@project: maxkb
+@Author:虎
+@file: base_search_dataset_step.py
+@date:2024/1/10 10:33
+@desc:
"""
+
import os
from typing import List, Dict
@@ -16,20 +17,21 @@
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
from application.chat_pipeline.step.search_dataset_step.i_search_dataset_step import ISearchDatasetStep
from common.config.embedding_config import VectorStore, ModelManage
-from common.constants.permission_constants import RoleConstants
+from common.auth.constants.role_constants import RoleConstants
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.db.search import native_search
from common.utils.common import get_file_content
from knowledge.models import Paragraph, Knowledge
from knowledge.models import SearchMode
+from knowledge.services.retrieval_stats import get_recall_tracker, record_recall_safely
from maxkb.conf import PROJECT_DIR
from models_provider.models import Model
from models_provider.tools import get_model, get_model_by_id, get_model_default_params
def reset_meta(meta):
- if not meta.get('allow_download', False):
- return {'allow_download': False}
+ if not meta.get("allow_download", False):
+ return {"allow_download": False}
return meta
@@ -37,25 +39,34 @@ def get_embedding_id(knowledge_id_list):
knowledge_list = QuerySet(Knowledge).filter(id__in=knowledge_id_list)
if len(set([knowledge.embedding_model_id for knowledge in knowledge_list])) > 1:
raise Exception(
- _("The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled."))
+ _(
+ "The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled."
+ )
+ )
if len(knowledge_list) == 0:
raise Exception(_("The knowledge base setting is wrong, please reset the knowledge base"))
return knowledge_list[0].embedding_model_id
class BaseSearchDatasetStep(ISearchDatasetStep):
-
- def execute(self, problem_text: str, knowledge_id_list: list[str], exclude_document_id_list: list[str],
- exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
- search_mode: str = None,
- workspace_id=None,
- manage=None,
- **kwargs) -> List[ParagraphPipelineModel]:
- get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized')
- chat_user_type = manage.context.get('chat_user_type')
+ def execute(
+ self,
+ problem_text: str,
+ knowledge_id_list: list[str],
+ exclude_document_id_list: list[str],
+ exclude_paragraph_id_list: list[str],
+ top_n: int,
+ similarity: float,
+ padding_problem_text: str = None,
+ search_mode: str = None,
+ workspace_id=None,
+ manage=None,
+ **kwargs,
+ ) -> List[ParagraphPipelineModel]:
+ get_knowledge_list_of_authorized = DatabaseModelManage.get_model("get_knowledge_list_of_authorized")
+ chat_user_type = manage.context.get("chat_user_type")
if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
- knowledge_id_list = get_knowledge_list_of_authorized(manage.context.get('chat_user_id'),
- knowledge_id_list)
+ knowledge_id_list = get_knowledge_list_of_authorized(manage.context.get("chat_user_id"), knowledge_id_list)
if len(knowledge_id_list) == 0:
return []
exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
@@ -63,88 +74,121 @@ def execute(self, problem_text: str, knowledge_id_list: list[str], exclude_docum
model = get_model_by_id(model_id, workspace_id)
if model.model_type != "EMBEDDING":
raise Exception(_("Model does not exist"))
- self.context['model_name'] = model.name
+ self.context["model_name"] = model.name
default_params = get_model_default_params(model)
embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model, **{**default_params}))
embedding_value = embedding_model.embed_query(exec_problem_text)
vector = VectorStore.get_embedding_vector()
- embedding_list = vector.query(exec_problem_text, embedding_value, knowledge_id_list, None,
- exclude_document_id_list,
- exclude_paragraph_id_list, True, top_n, similarity, SearchMode(search_mode))
+ embedding_list = vector.query(
+ exec_problem_text,
+ embedding_value,
+ knowledge_id_list,
+ None,
+ exclude_document_id_list,
+ exclude_paragraph_id_list,
+ True,
+ top_n,
+ similarity,
+ SearchMode(search_mode),
+ )
if embedding_list is None:
return []
paragraph_list = self.list_paragraph(embedding_list, vector)
result = [self.reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
+ if not manage.debug:
+ recalled_paragraph_ids = {paragraph.id for paragraph in result}
+ record_recall_safely(
+ [
+ embedding
+ for embedding in embedding_list
+ if str(embedding.get("paragraph_id")) in recalled_paragraph_ids
+ ],
+ tracker=get_recall_tracker(manage),
+ )
return result
@staticmethod
def reset_paragraph(paragraph: Dict, embedding_list: List) -> ParagraphPipelineModel:
- filter_embedding_list = [embedding for embedding in embedding_list if
- str(embedding.get('paragraph_id')) == str(paragraph.get('id'))]
+ filter_embedding_list = [
+ embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
+ ]
if filter_embedding_list is not None and len(filter_embedding_list) > 0:
find_embedding = filter_embedding_list[-1]
- return (ParagraphPipelineModel.builder()
- .add_paragraph(paragraph)
- .add_similarity(find_embedding.get('similarity'))
- .add_comprehensive_score(find_embedding.get('comprehensive_score'))
- .add_knowledge_name(paragraph.get('knowledge_name'))
- .add_knowledge_type(paragraph.get('knowledge_type'))
- .add_document_name(paragraph.get('document_name'))
- .add_hit_handling_method(paragraph.get('hit_handling_method'))
- .add_directly_return_similarity(paragraph.get('directly_return_similarity'))
- .add_meta(reset_meta(paragraph.get('meta')))
- .build())
+ return (
+ ParagraphPipelineModel.builder()
+ .add_paragraph(paragraph)
+ .add_similarity(find_embedding.get("similarity"))
+ .add_comprehensive_score(find_embedding.get("comprehensive_score"))
+ .add_knowledge_name(paragraph.get("knowledge_name"))
+ .add_knowledge_type(paragraph.get("knowledge_type"))
+ .add_document_name(paragraph.get("document_name"))
+ .add_hit_handling_method(paragraph.get("hit_handling_method"))
+ .add_directly_return_similarity(paragraph.get("directly_return_similarity"))
+ .add_meta(reset_meta(paragraph.get("meta")))
+ .build()
+ )
@staticmethod
def get_similarity(paragraph, embedding_list: List):
- filter_embedding_list = [embedding for embedding in embedding_list if
- str(embedding.get('paragraph_id')) == str(paragraph.get('id'))]
+ filter_embedding_list = [
+ embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
+ ]
if filter_embedding_list is not None and len(filter_embedding_list) > 0:
find_embedding = filter_embedding_list[-1]
- return find_embedding.get('comprehensive_score')
+ return find_embedding.get("comprehensive_score")
return 0
@staticmethod
def list_paragraph(embedding_list: List, vector):
- paragraph_id_list = [row.get('paragraph_id') for row in embedding_list]
+ paragraph_id_list = [row.get("paragraph_id") for row in embedding_list]
if paragraph_id_list is None or len(paragraph_id_list) == 0:
return []
- paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list),
- get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql',
- 'list_knowledge_paragraph_by_paragraph_id.sql')),
- with_table_name=True)
+ paragraph_list = native_search(
+ QuerySet(Paragraph).filter(id__in=paragraph_id_list),
+ get_file_content(
+ os.path.join(PROJECT_DIR, "apps", "application", "sql", "list_knowledge_paragraph_by_paragraph_id.sql")
+ ),
+ with_table_name=True,
+ )
# 如果向量库中存在脏数据 直接删除
if len(paragraph_list) != len(paragraph_id_list):
- exist_paragraph_list = [row.get('id') for row in paragraph_list]
+ exist_paragraph_list = [row.get("id") for row in paragraph_list]
for paragraph_id in paragraph_id_list:
if not exist_paragraph_list.__contains__(paragraph_id):
vector.delete_by_paragraph_id(paragraph_id)
# 如果存在直接返回的则取直接返回段落
- hit_handling_method_paragraph = [paragraph for paragraph in paragraph_list if
- (paragraph.get(
- 'hit_handling_method') == 'directly_return' and BaseSearchDatasetStep.get_similarity(
- paragraph, embedding_list) >= paragraph.get(
- 'directly_return_similarity'))]
+ hit_handling_method_paragraph = [
+ paragraph
+ for paragraph in paragraph_list
+ if (
+ paragraph.get("hit_handling_method") == "directly_return"
+ and BaseSearchDatasetStep.get_similarity(paragraph, embedding_list)
+ >= paragraph.get("directly_return_similarity")
+ )
+ ]
if len(hit_handling_method_paragraph) > 0:
# 找到评分最高的
- return [sorted(hit_handling_method_paragraph,
- key=lambda p: BaseSearchDatasetStep.get_similarity(p, embedding_list))[-1]]
+ return [
+ sorted(
+ hit_handling_method_paragraph, key=lambda p: BaseSearchDatasetStep.get_similarity(p, embedding_list)
+ )[-1]
+ ]
return paragraph_list
def get_details(self, manage, **kwargs):
- step_args = self.context.get('step_args') or {}
+ step_args = self.context.get("step_args") or {}
return {
- 'status': self.status,
- 'err_message': self.err_message,
- 'step_type': 'search_step',
- 'paragraph_list': [row.to_dict() for row in (self.context.get('paragraph_list') or [])],
- 'run_time': self.context.get('run_time') or 0,
- 'problem_text': step_args.get(
- 'padding_problem_text') if 'padding_problem_text' in step_args else step_args.get('problem_text'),
- 'model_name': self.context.get('model_name'),
- 'message_tokens': 0,
- 'answer_tokens': 0,
- 'cost': 0
+ "status": self.status,
+ "err_message": self.err_message,
+ "step_type": "search_step",
+ "paragraph_list": [row.to_dict() for row in (self.context.get("paragraph_list") or [])],
+ "run_time": self.context.get("run_time") or 0,
+ "problem_text": step_args.get("padding_problem_text")
+ if "padding_problem_text" in step_args
+ else step_args.get("problem_text"),
+ "model_name": self.context.get("model_name"),
+ "message_tokens": 0,
+ "answer_tokens": 0,
+ "cost": 0,
}
diff --git a/apps/application/flow/backend/sandbox_shell.py b/apps/application/flow/backend/sandbox_shell.py
index 6ed92781393..9b1c747c947 100644
--- a/apps/application/flow/backend/sandbox_shell.py
+++ b/apps/application/flow/backend/sandbox_shell.py
@@ -1,9 +1,12 @@
import getpass
import os
import re
+import shlex
from deepagents.backends import LocalShellBackend
from deepagents.backends.protocol import ExecuteResponse
+
+from common.utils.logger import maxkb_logger
from maxkb.const import CONFIG
_enable_sandbox = bool(int(CONFIG.get("SANDBOX", 0)))
@@ -53,6 +56,237 @@ def translate(m: re.Match) -> str:
# Only translate when virtual_mode is active.
return re.sub(r'(?<:,]*', translate, command)
+ def _consume_group(self, command: str, start_index: int) -> tuple[str, int]:
+ current = []
+ in_single_quote = False
+ in_double_quote = False
+ in_backticks = False
+ escaped = False
+ substitution_depth = 0
+ group_depth = 1
+ index = start_index + 1
+
+ while index < len(command):
+ char = command[index]
+
+ if escaped:
+ current.append(char)
+ escaped = False
+ index += 1
+ continue
+
+ if char == "\\" and not in_single_quote:
+ current.append(char)
+ escaped = True
+ index += 1
+ continue
+
+ if char == "`" and not in_single_quote:
+ in_backticks = not in_backticks
+ current.append(char)
+ index += 1
+ continue
+
+ if in_backticks:
+ current.append(char)
+ index += 1
+ continue
+
+ if char == "'" and not in_double_quote:
+ in_single_quote = not in_single_quote
+ current.append(char)
+ index += 1
+ continue
+
+ if char == '"' and not in_single_quote:
+ in_double_quote = not in_double_quote
+ current.append(char)
+ index += 1
+ continue
+
+ if in_single_quote or in_double_quote:
+ current.append(char)
+ index += 1
+ continue
+
+ if command.startswith("$(", index):
+ substitution_depth += 1
+ current.append("$(")
+ index += 2
+ continue
+
+ if substitution_depth:
+ if char == ")":
+ substitution_depth -= 1
+ current.append(char)
+ index += 1
+ continue
+
+ if char == "(":
+ group_depth += 1
+ current.append(char)
+ index += 1
+ continue
+
+ if char == ")":
+ group_depth -= 1
+ if group_depth == 0:
+ return "".join(current).strip(), index + 1
+ current.append(char)
+ index += 1
+ continue
+
+ current.append(char)
+ index += 1
+
+ raise ValueError("unclosed command group")
+
+ def _append_pending_command_part(self, parts: list[str | tuple[str, str]], current: list[str]) -> None:
+ part = "".join(current).strip()
+ if part:
+ parts.append(part)
+ return
+
+ if not parts:
+ parts.append("")
+ return
+
+ last_part = parts[-1]
+ if isinstance(last_part, str) and last_part in {";", "&&", "||", "|", "&"}:
+ parts.append("")
+
+ def _split_shell_command_list(self, command: str) -> list[str | tuple[str, str]]:
+ parts = []
+ current = []
+ in_single_quote = False
+ in_double_quote = False
+ in_backticks = False
+ escaped = False
+ substitution_depth = 0
+ index = 0
+
+ while index < len(command):
+ char = command[index]
+
+ if escaped:
+ current.append(char)
+ escaped = False
+ index += 1
+ continue
+
+ if char == "\\" and not in_single_quote:
+ current.append(char)
+ escaped = True
+ index += 1
+ continue
+
+ if char == "`" and not in_single_quote:
+ in_backticks = not in_backticks
+ current.append(char)
+ index += 1
+ continue
+
+ if in_backticks:
+ current.append(char)
+ index += 1
+ continue
+
+ if char == "'" and not in_double_quote:
+ in_single_quote = not in_single_quote
+ current.append(char)
+ index += 1
+ continue
+
+ if char == '"' and not in_single_quote:
+ in_double_quote = not in_double_quote
+ current.append(char)
+ index += 1
+ continue
+
+ if not in_single_quote and not in_double_quote:
+ if command.startswith("$(", index):
+ substitution_depth += 1
+ current.append("$(")
+ index += 2
+ continue
+
+ if substitution_depth:
+ if char == ")":
+ substitution_depth -= 1
+ current.append(char)
+ index += 1
+ continue
+
+ if char == "(" and not "".join(current).strip():
+ group_content, index = self._consume_group(command, index)
+ parts.append(("group", group_content))
+ current = []
+ continue
+
+ if command.startswith("&&", index) or command.startswith("||", index):
+ self._append_pending_command_part(parts, current)
+ parts.append(command[index : index + 2])
+ current = []
+ index += 2
+ continue
+
+ if char in {";", "|", "&"}:
+ self._append_pending_command_part(parts, current)
+ parts.append(char)
+ current = []
+ index += 1
+ continue
+
+ if char == "\n":
+ self._append_pending_command_part(parts, current)
+ parts.append(";")
+ current = []
+ index += 1
+ continue
+
+ current.append(char)
+ index += 1
+
+ self._append_pending_command_part(parts, current)
+ return parts
+
+ def _build_sandbox_command(self, command: str) -> str:
+ prefix = (
+ "env -i LD_PRELOAD=/opt/maxkb-app/sandbox/lib/sandbox.so "
+ f'PATH="${{PATH}}" PYTHONPATH="${{PYTHONPATH}}" gosu {_run_user} '
+ )
+ parts = self._split_shell_command_list(command)
+ sandboxed_parts = []
+ expect_command = True
+
+ for part in parts:
+ if expect_command:
+ if isinstance(part, tuple):
+ group_kind, group_content = part
+ if group_kind != "group":
+ raise ValueError(f"unsupported command part: {group_kind}")
+ if not group_content:
+ raise ValueError("empty command group")
+ sandboxed_parts.append(f"( {self._build_sandbox_command(group_content)} )")
+ elif not part:
+ raise ValueError("empty command")
+ else:
+ tokens = shlex.split(part)
+ if not tokens:
+ raise ValueError("empty command")
+ sandboxed_parts.append(prefix + " ".join(shlex.quote(token) for token in tokens))
+ else:
+ if part not in {";", "&&", "||", "|", "&"}:
+ raise ValueError(f"unsupported shell operator: {part}")
+ sandboxed_parts.append(part)
+
+ expect_command = not expect_command
+
+ if expect_command:
+ raise ValueError("command cannot end with a shell operator")
+
+ return " ".join(sandboxed_parts)
+
def execute(
self,
command: str,
@@ -65,11 +299,13 @@ def execute(
if _enable_sandbox:
# 用 runuser 在子进程里切换用户,父进程凭据保持不变,
# 避免父进程 ruid/euid 不一致导致 execve 报 Permission denied
- command = (
- "env -i LD_PRELOAD=/opt/maxkb-app/sandbox/lib/sandbox.so "
- f'PATH="${{PATH}}" PYTHONPATH="${{PYTHONPATH}}" gosu {_run_user} {command}'
- )
+ try:
+ # 将命令列表拆成多个简单命令,并分别在 sandbox 用户下执行。
+ # 每个简单命令仍按 argv 重新 quote,避免 $()、反引号等在父 shell 中展开。
+ command = self._build_sandbox_command(command)
+ except ValueError as e:
+ return ExecuteResponse(output=f"Invalid command: {e}", exit_code=1)
# command = f"runuser -u {_run_user} -- env -i PATH=${{PATH}} {command}"
- # print(f"Executing command in sandbox: {command}")
+ maxkb_logger.debug(f"Executing command in sandbox: {command}")
return super().execute(command=command, timeout=timeout)
diff --git a/apps/application/flow/i_step_node.py b/apps/application/flow/i_step_node.py
index c0fb5e68ef9..1f6fb09207b 100644
--- a/apps/application/flow/i_step_node.py
+++ b/apps/application/flow/i_step_node.py
@@ -1,28 +1,36 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: i_step_node.py
- @date:2024/6/3 14:57
- @desc:
+@project: maxkb
+@Author:虎
+@file: i_step_node.py
+@date:2024/6/3 14:57
+@desc:
"""
+
import time
import uuid
from abc import abstractmethod
from hashlib import sha1
-from typing import Type, Dict, List
-
-from django.core import cache
-from django.db.models import QuerySet
-from rest_framework import serializers
-from rest_framework.exceptions import ValidationError, ErrorDetail
+from typing import Dict, List, Type
from application.flow.common import Answer, NodeChunk
from application.long_term_memory import extract_long_term_memory
-from application.models import ApplicationChatUserStats
-from application.models import ChatRecord, ChatUserType
+from application.models import ApplicationChatUserStats, ChatRecord, ChatUserType
from common.field.common import InstanceField
+from django.core import cache
+from django.db.models import QuerySet
+from knowledge.models import (
+ Document,
+ DocumentResourceType,
+ KnowledgeSyncLog,
+ KnowledgeSyncStatus,
+ KnowledgeSyncType,
+ KnowledgeType,
+ Paragraph,
+)
from knowledge.models.knowledge_action import KnowledgeAction, State
+from rest_framework import serializers
+from rest_framework.exceptions import ErrorDetail, ValidationError
from tools.models import ToolRecord
chat_cache = cache
@@ -32,18 +40,18 @@ def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
if step_variable is not None:
for key in step_variable:
node.context[key] = step_variable[key]
- if workflow.is_result(node, NodeResult(step_variable, global_variable)) and 'answer' in step_variable:
- answer = step_variable['answer']
+ if workflow.is_result(node, NodeResult(step_variable, global_variable)) and "answer" in step_variable:
+ answer = step_variable["answer"]
yield answer
node.answer_text = answer
if global_variable is not None:
for key in global_variable:
workflow.context[key] = global_variable[key]
- node.context['run_time'] = time.time() - node.context['start_time']
+ node.context["run_time"] = time.time() - node.context["start_time"]
def is_interrupt(node, step_variable: Dict, global_variable: Dict):
- return node.type == 'form-node' and not node.context.get('is_submit', False)
+ return node.type == "form-node" and not node.context.get("is_submit", False)
class WorkFlowPostHandler:
@@ -52,18 +60,26 @@ def __init__(self, chat_info):
def handler(self, workflow):
workflow_body = workflow.get_body()
- question = workflow_body.get('question')
- chat_record_id = workflow_body.get('chat_record_id')
- chat_id = workflow_body.get('chat_id')
+ question = workflow_body.get("question")
+ chat_record_id = workflow_body.get("chat_record_id")
+ chat_id = workflow_body.get("chat_id")
details = workflow.get_runtime_details()
- message_tokens = sum([row.get('message_tokens') for row in details.values() if
- 'message_tokens' in row and row.get('message_tokens') is not None])
- answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
- 'answer_tokens' in row and row.get('answer_tokens') is not None])
+ message_tokens = sum(
+ [
+ row.get("message_tokens")
+ for row in details.values()
+ if "message_tokens" in row and row.get("message_tokens") is not None
+ ]
+ )
+ answer_tokens = sum(
+ [
+ row.get("answer_tokens")
+ for row in details.values()
+ if "answer_tokens" in row and row.get("answer_tokens") is not None
+ ]
+ )
answer_text_list = workflow.get_answer_text_list()
- answer_text = '\n\n'.join(
- '\n\n'.join([a.get('content') for a in answer]) for answer in
- answer_text_list)
+ answer_text = "\n\n".join("\n\n".join([a.get("content") for a in answer]) for answer in answer_text_list)
if workflow.chat_record is not None:
chat_record = workflow.chat_record
chat_record.problem_text = question
@@ -72,58 +88,148 @@ def handler(self, workflow):
chat_record.message_tokens = message_tokens
chat_record.answer_tokens = answer_tokens
chat_record.answer_text_list = answer_text_list
- chat_record.run_time = time.time() - workflow.context['start_time']
+ chat_record.run_time = time.time() - workflow.context["start_time"]
else:
- chat_record = ChatRecord(id=chat_record_id,
- chat_id=chat_id,
- problem_text=question,
- answer_text=answer_text,
- details=details,
- message_tokens=message_tokens,
- answer_tokens=answer_tokens,
- answer_text_list=answer_text_list,
- run_time=time.time() - workflow.context.get('start_time') if workflow.context.get(
- 'start_time') is not None else 0,
- index=0,
- ip_address=self.chat_info.ip_address,
- source=self.chat_info.source)
+ chat_record = ChatRecord(
+ id=chat_record_id,
+ chat_id=chat_id,
+ problem_text=question,
+ answer_text=answer_text,
+ details=details,
+ message_tokens=message_tokens,
+ answer_tokens=answer_tokens,
+ answer_text_list=answer_text_list,
+ run_time=time.time() - workflow.context.get("start_time")
+ if workflow.context.get("start_time") is not None
+ else 0,
+ index=0,
+ ip_address=self.chat_info.ip_address,
+ source=self.chat_info.source,
+ )
self.chat_info.append_chat_record(chat_record)
self.chat_info.set_cache()
if not self.chat_info.debug and [ChatUserType.ANONYMOUS_USER.value, ChatUserType.CHAT_USER.value].__contains__(
- workflow_body.get('chat_user_type')):
- application_public_access_client = (QuerySet(ApplicationChatUserStats)
- .filter(chat_user_id=workflow_body.get('chat_user_id'),
- chat_user_type=workflow_body.get('chat_user_type'),
- application_id=self.chat_info.application_id).first())
+ workflow_body.get("chat_user_type")
+ ):
+ application_public_access_client = (
+ QuerySet(ApplicationChatUserStats)
+ .filter(
+ chat_user_id=workflow_body.get("chat_user_id"),
+ chat_user_type=workflow_body.get("chat_user_type"),
+ application_id=self.chat_info.application_id,
+ )
+ .first()
+ )
if application_public_access_client is not None:
application_public_access_client.access_num = application_public_access_client.access_num + 1
- application_public_access_client.intraday_access_num = application_public_access_client.intraday_access_num + 1
+ application_public_access_client.intraday_access_num = (
+ application_public_access_client.intraday_access_num + 1
+ )
application_public_access_client.save()
self.chat_info = None
extract_long_term_memory.apply_async(
args=(
- workflow_body.get('workspace_id'),
- workflow_body.get('application_id'),
- workflow_body.get('chat_user_id'),
+ workflow_body.get("workspace_id"),
+ workflow_body.get("application_id"),
+ workflow_body.get("chat_user_id"),
),
countdown=1,
)
class KnowledgeWorkflowPostHandler(WorkFlowPostHandler):
- def __init__(self, chat_info, knowledge_action_id):
+ def __init__(self, chat_info, knowledge_action_id, sync_log_id=None, document_cleanup=None):
super().__init__(chat_info)
self.knowledge_action_id = knowledge_action_id
+ self.sync_log_id = sync_log_id
+ self.document_cleanup = document_cleanup
def handler(self, workflow):
state = get_workflow_state(workflow)
- QuerySet(KnowledgeAction).filter(id=self.knowledge_action_id).update(
- state=state,
- run_time=time.time() - workflow.context.get('start_time') if workflow.context.get(
- 'start_time') is not None else 0)
+ run_time = (
+ time.time() - workflow.context.get("start_time") if workflow.context.get("start_time") is not None else 0
+ )
+ QuerySet(KnowledgeAction).filter(id=self.knowledge_action_id).update(state=state, run_time=run_time)
+ if self.sync_log_id is not None:
+ sync_log = QuerySet(KnowledgeSyncLog).filter(id=self.sync_log_id).first()
+ if sync_log is not None:
+ new_documents = list(
+ QuerySet(Document).filter(
+ knowledge_id=sync_log.knowledge_id,
+ type=KnowledgeType.WORKFLOW,
+ resource_type=DocumentResourceType.DOCUMENT,
+ create_time__gte=sync_log.create_time,
+ )
+ )
+ synced_count = len(new_documents)
+ skipped_count = 0
+ if (
+ state == State.SUCCESS
+ and sync_log.sync_type == KnowledgeSyncType.INCREMENTAL
+ and self.document_cleanup is not None
+ ):
+ old_documents = list(
+ QuerySet(Document).filter(
+ knowledge_id=sync_log.knowledge_id,
+ type=KnowledgeType.WORKFLOW,
+ resource_type=DocumentResourceType.DOCUMENT,
+ create_time__lt=sync_log.create_time,
+ )
+ )
+ old_by_name = {}
+ for document in old_documents:
+ old_by_name.setdefault(document.name, []).append(document)
+ for new_document in new_documents:
+ matched = old_by_name.get(new_document.name, [])
+ if not matched:
+ continue
+ new_content = list(
+ QuerySet(Paragraph)
+ .filter(document_id=new_document.id)
+ .order_by("position")
+ .values_list("title", "content")
+ )
+ unchanged = next(
+ (
+ old_document
+ for old_document in matched
+ if list(
+ QuerySet(Paragraph)
+ .filter(document_id=old_document.id)
+ .order_by("position")
+ .values_list("title", "content")
+ )
+ == new_content
+ ),
+ None,
+ )
+ if unchanged is not None:
+ self.document_cleanup([new_document.id])
+ synced_count -= 1
+ skipped_count += 1
+ else:
+ self.document_cleanup([document.id for document in matched])
+ total_count = (
+ QuerySet(Document)
+ .filter(
+ knowledge_id=sync_log.knowledge_id,
+ resource_type=DocumentResourceType.DOCUMENT,
+ )
+ .count()
+ )
+ is_success = state == State.SUCCESS
+ QuerySet(KnowledgeSyncLog).filter(id=sync_log.id).update(
+ status=KnowledgeSyncStatus.SUCCESS if is_success else KnowledgeSyncStatus.FAILURE,
+ total_count=total_count,
+ synced_count=synced_count,
+ skipped_count=skipped_count,
+ failed_count=0 if is_success else 1,
+ duration_ms=max(0, round(run_time * 1000)),
+ message=f"Workflow action {self.knowledge_action_id}: {state}",
+ )
def get_tool_workflow_state(workflow):
@@ -132,7 +238,7 @@ def get_tool_workflow_state(workflow):
details = workflow.get_runtime_details()
node_list = details.values()
all_node = [*node_list, *get_loop_workflow_node(node_list)]
- err = any([True for value in all_node if value.get('status') == 500 and not value.get('enableException')])
+ err = any([True for value in all_node if value.get("status") == 500 and not value.get("enableException")])
if err:
return State.FAILURE
return State.SUCCESS
@@ -155,21 +261,25 @@ def __init__(self, chat_info, tool_id):
def handler(self, workflow):
state = get_tool_workflow_state(workflow)
- record = ToolRecord(id=self.chat_info.tool_record_id, tool_id=self.tool_id,
- workspace_id=self.chat_info.workspace_id,
- source_type=self.chat_info.source_type,
- source_id=self.chat_info.source_id,
- state=state,
- run_time=time.time() - workflow.context.get('start_time') if workflow.context.get(
- 'start_time') is not None else 0,
- meta={
- 'input_field_list': workflow.get_input_field_list(),
- 'output_field_list': workflow.get_output_field_list(),
- 'input': workflow.get_input(),
- 'output': workflow.out_context,
- 'details': workflow.get_runtime_details(),
- 'answer_text_list': workflow.get_answer_text_list()
- })
+ record = ToolRecord(
+ id=self.chat_info.tool_record_id,
+ tool_id=self.tool_id,
+ workspace_id=self.chat_info.workspace_id,
+ source_type=self.chat_info.source_type,
+ source_id=self.chat_info.source_id,
+ state=state,
+ run_time=time.time() - workflow.context.get("start_time")
+ if workflow.context.get("start_time") is not None
+ else 0,
+ meta={
+ "input_field_list": workflow.get_input_field_list(),
+ "output_field_list": workflow.get_output_field_list(),
+ "input": workflow.get_input(),
+ "output": workflow.out_context,
+ "details": workflow.get_runtime_details(),
+ "answer_text_list": workflow.get_answer_text_list(),
+ },
+ )
self.chat_info.set_record(record)
self.chat_info = None
self.tool_id = None
@@ -178,8 +288,8 @@ def handler(self, workflow):
def get_loop_workflow_node(node_list):
result = []
for item in node_list:
- if item.get('type') == 'loop-node':
- for loop_item in item.get('loop_node_data') or []:
+ if item.get("type") == "loop-node":
+ for loop_item in item.get("loop_node_data") or []:
for inner_item in loop_item.values():
result.append(inner_item)
return result
@@ -191,18 +301,19 @@ def get_workflow_state(workflow):
details = workflow.get_runtime_details()
node_list = details.values()
all_node = [*node_list, *get_loop_workflow_node(node_list)]
- err = any([True for value in all_node if value.get('status') == 500 and not value.get('enableException')])
+ err = any([True for value in all_node if value.get("status") == 500 and not value.get("enableException")])
if err:
return State.FAILURE
- write_is_exist = any([True for value in all_node if value.get('type') == 'knowledge-write-node'])
+ write_is_exist = any([True for value in all_node if value.get("type") == "knowledge-write-node"])
if not write_is_exist:
return State.FAILURE
return State.SUCCESS
class NodeResult:
- def __init__(self, node_variable: Dict, workflow_variable: Dict,
- _write_context=write_context, _is_interrupt=is_interrupt):
+ def __init__(
+ self, node_variable: Dict, workflow_variable: Dict, _write_context=write_context, _is_interrupt=is_interrupt
+ ):
self._write_context = _write_context
self.node_variable = node_variable
self.workflow_variable = workflow_variable
@@ -212,7 +323,7 @@ def write_context(self, node, workflow):
return self._write_context(self.node_variable, self.workflow_variable, node, workflow)
def is_assertion_result(self):
- return 'branch_id' in self.node_variable
+ return "branch_id" in self.node_variable
def is_interrupt_exec(self, current_node):
"""
@@ -226,14 +337,15 @@ def is_interrupt_exec(self, current_node):
class ReferenceAddressSerializer(serializers.Serializer):
node_id = serializers.CharField(required=True, label="节点id")
fields = serializers.ListField(
- child=serializers.CharField(required=True, label="节点字段"), required=True,
- label="节点字段数组")
+ child=serializers.CharField(required=True, label="节点字段"), required=True, label="节点字段数组"
+ )
class FlowParamsSerializer(serializers.Serializer):
# 历史对答
- history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
- label="历史对答")
+ history_chat_record = serializers.ListField(
+ child=InstanceField(model_type=ChatRecord, required=True), label="历史对答"
+ )
question = serializers.CharField(required=True, label="用户问题")
@@ -271,7 +383,7 @@ class ToolFlowParamsSerializer(serializers.Serializer):
class INode:
- view_type = 'many_view'
+ view_type = "many_view"
@abstractmethod
def save_context(self, details, workflow_manage):
@@ -280,17 +392,31 @@ def save_context(self, details, workflow_manage):
def get_answer_list(self) -> List[Answer] | None:
if self.answer_text is None:
return None
- reasoning_content_enable = self.context.get('model_setting', {}).get('reasoning_content_enable', False)
+ reasoning_content_enable = self.context.get("model_setting", {}).get("reasoning_content_enable", False)
return [
- Answer(self.answer_text, self.view_type, self.runtime_node_id, self.workflow_params.get('chat_record_id'),
- {},
- self.runtime_node_id, self.context.get('reasoning_content', '') if reasoning_content_enable else '')]
-
- def __init__(self, node, workflow_params, workflow_manage, up_node_id_list=None,
- get_node_params=lambda node: node.properties.get('node_data'), salt=None):
+ Answer(
+ self.answer_text,
+ self.view_type,
+ self.runtime_node_id,
+ self.workflow_params.get("chat_record_id"),
+ {},
+ self.runtime_node_id,
+ self.context.get("reasoning_content", "") if reasoning_content_enable else "",
+ )
+ ]
+
+ def __init__(
+ self,
+ node,
+ workflow_params,
+ workflow_manage,
+ up_node_id_list=None,
+ get_node_params=lambda node: node.properties.get("node_data"),
+ salt=None,
+ ):
# 当前步骤上下文,用于存储当前步骤信息
self.status = 200
- self.err_message = ''
+ self.err_message = ""
self.node = node
self.node_params = get_node_params(node)
self.workflow_params = workflow_params
@@ -304,11 +430,10 @@ def __init__(self, node, workflow_params, workflow_manage, up_node_id_list=None,
up_node_id_list = []
self.up_node_id_list = up_node_id_list
self.node_chunk = NodeChunk()
- self.runtime_node_id = sha1(uuid.NAMESPACE_DNS.bytes + bytes(str(uuid.uuid5(uuid.NAMESPACE_DNS,
- "".join([*sorted(up_node_id_list),
- node.id]))),
- "utf-8")).hexdigest() + (
- "__" + str(salt) if salt is not None else '')
+ self.runtime_node_id = sha1(
+ uuid.NAMESPACE_DNS.bytes
+ + bytes(str(uuid.uuid5(uuid.NAMESPACE_DNS, "".join([*sorted(up_node_id_list), node.id]))), "utf-8")
+ ).hexdigest() + ("__" + str(salt) if salt is not None else "")
self.extra = {}
def valid_args(self, node_params, flow_params):
@@ -320,8 +445,8 @@ def valid_args(self, node_params, flow_params):
if node_params_serializer_class is not None:
self.node_params_serializer = node_params_serializer_class(data=node_params)
self.node_params_serializer.is_valid(raise_exception=True)
- if self.node.properties.get('status', 200) != 200:
- raise ValidationError(ErrorDetail(f'节点{self.node.properties.get("stepName")} 不可用'))
+ if self.node.properties.get("status", 200) != 200:
+ raise ValidationError(ErrorDetail(f"节点{self.node.properties.get('stepName')} 不可用"))
def get_reference_field(self, fields: List[str]):
return self.get_field(self.context, fields)
@@ -348,7 +473,7 @@ def get_write_error_context(self, e):
self.answer_text = str(e)
self.err_message = str(e)
current_time = time.time()
- self.context['run_time'] = current_time - (self.context.get('start_time') or current_time)
+ self.context["run_time"] = current_time - (self.context.get("start_time") or current_time)
def write_error_context(answer, status=200):
pass
@@ -360,9 +485,9 @@ def run(self) -> NodeResult:
:return: 执行结果
"""
start_time = time.time()
- self.context['start_time'] = start_time
+ self.context["start_time"] = start_time
result = self._run()
- self.context['run_time'] = time.time() - start_time
+ self.context["run_time"] = time.time() - start_time
return result
def _run(self):
diff --git a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py
index 5e2b94ade0c..1164d419c23 100644
--- a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py
+++ b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py
@@ -1,49 +1,51 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: base_question_node.py
- @date:2024/6/4 14:30
- @desc:
+@project: maxkb
+@Author:虎
+@file: base_question_node.py
+@date:2024/6/4 14:30
+@desc:
"""
+
import base64
import json
import re
import time
from functools import reduce
-from imghdr import what
-from typing import List, Dict
+from typing import Dict, List
-from django.db.models import QuerySet
-from django.utils.translation import gettext as _
-from langchain_core.messages import BaseMessage, AIMessage, HumanMessage, SystemMessage
-
-from application.flow.common import WorkflowMode
-from application.flow.i_step_node import NodeResult, INode
-from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
-from application.flow.tools import Reasoning, mcp_response_generator, get_tools
-from application.models import Application, ApplicationApiKey, ApplicationAccessToken
from common.exception.app_exception import AppApiException
+from common.utils.common import guess_image_format
from common.utils.rsa_util import rsa_long_decrypt
from common.utils.shared_resource_auth import filter_authorized_ids
from common.utils.tool_code import ToolExecutor
+from django.db.models import QuerySet
+from django.utils.translation import gettext as _
from knowledge.models import File
+from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
from models_provider.models import Model
from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id
from tools.models import Tool, ToolType
+from application.flow.common import WorkflowMode
+from application.flow.i_step_node import INode, NodeResult
+from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
+from application.flow.tools import Reasoning, get_tools, mcp_response_generator
+from application.models import Application, ApplicationAccessToken, ApplicationApiKey
-def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
- reasoning_content: str):
- chat_model = node_variable.get('chat_model')
- message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
+
+def _write_context(
+ node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str, reasoning_content: str
+):
+ chat_model = node_variable.get("chat_model")
+ message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get("message_list"))
answer_tokens = chat_model.get_num_tokens(answer)
- node.context['message_tokens'] = message_tokens
- node.context['answer_tokens'] = answer_tokens
- node.context['answer'] = answer
- node.context['question'] = node_variable['question']
- node.context['run_time'] = time.time() - node.context['start_time']
- node.context['reasoning_content'] = reasoning_content
+ node.context["message_tokens"] = message_tokens
+ node.context["answer_tokens"] = answer_tokens
+ node.context["answer"] = answer
+ node.context["question"] = node_variable["question"]
+ node.context["run_time"] = time.time() - node.context["start_time"]
+ node.context["reasoning_content"] = reasoning_content
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
node.answer_text = answer
@@ -56,43 +58,48 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
@param node: 节点
@param workflow: 工作流管理器
"""
- response = node_variable.get('result')
- answer = ''
- reasoning_content = ''
- model_setting = node.context.get('model_setting',
- {'reasoning_content_enable': False, 'reasoning_content_end': '',
- 'reasoning_content_start': ''})
- reasoning = Reasoning(model_setting.get('reasoning_content_start', ''),
- model_setting.get('reasoning_content_end', ''))
+ response = node_variable.get("result")
+ answer = ""
+ reasoning_content = ""
+ model_setting = node.context.get(
+ "model_setting",
+ {"reasoning_content_enable": False, "reasoning_content_end": "", "reasoning_content_start": ""},
+ )
+ reasoning = Reasoning(
+ model_setting.get("reasoning_content_start", ""), model_setting.get("reasoning_content_end", "")
+ )
response_reasoning_content = False
for chunk in response:
if workflow.is_the_task_interrupted():
break
reasoning_chunk = reasoning.get_reasoning_content(chunk)
- content_chunk = reasoning_chunk.get('content')
- if 'reasoning_content' in chunk.additional_kwargs:
+ content_chunk = reasoning_chunk.get("content")
+ if "reasoning_content" in chunk.additional_kwargs:
response_reasoning_content = True
- reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
+ reasoning_content_chunk = chunk.additional_kwargs.get("reasoning_content", "")
else:
- reasoning_content_chunk = reasoning_chunk.get('reasoning_content')
+ reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
answer += content_chunk
if reasoning_content_chunk is None:
- reasoning_content_chunk = ''
+ reasoning_content_chunk = ""
reasoning_content += reasoning_content_chunk
- yield {'content': content_chunk,
- 'reasoning_content': reasoning_content_chunk if model_setting.get('reasoning_content_enable',
- False) else ''}
+ yield {
+ "content": content_chunk,
+ "reasoning_content": reasoning_content_chunk
+ if model_setting.get("reasoning_content_enable", False)
+ else "",
+ }
reasoning_chunk = reasoning.get_end_reasoning_content()
- answer += reasoning_chunk.get('content')
+ answer += reasoning_chunk.get("content")
reasoning_content_chunk = ""
if not response_reasoning_content:
- reasoning_content_chunk = reasoning_chunk.get(
- 'reasoning_content')
- yield {'content': reasoning_chunk.get('content'),
- 'reasoning_content': reasoning_content_chunk if model_setting.get('reasoning_content_enable',
- False) else ''}
+ reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
+ yield {
+ "content": reasoning_chunk.get("content"),
+ "reasoning_content": reasoning_content_chunk if model_setting.get("reasoning_content_enable", False) else "",
+ }
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
@@ -104,28 +111,32 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
@param node: 节点实例对象
@param workflow: 工作流管理器
"""
- response = node_variable.get('result')
- model_setting = node.context.get('model_setting',
- {'reasoning_content_enable': False, 'reasoning_content_end': '',
- 'reasoning_content_start': ''})
- reasoning = Reasoning(model_setting.get('reasoning_content_start'), model_setting.get('reasoning_content_end'))
+ response = node_variable.get("result")
+ model_setting = node.context.get(
+ "model_setting",
+ {"reasoning_content_enable": False, "reasoning_content_end": "", "reasoning_content_start": ""},
+ )
+ reasoning = Reasoning(model_setting.get("reasoning_content_start"), model_setting.get("reasoning_content_end"))
reasoning_result = reasoning.get_reasoning_content(response)
reasoning_result_end = reasoning.get_end_reasoning_content()
- content = reasoning_result.get('content') + reasoning_result_end.get('content')
+ content = reasoning_result.get("content") + reasoning_result_end.get("content")
meta = {**response.response_metadata, **response.additional_kwargs}
- if 'reasoning_content' in meta:
- reasoning_content = (meta.get('reasoning_content', '') or '')
+ if "reasoning_content" in meta:
+ reasoning_content = meta.get("reasoning_content", "") or ""
else:
- reasoning_content = (reasoning_result.get('reasoning_content') or '') + (
- reasoning_result_end.get('reasoning_content') or '')
+ reasoning_content = (reasoning_result.get("reasoning_content") or "") + (
+ reasoning_result_end.get("reasoning_content") or ""
+ )
_write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content)
+CHAT_FILE_LIST_FIELDS = ("image_list", "document_list", "audio_list", "video_list", "other_list")
+
+
def get_default_model_params_setting(model_id):
model = QuerySet(Model).filter(id=model_id).first()
credential = get_model_credential(model.provider, model.model_type, model.model_name)
- model_params_setting = credential.get_model_params_setting_form(
- model.model_name).get_default_form_data()
+ model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
return model_params_setting
@@ -133,7 +144,7 @@ def get_node_message(chat_record, runtime_node_id):
node_details = chat_record.get_node_details_runtime_node_id(runtime_node_id)
if node_details is None:
return []
- return [HumanMessage(node_details.get('question')), AIMessage(node_details.get('answer'))]
+ return [HumanMessage(node_details.get("question")), AIMessage(node_details.get("answer"))]
def get_workflow_message(chat_record):
@@ -141,79 +152,96 @@ def get_workflow_message(chat_record):
def get_message(chat_record, dialogue_type, runtime_node_id):
- return get_node_message(chat_record, runtime_node_id) if dialogue_type == 'NODE' else get_workflow_message(
- chat_record)
+ return (
+ get_node_message(chat_record, runtime_node_id) if dialogue_type == "NODE" else get_workflow_message(chat_record)
+ )
class BaseChatNode(IChatNode):
def save_context(self, details, workflow_manage):
- self.context['answer'] = details.get('answer')
- self.context['question'] = details.get('question')
- self.context['reasoning_content'] = details.get('reasoning_content')
- self.context['exception_message'] = details.get('err_message')
- if self.node_params.get('is_result', False):
- self.answer_text = details.get('answer')
-
- def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
- model_params_setting=None,
- model_id_type=None,
- model_id_reference=None,
- dialogue_type=None,
- model_setting=None,
- mcp_servers=None,
- mcp_tool_id=None,
- mcp_tool_ids=None,
- mcp_source=None,
- tool_ids=None,
- application_ids=None,
- skill_tool_ids=None,
- mcp_output_enable=True,
- **kwargs) -> NodeResult:
+ self.context["answer"] = details.get("answer")
+ self.context["question"] = details.get("question")
+ self.context["reasoning_content"] = details.get("reasoning_content")
+ self.context["exception_message"] = details.get("err_message")
+ if self.node_params.get("is_result", False):
+ self.answer_text = details.get("answer")
+
+ def execute(
+ self,
+ model_id,
+ system,
+ prompt,
+ dialogue_number,
+ history_chat_record,
+ stream,
+ chat_id,
+ chat_record_id,
+ model_params_setting=None,
+ model_id_type=None,
+ model_id_reference=None,
+ dialogue_type=None,
+ model_setting=None,
+ mcp_servers=None,
+ mcp_tool_id=None,
+ mcp_tool_ids=None,
+ mcp_source=None,
+ tool_ids=None,
+ application_ids=None,
+ skill_tool_ids=None,
+ mcp_output_enable=True,
+ **kwargs,
+ ) -> NodeResult:
if dialogue_type is None:
- dialogue_type = 'WORKFLOW'
-
- if model_id_type == 'reference' and model_id_reference:
+ dialogue_type = "WORKFLOW"
+ if model_id_type == "reference" and model_id_reference:
reference_data = self.workflow_manage.get_reference_field(
model_id_reference[0],
model_id_reference[1:],
)
if reference_data and isinstance(reference_data, dict):
- model_id = reference_data.get('model_id', model_id)
- model_params_setting = reference_data.get('model_params_setting')
- if model_id is None or model_id == '':
- raise Exception(_('Model is not allowed to be empty'))
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+ if model_id is None or model_id == "":
+ raise Exception(_("Model is not allowed to be empty"))
if model_params_setting is None and model_id:
model_params_setting = get_default_model_params_setting(model_id)
if model_setting is None:
- model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '',
- 'reasoning_content_start': ''}
- self.context['model_setting'] = model_setting
- workspace_id = self.workflow_manage.get_body().get('workspace_id')
- chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
- **(model_params_setting or {}))
- history_message = self.get_history_message(history_chat_record, dialogue_number, dialogue_type,
- self.runtime_node_id)
- self.context['history_message'] = [{'content': message.content, 'role': message.type} for message in
- (history_message if history_message is not None else [])]
+ model_setting = {
+ "reasoning_content_enable": False,
+ "reasoning_content_end": "",
+ "reasoning_content_start": "",
+ }
+ self.context["model_setting"] = model_setting
+ workspace_id = self.workflow_manage.get_body().get("workspace_id")
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+ history_message = self.get_history_message(
+ history_chat_record, dialogue_number, dialogue_type, self.runtime_node_id
+ )
+ self.context["history_message"] = [
+ {"content": message.content, "role": message.type}
+ for message in (history_message if history_message is not None else [])
+ ]
question = self.generate_prompt_question(prompt, chat_model)
- self.context['question'] = question.content
+ self.context["question"] = question.content
system = self.workflow_manage.generate_prompt(system)
- self.context['system'] = system
+ self.context["system"] = system
message_list = self.generate_message_list(question, history_message)
- self.context['message_list'] = message_list
+ self.context["message_list"] = message_list
# 过滤tool_id
- all_tool_ids = list(set(
- (mcp_tool_ids or []) +
- (tool_ids or []) +
- (skill_tool_ids or []) +
- ([mcp_tool_id] if mcp_tool_id else [])
- ))
- authorized_set = set(filter_authorized_ids('tool', all_tool_ids, workspace_id))
+ all_tool_ids = list(
+ set(
+ (mcp_tool_ids or [])
+ + (tool_ids or [])
+ + (skill_tool_ids or [])
+ + ([mcp_tool_id] if mcp_tool_id else [])
+ )
+ )
+ authorized_set = set(filter_authorized_ids("tool", all_tool_ids, workspace_id))
mcp_tool_ids = [i for i in (mcp_tool_ids or []) if i in authorized_set]
tool_ids = [i for i in (tool_ids or []) if i in authorized_set]
@@ -221,82 +249,125 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
mcp_tool_id = mcp_tool_id if (mcp_tool_id and mcp_tool_id in authorized_set) else None
# 处理 MCP 请求
mcp_result = self._handle_mcp_request(
- mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
- application_ids, skill_tool_ids, mcp_output_enable,
- chat_model, SystemMessage(system), message_list, history_message, question, chat_id, workspace_id
+ mcp_source,
+ mcp_servers,
+ mcp_tool_id,
+ mcp_tool_ids,
+ tool_ids,
+ application_ids,
+ skill_tool_ids,
+ mcp_output_enable,
+ chat_model,
+ SystemMessage(system),
+ message_list,
+ history_message,
+ question,
+ chat_id,
+ workspace_id,
)
if mcp_result:
return mcp_result
message_list = [SystemMessage(system)] + message_list
if stream:
r = chat_model.stream(message_list)
- return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
- 'question': question.content}, {},
- _write_context=write_context_stream)
+ return NodeResult(
+ {"result": r, "chat_model": chat_model, "message_list": message_list, "question": question.content},
+ {},
+ _write_context=write_context_stream,
+ )
else:
r = chat_model.invoke(message_list)
- return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,
- 'history_message': [{'content': message.content, 'role': message.type} for message in
- (history_message if history_message is not None else [])],
- 'question': question.content}, {},
- _write_context=write_context)
+ return NodeResult(
+ {
+ "result": r,
+ "chat_model": chat_model,
+ "message_list": message_list,
+ "history_message": [
+ {"content": message.content, "role": message.type}
+ for message in (history_message if history_message is not None else [])
+ ],
+ "question": question.content,
+ },
+ {},
+ _write_context=write_context,
+ )
- def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids, tool_ids,
- application_ids, skill_tool_ids,
- mcp_output_enable, chat_model, system_prompt, message_list, history_message, question,
- chat_id, workspace_id):
+ def _handle_mcp_request(
+ self,
+ mcp_source,
+ mcp_servers,
+ mcp_tool_id,
+ mcp_tool_ids,
+ tool_ids,
+ application_ids,
+ skill_tool_ids,
+ mcp_output_enable,
+ chat_model,
+ system_prompt,
+ message_list,
+ history_message,
+ question,
+ chat_id,
+ workspace_id,
+ ):
mcp_servers_config = {}
# 迁移过来mcp_source是None
if mcp_source is None:
- mcp_source = 'custom'
+ mcp_source = "custom"
# 兼容老数据
if not mcp_tool_ids:
mcp_tool_ids = []
if mcp_tool_id:
mcp_tool_ids = list(set(mcp_tool_ids + [mcp_tool_id]))
- if mcp_source == 'custom' and mcp_servers:
+ if mcp_source == "custom" and mcp_servers:
mcp_servers_config = json.loads(mcp_servers)
mcp_servers_config = self.handle_variables(mcp_servers_config)
elif mcp_tool_ids:
mcp_tools = QuerySet(Tool).filter(id__in=mcp_tool_ids).values()
for mcp_tool in mcp_tools:
- if mcp_tool and mcp_tool['is_active']:
- mcp_servers_config = {**mcp_servers_config, **json.loads(mcp_tool['code'])}
+ if mcp_tool and mcp_tool["is_active"]:
+ mcp_servers_config = {**mcp_servers_config, **json.loads(mcp_tool["code"])}
mcp_servers_config = self.handle_variables(mcp_servers_config)
# 校验代码是否包括禁止的关键字
ToolExecutor().validate_mcp_transport(json.dumps(mcp_servers_config))
tool_init_params = {}
- tools = get_tools(self.workflow_manage.get_source_type(), self.workflow_manage.get_source_id(), tool_ids,
- workspace_id)
+ tools = get_tools(
+ self.workflow_manage.get_source_type(), self.workflow_manage.get_source_id(), tool_ids, workspace_id
+ )
if tool_ids and len(tool_ids) > 0: # 如果有工具ID,则将其转换为MCP
- self.context['tool_ids'] = tool_ids
- custom_tools_map = {str(t.id): t for t in
- QuerySet(Tool).filter(id__in=tool_ids, tool_type=ToolType.CUSTOM, is_active=True)}
+ self.context["tool_ids"] = tool_ids
+ custom_tools_map = {
+ str(t.id): t for t in QuerySet(Tool).filter(id__in=tool_ids, tool_type=ToolType.CUSTOM, is_active=True)
+ }
for tool_id in tool_ids:
tool = custom_tools_map.get(str(tool_id))
if tool is None:
continue
executor = ToolExecutor()
+ init_params_default_value = {i["field"]: i.get("default_value") for i in tool.init_field_list}
if tool.init_params is not None:
- tool_init_params = json.loads(rsa_long_decrypt(tool.init_params))
+ tool_init_params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
else:
- tool_init_params = {i["field"]: i.get('default_value') for i in tool.init_field_list}
+ tool_init_params = init_params_default_value
+
tool_config = executor.get_tool_mcp_config(tool, tool_init_params)
mcp_servers_config[str(tool.id)] = tool_config
if application_ids and len(application_ids) > 0:
- self.context['application_ids'] = application_ids
- apps_map = {str(a.id): a for a in
- QuerySet(Application).filter(id__in=application_ids, is_publish=True)}
- app_keys_map = {str(ak.application_id): ak for ak in
- QuerySet(ApplicationApiKey).filter(application_id__in=application_ids, is_active=True)}
- app_access_tokens_map = {str(at.application_id): at for at in
- QuerySet(ApplicationAccessToken).filter(
- application_id__in=application_ids)}
+ self.context["application_ids"] = application_ids
+ apps_map = {str(a.id): a for a in QuerySet(Application).filter(id__in=application_ids, is_publish=True)}
+ app_keys_map = {
+ str(ak.application_id): ak
+ for ak in QuerySet(ApplicationApiKey).filter(application_id__in=application_ids, is_active=True)
+ }
+ app_access_tokens_map = {
+ str(at.application_id): at
+ for at in QuerySet(ApplicationAccessToken).filter(application_id__in=application_ids)
+ }
for application_id in application_ids:
app = apps_map.get(str(application_id))
if app is None:
@@ -308,39 +379,34 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
if application_access_token is not None and application_access_token.authentication:
raise AppApiException(
500,
- _('Agent 【{name}】 access token authentication is not supported for agent tool').format(
- name=app.name)
+ _("Agent 【{name}】 access token authentication is not supported for agent tool").format(
+ name=app.name
+ ),
)
else:
raise AppApiException(
- 500,
- _('Agent Key is required for agent tool 【{name}】').format(name=app.name)
+ 500, _("Agent Key is required for agent tool 【{name}】").format(name=app.name)
)
executor = ToolExecutor()
- app_config = executor.get_app_mcp_config(api_key)
+ app_config = executor.get_app_mcp_config(api_key, self.get_chat_files(), self.get_form_data())
mcp_servers_config[app.name] = app_config
if skill_tool_ids and len(skill_tool_ids) > 0:
- self.context['skill_tool_ids'] = skill_tool_ids
+ self.context["skill_tool_ids"] = skill_tool_ids
skill_file_items = []
- skill_tools_map = {str(t.id): t for t in
- QuerySet(Tool).filter(id__in=skill_tool_ids, is_active=True)}
+ skill_tools_map = {str(t.id): t for t in QuerySet(Tool).filter(id__in=skill_tool_ids, is_active=True)}
for tool_id in skill_tool_ids:
tool = skill_tools_map.get(str(tool_id))
if tool is None:
continue
- init_params_default_value = {i["field"]: i.get('default_value') for i in tool.init_field_list}
+ init_params_default_value = {i["field"]: i.get("default_value") for i in tool.init_field_list}
if tool.init_params is not None:
params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
else:
params = init_params_default_value
- skill_file_items.append({
- 'tool_id': str(tool.id),
- 'file_id': tool.code,
- 'params': params
- })
- mcp_servers_config['skills'] = skill_file_items
+ skill_file_items.append({"tool_id": str(tool.id), "file_id": tool.code, "params": params})
+ mcp_servers_config["skills"] = skill_file_items
if len(mcp_servers_config) > 0 or len(tools) > 0:
# 安全获取 application
@@ -348,28 +414,80 @@ def _handle_mcp_request(self, mcp_source, mcp_servers, mcp_tool_id, mcp_tool_ids
tool_id = None
knowledge_id = None
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__(
- self.workflow_manage.flow.workflow_mode):
- knowledge_id = self.workflow_params.get('knowledge_id')
+ self.workflow_manage.flow.workflow_mode
+ ):
+ knowledge_id = self.workflow_params.get("knowledge_id")
elif [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP].__contains__(
- self.workflow_manage.flow.workflow_mode):
+ self.workflow_manage.flow.workflow_mode
+ ):
application_id = self.workflow_manage.work_flow_post_handler.chat_info.application.id
elif [WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP].__contains__(self.workflow_manage.flow.workflow_mode):
- tool_id = self.workflow_params.get('tool_id')
+ tool_id = self.workflow_params.get("tool_id")
source_id = application_id or knowledge_id or tool_id
- source_type = 'APPLICATION' if application_id else 'KNOWLEDGE' if knowledge_id else 'TOOL'
- r = mcp_response_generator(chat_model, system_prompt, message_list, json.dumps(mcp_servers_config),
- mcp_output_enable,
- tool_init_params, source_id, source_type, chat_id, tools)
+ source_type = "APPLICATION" if application_id else "KNOWLEDGE" if knowledge_id else "TOOL"
+ r = mcp_response_generator(
+ chat_model,
+ system_prompt,
+ message_list,
+ json.dumps(mcp_servers_config),
+ mcp_output_enable,
+ tool_init_params,
+ source_id,
+ source_type,
+ chat_id,
+ tools,
+ )
return NodeResult(
- {'result': r, 'chat_model': chat_model, 'message_list': message_list,
- 'history_message': [{'content': message.content, 'role': message.type} for message in
- (history_message if history_message is not None else [])],
- 'question': question.content}, {},
- _write_context=write_context_stream)
+ {
+ "result": r,
+ "chat_model": chat_model,
+ "message_list": message_list,
+ "history_message": [
+ {"content": message.content, "role": message.type}
+ for message in (history_message if history_message is not None else [])
+ ],
+ "question": question.content,
+ },
+ {},
+ _write_context=write_context_stream,
+ )
return None
+ def get_chat_files(self):
+ """
+ 获取本次对话上传的文件, 用于透传给被当作工具调用的应用/MCP
+ """
+ chat_files = {}
+ for field in CHAT_FILE_LIST_FIELDS:
+ file_list = getattr(self.workflow_manage, field, None) or []
+ items = [
+ {key: item.get(key) for key in ("name", "url", "file_id") if item.get(key) is not None}
+ for item in file_list
+ if isinstance(item, dict)
+ ]
+ if items:
+ chat_files[field] = items
+ return chat_files
+
+ def get_form_data(self):
+ """
+ 获取当前会话的用户输入参数,用于透传给作为工具调用的子智能体。
+
+ 循环工作流会创建独立的 WorkflowManage,并将自身的 form_data 初始化为
+ 空字典,因此需要继续从父工作流中查找原始用户输入。
+ """
+ workflow_manage = self.workflow_manage
+ visited = set()
+ while workflow_manage is not None and id(workflow_manage) not in visited:
+ visited.add(id(workflow_manage))
+ form_data = getattr(workflow_manage, "form_data", None)
+ if isinstance(form_data, dict) and form_data:
+ return form_data.copy()
+ workflow_manage = getattr(workflow_manage, "parentWorkflowManage", None)
+ return {}
+
def handle_variables(self, tool_params):
# 处理参数中的变量
for k, v in tool_params.items():
@@ -382,20 +500,22 @@ def handle_variables(self, tool_params):
return tool_params
def get_reference_content(self, fields: List[str]):
- return str(self.workflow_manage.get_reference_field(
- fields[0],
- fields[1:])) if fields else ''
+ return str(self.workflow_manage.get_reference_field(fields[0], fields[1:])) if fields else ""
@staticmethod
def get_history_message(history_chat_record, dialogue_number, dialogue_type, runtime_node_id):
start_index = len(history_chat_record) - dialogue_number
- history_message = reduce(lambda x, y: [*x, *y], [
- get_message(history_chat_record[index], dialogue_type, runtime_node_id)
- for index in
- range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ get_message(history_chat_record[index], dialogue_type, runtime_node_id)
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
for message in history_message:
if isinstance(message.content, str):
- message.content = re.sub(r'.*?<\/form_rander>', '', message.content, flags=re.DOTALL)
+ message.content = re.sub(r".*?", "", message.content, flags=re.DOTALL)
return history_message
def generate_prompt_question(self, prompt, model):
@@ -408,43 +528,47 @@ def generate_prompt_question(self, prompt, model):
images = self._process_images(image)
if video and vision:
videos = self._process_videos(video, model)
- return HumanMessage(
- content=[*videos, *images, {'type': 'text', 'text': self.workflow_manage.generate_prompt(prompt)}])
+ prompt = self.workflow_manage.generate_prompt(prompt)
+ if images or videos:
+ return HumanMessage(content=[*videos, *images, {"type": "text", "text": prompt}])
+ return HumanMessage(content=prompt)
def is_vision(self):
- if 'vision' in self.node_params_serializer.data:
- return self.node_params_serializer.data.get('vision')
+ if "vision" in self.node_params_serializer.data:
+ return self.node_params_serializer.data.get("vision")
return False
def get_image(self):
- if 'image_list' in self.node_params_serializer.data:
- image = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('image_list')[0],
- self.node_params_serializer.data.get('image_list')[1:])
+ if "image_list" in self.node_params_serializer.data:
+ image = self.workflow_manage.get_reference_field(
+ self.node_params_serializer.data.get("image_list")[0],
+ self.node_params_serializer.data.get("image_list")[1:],
+ )
return image
return None
def get_video(self):
- if 'video_list' in self.node_params_serializer.data:
- video = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('video_list')[0],
- self.node_params_serializer.data.get('video_list')[1:])
+ if "video_list" in self.node_params_serializer.data:
+ video = self.workflow_manage.get_reference_field(
+ self.node_params_serializer.data.get("video_list")[0],
+ self.node_params_serializer.data.get("video_list")[1:],
+ )
return video
return None
def _process_videos(self, image, video_model):
videos = []
- if isinstance(image, str) and image.startswith('http'):
- videos.append({'type': 'video_url', 'video_url': {'url': image}})
+ if isinstance(image, str) and image.startswith("http"):
+ videos.append({"type": "video_url", "video_url": {"url": image}})
elif image is not None and len(image) > 0:
for img in image:
- if 'file_id' in img:
- file_id = img['file_id']
+ if "file_id" in img:
+ file_id = img["file_id"]
file = QuerySet(File).filter(id=file_id).first()
url = video_model.upload_file_and_get_url(file.get_bytes(), file.file_name)
- videos.append(
- {'type': 'video_url', 'video_url': {'url': url}})
- elif 'url' in img and img['url'].startswith('http'):
- videos.append(
- {'type': 'video_url', 'video_url': {'url': img['url']}})
+ videos.append({"type": "video_url", "video_url": {"url": url}})
+ elif "url" in img and img["url"].startswith("http"):
+ videos.append({"type": "video_url", "video_url": {"url": img["url"]}})
return videos
def _process_images(self, image):
@@ -452,21 +576,21 @@ def _process_images(self, image):
处理图像数据,转换为模型可识别的格式
"""
images = []
- if isinstance(image, str) and image.startswith('http'):
- images.append({'type': 'image_url', 'image_url': {'url': image}})
+ if isinstance(image, str) and image.startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": image}})
elif image is not None and len(image) > 0:
for img in image:
- if 'file_id' in img:
- file_id = img['file_id']
+ if "file_id" in img:
+ file_id = img["file_id"]
file = QuerySet(File).filter(id=file_id).first()
image_bytes = file.get_bytes()
base64_image = base64.b64encode(image_bytes).decode("utf-8")
- image_format = what(None, image_bytes)
+ image_format = guess_image_format(image_bytes)
images.append(
- {'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
- elif 'url' in img and img['url'].startswith('http'):
- images.append(
- {'type': 'image_url', 'image_url': {'url': img["url"]}})
+ {"type": "image_url", "image_url": {"url": f"data:image/{image_format};base64,{base64_image}"}}
+ )
+ elif "url" in img and img["url"].startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": img["url"]}})
return images
def generate_message_list(self, question, history_message):
@@ -474,27 +598,27 @@ def generate_message_list(self, question, history_message):
@staticmethod
def reset_message_list(message_list: List[BaseMessage], answer_text):
- result = [{'role': 'user' if isinstance(message, HumanMessage) else 'ai', 'content': message.content} for
- message
- in
- message_list]
- result.append({'role': 'ai', 'content': answer_text})
+ result = [
+ {"role": "user" if isinstance(message, HumanMessage) else "ai", "content": message.content}
+ for message in message_list
+ ]
+ result.append({"role": "ai", "content": answer_text})
return result
def get_details(self, index: int, **kwargs):
return {
- 'name': self.node.properties.get('stepName'),
+ "name": self.node.properties.get("stepName"),
"index": index,
- 'run_time': self.context.get('run_time'),
- 'system': self.context.get('system'),
- 'history_message': self.context.get('history_message'),
- 'question': self.context.get('question'),
- 'answer': self.context.get('answer'),
- 'reasoning_content': self.context.get('reasoning_content'),
- 'enableException': self.node.properties.get('enableException'),
- 'type': self.node.type,
- 'message_tokens': self.context.get('message_tokens'),
- 'answer_tokens': self.context.get('answer_tokens'),
- 'status': self.status,
- 'err_message': self.err_message
+ "run_time": self.context.get("run_time"),
+ "system": self.context.get("system"),
+ "history_message": self.context.get("history_message"),
+ "question": self.context.get("question"),
+ "answer": self.context.get("answer"),
+ "reasoning_content": self.context.get("reasoning_content"),
+ "enableException": self.node.properties.get("enableException"),
+ "type": self.node.type,
+ "message_tokens": self.context.get("message_tokens"),
+ "answer_tokens": self.context.get("answer_tokens"),
+ "status": self.status,
+ "err_message": self.err_message,
}
diff --git a/apps/application/flow/step_node/application_node/impl/base_application_node.py b/apps/application/flow/step_node/application_node/impl/base_application_node.py
index 7622de3a75f..1288facc819 100644
--- a/apps/application/flow/step_node/application_node/impl/base_application_node.py
+++ b/apps/application/flow/step_node/application_node/impl/base_application_node.py
@@ -132,14 +132,14 @@ def reset_application_node_dict(application_node_dict, runtime_node_id, node_dat
application_node = application_node_dict[key]
if application_node.get('runtime_node_id') == runtime_node_id:
content: str = application_node.get('content')
- match = re.search(r'.*?<\/form_rander>', content, flags=re.DOTALL)
+ match = re.search(r'.*?', content, flags=re.DOTALL)
if match:
form_setting_str = match.group().replace('', '').replace('', '')
form_setting = json.loads(form_setting_str)
form_setting['is_submit'] = True
form_setting['form_data'] = node_data
value = f'{json.dumps(form_setting)}'
- res = re.sub(r'.*?<\/form_rander>', '${value}', content, flags=re.DOTALL)
+ res = re.sub(r'.*?', '${value}', content, flags=re.DOTALL)
application_node['content'] = res.replace('${value}', value)
except Exception as e:
maxkb_logger.warning(f'reset_application_node_dict error: {e}', exc_info=True)
diff --git a/apps/application/flow/step_node/document_extract_node/impl/base_document_extract_node.py b/apps/application/flow/step_node/document_extract_node/impl/base_document_extract_node.py
index 7bd910c31f3..b285e90b07d 100644
--- a/apps/application/flow/step_node/document_extract_node/impl/base_document_extract_node.py
+++ b/apps/application/flow/step_node/document_extract_node/impl/base_document_extract_node.py
@@ -35,7 +35,7 @@ def execute(self, document, chat_id=None, **kwargs):
knowledge_id = self.workflow_params.get('knowledge_id')
elif [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP].__contains__(
self.workflow_manage.flow.workflow_mode):
- application_id = self.workflow_manage.work_flow_post_handler.chat_info.application.id
+ application_id = self.workflow_manage.work_flow_post_handler.chat_info.application_id
elif [WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP].__contains__(self.workflow_manage.flow.workflow_mode):
tool_id = self.workflow_params.get('tool_id')
diff --git a/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py b/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py
index 281122364be..bbd0fc1d12a 100644
--- a/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py
+++ b/apps/application/flow/step_node/image_generate_step_node/impl/base_image_generate_node.py
@@ -153,12 +153,12 @@ def upload_tool_file(self, file):
return file_url
def upload_application_file(self, file):
- application = self.workflow_manage.work_flow_post_handler.chat_info.application
+ application_id = self.workflow_manage.work_flow_post_handler.chat_info.application_id
chat_id = self.workflow_params.get('chat_id')
meta = {
- 'debug': False if application.id else True,
+ 'debug': False if application_id else True,
'chat_id': chat_id,
- 'application_id': str(application.id) if application.id else None,
+ 'application_id': str(application_id) if application_id else None,
}
file_url = FileSerializer(data={
'file': file,
diff --git a/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py b/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py
index 97acad76337..0f19568c6f9 100644
--- a/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py
+++ b/apps/application/flow/step_node/image_to_video_step_node/impl/base_image_to_video_node.py
@@ -5,8 +5,8 @@
import requests
from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _, gettext
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
-from django.utils.translation import gettext_lazy as _
from application.flow.common import WorkflowMode
from application.flow.i_step_node import NodeResult
from application.flow.step_node.image_to_video_step_node.i_image_to_video_node import IImageToVideoNode
@@ -14,7 +14,6 @@
from knowledge.models import FileSourceType, File
from oss.serializers.file import FileSerializer, mime_types
from models_provider.tools import get_model_instance_by_model_workspace_id
-from django.utils.translation import gettext
class BaseImageToVideoNode(IImageToVideoNode):
@@ -128,12 +127,12 @@ def upload_tool_file(self, file):
return file_url
def upload_application_file(self, file):
- application = self.workflow_manage.work_flow_post_handler.chat_info.application
+ application_id = self.workflow_manage.work_flow_post_handler.chat_info.application_id
chat_id = self.workflow_params.get('chat_id')
meta = {
- 'debug': False if application.id else True,
+ 'debug': False if application_id else True,
'chat_id': chat_id,
- 'application_id': str(application.id) if application.id else None,
+ 'application_id': str(application_id) if application_id else None,
}
file_url = FileSerializer(data={
'file': file,
diff --git a/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py b/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py
index 43ad363d106..00ed9b3d128 100644
--- a/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py
+++ b/apps/application/flow/step_node/image_understand_step_node/impl/base_image_understand_node.py
@@ -2,7 +2,6 @@
import base64
import time
from functools import reduce
-from imghdr import what
from typing import List, Dict
from django.db.models import QuerySet
@@ -11,6 +10,7 @@
from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode
from application.flow.tools import Reasoning
+from common.utils.common import guess_image_format
from knowledge.models import File
from models_provider.tools import get_model_instance_by_model_workspace_id
@@ -121,7 +121,7 @@ def file_id_to_base64(file_id: str):
file = QuerySet(File).filter(id=file_id).first()
file_bytes = file.get_bytes()
base64_image = base64.b64encode(file_bytes).decode("utf-8")
- return [base64_image, what(None, file_bytes)]
+ return [base64_image, guess_image_format(file_bytes, file.file_name)]
class BaseImageUnderstandNode(IImageUnderstandNode):
@@ -279,7 +279,7 @@ def _process_images(self, image):
file = QuerySet(File).filter(id=file_id).first()
image_bytes = file.get_bytes()
base64_image = base64.b64encode(image_bytes).decode("utf-8")
- image_format = what(None, image_bytes)
+ image_format = guess_image_format(image_bytes, file.file_name)
images.append(
{'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
elif 'url' in img and img['url'].startswith('http'):
diff --git a/apps/application/flow/step_node/intent_node/impl/base_intent_node.py b/apps/application/flow/step_node/intent_node/impl/base_intent_node.py
index b3f1608acc2..872e180efdd 100644
--- a/apps/application/flow/step_node/intent_node/impl/base_intent_node.py
+++ b/apps/application/flow/step_node/intent_node/impl/base_intent_node.py
@@ -6,6 +6,7 @@
from functools import reduce
from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
from langchain_core.messages import HumanMessage, SystemMessage
from application.flow.i_step_node import INode, NodeResult
@@ -133,7 +134,7 @@ def get_history_message(history_chat_record, dialogue_number):
for message in history_message:
if isinstance(message.content, str):
- message.content = re.sub(r'.*?<\/form_rander>', '', message.content, flags=re.DOTALL)
+ message.content = re.sub(r'.*?', '', message.content, flags=re.DOTALL)
return history_message
def build_system_prompt(self) -> str:
diff --git a/apps/application/flow/step_node/loop_node/i_loop_node.py b/apps/application/flow/step_node/loop_node/i_loop_node.py
index e16dbebc059..527487f5412 100644
--- a/apps/application/flow/step_node/loop_node/i_loop_node.py
+++ b/apps/application/flow/step_node/loop_node/i_loop_node.py
@@ -30,12 +30,12 @@ def is_valid(self, *, raise_exception=False):
if loop_type == 'ARRAY':
array = self.data.get('array')
if array is None or len(array) == 0:
- message = _('{field}, this field is required.', field='array')
+ message = _('{field}, this field is required.').format(field='array')
raise AppApiException(500, message)
elif loop_type == 'NUMBER':
number = self.data.get('number')
if number is None:
- message = _('{field}, this field is required.', field='number')
+ message = _('{field}, this field is required.').format(field='number')
raise AppApiException(500, message)
diff --git a/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py b/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py
index 2e686743b69..8e39084ab75 100644
--- a/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py
+++ b/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py
@@ -10,6 +10,7 @@
import re
from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
from langchain_core.messages import HumanMessage
from langchain_core.prompts import PromptTemplate
diff --git a/apps/application/flow/step_node/question_node/impl/base_question_node.py b/apps/application/flow/step_node/question_node/impl/base_question_node.py
index 34000542db3..665ba99326d 100644
--- a/apps/application/flow/step_node/question_node/impl/base_question_node.py
+++ b/apps/application/flow/step_node/question_node/impl/base_question_node.py
@@ -12,6 +12,7 @@
from typing import List, Dict
from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from application.flow.i_step_node import NodeResult, INode
@@ -130,7 +131,7 @@ def get_history_message(history_chat_record, dialogue_number):
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
for message in history_message:
if isinstance(message.content, str):
- message.content = re.sub(r'.*?<\/form_rander>', '', message.content, flags=re.DOTALL)
+ message.content = re.sub(r'.*?', '', message.content, flags=re.DOTALL)
return history_message
def generate_prompt_question(self, prompt):
diff --git a/apps/application/flow/step_node/search_document_node/impl/base_search_document_node.py b/apps/application/flow/step_node/search_document_node/impl/base_search_document_node.py
index 1d85cff5331..d118baa9424 100644
--- a/apps/application/flow/step_node/search_document_node/impl/base_search_document_node.py
+++ b/apps/application/flow/step_node/search_document_node/impl/base_search_document_node.py
@@ -7,7 +7,7 @@
from application.flow.i_step_node import NodeResult
from application.flow.step_node.search_document_node.i_search_document_node import ISearchDocumentStepNode
-from common.constants.permission_constants import RoleConstants
+from common.auth.constants.role_constants import RoleConstants
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.utils.shared_resource_auth import filter_authorized_ids
from knowledge.models import Document, DocumentTag, Knowledge
diff --git a/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py b/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py
index 35a6fbd19b3..b11cc950542 100644
--- a/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py
+++ b/apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: base_search_dataset_node.py
- @date:2024/6/4 11:56
- @desc:
+@project: maxkb
+@Author:虎
+@file: base_search_dataset_node.py
+@date:2024/6/4 11:56
+@desc:
"""
+
import os
from typing import List, Dict
@@ -15,12 +16,13 @@
from application.flow.i_step_node import NodeResult
from application.flow.step_node.search_knowledge_node.i_search_knowledge_node import ISearchKnowledgeStepNode
from common.config.embedding_config import VectorStore
-from common.constants.permission_constants import RoleConstants
+from common.auth.constants.role_constants import RoleConstants
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.db.search import native_search
from common.utils.common import get_file_content
from common.utils.shared_resource_auth import filter_authorized_ids
from knowledge.models import Document, Paragraph, Knowledge, SearchMode
+from knowledge.services.retrieval_stats import get_recall_tracker, record_recall_safely
from maxkb.conf import PROJECT_DIR
from models_provider.tools import get_model_instance_by_model_workspace_id
@@ -36,8 +38,9 @@ def get_embedding_id(dataset_id_list):
def get_none_result(question):
return NodeResult(
- {'paragraph_list': [], 'is_hit_handling_method': [], 'question': question, 'data': '',
- 'directly_return': ''}, {})
+ {"paragraph_list": [], "is_hit_handling_method": [], "question": question, "data": "", "directly_return": ""},
+ {},
+ )
def reset_title(title):
@@ -48,125 +51,161 @@ def reset_title(title):
def reset_meta(meta):
- if not meta.get('allow_download', False):
- return {'allow_download': False}
+ if not meta.get("allow_download", False):
+ return {"allow_download": False}
return meta
class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
def save_context(self, details, workflow_manage):
- result = details.get('paragraph_list', [])
- knowledge_setting = self.node_params_serializer.data.get('knowledge_setting')
- directly_return = '\n'.join(
- [f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if
- paragraph.get('is_hit_handling_method')])
- self.context['paragraph_list'] = result
- self.context['question'] = details.get('question')
- self.context['run_time'] = details.get('run_time')
- self.context['is_hit_handling_method_list'] = [row for row in result if row.get('is_hit_handling_method')]
- self.context['data'] = '\n'.join(
- [f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in
- result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)]
- self.context['directly_return'] = directly_return
- self.context['exception_message'] = details.get('err_message')
+ result = details.get("paragraph_list", [])
+ knowledge_setting = self.node_params_serializer.data.get("knowledge_setting")
+ directly_return = "\n".join(
+ [
+ f"{paragraph.get('title', '')}:{paragraph.get('content')}"
+ for paragraph in result
+ if paragraph.get("is_hit_handling_method")
+ ]
+ )
+ self.context["paragraph_list"] = result
+ self.context["question"] = details.get("question")
+ self.context["run_time"] = details.get("run_time")
+ self.context["is_hit_handling_method_list"] = [row for row in result if row.get("is_hit_handling_method")]
+ self.context["data"] = "\n".join(
+ [f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result]
+ )[0 : knowledge_setting.get("max_paragraph_char_number", 5000)]
+ self.context["directly_return"] = directly_return
+ self.context["exception_message"] = details.get("err_message")
def get_reference_content(self, fields: List[str]):
return self.workflow_manage.get_reference_field(fields[0], fields[1:]) if fields else None
- def execute(self, knowledge_id_list, knowledge_setting, question, show_knowledge, search_scope_type,
- search_scope_source,
- search_scope_reference,
- exclude_paragraph_id_list=None,
- **kwargs) -> NodeResult:
- self.context['question'] = question
- self.context['show_knowledge'] = show_knowledge
+ def execute(
+ self,
+ knowledge_id_list,
+ knowledge_setting,
+ question,
+ show_knowledge,
+ search_scope_type,
+ search_scope_source,
+ search_scope_reference,
+ exclude_paragraph_id_list=None,
+ **kwargs,
+ ) -> NodeResult:
+ self.context["question"] = question
+ self.context["show_knowledge"] = show_knowledge
document_id_list = None
- if search_scope_type == 'referencing': # 引用上一步知识库/文档
- if search_scope_source == 'knowledge': # 知识库
+ if search_scope_type == "referencing": # 引用上一步知识库/文档
+ if search_scope_source == "knowledge": # 知识库
knowledge_id_list = self.get_reference_content(search_scope_reference)
else: # 文档
document_id_list = self.get_reference_content(search_scope_reference)
- knowledge_id_list = [str(k) for k in QuerySet(Document).filter(
- id__in=document_id_list
- ).values_list(
- 'knowledge_id', flat=True
- ).distinct()]
-
- get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized')
- chat_user_type = self.workflow_manage.get_body().get('chat_user_type')
+ knowledge_id_list = [
+ str(k)
+ for k in QuerySet(Document)
+ .filter(id__in=document_id_list)
+ .values_list("knowledge_id", flat=True)
+ .distinct()
+ ]
+
+ get_knowledge_list_of_authorized = DatabaseModelManage.get_model("get_knowledge_list_of_authorized")
+ chat_user_type = self.workflow_manage.get_body().get("chat_user_type")
if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
- knowledge_id_list = get_knowledge_list_of_authorized(self.workflow_manage.get_body().get('chat_user_id'),
- knowledge_id_list)
- workspace_id = self.workflow_manage.get_body().get('workspace_id')
- knowledge_id_list = filter_authorized_ids('knowledge', knowledge_id_list, workspace_id)
+ knowledge_id_list = get_knowledge_list_of_authorized(
+ self.workflow_manage.get_body().get("chat_user_id"), knowledge_id_list
+ )
+ workspace_id = self.workflow_manage.get_body().get("workspace_id")
+ knowledge_id_list = filter_authorized_ids("knowledge", knowledge_id_list, workspace_id)
if len(knowledge_id_list) == 0:
return get_none_result(question)
model_id = get_embedding_id(knowledge_id_list)
embedding_model = get_model_instance_by_model_workspace_id(model_id, workspace_id)
embedding_value = embedding_model.embed_query(question)
vector = VectorStore.get_embedding_vector()
- exclude_document_id_list = [str(document.id) for document in
- QuerySet(Document).filter(
- knowledge_id__in=knowledge_id_list,
- is_active=False)]
- embedding_list = vector.query(question, embedding_value, knowledge_id_list, document_id_list,
- exclude_document_id_list,
- exclude_paragraph_id_list, True, knowledge_setting.get('top_n'),
- knowledge_setting.get('similarity'),
- SearchMode(knowledge_setting.get('search_mode')))
+ exclude_document_id_list = [
+ str(document.id)
+ for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
+ ]
+ embedding_list = vector.query(
+ question,
+ embedding_value,
+ knowledge_id_list,
+ document_id_list,
+ exclude_document_id_list,
+ exclude_paragraph_id_list,
+ True,
+ knowledge_setting.get("top_n"),
+ knowledge_setting.get("similarity"),
+ SearchMode(knowledge_setting.get("search_mode")),
+ )
# 手动关闭数据库连接
connection.close()
if embedding_list is None:
return get_none_result(question)
paragraph_list = self.list_paragraph(embedding_list, vector)
result = [self.reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
- result = sorted(result, key=lambda p: p.get('similarity'), reverse=True)
- return NodeResult({'paragraph_list': result,
- 'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
- 'data': '\n'.join(
- [f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
- result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)],
- 'directly_return': '\n'.join(
- [paragraph.get('content') for paragraph in
- result if
- paragraph.get('is_hit_handling_method')]),
- 'question': question},
-
- {})
+ result = sorted(result, key=lambda p: p.get("similarity"), reverse=True)
+ if not self.workflow_manage.get_body().get("debug", False):
+ recalled_paragraph_ids = {paragraph.get("id") for paragraph in result}
+ record_recall_safely(
+ [
+ embedding
+ for embedding in embedding_list
+ if str(embedding.get("paragraph_id")) in recalled_paragraph_ids
+ ],
+ tracker=get_recall_tracker(self.workflow_manage),
+ )
+ return NodeResult(
+ {
+ "paragraph_list": result,
+ "is_hit_handling_method_list": [row for row in result if row.get("is_hit_handling_method")],
+ "data": "\n".join(
+ [f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in result]
+ )[0 : knowledge_setting.get("max_paragraph_char_number", 5000)],
+ "directly_return": "\n".join(
+ [paragraph.get("content") for paragraph in result if paragraph.get("is_hit_handling_method")]
+ ),
+ "question": question,
+ },
+ {},
+ )
@staticmethod
def reset_paragraph(paragraph: Dict, embedding_list: List):
- filter_embedding_list = [embedding for embedding in embedding_list if
- str(embedding.get('paragraph_id')) == str(paragraph.get('id'))]
+ filter_embedding_list = [
+ embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
+ ]
if filter_embedding_list is not None and len(filter_embedding_list) > 0:
find_embedding = filter_embedding_list[-1]
return {
**paragraph,
- 'similarity': find_embedding.get('similarity'),
- 'is_hit_handling_method': find_embedding.get('similarity') > paragraph.get(
- 'directly_return_similarity') and paragraph.get('hit_handling_method') == 'directly_return',
- 'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"),
- 'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"),
- 'id': str(paragraph.get('id')),
- 'knowledge_id': str(paragraph.get('knowledge_id')),
- 'document_id': str(paragraph.get('document_id')),
- 'meta': reset_meta(paragraph.get('meta'))
+ "similarity": find_embedding.get("similarity"),
+ "is_hit_handling_method": find_embedding.get("similarity") > paragraph.get("directly_return_similarity")
+ and paragraph.get("hit_handling_method") == "directly_return",
+ "update_time": paragraph.get("update_time").strftime("%Y-%m-%d %H:%M:%S"),
+ "create_time": paragraph.get("create_time").strftime("%Y-%m-%d %H:%M:%S"),
+ "id": str(paragraph.get("id")),
+ "knowledge_id": str(paragraph.get("knowledge_id")),
+ "document_id": str(paragraph.get("document_id")),
+ "meta": reset_meta(paragraph.get("meta")),
}
@staticmethod
def list_paragraph(embedding_list: List, vector):
- paragraph_id_list = [row.get('paragraph_id') for row in embedding_list]
+ paragraph_id_list = [row.get("paragraph_id") for row in embedding_list]
if paragraph_id_list is None or len(paragraph_id_list) == 0:
return []
- paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list),
- get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql',
- 'list_knowledge_paragraph_by_paragraph_id.sql')),
- with_table_name=True)
+ paragraph_list = native_search(
+ QuerySet(Paragraph).filter(id__in=paragraph_id_list),
+ get_file_content(
+ os.path.join(PROJECT_DIR, "apps", "application", "sql", "list_knowledge_paragraph_by_paragraph_id.sql")
+ ),
+ with_table_name=True,
+ )
# 如果向量库中存在脏数据 直接删除
if len(paragraph_list) != len(paragraph_id_list):
- exist_paragraph_list = [row.get('id') for row in paragraph_list]
+ exist_paragraph_list = [row.get("id") for row in paragraph_list]
for paragraph_id in paragraph_id_list:
if not exist_paragraph_list.__contains__(paragraph_id):
vector.delete_by_paragraph_id(paragraph_id)
@@ -174,14 +213,14 @@ def list_paragraph(embedding_list: List, vector):
def get_details(self, index: int, **kwargs):
return {
- 'name': self.node.properties.get('stepName'),
- 'show_knowledge': self.context.get('show_knowledge'),
- 'question': self.context.get('question'),
+ "name": self.node.properties.get("stepName"),
+ "show_knowledge": self.context.get("show_knowledge"),
+ "question": self.context.get("question"),
"index": index,
- 'run_time': self.context.get('run_time'),
- 'paragraph_list': self.context.get('paragraph_list'),
- 'type': self.node.type,
- 'status': self.status,
- 'err_message': self.err_message,
- 'enableException': self.node.properties.get('enableException'),
+ "run_time": self.context.get("run_time"),
+ "paragraph_list": self.context.get("paragraph_list"),
+ "type": self.node.type,
+ "status": self.status,
+ "err_message": self.err_message,
+ "enableException": self.node.properties.get("enableException"),
}
diff --git a/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py b/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py
index 6e740c5971a..861f27312aa 100644
--- a/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py
+++ b/apps/application/flow/step_node/text_to_speech_step_node/impl/base_text_to_speech_node.py
@@ -149,12 +149,12 @@ def upload_tool_file(self, file):
return file_url
def upload_application_file(self, file):
- application = self.workflow_manage.work_flow_post_handler.chat_info.application
+ application_id = self.workflow_manage.work_flow_post_handler.chat_info.application_id
chat_id = self.workflow_params.get('chat_id')
meta = {
- 'debug': False if application.id else True,
+ 'debug': False if application_id else True,
'chat_id': chat_id,
- 'application_id': str(application.id) if application.id else None,
+ 'application_id': str(application_id) if application_id else None,
}
file_url = FileSerializer(data={
'file': file,
diff --git a/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py b/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py
index fd4ae5ad2f3..af787235dbe 100644
--- a/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py
+++ b/apps/application/flow/step_node/text_to_video_step_node/impl/base_text_to_video_node.py
@@ -12,7 +12,7 @@
from knowledge.models import FileSourceType
from oss.serializers.file import FileSerializer
from models_provider.tools import get_model_instance_by_model_workspace_id
-from django.utils.translation import gettext
+from django.utils.translation import gettext_lazy as _, gettext
class BaseTextToVideoNode(ITextToVideoNode):
@@ -38,8 +38,6 @@ def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_t
model_id = reference_data.get('model_id', model_id)
model_params_setting = reference_data.get('model_params_setting')
- from django.utils.translation import gettext_lazy as _
-
if model_id is None or model_id == '':
raise Exception(_('Model is not allowed to be empty'))
workspace_id = self.workflow_manage.get_body().get('workspace_id')
@@ -105,12 +103,12 @@ def upload_tool_file(self, file):
return file_url
def upload_application_file(self, file):
- application = self.workflow_manage.work_flow_post_handler.chat_info.application
+ application_id = self.workflow_manage.work_flow_post_handler.chat_info.application_id
chat_id = self.workflow_params.get('chat_id')
meta = {
- 'debug': False if application.id else True,
+ 'debug': False if application_id else True,
'chat_id': chat_id,
- 'application_id': str(application.id) if application.id else None,
+ 'application_id': str(application_id) if application_id else None,
}
file_url = FileSerializer(data={
'file': file,
diff --git a/apps/application/flow/step_node/tool_node/impl/base_tool_node.py b/apps/application/flow/step_node/tool_node/impl/base_tool_node.py
index c5595bc805e..e269a2b2ba7 100644
--- a/apps/application/flow/step_node/tool_node/impl/base_tool_node.py
+++ b/apps/application/flow/step_node/tool_node/impl/base_tool_node.py
@@ -100,8 +100,19 @@ def execute(self, input_field_list, code, **kwargs) -> NodeResult:
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
field.get('is_required'), field.get('source'), self)
for field in input_field_list}
- result = function_executor.exec_code(code, params)
- self.context['params'] = params
+ # 合并启动参数默认值(如果有 init_field_list 定义)
+ init_field_list = self.node_params.get('init_field_list', [])
+ if init_field_list:
+ init_params_default_value = {i["field"]: i.get('default_value') for i in init_field_list}
+ init_params = kwargs.get('init_params')
+ if init_params is not None:
+ all_params = init_params_default_value | init_params | params
+ else:
+ all_params = init_params_default_value | params
+ else:
+ all_params = params
+ result = function_executor.exec_code(code, all_params)
+ self.context['params'] = all_params
return NodeResult({'result': result}, {}, _write_context=write_context)
def get_details(self, index: int, **kwargs):
diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py
index d158878454e..eaaedb28c0a 100644
--- a/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py
+++ b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py
@@ -14,7 +14,7 @@
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
-from application.flow.common import WorkflowMode, Workflow
+from application.flow.common import WorkflowMode, Workflow, Answer
from application.flow.i_step_node import NodeResult, ToolWorkflowPostHandler, INode
from application.flow.step_node.tool_workflow_lib_node.i_tool_workflow_lib_node import IToolWorkflowLibNode
from application.models import ChatRecord
@@ -145,6 +145,17 @@ def valid_function(tool_lib, workspace_id):
class BaseToolWorkflowLibNodeNode(IToolWorkflowLibNode):
+ def get_answer_list(self):
+ try:
+ child_answer_data = self.context.get('child_answer_data') or []
+ return [
+ Answer(item.get('content'), item.get('view_type'), item.get('runtime_node_id'),
+ item.get('chat_record_id'),
+ item.get('child_node'), item.get('real_node_id'), item.get('reasoning_content')) for item in
+ child_answer_data]
+ except Exception as e:
+ return []
+
def get_parameters(self, input_field_list):
result = {}
for input in input_field_list:
diff --git a/apps/application/long_term_memory/__init__.py b/apps/application/long_term_memory/__init__.py
index ea03cf58bcc..ba2419d6bce 100644
--- a/apps/application/long_term_memory/__init__.py
+++ b/apps/application/long_term_memory/__init__.py
@@ -270,7 +270,7 @@ def _run_extract(workspace_id, application_id, chat_user_id, config, history_lim
]):
content += chunk.content
- content = re.sub(r'.*?<\/think>', '', content, flags=re.DOTALL).strip()
+ content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip()
if long_term_memory:
long_term_memory.memory = content
diff --git a/apps/application/mcp_tools.py b/apps/application/mcp_tools.py
new file mode 100644
index 00000000000..d48d6480b26
--- /dev/null
+++ b/apps/application/mcp_tools.py
@@ -0,0 +1,8 @@
+"""Shared MCP tool loading helpers."""
+
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+
+async def get_mcp_tools(servers):
+ client = MultiServerMCPClient(servers)
+ return await client.get_tools()
diff --git a/apps/application/migrations/0014_applicationversion_knowledge_ids.py b/apps/application/migrations/0014_applicationversion_knowledge_ids.py
new file mode 100644
index 00000000000..9457355f724
--- /dev/null
+++ b/apps/application/migrations/0014_applicationversion_knowledge_ids.py
@@ -0,0 +1,61 @@
+# Generated by Django 5.2.15 on 2026-07-29 02:42
+
+from django.db import migrations, models
+
+
+def forwards(apps, schema_editor):
+ Application = apps.get_model("application", "Application")
+ ResourceMapping = apps.get_model("system_manage", "ResourceMapping")
+ ApplicationVersion = apps.get_model("application", "ApplicationVersion")
+
+ APPLICATION = "APPLICATION"
+ KNOWLEDGE = "KNOWLEDGE"
+ SIMPLE = "SIMPLE"
+ db_alias = schema_editor.connection.alias
+ simple_application_ids = {
+ str(app_id)
+ for app_id in Application.objects.using(db_alias)
+ .filter(type=SIMPLE)
+ .values_list("id", flat=True)
+ }
+ mapping = {}
+ qs = (
+ ResourceMapping.objects.using(db_alias)
+ .filter(source_type=APPLICATION, target_type=KNOWLEDGE)
+ .values_list("source_id", "target_id")
+ )
+ for source_id, target_id in qs.iterator():
+ if source_id in simple_application_ids:
+ mapping.setdefault(source_id, []).append(target_id)
+ mapping = {k: list(dict.fromkeys(v)) for k, v in mapping.items()}
+
+ updates = []
+ for obj in ApplicationVersion.objects.using(db_alias).iterator():
+ app_id = str(obj.application_id)
+ if app_id not in simple_application_ids:
+ continue
+ knowledge_ids = mapping.get(app_id)
+ if knowledge_ids:
+ obj.knowledge_ids = knowledge_ids
+ updates.append(obj)
+ if updates:
+ ApplicationVersion.objects.using(db_alias).bulk_update(
+ updates, ["knowledge_ids"], batch_size=500
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('application', '0013_application_long_term_enable_and_more'),
+ ('system_manage', '0005_resourcemapping'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="applicationversion",
+ name="knowledge_ids",
+ field=models.JSONField(default=list, verbose_name="数据集id列表"),
+ ),
+ migrations.RunPython(forwards, migrations.RunPython.noop),
+ ]
diff --git a/apps/application/migrations/0015_chat_execute_type.py b/apps/application/migrations/0015_chat_execute_type.py
new file mode 100644
index 00000000000..9f0e446d4f9
--- /dev/null
+++ b/apps/application/migrations/0015_chat_execute_type.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.14 on 2026-07-21 08:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('application', '0014_applicationversion_knowledge_ids'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='chat',
+ name='execute_type',
+ field=models.CharField(choices=[('ANONYMOUS_USER', '匿名用户'), ('CHAT_USER', '对话用户'), ('SYSTEM_API_KEY', '系统API_KEY'), ('APPLICATION_API_KEY', '应用API_KEY'), ('PLATFORM_USER', '平台用户')], default='CHAT', max_length=64, verbose_name='执行类型'),
+ ),
+ migrations.AddField(
+ model_name="chatrecord",
+ name="workflow_context",
+ field=models.JSONField(blank=True, default=dict, null=True, verbose_name="工作流上下文"),
+ ),
+ ]
diff --git a/apps/application/migrations/0016_chatrecord_messages_chatrecord_question_and_more.py b/apps/application/migrations/0016_chatrecord_messages_chatrecord_question_and_more.py
new file mode 100644
index 00000000000..bf835deb315
--- /dev/null
+++ b/apps/application/migrations/0016_chatrecord_messages_chatrecord_question_and_more.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.2.14 on 2026-07-22 08:33
+
+import common.encoder.encoder
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('application', '0015_chat_execute_type'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='chatrecord',
+ name='messages',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), default=list, size=None, verbose_name='响应message'),
+ ),
+ migrations.AddField(
+ model_name='chatrecord',
+ name='question',
+ field=models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='用户的消息'),
+ ),
+ migrations.AddField(
+ model_name='chatrecord',
+ name='version',
+ field=models.IntegerField(default=1, verbose_name='版本号'),
+ ),
+ ]
diff --git a/apps/application/migrations/0017_application_is_portal_and_more.py b/apps/application/migrations/0017_application_is_portal_and_more.py
new file mode 100644
index 00000000000..8b4dd159ddc
--- /dev/null
+++ b/apps/application/migrations/0017_application_is_portal_and_more.py
@@ -0,0 +1,17 @@
+# Generated by Django 6.1 on 2026-08-24 08:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("application", "0016_chatrecord_messages_chatrecord_question_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="application",
+ name="is_portal",
+ field=models.BooleanField(default=False, verbose_name="是否在门户上架"),
+ )
+ ]
diff --git a/apps/application/models/application.py b/apps/application/models/application.py
index 34824b29eee..28a8f402ed4 100644
--- a/apps/application/models/application.py
+++ b/apps/application/models/application.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application.py
- @date:2025/5/7 15:29
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application.py
+@date:2025/5/7 15:29
+@desc:
"""
+
import uuid_utils.compat as uuid
from django.db import models
from mptt.fields import TreeForeignKey
@@ -23,45 +24,50 @@ class ApplicationFolder(MPTTModel, AppModelMixin):
desc = models.CharField(max_length=200, null=True, blank=True, verbose_name="描述")
user = models.ForeignKey(User, on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True)
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
- parent = TreeForeignKey('self', on_delete=models.DO_NOTHING, null=True, blank=True, related_name='children')
+ parent = TreeForeignKey("self", on_delete=models.DO_NOTHING, null=True, blank=True, related_name="children")
class Meta:
db_table = "application_folder"
class MPTTMeta:
- order_insertion_by = ['name']
+ order_insertion_by = ["name"]
class ApplicationTypeChoices(models.TextChoices):
"""订单类型"""
- SIMPLE = 'SIMPLE', '简易'
- WORK_FLOW = 'WORK_FLOW', '工作流'
+
+ SIMPLE = "SIMPLE", "简易"
+ WORK_FLOW = "WORK_FLOW", "工作流"
def get_dataset_setting_dict():
- return {'top_n': 3, 'similarity': 0.6, 'max_paragraph_char_number': 5000, 'search_mode': 'embedding',
- 'no_references_setting': {
- 'status': 'ai_questioning',
- 'value': '{question}'
- }}
+ return {
+ "top_n": 3,
+ "similarity": 0.6,
+ "max_paragraph_char_number": 5000,
+ "search_mode": "embedding",
+ "no_references_setting": {"status": "ai_questioning", "value": "{question}"},
+ }
def get_model_setting_dict():
return {
- 'prompt': Application.get_default_model_prompt(),
- 'no_references_prompt': '{question}',
- 'reasoning_content_start': '',
- 'reasoning_content_end': '',
- 'reasoning_content_enable': False,
+ "prompt": Application.get_default_model_prompt(),
+ "no_references_prompt": "{question}",
+ "reasoning_content_start": "",
+ "reasoning_content_end": "",
+ "reasoning_content_enable": False,
}
class Application(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
- folder = models.ForeignKey(ApplicationFolder, on_delete=models.DO_NOTHING, verbose_name="文件夹id",
- default='default')
+ folder = models.ForeignKey(
+ ApplicationFolder, on_delete=models.DO_NOTHING, verbose_name="文件夹id", default="default"
+ )
is_publish = models.BooleanField(verbose_name="是否发布", default=False)
+ is_portal = models.BooleanField(verbose_name="是否在门户上架", default=False)
name = models.CharField(max_length=128, verbose_name="应用名称", db_index=True)
desc = models.CharField(max_length=512, verbose_name="引用描述", default="")
prologue = models.CharField(max_length=40960, verbose_name="开场白", default="")
@@ -76,15 +82,25 @@ class Application(AppModelMixin):
problem_optimization = models.BooleanField(verbose_name="问题优化", default=False)
icon = models.CharField(max_length=256, verbose_name="应用icon", default="./favicon.ico")
work_flow = models.JSONField(verbose_name="工作流数据", default=dict)
- type = models.CharField(verbose_name="应用类型", choices=ApplicationTypeChoices.choices,
- default=ApplicationTypeChoices.SIMPLE, max_length=256)
- problem_optimization_prompt = models.CharField(verbose_name="问题优化提示词", max_length=102400, blank=True,
- null=True,
- default="()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在标签中")
- tts_model = models.ForeignKey(Model, related_name='tts_model_id', on_delete=models.SET_NULL, db_constraint=False,
- blank=True, null=True)
- stt_model = models.ForeignKey(Model, related_name='stt_model_id', on_delete=models.SET_NULL, db_constraint=False,
- blank=True, null=True)
+ type = models.CharField(
+ verbose_name="应用类型",
+ choices=ApplicationTypeChoices.choices,
+ default=ApplicationTypeChoices.SIMPLE,
+ max_length=256,
+ )
+ problem_optimization_prompt = models.CharField(
+ verbose_name="问题优化提示词",
+ max_length=102400,
+ blank=True,
+ null=True,
+ default="()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在标签中",
+ )
+ tts_model = models.ForeignKey(
+ Model, related_name="tts_model_id", on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True
+ )
+ stt_model = models.ForeignKey(
+ Model, related_name="stt_model_id", on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True
+ )
tts_model_enable = models.BooleanField(verbose_name="语音合成模型是否启用", default=False)
stt_model_enable = models.BooleanField(verbose_name="语音识别模型是否启用", default=False)
tts_type = models.CharField(verbose_name="语音播放类型", max_length=20, default="BROWSER")
@@ -105,26 +121,29 @@ class Application(AppModelMixin):
skill_tool_ids = models.JSONField(verbose_name="技能ID列表", default=list)
mcp_output_enable = models.BooleanField(verbose_name="MCP输出是否启用", default=True)
file_clean_time = models.IntegerField(verbose_name="文件清理时间", default=180)
- long_term_enable = models.BooleanField(verbose_name='长期记忆是否开启', default=False)
- long_term_model = models.ForeignKey(Model, related_name='long_term_model_id', on_delete=models.SET_NULL,
- db_constraint=False, blank=True, null=True)
+ long_term_enable = models.BooleanField(verbose_name="长期记忆是否开启", default=False)
+ long_term_model = models.ForeignKey(
+ Model, related_name="long_term_model_id", on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True
+ )
long_term_model_params_setting = models.JSONField(verbose_name="长期记忆模型参数相关设置", default=dict)
- long_term_trigger_type = models.CharField(verbose_name='长期记忆触发类型', default='ROUND')
- long_term_trigger_setting = models.JSONField(verbose_name='长期记忆触发配置', default=dict)
+ long_term_trigger_type = models.CharField(verbose_name="长期记忆触发类型", default="ROUND")
+ long_term_trigger_setting = models.JSONField(verbose_name="长期记忆触发配置", default=dict)
@staticmethod
def get_default_model_prompt():
- return ('已知信息:'
- '\n{data}'
- '\n回答要求:'
- '\n- 如果你不知道答案或者没有从获取答案,请回答“没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作”。'
- '\n- 避免提及你是从中获得的知识。'
- '\n- 请保持答案与中描述的一致。'
- '\n- 请使用markdown 语法优化答案的格式。'
- '\n- 中的图片链接、链接地址和脚本语言请完整返回。'
- '\n- 请使用与问题相同的语言来回答。'
- '\n问题:'
- '\n{question}')
+ return (
+ "已知信息:"
+ "\n{data}"
+ "\n回答要求:"
+ "\n- 如果你不知道答案或者没有从获取答案,请回答“没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作”。"
+ "\n- 避免提及你是从中获得的知识。"
+ "\n- 请保持答案与中描述的一致。"
+ "\n- 请使用markdown 语法优化答案的格式。"
+ "\n- 中的图片链接、链接地址和脚本语言请完整返回。"
+ "\n- 请使用与问题相同的语言来回答。"
+ "\n问题:"
+ "\n{question}"
+ )
class Meta:
db_table = "application"
@@ -160,15 +179,21 @@ class ApplicationVersion(AppModelMixin):
problem_optimization = models.BooleanField(verbose_name="问题优化", default=False)
icon = models.CharField(max_length=256, verbose_name="应用icon", default="./favicon.ico")
work_flow = models.JSONField(verbose_name="工作流数据", default=dict)
- type = models.CharField(verbose_name="应用类型", choices=ApplicationTypeChoices.choices,
- default=ApplicationTypeChoices.SIMPLE, max_length=256)
- problem_optimization_prompt = models.CharField(verbose_name="问题优化提示词", max_length=102400, blank=True,
- null=True,
- default="()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在标签中")
- tts_model_id = models.UUIDField(verbose_name="文本转语音模型id",
- blank=True, null=True)
- stt_model_id = models.UUIDField(verbose_name="语音转文本模型id",
- blank=True, null=True)
+ type = models.CharField(
+ verbose_name="应用类型",
+ choices=ApplicationTypeChoices.choices,
+ default=ApplicationTypeChoices.SIMPLE,
+ max_length=256,
+ )
+ problem_optimization_prompt = models.CharField(
+ verbose_name="问题优化提示词",
+ max_length=102400,
+ blank=True,
+ null=True,
+ default="()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在标签中",
+ )
+ tts_model_id = models.UUIDField(verbose_name="文本转语音模型id", blank=True, null=True)
+ stt_model_id = models.UUIDField(verbose_name="语音转文本模型id", blank=True, null=True)
tts_model_enable = models.BooleanField(verbose_name="语音合成模型是否启用", default=False)
stt_model_enable = models.BooleanField(verbose_name="语音识别模型是否启用", default=False)
tts_type = models.CharField(verbose_name="语音播放类型", max_length=20, default="BROWSER")
@@ -187,11 +212,12 @@ class ApplicationVersion(AppModelMixin):
application_ids = models.JSONField(verbose_name="应用ID列表", default=list)
skill_tool_ids = models.JSONField(verbose_name="技能ID列表", default=list)
mcp_output_enable = models.BooleanField(verbose_name="MCP输出是否启用", default=True)
- long_term_enable = models.BooleanField(verbose_name='长期记忆是否开启', default=False)
+ long_term_enable = models.BooleanField(verbose_name="长期记忆是否开启", default=False)
long_term_model_id = models.UUIDField(verbose_name="长期记忆模型id", blank=True, null=True)
long_term_model_params_setting = models.JSONField(verbose_name="长期记忆模型参数相关设置", default=dict)
- long_term_trigger_type = models.CharField(verbose_name='长期记忆触发类型', default='ROUND')
- long_term_trigger_setting = models.JSONField(verbose_name='长期记忆触发配置', default=dict)
+ long_term_trigger_type = models.CharField(verbose_name="长期记忆触发类型", default="ROUND")
+ long_term_trigger_setting = models.JSONField(verbose_name="长期记忆触发配置", default=dict)
+ knowledge_ids = models.JSONField(verbose_name="数据集id列表", default=list)
class Meta:
db_table = "application_version"
diff --git a/apps/application/models/application_chat.py b/apps/application/models/application_chat.py
index e9a3efbc697..d617b65649a 100644
--- a/apps/application/models/application_chat.py
+++ b/apps/application/models/application_chat.py
@@ -24,6 +24,12 @@ class ChatUserType(models.TextChoices):
SYSTEM_API_KEY = "SYSTEM_API_KEY", "系统API_KEY"
APPLICATION_API_KEY = "APPLICATION_API_KEY", "应用API_KEY"
PLATFORM_USER = "PLATFORM_USER", "平台用户"
+ SYSTEM_USER = "SYSTEM_USER", '系统用户'
+
+
+class ExecuteType(models.TextChoices):
+ DEBUG = "DEBUG"
+ CHAT = "CHAT"
def default_asker():
@@ -37,6 +43,8 @@ class Chat(AppModelMixin):
chat_user_id = models.CharField(verbose_name="对话用户id", default=None, null=True)
chat_user_type = models.CharField(max_length=64, verbose_name="客户端类型", choices=ChatUserType.choices,
default=ChatUserType.ANONYMOUS_USER)
+ execute_type = models.CharField(max_length=64, verbose_name="执行类型", choices=ChatUserType.choices,
+ default=ExecuteType.CHAT)
is_deleted = models.BooleanField(verbose_name="逻辑删除", default=False)
asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder)
meta = models.JSONField(verbose_name="元数据", default=dict)
@@ -65,10 +73,12 @@ class VoteReasonChoices(models.TextChoices):
INCOMPLETE = 'incomplete', '内容不完善'
OTHER = 'other', '其他'
+
class ShareLinkType(models.TextChoices):
PUBLIC = "PUBLIC", 'public'
PRIVATE = "PRIVATE", 'private'
+
class ChatSourceChoices(models.TextChoices):
ONLINE = "ONLINE", "线上使用"
API_CALL = "API_CALL", "API调用"
@@ -108,6 +118,13 @@ class ChatRecord(AppModelMixin):
index = models.IntegerField(verbose_name="对话下标")
source = models.JSONField(verbose_name="来源", default=dict)
ip_address = models.CharField(max_length=128, verbose_name="ip地址", default='')
+ version = models.IntegerField(verbose_name="版本号", default=1)
+ question = models.JSONField(verbose_name="用户的消息", default=dict, encoder=SystemEncoder)
+ messages = ArrayField(verbose_name="响应message",
+ base_field=models.JSONField()
+ , default=list)
+
+ workflow_context = models.JSONField(verbose_name="工作流上下文", default=dict, null=True, blank=True)
def get_human_message(self):
if 'problem_padding' in self.details:
@@ -143,10 +160,11 @@ class Meta:
models.Index(fields=['application_id', 'chat_user_id']),
]
+
class ChatShareLink(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
- application = models.ForeignKey(Application,on_delete=models.CASCADE)
+ application = models.ForeignKey(Application, on_delete=models.CASCADE)
share_type = models.CharField(max_length=20, choices=ShareLinkType.choices, default=ShareLinkType.PUBLIC)
user = models.ForeignKey(User, on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True)
chat_record_ids = ArrayField(base_field=models.UUIDField(max_length=128))
@@ -155,11 +173,10 @@ class Meta:
db_table = "application_chat_share_link"
-
class ApplicationLongTermMemory(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
application = models.ForeignKey(Application, on_delete=models.CASCADE, db_constraint=False, verbose_name="所属应用")
- chat_user_id = models.CharField( max_length=128, verbose_name="对话用户id", db_index=True)
+ chat_user_id = models.CharField(max_length=128, verbose_name="对话用户id", db_index=True)
memory = models.TextField(verbose_name="长期记忆内容", default="")
class Meta:
diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py
index 5c74570911b..9b6c3bf5fed 100644
--- a/apps/application/serializers/application.py
+++ b/apps/application/serializers/application.py
@@ -21,6 +21,11 @@
import requests
import uuid_utils.compat as uuid
+from application.flow.common import Workflow
+from application.long_term_memory import schedule_extract_long_term_memory
+from application.models.application import Application, ApplicationFolder, ApplicationTypeChoices, ApplicationVersion
+from application.models.application_access_token import ApplicationAccessToken
+from application.serializers.common import update_resource_mapping_by_application
from common import result
from common.cache_data.application_access_token_cache import del_application_access_token
from common.database_model_manage.database_model_manage import DatabaseModelManage
@@ -36,6 +41,7 @@
)
from common.utils.logger import maxkb_logger
from common.utils.tool_code import ToolExecutor
+from common.utils.url_validator import ALLOWED_CALLBACK_HOSTS, ALLOWED_DOWNLOAD_HOSTS, validate_trusted_url
from django.core import validators
from django.db import models, transaction
from django.db.models import Q, QuerySet
@@ -52,7 +58,7 @@
from models_provider.tools import get_model_instance_by_model_workspace_id
from rest_framework import serializers, status
from rest_framework.utils.formatting import lazy_format
-from system_manage.models import AuthTargetType, WorkspaceUserResourcePermission
+from system_manage.models import AuthTargetType, WorkspaceUserResourcePermission, WorkspaceUserGroupResourcePermission
from system_manage.models.resource_mapping import ResourceMapping
from system_manage.serializers.resource_mapping_serializers import ResourceMappingSerializer
from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer
@@ -62,11 +68,133 @@
from users.models import User
from users.serializers.user import is_workspace_manage, is_workspace_manage_permission_read
-from application.flow.common import Workflow
-from application.long_term_memory import schedule_extract_long_term_memory
-from application.models.application import Application, ApplicationFolder, ApplicationTypeChoices, ApplicationVersion
-from application.models.application_access_token import ApplicationAccessToken
-from application.serializers.common import update_resource_mapping_by_application
+
+def _walk_workflow_nodes(work_flow, collector):
+ """遍历工作流节点(含 loop-node 嵌套),对每个节点的 node_data 调用 collector。"""
+ if not work_flow:
+ return
+ for node in work_flow.get("nodes", []) or []:
+ node_data = (node.get("properties") or {}).get("node_data") or {}
+ collector(node_data)
+ if node.get("type") == "loop-node":
+ _walk_workflow_nodes(node_data.get("loop_body"), collector)
+
+
+def get_bound_tool_ids(instance: Dict) -> List[str]:
+ """
+ 收集应用配置(含工作流节点)中引用的所有工具id,用于绑定前的权限校验
+ """
+ tool_ids = set()
+ for key in ("tool_ids", "skill_tool_ids", "mcp_tool_ids"):
+ for tool_id in instance.get(key) or []:
+ tool_ids.add(str(tool_id))
+ if instance.get("mcp_tool_id"):
+ tool_ids.add(str(instance.get("mcp_tool_id")))
+
+ def collect(node_data):
+ for key in ("tool_lib_id", "mcp_tool_id"):
+ if node_data.get(key):
+ tool_ids.add(str(node_data.get(key)))
+ for key in ("mcp_tool_ids", "tool_ids", "skill_tool_ids"):
+ for tool_id in node_data.get(key) or []:
+ tool_ids.add(str(tool_id))
+
+ _walk_workflow_nodes(instance.get("work_flow"), collect)
+ return list(tool_ids)
+
+
+def get_bound_application_ids(instance: Dict) -> List[str]:
+ """
+ 收集应用配置(含工作流节点)中引用的所有 application_id,用于绑定前的权限校验。
+ ai-chat-node 的 node_data 包含 application_ids 列表。
+ """
+ application_ids = set()
+ for app_id in instance.get("application_ids") or []:
+ application_ids.add(str(app_id))
+
+ def collect(node_data):
+ for app_id in node_data.get("application_ids") or []:
+ application_ids.add(str(app_id))
+
+ _walk_workflow_nodes(instance.get("work_flow"), collect)
+ return list(application_ids)
+
+
+def get_authorized_tool_ids(user_id: str, workspace_id: str, tool_ids: List[str]) -> List[str]:
+ """
+ 返回 tool_ids 中当前用户被授权绑定/使用的工具id。
+ 工作空间管理员默认拥有全部工具权限;其他用户必须在 workspace_user_resource_permission
+ 中存在针对该工具的显式授权记录(默认拒绝)。
+ """
+ if not tool_ids:
+ return []
+ tool_ids = list({str(t) for t in tool_ids})
+ if is_workspace_manage(user_id, workspace_id):
+ return tool_ids
+ granted_tool_ids = {
+ str(permission.target)
+ for permission in QuerySet(WorkspaceUserResourcePermission).filter(
+ workspace_id=workspace_id,
+ user_id=user_id,
+ auth_target_type=AuthTargetType.TOOL.value,
+ target__in=tool_ids,
+ )
+ if "VIEW" in permission.permission_list or "ROLE" in permission.permission_list
+ }
+ return [tool_id for tool_id in tool_ids if tool_id in granted_tool_ids]
+
+
+def get_authorized_application_ids(user_id: str, workspace_id: str, application_ids: List[str]) -> List[str]:
+ """
+ 返回 application_ids 中当前用户被授权绑定/使用的应用id。
+ 工作空间管理员默认拥有全部应用权限;其他用户必须在 workspace_user_resource_permission
+ 中存在针对该应用的显式授权记录(默认拒绝)。
+ """
+ if not application_ids:
+ return []
+ application_ids = list({str(a) for a in application_ids})
+ if is_workspace_manage(user_id, workspace_id):
+ return application_ids
+ granted_application_ids = {
+ str(permission.target)
+ for permission in QuerySet(WorkspaceUserResourcePermission).filter(
+ workspace_id=workspace_id,
+ user_id=user_id,
+ auth_target_type=AuthTargetType.APPLICATION.value,
+ target__in=application_ids,
+ )
+ if "VIEW" in permission.permission_list or "ROLE" in permission.permission_list
+ }
+ return [app_id for app_id in application_ids if app_id in granted_application_ids]
+
+
+def validate_bound_tool_permissions(user_id: str, workspace_id: str, instance: Dict):
+ """
+ 校验应用/工作流中绑定的工具和子应用,当前用户是否都有权限使用,防止低权限成员
+ 绑定自己被禁止访问的资源,并通过应用/工作流执行绕过单独授权控制。
+ """
+ tool_ids = get_bound_tool_ids(instance)
+ if tool_ids:
+ authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, tool_ids))
+ unauthorized_tool_ids = [tool_id for tool_id in tool_ids if tool_id not in authorized_tool_ids]
+ if unauthorized_tool_ids:
+ message = lazy_format(
+ _("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids)
+ )
+ raise AppApiException(403, str(message))
+
+ application_ids = get_bound_application_ids(instance)
+ if application_ids:
+ authorized_application_ids = set(get_authorized_application_ids(user_id, workspace_id, application_ids))
+ unauthorized_application_ids = [
+ app_id for app_id in application_ids if app_id not in authorized_application_ids
+ ]
+ if unauthorized_application_ids:
+ message = lazy_format(
+ _("No permission to use application(s): {application_ids}"),
+ application_ids=", ".join(unauthorized_application_ids),
+ )
+ raise AppApiException(403, str(message))
def get_base_node_work_flow(work_flow):
@@ -103,6 +231,42 @@ def hand_node(node, update_tool_map):
node.get("properties", {}).get("node_data", {})["tool_lib_id"] = update_tool_map.get(tool_lib_id, tool_lib_id)
+def update_form_knowledge_fields(workflow):
+ if workflow is None:
+ return
+ for node in workflow.get("nodes", []):
+ if node.get("type") == "form-node":
+ form_field_list = node.get("properties", {}).get("node_data", {}).get("form_field_list", [])
+ for field in form_field_list:
+ if field.get("input_type") != "Knowledge":
+ continue
+ knowledge_list = field.get("attrs", {}).get("knowledge_list", [])
+ knowledge_id_list = [
+ str(knowledge.get("id")) for knowledge in knowledge_list if knowledge.get("id") is not None
+ ]
+ current_knowledge_dict = {
+ str(knowledge.id): knowledge
+ for knowledge in QuerySet(Knowledge).filter(id__in=list(set(knowledge_id_list)))
+ }
+ refreshed_knowledge_list = []
+ for knowledge in knowledge_list:
+ current_knowledge = current_knowledge_dict.get(str(knowledge.get("id")))
+ if current_knowledge is None:
+ refreshed_knowledge_list.append(knowledge)
+ else:
+ refreshed_knowledge_list.append(
+ {
+ **knowledge,
+ "name": current_knowledge.name,
+ "type": current_knowledge.type,
+ "embedding_model_id": current_knowledge.embedding_model_id,
+ }
+ )
+ field.setdefault("attrs", {})["knowledge_list"] = refreshed_knowledge_list
+ if node.get("type") == "loop-node":
+ update_form_knowledge_fields(node.get("properties", {}).get("node_data", {}).get("loop_body") or {})
+
+
class MKInstance:
def __init__(self, application: dict, function_lib_list: List[dict], version: str, tool_list: List[dict]):
self.application = application
@@ -393,6 +557,9 @@ class ApplicationListResponse(serializers.Serializer):
required=True, label=_("Application Description"), help_text=_("Application Description")
)
is_publish = serializers.BooleanField(required=True, label=_("Model id"), help_text=_("Model id"))
+ is_portal = serializers.BooleanField(
+ required=True, label=_("Whether to publish on portal"), help_text=_("Whether to publish on portal")
+ )
type = serializers.CharField(required=True, label=_("Application type"), help_text=_("Application type"))
resource_type = serializers.CharField(required=True, label=_("Resource type"), help_text=_("Resource type"))
user_id = serializers.CharField(required=True, label=_("Affiliation user"), help_text=_("Affiliation user"))
@@ -437,11 +604,15 @@ def get_query_set(self, instance: Dict, workspace_manage: bool, is_x_pack_ee: bo
resource_and_folder_query_set = QuerySet(WorkspaceUserResourcePermission).filter(
auth_target_type="APPLICATION", workspace_id=workspace_id, user_id=user_id
)
+ resource_and_group_query_set = self.get_workspace_user_group_resource_permission_query_set(
+ workspace_id, user_id
+ )
return (
{
"application_query_set": application_query_set,
"workspace_user_resource_permission_query_set": resource_and_folder_query_set,
+ "workspace_user_group_resource_permission_query_set": resource_and_group_query_set,
}
if (not workspace_manage)
else {
@@ -456,6 +627,14 @@ def is_x_pack_ee():
role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model")
return workspace_user_role_mapping_model is not None and role_permission_mapping_model is not None
+ @staticmethod
+ def get_workspace_user_group_resource_permission_query_set(workspace_id, user_id):
+ return QuerySet(WorkspaceUserGroupResourcePermission).filter(
+ auth_target_type="APPLICATION",
+ workspace_id=workspace_id,
+ user_group__user_relations__user_id=user_id,
+ )
+
def list(self, instance: Dict):
self.is_valid(raise_exception=True)
workspace_id = self.data.get("workspace_id")
@@ -514,6 +693,7 @@ class ApplicationImportRequest(serializers.Serializer):
class ApplicationEditSerializer(serializers.Serializer):
name = serializers.CharField(required=False, max_length=64, min_length=1, label=_("Application Name"))
+ is_portal = serializers.BooleanField(required=False, label=_("Whether to publish on portal"))
desc = serializers.CharField(
required=False,
max_length=256,
@@ -586,10 +766,10 @@ def insert_template_workflow(self, instance: Dict):
self.is_valid(raise_exception=True)
work_flow_template = instance.get("work_flow_template")
download_url = work_flow_template.get("downloadUrl")
- if not download_url.startswith("https://apps-assets.fit2cloud.com/"):
+ if not validate_trusted_url(download_url, ALLOWED_DOWNLOAD_HOSTS):
raise AppApiException(500, _("Illegal download url"))
# 查找匹配的版本名称
- res = requests.get(download_url, timeout=5)
+ res = requests.get(download_url, timeout=5, allow_redirects=False)
app = ApplicationSerializer(
data={"user_id": self.data.get("user_id"), "workspace_id": self.data.get("workspace_id")}
).import_(
@@ -610,9 +790,9 @@ def insert_template_workflow(self, instance: Dict):
)
try:
download_callback_url = work_flow_template.get("downloadCallbackUrl", "")
- if not download_callback_url.startswith("https://apps.fit2cloud.com"):
+ if not validate_trusted_url(download_callback_url, ALLOWED_CALLBACK_HOSTS):
raise AppApiException(500, _("Illegal download callback url"))
- requests.get(download_callback_url, timeout=5)
+ requests.get(download_callback_url, timeout=5, allow_redirects=False)
except Exception as e:
maxkb_logger.error(f"callback appstore tool download error: {e}")
return app
@@ -623,6 +803,7 @@ def insert_workflow(self, instance: Dict):
workspace_id = self.data.get("workspace_id")
wq = ApplicationCreateSerializer.WorkflowRequest(data=instance)
wq.is_valid(raise_exception=True)
+ validate_bound_tool_permissions(user_id, workspace_id, instance)
application_model = wq.to_application_model(user_id, workspace_id, instance)
application_model.save()
# 插入认证信息
@@ -675,7 +856,7 @@ def import_(self, instance: dict, is_import_tool, with_valid=True):
mk_instance_bytes = instance.get("file").read()
try:
mk_instance = restricted_loads(mk_instance_bytes)
- except Exception as e:
+ except Exception:
raise AppApiException(1001, _("Unsupported file format"))
application = mk_instance.application
tool_list = mk_instance.get_tool_list()
@@ -703,6 +884,20 @@ def import_(self, instance: dict, is_import_tool, with_valid=True):
if not exits_tool_id_list.__contains__(tool.get("id"))
and not exits_tool_id_list.__contains__(generate_uuid((tool.get("id") + workspace_id or "")))
]
+ # 导入包内新建的工具由导入者本人持有,无需校验;仅需校验绑定到已存在工具的引用
+ existing_bound_tool_ids = [
+ tool_id for tool_id in get_bound_tool_ids(application) if tool_id not in update_tool_map
+ ]
+ if existing_bound_tool_ids:
+ authorized_tool_ids = set(get_authorized_tool_ids(user_id, workspace_id, existing_bound_tool_ids))
+ unauthorized_tool_ids = [
+ tool_id for tool_id in existing_bound_tool_ids if tool_id not in authorized_tool_ids
+ ]
+ if unauthorized_tool_ids:
+ message = lazy_format(
+ _("No permission to use tool(s): {tool_ids}"), tool_ids=", ".join(unauthorized_tool_ids)
+ )
+ raise AppApiException(403, str(message))
application_model = self.to_application(application, workspace_id, user_id, update_tool_map, folder_id)
tool_model_list = [self.to_tool(f, workspace_id, user_id) for f in tool_list]
application_model.save()
@@ -1091,6 +1286,14 @@ def publish(self, instance, with_valid=True):
workspace_id=workspace_id,
)
self.reset_application_version(work_flow_version, application)
+ # 如果是简易应用 需要存入 knowledge_ids
+ if application.type == ApplicationTypeChoices.SIMPLE:
+ work_flow_version.knowledge_ids = [
+ str(row.target_id)
+ for row in QuerySet(ResourceMapping).filter(
+ source_id=str(application.id), source_type="APPLICATION", target_type="KNOWLEDGE"
+ )
+ ]
work_flow_version.save()
access_token = hashlib.md5(str(uuid.uuid7()).encode()).hexdigest()[8:24]
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application.id).first()
@@ -1190,6 +1393,8 @@ def edit(self, instance: Dict, with_valid=True):
if "work_flow_template" in instance:
return self.update_template_workflow(instance, application)
+ validate_bound_tool_permissions(self.data.get("user_id"), self.data.get("workspace_id"), instance)
+
if instance.get("model_id") is None or len(instance.get("model_id")) == 0:
application.model_id = None
else:
@@ -1221,6 +1426,7 @@ def edit(self, instance: Dict, with_valid=True):
ToolExecutor().validate_mcp_transport(json.dumps(instance.get("mcp_servers")))
update_keys = [
"name",
+ "is_portal",
"desc",
"model_id",
"multiple_rounds_dialogue",
@@ -1304,13 +1510,13 @@ def update_template_workflow(self, instance: Dict, app: Application):
self.is_valid(raise_exception=True)
work_flow_template = instance.get("work_flow_template")
download_url = work_flow_template.get("downloadUrl")
- if not download_url.startswith("https://apps-assets.fit2cloud.com/"):
+ if not validate_trusted_url(download_url, ALLOWED_DOWNLOAD_HOSTS):
raise AppApiException(500, _("Illegal download url"))
# 查找匹配的版本名称
- res = requests.get(download_url, timeout=5)
+ res = requests.get(download_url, timeout=5, allow_redirects=False)
try:
mk_instance = restricted_loads(res.content)
- except Exception as e:
+ except Exception:
raise AppApiException(1001, _("Unsupported file format"))
application = mk_instance.application
tool_list = mk_instance.get_tool_list()
@@ -1363,9 +1569,9 @@ def update_template_workflow(self, instance: Dict, app: Application):
).auth_resource_batch([t.id for t in tool_model_list])
try:
download_callback_url = work_flow_template.get("downloadCallbackUrl", "")
- if not download_callback_url.startswith("https://apps.fit2cloud.com"):
+ if not validate_trusted_url(download_callback_url, ALLOWED_CALLBACK_HOSTS):
raise AppApiException(500, _("Illegal download callback url"))
- requests.get(download_callback_url, timeout=5)
+ requests.get(download_callback_url, timeout=5, allow_redirects=False)
except Exception as e:
maxkb_logger.error(f"callback appstore tool download error: {e}")
@@ -1408,6 +1614,7 @@ def one(self, with_valid=True):
knowledge_id_list = [k.get("id") for k in knowledge_list]
else:
self.update_knowledge_node(application.work_flow, available_knowledge_dict)
+ update_form_knowledge_fields(application.work_flow)
return {
**ApplicationSerializerModel(application).data,
@@ -1600,6 +1807,9 @@ def batch_delete(self, instance: Dict, with_valid=True):
self.is_valid(raise_exception=True)
id_list = instance.get("id_list")
workspace_id = self.data.get("workspace_id")
+ id_list = list(
+ QuerySet(Application).filter(id__in=id_list, workspace_id=workspace_id).values_list("id", flat=True)
+ )
QuerySet(ApplicationVersion).filter(application_id__in=id_list).delete()
QuerySet(ResourceMapping).filter(Q(target_id__in=id_list) | Q(source_id__in=id_list)).delete()
@@ -1657,9 +1867,7 @@ def batch_clean_time(self, instance: Dict, with_valid=True):
class BatchCleanTimeSerializer(BatchSerializer):
clean_time = serializers.IntegerField(required=True, min_value=1, max_value=100000, label=_("Clean time"))
- file_clean_time = serializers.IntegerField(
- required=True, min_value=1, max_value=100000, label=_("File clean time")
- )
+ file_clean_time = serializers.IntegerField(required=True, min_value=1, max_value=100000, label=_("File clean time"))
def is_valid(self, *, model=None, raise_exception=False):
super().is_valid(model=model, raise_exception=True)
diff --git a/apps/application/serializers/application_chat.py b/apps/application/serializers/application_chat.py
index dd9464a9ba5..0236b178eb5 100644
--- a/apps/application/serializers/application_chat.py
+++ b/apps/application/serializers/application_chat.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application_chat.py
- @date:2025/6/10 11:06
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application_chat.py
+@date:2025/6/10 11:06
+@desc:
"""
+
import datetime
import os
import re
@@ -45,80 +46,90 @@ class ApplicationChatResponseSerializers(serializers.Serializer):
class ApplicationChatRecordExportRequest(serializers.Serializer):
- select_ids = serializers.ListField(required=True, label=_("Chat ID List"),
- child=serializers.UUIDField(required=True, label=_("Chat ID")))
+ select_ids = serializers.ListField(
+ required=True, label=_("Chat ID List"), child=serializers.UUIDField(required=True, label=_("Chat ID"))
+ )
class ApplicationChatQuerySerializers(serializers.Serializer):
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
abstract = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("summary"))
username = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("username"))
- start_time = serializers.DateField(format='%Y-%m-%d', label=_("Start time"))
- end_time = serializers.DateField(format='%Y-%m-%d', label=_("End time"))
+ start_time = serializers.DateField(format="%Y-%m-%d", label=_("Start time"))
+ end_time = serializers.DateField(format="%Y-%m-%d", label=_("End time"))
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
- min_star = serializers.IntegerField(required=False, min_value=0,
- label=_("Minimum number of likes"))
- min_trample = serializers.IntegerField(required=False, min_value=0,
- label=_("Minimum number of clicks"))
- comparer = serializers.CharField(required=False, label=_("Comparator"), validators=[
- validators.RegexValidator(regex=re.compile("^and|or$"),
- message=_("Only supports and|or"), code=500)
- ])
+ min_star = serializers.IntegerField(required=False, min_value=0, label=_("Minimum number of likes"))
+ min_trample = serializers.IntegerField(required=False, min_value=0, label=_("Minimum number of clicks"))
+ comparer = serializers.CharField(
+ required=False,
+ label=_("Comparator"),
+ validators=[
+ validators.RegexValidator(regex=re.compile("^and|or$"), message=_("Only supports and|or"), code=500)
+ ],
+ )
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
+ raise AppApiException(500, _("Application id does not exist"))
def get_end_time(self):
- d = datetime.datetime.strptime(self.data.get('end_time'), '%Y-%m-%d').date()
+ d = datetime.datetime.strptime(self.data.get("end_time"), "%Y-%m-%d").date()
naive = datetime.datetime.combine(d, datetime.time.max)
return timezone.make_aware(naive, timezone.get_default_timezone())
def get_start_time(self):
- d = datetime.datetime.strptime(self.data.get('start_time'), '%Y-%m-%d').date()
+ d = datetime.datetime.strptime(self.data.get("start_time"), "%Y-%m-%d").date()
naive = datetime.datetime.combine(d, datetime.time.min)
return timezone.make_aware(naive, timezone.get_default_timezone())
def get_query_set(self, select_ids=None):
end_time = self.get_end_time()
start_time = self.get_start_time()
- query_set = QuerySet(model=get_dynamics_model(
- {'application_chat.application_id': models.CharField(),
- 'application_chat.abstract': models.CharField(),
- 'application_chat.asker': models.JSONField(),
- "star_num": models.IntegerField(),
- 'trample_num': models.IntegerField(),
- 'comparer': models.CharField(),
- 'application_chat.update_time': models.DateTimeField(),
- 'application_chat.id': models.UUIDField(),
- 'application_chat_record_temp.id': models.UUIDField()}))
-
- base_query_dict = {'application_chat.application_id': self.data.get("application_id"),
- 'application_chat.update_time__gte': start_time,
- 'application_chat.update_time__lte': end_time,
- }
- if 'abstract' in self.data and self.data.get('abstract') is not None:
- base_query_dict['application_chat.abstract__icontains'] = self.data.get('abstract')
- if 'username' in self.data and self.data.get('username') is not None:
- base_query_dict['application_chat.asker__username__icontains'] = self.data.get('username')
-
+ query_set = QuerySet(
+ model=get_dynamics_model(
+ {
+ "application_chat.application_id": models.CharField(),
+ "application_chat.abstract": models.CharField(),
+ "application_chat.asker": models.JSONField(),
+ "star_num": models.IntegerField(),
+ "trample_num": models.IntegerField(),
+ "comparer": models.CharField(),
+ "application_chat.update_time": models.DateTimeField(),
+ "application_chat.id": models.UUIDField(),
+ "application_chat_record_temp.id": models.UUIDField(),
+ }
+ )
+ )
+
+ base_query_dict = {
+ "application_chat.application_id": self.data.get("application_id"),
+ "application_chat.update_time__gte": start_time,
+ "application_chat.update_time__lte": end_time,
+ }
+ if "abstract" in self.data and self.data.get("abstract") is not None:
+ base_query_dict["application_chat.abstract__icontains"] = self.data.get("abstract")
if select_ids is not None and len(select_ids) > 0:
- base_query_dict['application_chat.id__in'] = select_ids
+ base_query_dict["application_chat.id__in"] = select_ids
base_condition = Q(**base_query_dict)
+ if "username" in self.data and self.data.get("username") is not None:
+ username = self.data.get("username")
+ base_condition = base_condition & (
+ Q(**{"application_chat.asker__username__icontains": username})
+ | Q(**{"application_chat.asker__nick_name__icontains": username})
+ )
min_star_query = None
min_trample_query = None
- if 'min_star' in self.data and self.data.get('min_star') is not None:
- min_star_query = Q(star_num__gte=self.data.get('min_star'))
- if 'min_trample' in self.data and self.data.get('min_trample') is not None:
- min_trample_query = Q(trample_num__gte=self.data.get('min_trample'))
+ if "min_star" in self.data and self.data.get("min_star") is not None:
+ min_star_query = Q(star_num__gte=self.data.get("min_star"))
+ if "min_trample" in self.data and self.data.get("min_trample") is not None:
+ min_trample_query = Q(trample_num__gte=self.data.get("min_trample"))
if min_star_query is not None and min_trample_query is not None:
- if self.data.get(
- 'comparer') is not None and self.data.get('comparer') == 'or':
+ if self.data.get("comparer") is not None and self.data.get("comparer") == "or":
condition = base_condition & (min_star_query | min_trample_query)
else:
condition = base_condition & (min_star_query & min_trample_query)
@@ -129,71 +140,113 @@ def get_query_set(self, select_ids=None):
else:
condition = base_condition
- return {
- 'default_queryset': query_set.filter(condition).order_by("-application_chat.update_time")
- }
+ return {"default_queryset": query_set.filter(condition).order_by("-application_chat.update_time")}
def list(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- return native_search(self.get_query_set(), select_string=get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql',
- ('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__(
- edition) else 'list_application_chat.sql'))),
- with_table_name=False)
+ return native_search(
+ self.get_query_set(),
+ select_string=get_file_content(
+ os.path.join(
+ PROJECT_DIR,
+ "apps",
+ "application",
+ "sql",
+ (
+ "list_application_chat_ee.sql"
+ if ["PE", "EE"].__contains__(edition)
+ else "list_application_chat.sql"
+ ),
+ )
+ ),
+ with_table_name=False,
+ )
@staticmethod
def paragraph_list_to_string(paragraph_list):
return "\n**********\n".join(
- [f"{paragraph.get('title')}:\n{paragraph.get('content')}" for paragraph in
- paragraph_list] if paragraph_list is not None else '')
+ [f"{paragraph.get('title')}:\n{paragraph.get('content')}" for paragraph in paragraph_list]
+ if paragraph_list is not None
+ else ""
+ )
@staticmethod
def to_row(row: Dict):
- details = row.get('details') or {}
- padding_problem_text = ' '.join((node.get("answer", "") or "") for key, node in details.items() if
- node.get("type") == 'question-node')
- search_dataset_node_list = [(key, node) for key, node in details.items() if
- node.get("type") == 'search-dataset-node' or node.get(
- "step_type") == 'search_step' or node.get("type") == 'search-knowledge-node']
- reference_paragraph_len = '\n'.join([str(len(node.get('paragraph_list',
- []))) if key == 'search_step' else node.get(
- 'name') + ':' + str(
- len(node.get('paragraph_list', [])) if node.get('paragraph_list', []) is not None else '0') for
- key, node in search_dataset_node_list])
- reference_paragraph = '\n----------\n'.join(
- [ApplicationChatQuerySerializers.paragraph_list_to_string(node.get('paragraph_list',
- [])) if key == 'search_step' else node.get(
- 'name') + ':\n' + ApplicationChatQuerySerializers.paragraph_list_to_string(node.get('paragraph_list',
- [])) for
- key, node in search_dataset_node_list])
- improve_paragraph_list = row.get('improve_paragraph_list') or []
- vote_status_map = {'-1': '未投票', '0': '赞同', '1': '反对'}
- vote_reason_map = {'accurate': gettext('accurate'), 'complete': gettext('complete'),
- 'inaccurate': gettext('inaccurate'), 'incomplete': gettext('incomplete'),
- 'other': gettext('Other'), }
- return [str(row.get('chat_id')), row.get('abstract'), row.get('problem_text'), padding_problem_text,
- row.get('answer_text'), vote_status_map.get(row.get('vote_status')),
- vote_reason_map.get(row.get('vote_reason')),
- row.get('vote_other_content'),
- reference_paragraph_len,
- reference_paragraph,
- "\n".join([
+ details = row.get("details") or {}
+ padding_problem_text = " ".join(
+ (node.get("answer", "") or "") for key, node in details.items() if node.get("type") == "question-node"
+ )
+ search_dataset_node_list = [
+ (key, node)
+ for key, node in details.items()
+ if node.get("type") == "search-dataset-node"
+ or node.get("step_type") == "search_step"
+ or node.get("type") == "search-knowledge-node"
+ ]
+ reference_paragraph_len = "\n".join(
+ [
+ str(len(node.get("paragraph_list", [])))
+ if key == "search_step"
+ else node.get("name")
+ + ":"
+ + str(len(node.get("paragraph_list", [])) if node.get("paragraph_list", []) is not None else "0")
+ for key, node in search_dataset_node_list
+ ]
+ )
+ reference_paragraph = "\n----------\n".join(
+ [
+ ApplicationChatQuerySerializers.paragraph_list_to_string(node.get("paragraph_list", []))
+ if key == "search_step"
+ else node.get("name")
+ + ":\n"
+ + ApplicationChatQuerySerializers.paragraph_list_to_string(node.get("paragraph_list", []))
+ for key, node in search_dataset_node_list
+ ]
+ )
+ improve_paragraph_list = row.get("improve_paragraph_list") or []
+ vote_status_map = {"-1": "未投票", "0": "赞同", "1": "反对"}
+ vote_reason_map = {
+ "accurate": gettext("accurate"),
+ "complete": gettext("complete"),
+ "inaccurate": gettext("inaccurate"),
+ "incomplete": gettext("incomplete"),
+ "other": gettext("Other"),
+ }
+ return [
+ str(row.get("chat_id")),
+ row.get("abstract"),
+ row.get("problem_text"),
+ padding_problem_text,
+ row.get("answer_text"),
+ vote_status_map.get(row.get("vote_status")),
+ vote_reason_map.get(row.get("vote_reason")),
+ row.get("vote_other_content"),
+ reference_paragraph_len,
+ reference_paragraph,
+ "\n".join(
+ [
f"{improve_paragraph_list[index].get('title')}\n{improve_paragraph_list[index].get('content')}"
- for index in range(len(improve_paragraph_list))]),
- row.get('asker').get('username'),
- (row.get('message_tokens') or 0) + (row.get('answer_tokens') or 0),
- row.get('ip_address') or '-',
- get_source_display(row.get('source')),
- row.get('run_time'),
- str(row.get('create_time').astimezone(pytz.timezone(TIME_ZONE)).strftime('%Y-%m-%d %H:%M:%S')
- if row.get('create_time') is not None else None)]
+ for index in range(len(improve_paragraph_list))
+ ]
+ ),
+ row.get("asker").get("username"),
+ (row.get("message_tokens") or 0) + (row.get("answer_tokens") or 0),
+ row.get("ip_address") or "-",
+ get_source_display(row.get("source")),
+ row.get("run_time"),
+ str(
+ row.get("create_time").astimezone(pytz.timezone(TIME_ZONE)).strftime("%Y-%m-%d %H:%M:%S")
+ if row.get("create_time") is not None
+ else None
+ ),
+ ]
@staticmethod
def reset_value(value):
if isinstance(value, str):
- value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
- if value.startswith(('=', '+', '-', '@')):
+ value = re.sub(ILLEGAL_CHARACTERS_RE, "", value)
+ if value.startswith(("=", "+", "-", "@")):
value = "'" + value
if isinstance(value, datetime.datetime):
eastern = pytz.timezone(TIME_ZONE)
@@ -208,31 +261,50 @@ def export(self, data, with_valid=True):
def stream_response():
workbook = openpyxl.Workbook(write_only=True)
- worksheet = workbook.create_sheet(title='Sheet1')
+ worksheet = workbook.create_sheet(title="Sheet1")
current_page = 1
page_size = 500
- headers = [gettext('Conversation ID'), gettext('summary'), gettext('User Questions'),
- gettext('Problem after optimization'),
- gettext('answer'), gettext('User feedback'), gettext('Feedback reason'),
- gettext('Other reason content'),
- gettext('Reference segment number'),
- gettext('Section title + content'),
- gettext('Annotation'), gettext('User'), gettext('Consuming tokens'),
- gettext('Ip Address'), gettext('source'),
- gettext('Time consumed (s)'),
- gettext('Question Time')]
+ headers = [
+ gettext("Conversation ID"),
+ gettext("summary"),
+ gettext("User Questions"),
+ gettext("Problem after optimization"),
+ gettext("answer"),
+ gettext("User feedback"),
+ gettext("Feedback reason"),
+ gettext("Other reason content"),
+ gettext("Reference segment number"),
+ gettext("Section title + content"),
+ gettext("Annotation"),
+ gettext("User"),
+ gettext("Consuming tokens"),
+ gettext("Ip Address"),
+ gettext("source"),
+ gettext("Time consumed (s)"),
+ gettext("Question Time"),
+ ]
worksheet.append(headers)
- for data_list in native_page_handler(page_size, self.get_query_set(data.get('select_ids')),
- primary_key='application_chat_record_temp.id',
- primary_queryset='default_queryset',
- get_primary_value=lambda item: item.get('id'),
- select_string=get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql',
- ('export_application_chat_ee.sql' if ['PE',
- 'EE'].__contains__(
- edition) else 'export_application_chat.sql'))),
- with_table_name=False):
-
+ for data_list in native_page_handler(
+ page_size,
+ self.get_query_set(data.get("select_ids")),
+ primary_key="application_chat_record_temp.id",
+ primary_queryset="default_queryset",
+ get_primary_value=lambda item: item.get("id"),
+ select_string=get_file_content(
+ os.path.join(
+ PROJECT_DIR,
+ "apps",
+ "application",
+ "sql",
+ (
+ "export_application_chat_ee.sql"
+ if ["PE", "EE"].__contains__(edition)
+ else "export_application_chat.sql"
+ ),
+ )
+ ),
+ with_table_name=False,
+ ):
for item in data_list:
row = [self.reset_value(v) for v in self.to_row(item)]
worksheet.append(row)
@@ -244,56 +316,74 @@ def stream_response():
output.close()
workbook.close()
- response = StreamingHttpResponse(stream_response(),
- content_type='application/vnd.open.xmlformats-officedocument.spreadsheetml.sheet')
- response['Content-Disposition'] = 'attachment; filename="data.xlsx"'
+ response = StreamingHttpResponse(
+ stream_response(), content_type="application/vnd.open.xmlformats-officedocument.spreadsheetml.sheet"
+ )
+ response["Content-Disposition"] = 'attachment; filename="data.xlsx"'
return response
def page(self, current_page: int, page_size: int, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- return native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql',
- ('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__(
- edition) else 'list_application_chat.sql'))),
- with_table_name=False)
+ return native_page_search(
+ current_page,
+ page_size,
+ self.get_query_set(),
+ select_string=get_file_content(
+ os.path.join(
+ PROJECT_DIR,
+ "apps",
+ "application",
+ "sql",
+ (
+ "list_application_chat_ee.sql"
+ if ["PE", "EE"].__contains__(edition)
+ else "list_application_chat.sql"
+ ),
+ )
+ ),
+ with_table_name=False,
+ )
class ChatCountSerializer(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
def get_query_set(self):
- return QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id'))
+ return QuerySet(ChatRecord).filter(chat_id=self.data.get("chat_id"))
def update_chat(self):
self.is_valid(raise_exception=True)
- count_chat_record = native_search(self.get_query_set(), get_file_content(
- os.path.join(PROJECT_DIR, "apps", "application", 'sql', 'count_chat_record.sql')), with_search_one=True)
- QuerySet(Chat).filter(id=self.data.get('chat_id')).update(star_num=count_chat_record.get('star_num', 0) or 0,
- trample_num=count_chat_record.get('trample_num',
- 0) or 0,
- chat_record_count=count_chat_record.get(
- 'chat_record_count', 0) or 0,
- mark_sum=count_chat_record.get('mark_sum', 0) or 0)
+ count_chat_record = native_search(
+ self.get_query_set(),
+ get_file_content(os.path.join(PROJECT_DIR, "apps", "application", "sql", "count_chat_record.sql")),
+ with_search_one=True,
+ )
+ QuerySet(Chat).filter(id=self.data.get("chat_id")).update(
+ star_num=count_chat_record.get("star_num", 0) or 0,
+ trample_num=count_chat_record.get("trample_num", 0) or 0,
+ chat_record_count=count_chat_record.get("chat_record_count", 0) or 0,
+ mark_sum=count_chat_record.get("mark_sum", 0) or 0,
+ )
return True
def get_source_display(source):
- if not source or not isinstance(source, dict) or 'type' not in source:
- return '-'
- source_type = source.get('type')
+ if not source or not isinstance(source, dict) or "type" not in source:
+ return "-"
+ source_type = source.get("type")
# 定义映射关系
source_mapping = {
- ChatSourceChoices.ONLINE.value: gettext('Online Usage'),
- ChatSourceChoices.API_CALL.value: gettext('API Call'),
- ChatSourceChoices.ENTERPRISE_WECHAT.value: gettext('Enterprise WeChat'),
- ChatSourceChoices.WECHAT_PUBLIC_ACCOUNT.value: gettext('WeChat Public Account'),
- ChatSourceChoices.LARK.value: gettext('Lark'),
- ChatSourceChoices.DINGTALK.value: gettext('DingTalk'),
- ChatSourceChoices.ENTERPRISE_WECHAT_ROBOT.value: gettext('Enterprise WeChat Robot'),
- ChatSourceChoices.TRIGGER.value: gettext('Trigger'),
- ChatSourceChoices.SLACK.value: gettext('Slack'),
+ ChatSourceChoices.ONLINE.value: gettext("Online Usage"),
+ ChatSourceChoices.API_CALL.value: gettext("API Call"),
+ ChatSourceChoices.ENTERPRISE_WECHAT.value: gettext("Enterprise WeChat"),
+ ChatSourceChoices.WECHAT_PUBLIC_ACCOUNT.value: gettext("WeChat Public Account"),
+ ChatSourceChoices.LARK.value: gettext("Lark"),
+ ChatSourceChoices.DINGTALK.value: gettext("DingTalk"),
+ ChatSourceChoices.ENTERPRISE_WECHAT_ROBOT.value: gettext("Enterprise WeChat Robot"),
+ ChatSourceChoices.TRIGGER.value: gettext("Trigger"),
+ ChatSourceChoices.SLACK.value: gettext("Slack"),
}
return source_mapping.get(source_type, str(source_type))
diff --git a/apps/application/serializers/application_chat_link.py b/apps/application/serializers/application_chat_link.py
index a283d264f9b..f295d823406 100644
--- a/apps/application/serializers/application_chat_link.py
+++ b/apps/application/serializers/application_chat_link.py
@@ -5,18 +5,21 @@
@date: 2026/2/9 10:50
@desc:
"""
+import re
+
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.models import Chat, ChatShareLink, ShareLinkType, ChatRecord
from common.exception.app_exception import AppApiException
from common.utils.chat_link_code import UUIDEncoder
+from knowledge.models import PublicFileAccess
import uuid_utils.compat as uuid
class ShareChatRecordModelSerializer(serializers.ModelSerializer):
-
execution_details = serializers.SerializerMethodField()
+
class Meta:
model = ChatRecord
fields = ['id', 'problem_text', 'answer_text', 'answer_text_list',
@@ -38,6 +41,7 @@ def get_execution_details(chat_record):
for v in details.values() if v.get('type') == 'start-node'
]
+
class ChatRecordShareLinkRequestSerializer(serializers.Serializer):
chat_record_ids = serializers.ListSerializer(
child=serializers.UUIDField(),
@@ -52,6 +56,53 @@ def validate(self, attrs):
raise serializers.ValidationError(_('Chat record ids can not be empty'))
return attrs
+
+def extract_oss_file_urls(answer_text_list):
+ """从 answer_text_list 中提取所有 ./oss/file/ 开头的链接"""
+ file_urls = []
+ for answer_group in answer_text_list:
+ if not isinstance(answer_group, list):
+ answer_group = [answer_group]
+ for item in answer_group:
+ content = item.get('content', '')
+ urls = re.findall(r'\./oss/file/[\w-]+', content)
+ file_urls.extend(urls)
+ return file_urls
+
+
+def save_public_file_access(chat_record_list):
+ """提取聊天记录中的所有文件ID并入库 PublicFileAccess"""
+ file_ids = set()
+ for chat_record in chat_record_list:
+ urls = extract_oss_file_urls(chat_record.answer_text_list)
+ for url in urls:
+ file_id = url.replace('./oss/file/', '')
+ if file_id:
+ file_ids.add(file_id)
+
+ if not file_ids:
+ return
+
+ existing = set(
+ PublicFileAccess.objects.filter(
+ source_type='FILE',
+ source_id__in=list(file_ids)
+ ).values_list('source_id', flat=True)
+ )
+
+ new_records = [
+ PublicFileAccess(
+ id=uuid.uuid7(),
+ source_type='FILE',
+ source_id=file_id
+ )
+ for file_id in file_ids if file_id not in existing
+ ]
+
+ if new_records:
+ PublicFileAccess.objects.bulk_create(new_records)
+
+
class ChatRecordShareLinkSerializer(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
@@ -61,8 +112,11 @@ def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
chat_id = self.data.get('chat_id')
application_id = self.data.get('application_id')
+ user_id = self.data.get('user_id')
- chat_query_set = Chat.objects.filter(id=chat_id, application_id=application_id, is_deleted=False)
+ chat_query_set = Chat.objects.filter(
+ id=chat_id, application_id=application_id, chat_user_id=user_id, is_deleted=False
+ )
if not chat_query_set.exists():
raise AppApiException(500, _('Chat id does not exist'))
@@ -74,7 +128,8 @@ def generate_link(self, instance, with_valid=True):
if not instance.get('is_current_all', False):
chat_record_ids: list[str] = instance.get('chat_record_ids')
- record_count = ChatRecord.objects.filter(id__in=chat_record_ids, chat_id=self.data.get('chat_id')).count()
+ record_count = ChatRecord.objects.filter(id__in=chat_record_ids,
+ chat_id=self.data.get('chat_id')).count()
if record_count != len(chat_record_ids):
raise AppApiException(500, _('Invalid chat record ids'))
chat_id = self.data.get('chat_id')
@@ -99,7 +154,8 @@ def generate_link(self, instance, with_valid=True):
if existing:
return {'link': UUIDEncoder.encode(existing.id)}
-
+ chat_record_list = ChatRecord.objects.filter(id__in=sorted_ids)
+ save_public_file_access(chat_record_list)
chat_share_link_model = ChatShareLink(
id=uuid.uuid7(),
chat_id=chat_id,
diff --git a/apps/application/serializers/application_chat_record.py b/apps/application/serializers/application_chat_record.py
index b2f21f72008..c91c564f228 100644
--- a/apps/application/serializers/application_chat_record.py
+++ b/apps/application/serializers/application_chat_record.py
@@ -1,44 +1,65 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application_chat_record.py
- @date:2025/6/10 15:10
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application_chat_record.py
+@date:2025/6/10 15:10
+@desc:
"""
+
from functools import reduce
from typing import Dict
import uuid_utils.compat as uuid
-from django.db import transaction
-from django.db.models import QuerySet
-from django.db.models.aggregates import Max, Min
-from django.utils.translation import gettext_lazy as _, gettext
-from rest_framework import serializers
-from rest_framework.utils.formatting import lazy_format
-
-from application.models import ChatRecord, ApplicationAccessToken, Application
+from application.models import Application, ApplicationAccessToken, ChatRecord, Chat
from application.serializers.application_chat import ChatCountSerializer
from application.serializers.common import ChatInfo
from common.auth.authentication import get_is_permissions
from common.chunk import text_to_chunk
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
from common.db.search import page_search
from common.exception.app_exception import AppApiException, AppUnauthorizedFailed
from common.utils.common import post
-from knowledge.models import Paragraph, Document, Problem, ProblemParagraphMapping, Knowledge
+from django.db import transaction
+from django.db.models import QuerySet
+from django.db.models.aggregates import Max, Min
+from django.utils.translation import gettext
+from django.utils.translation import gettext_lazy as _
+from knowledge.models import Document, Knowledge, Paragraph, Problem, ProblemParagraphMapping
from knowledge.serializers.common import get_embedding_model_id_by_knowledge_id, update_document_char_length
from knowledge.serializers.paragraph import ParagraphSerializers
from knowledge.task.embedding import embedding_by_paragraph, embedding_by_paragraph_list
+from rest_framework import serializers
+from rest_framework.utils.formatting import lazy_format
class ChatRecordSerializerModel(serializers.ModelSerializer):
class Meta:
model = ChatRecord
- fields = ['id', 'chat_id', 'vote_status','vote_reason','vote_other_content', 'problem_text', 'answer_text',
- 'message_tokens', 'answer_tokens', 'const', 'improve_paragraph_id_list', 'run_time', 'index',
- 'answer_text_list',
- 'create_time', 'update_time']
+ fields = [
+ "id",
+ "chat_id",
+ "vote_status",
+ "vote_reason",
+ "vote_other_content",
+ "problem_text",
+ "answer_text",
+ "message_tokens",
+ "answer_tokens",
+ "const",
+ "improve_paragraph_id_list",
+ "run_time",
+ "index",
+ "answer_text_list",
+ "create_time",
+ "update_time",
+ "version",
+ "question",
+ "messages",
+ ]
class ChatRecordOperateSerializer(serializers.Serializer):
@@ -49,42 +70,57 @@ class ChatRecordOperateSerializer(serializers.Serializer):
def is_valid(self, *, debug=False, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
- application_access_token = QuerySet(ApplicationAccessToken).filter(
- application_id=self.data.get('application_id')).first()
+ raise AppApiException(500, _("Application id does not exist"))
+ if (
+ not ChatRecord.objects.filter(
+ chat_id=self.data.get("chat_id"), chat__application_id=self.data.get("application_id")
+ ).exists()
+ and not debug
+ ):
+ raise AppApiException(500, _("Chat records for the application do not exist"))
+ application_access_token = (
+ QuerySet(ApplicationAccessToken).filter(application_id=self.data.get("application_id")).first()
+ )
if application_access_token is None:
- raise AppApiException(500, gettext('Application authentication information does not exist'))
+ raise AppApiException(500, gettext("Application authentication information does not exist"))
def get_chat_record(self):
- chat_record_id = self.data.get('chat_record_id')
- chat_id = self.data.get('chat_id')
+ chat_record_id = self.data.get("chat_record_id")
+ chat_id = self.data.get("chat_id")
chat_info: ChatInfo = ChatInfo.get_cache(chat_id)
if chat_info is not None:
- chat_record_list = [chat_record for chat_record in chat_info.chat_record_list if
- str(chat_record.id) == str(chat_record_id)]
+ chat_record_list = [
+ chat_record for chat_record in chat_info.chat_record_list if str(chat_record.id) == str(chat_record_id)
+ ]
if chat_record_list is not None and len(chat_record_list):
return chat_record_list[-1]
- return QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first()
+ return (
+ QuerySet(ChatRecord)
+ .filter(id=chat_record_id, chat_id=chat_id, chat__application_id=self.data.get("application_id"))
+ .first()
+ )
def one(self, debug):
self.is_valid(debug=debug, raise_exception=True)
chat_record = self.get_chat_record()
if chat_record is None:
raise AppApiException(500, gettext("Conversation does not exist"))
- application_access_token = QuerySet(ApplicationAccessToken).filter(
- application_id=self.data.get('application_id')).first()
+ application_access_token = (
+ QuerySet(ApplicationAccessToken).filter(application_id=self.data.get("application_id")).first()
+ )
show_source = False
show_exec = False
if application_access_token is not None:
show_exec = application_access_token.show_exec
show_source = application_access_token.show_source
return ApplicationChatRecordQuerySerializers.reset_chat_record(
- chat_record, True if debug else show_source, True if debug else show_exec)
+ chat_record, True if debug else show_source, True if debug else show_exec
+ )
class ApplicationChatRecordQuerySerializers(serializers.Serializer):
@@ -95,28 +131,34 @@ class ApplicationChatRecordQuerySerializers(serializers.Serializer):
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
+ raise AppApiException(500, _("Application id does not exist"))
+ if not Chat.objects.filter(
+ id=self.data.get("chat_id"), application_id=self.data.get("application_id")
+ ).exists():
+ raise AppApiException(500, _("Chat records for the application do not exist"))
def list(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id'))
- order_by = 'create_time' if self.data.get('order_asc') is None or self.data.get(
- 'order_asc') else '-create_time'
- return [ChatRecordSerializerModel(chat_record).data for chat_record in
- QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by)]
+ order_by = "create_time" if self.data.get("order_asc") is None or self.data.get("order_asc") else "-create_time"
+ return [
+ ChatRecordSerializerModel(chat_record).data
+ for chat_record in QuerySet(ChatRecord)
+ .filter(chat_id=self.data.get("chat_id"), chat__application_id=self.data.get("application_id"))
+ .order_by(order_by)
+ ]
@staticmethod
def get_loop_workflow_node(details):
result = []
for item in details.values():
- if item.get('type') == 'loop-node':
- for loop_item in item.get('loop_node_data') or []:
+ if item.get("type") == "loop-node":
+ for loop_item in item.get("loop_node_data") or []:
for inner_item in loop_item.values():
result.append(inner_item)
return result
@@ -125,67 +167,89 @@ def get_loop_workflow_node(details):
def reset_chat_record(chat_record, show_source, show_exec):
knowledge_list = []
paragraph_list = []
- if 'search_step' in chat_record.details and chat_record.details.get('search_step').get(
- 'paragraph_list') is not None:
- paragraph_list = chat_record.details.get('search_step').get(
- 'paragraph_list')
-
- for item in [*chat_record.details.values(),
- *ApplicationChatRecordQuerySerializers.get_loop_workflow_node(chat_record.details)]:
- if item.get('type') == 'search-knowledge-node' and item.get('show_knowledge', False):
- paragraph_list = paragraph_list + (item.get(
- 'paragraph_list') or [])
-
- if item.get('type') == 'reranker-node' and item.get('show_knowledge', False):
- paragraph_list = paragraph_list + [rl.get('metadata') for rl in (item.get('result_list') or []) if
- 'document_id' in (rl.get('metadata') or {}) and 'knowledge_id' in (
- rl.get(
- 'metadata') or {})]
- paragraph_list = list({p.get('id'): p for p in paragraph_list}.values())
- knowledge_list = knowledge_list + [{'id': knowledge_id, **knowledge} for knowledge_id, knowledge in
- reduce(lambda x, y: {**x, **y},
- [{row.get(
- 'knowledge_id'): {'knowledge_name': row.get(
- "knowledge_name"),
- 'knowledge_type': row.get('knowledge_type')}} for
- row in
- paragraph_list],
- {}).items()]
+ if (
+ "search_step" in chat_record.details
+ and chat_record.details.get("search_step").get("paragraph_list") is not None
+ ):
+ paragraph_list = chat_record.details.get("search_step").get("paragraph_list")
+
+ for item in [
+ *chat_record.details.values(),
+ *ApplicationChatRecordQuerySerializers.get_loop_workflow_node(chat_record.details),
+ ]:
+ if item.get("type") == "search-knowledge-node" and item.get("show_knowledge", False):
+ paragraph_list = paragraph_list + (item.get("paragraph_list") or [])
+
+ if item.get("type") == "reranker-node" and item.get("show_knowledge", False):
+ paragraph_list = paragraph_list + [
+ rl.get("metadata")
+ for rl in (item.get("result_list") or [])
+ if "document_id" in (rl.get("metadata") or {}) and "knowledge_id" in (rl.get("metadata") or {})
+ ]
+ paragraph_list = list({p.get("id"): p for p in paragraph_list}.values())
+ knowledge_list = knowledge_list + [
+ {"id": knowledge_id, **knowledge}
+ for knowledge_id, knowledge in reduce(
+ lambda x, y: {**x, **y},
+ [
+ {
+ row.get("knowledge_id"): {
+ "knowledge_name": row.get("knowledge_name"),
+ "knowledge_type": row.get("knowledge_type"),
+ }
+ }
+ for row in paragraph_list
+ ],
+ {},
+ ).items()
+ ]
if len(chat_record.improve_paragraph_id_list) > 0:
paragraph_model_list = QuerySet(Paragraph).filter(id__in=chat_record.improve_paragraph_id_list)
if len(paragraph_model_list) < len(chat_record.improve_paragraph_id_list):
paragraph_model_id_list = [str(p.id) for p in paragraph_model_list]
chat_record.improve_paragraph_id_list = list(
- filter(lambda p_id: paragraph_model_id_list.__contains__(p_id),
- chat_record.improve_paragraph_id_list))
+ filter(
+ lambda p_id: paragraph_model_id_list.__contains__(p_id), chat_record.improve_paragraph_id_list
+ )
+ )
chat_record.save()
- show_source_dict = {'knowledge_list': knowledge_list,
- 'paragraph_list': paragraph_list, }
- show_exec_dict = {'execution_details': [chat_record.details[key] for key in chat_record.details if
- (True if show_exec else chat_record.details[key].get(
- 'type') == 'start-node')]}
+ show_source_dict = {
+ "knowledge_list": knowledge_list,
+ "paragraph_list": paragraph_list,
+ }
+ show_exec_dict = {
+ "execution_details": [
+ chat_record.details[key]
+ for key in chat_record.details
+ if (True if show_exec else chat_record.details[key].get("type") == "start-node")
+ ]
+ }
return {
**ChatRecordSerializerModel(chat_record).data,
- 'padding_problem_text': chat_record.details.get('problem_padding').get(
- 'padding_problem_text') if 'problem_padding' in chat_record.details else None,
+ "padding_problem_text": chat_record.details.get("problem_padding").get("padding_problem_text")
+ if "problem_padding" in chat_record.details
+ else None,
**(show_source_dict if show_source else {}),
- **(show_exec_dict if show_exec else show_exec_dict)
+ **(show_exec_dict if show_exec else show_exec_dict),
}
def page(self, current_page: int, page_size: int, with_valid=True, show_source=None, show_exec=None):
if with_valid:
self.is_valid(raise_exception=True)
- order_by = '-create_time' if self.data.get('order_asc') is None or self.data.get(
- 'order_asc') else 'create_time'
+ order_by = "-create_time" if self.data.get("order_asc") is None or self.data.get("order_asc") else "create_time"
if show_source is None:
show_source = True
if show_exec is None:
show_exec = True
- page = page_search(current_page, page_size,
- QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by),
- post_records_handler=lambda chat_record: self.reset_chat_record(chat_record, show_source,
- show_exec))
+ page = page_search(
+ current_page,
+ page_size,
+ QuerySet(ChatRecord)
+ .filter(chat_id=self.data.get("chat_id"), chat__application_id=self.data.get("application_id"))
+ .order_by(order_by),
+ post_records_handler=lambda chat_record: self.reset_chat_record(chat_record, show_source, show_exec),
+ )
return page
@@ -202,26 +266,25 @@ class ChatRecordImproveSerializer(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- chat_record_id = serializers.UUIDField(required=True,
- label=_("Conversation record id"))
+ chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
+ raise AppApiException(500, _("Application id does not exist"))
def get(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- chat_record_id = self.data.get('chat_record_id')
- chat_id = self.data.get('chat_id')
+ chat_record_id = self.data.get("chat_record_id")
+ chat_id = self.data.get("chat_id")
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first()
if chat_record is None:
- raise AppApiException(500, gettext('Conversation record does not exist'))
+ raise AppApiException(500, gettext("Conversation record does not exist"))
if chat_record.improve_paragraph_id_list is None or len(chat_record.improve_paragraph_id_list) == 0:
return []
@@ -229,19 +292,21 @@ def get(self, with_valid=True):
if len(paragraph_model_list) < len(chat_record.improve_paragraph_id_list):
paragraph_model_id_list = [str(p.id) for p in paragraph_model_list]
chat_record.improve_paragraph_id_list = list(
- filter(lambda p_id: paragraph_model_id_list.__contains__(p_id),
- chat_record.improve_paragraph_id_list))
+ filter(lambda p_id: paragraph_model_id_list.__contains__(p_id), chat_record.improve_paragraph_id_list)
+ )
chat_record.save()
return [ParagraphModel(p).data for p in paragraph_model_list]
class ApplicationChatRecordImproveInstanceSerializer(serializers.Serializer):
- title = serializers.CharField(required=False, max_length=256, allow_null=True, allow_blank=True,
- label=_("Section title"))
+ title = serializers.CharField(
+ required=False, max_length=256, allow_null=True, allow_blank=True, label=_("Section title")
+ )
content = serializers.CharField(required=True, label=_("Paragraph content"))
- problem_text = serializers.CharField(required=False, max_length=256, allow_null=True, allow_blank=True,
- label=_("question"))
+ problem_text = serializers.CharField(
+ required=False, max_length=256, allow_null=True, allow_blank=True, label=_("question")
+ )
class ApplicationChatRecordAddKnowledgeSerializer(serializers.Serializer):
@@ -249,19 +314,22 @@ class ApplicationChatRecordAddKnowledgeSerializer(serializers.Serializer):
application_id = serializers.UUIDField(required=True, label=_("Application ID"))
knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id"))
document_id = serializers.UUIDField(required=True, label=_("Document id"))
- chat_ids = serializers.ListSerializer(child=serializers.UUIDField(), required=True,
- label=_("Conversation ID"))
+ chat_ids = serializers.ListSerializer(child=serializers.UUIDField(), required=True, label=_("Conversation ID"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
- if not Document.objects.filter(id=self.data['document_id'], knowledge_id=self.data['knowledge_id']).exists():
+ raise AppApiException(500, _("Application id does not exist"))
+ if not Document.objects.filter(id=self.data["document_id"], knowledge_id=self.data["knowledge_id"]).exists():
raise AppApiException(500, gettext("The document id is incorrect"))
+ if not ChatRecord.objects.filter(
+ chat_id__in=self.data["chat_ids"], chat__application_id=self.data["application_id"]
+ ).exists():
+ raise AppApiException(500, gettext("The chat id is incorrect"))
@staticmethod
def post_embedding_paragraph(paragraph_ids, knowledge_id):
@@ -270,31 +338,33 @@ def post_embedding_paragraph(paragraph_ids, knowledge_id):
@post(post_function=post_embedding_paragraph)
@transaction.atomic
- def post_improve(self, instance: Dict, request=None, scope='WORKSPACE', with_valid=True):
+ def post_improve(self, instance: Dict, request=None, scope="WORKSPACE", with_valid=True):
if with_valid:
ApplicationChatRecordAddKnowledgeSerializer(data=instance).is_valid(raise_exception=True)
self.is_valid(raise_exception=True)
- if scope == 'WORKSPACE':
- is_permission = get_is_permissions(request=request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
+ if scope == "WORKSPACE":
+ is_permission = get_is_permissions(
+ request=request, workspace_id=self.data.get("workspace_id"), knowledge_id=self.data.get("knowledge_id")
+ )(
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_knowledge_permission(),
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_permission_workspace_manage_role(),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
- CompareConstants.AND),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
+ compare=CompareConstants.AND,
+ ),
)
else:
- is_permission = get_is_permissions(request=request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
- PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN
- )
+ is_permission = get_is_permissions(
+ request=request, workspace_id=self.data.get("workspace_id"), knowledge_id=self.data.get("knowledge_id")
+ )(PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN)
if not is_permission:
- raise AppUnauthorizedFailed(403, gettext('No permission to access'))
+ raise AppUnauthorizedFailed(403, gettext("No permission to access"))
- chat_ids = instance['chat_ids']
- document_id = instance['document_id']
- knowledge_id = instance['knowledge_id']
+ chat_ids = instance["chat_ids"]
+ document_id = instance["document_id"]
+ knowledge_id = instance["knowledge_id"]
# 获取所有聊天记录
chat_record_list = list(ChatRecord.objects.filter(chat_id__in=chat_ids))
@@ -312,7 +382,7 @@ def post_improve(self, instance: Dict, request=None, scope='WORKSPACE', with_val
content=chat_record.answer_text,
knowledge_id=knowledge_id,
title=chat_record.problem_text,
- chunks=text_to_chunk(chat_record.answer_text)
+ chunks=text_to_chunk(chat_record.answer_text),
)
problem, _ = Problem.objects.get_or_create(content=chat_record.problem_text, knowledge_id=knowledge_id)
problem_paragraph_mapping = ProblemParagraphMapping(
@@ -320,7 +390,7 @@ def post_improve(self, instance: Dict, request=None, scope='WORKSPACE', with_val
knowledge_id=knowledge_id,
document_id=document_id,
problem_id=problem.id,
- paragraph_id=paragraph.id
+ paragraph_id=paragraph.id,
)
paragraphs.append(paragraph)
paragraph_ids.append(paragraph.id)
@@ -335,19 +405,17 @@ def post_improve(self, instance: Dict, request=None, scope='WORKSPACE', with_val
ProblemParagraphMapping.objects.bulk_create(problem_paragraph_mappings)
# 批量保存聊天记录
- ChatRecord.objects.bulk_update(chat_record_list, ['improve_paragraph_id_list'])
+ ChatRecord.objects.bulk_update(chat_record_list, ["improve_paragraph_id_list"])
update_document_char_length(document_id)
for chat_id in chat_ids:
- ChatCountSerializer(data={'chat_id': chat_id}).update_chat()
+ ChatCountSerializer(data={"chat_id": chat_id}).update_chat()
return paragraph_ids, knowledge_id
@staticmethod
def prepend_paragraphs(document_id, paragraphs):
# 获取所有现有段落
- existing_paragraphs = list(Paragraph.objects.filter(
- document_id=document_id
- ).order_by('position'))
+ existing_paragraphs = list(Paragraph.objects.filter(document_id=document_id).order_by("position"))
# 计算新段落数量
new_count = len(paragraphs)
@@ -360,7 +428,7 @@ def prepend_paragraphs(document_id, paragraphs):
# 批量更新现有段落位置
if existing_paragraphs:
- Paragraph.objects.bulk_update(existing_paragraphs, ['position'])
+ Paragraph.objects.bulk_update(existing_paragraphs, ["position"])
# 为新段落分配位置,从1开始
for i, paragraph in enumerate(paragraphs):
@@ -370,8 +438,7 @@ def prepend_paragraphs(document_id, paragraphs):
class ApplicationChatRecordImproveSerializer(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- chat_record_id = serializers.UUIDField(required=True,
- label=_("Conversation record id"))
+ chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id"))
knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id"))
@@ -382,21 +449,24 @@ class ApplicationChatRecordImproveSerializer(serializers.Serializer):
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Application id does not exist'))
+ raise AppApiException(500, _("Application id does not exist"))
- query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id'))
+ query_set = QuerySet(Knowledge).filter(id=self.data.get("knowledge_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, _('Knowledge id does not exist'))
+ raise AppApiException(500, _("Knowledge id does not exist"))
- if not QuerySet(Document).filter(id=self.data.get('document_id'),
- knowledge_id=self.data.get('knowledge_id')).exists():
+ if (
+ not QuerySet(Document)
+ .filter(id=self.data.get("document_id"), knowledge_id=self.data.get("knowledge_id"))
+ .exists()
+ ):
raise AppApiException(500, gettext("The document id is incorrect"))
@staticmethod
@@ -408,38 +478,41 @@ def post_embedding_paragraph(chat_record, paragraph_id, knowledge_id):
@post(post_function=post_embedding_paragraph)
@transaction.atomic
- def improve(self, instance: Dict, request=None, scope='WORKSPACE', with_valid=True):
+ def improve(self, instance: Dict, request=None, scope="WORKSPACE", with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- if scope == 'WORKSPACE':
- is_permission = get_is_permissions(request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
+ if scope == "WORKSPACE":
+ is_permission = get_is_permissions(
+ request, workspace_id=self.data.get("workspace_id"), knowledge_id=self.data.get("knowledge_id")
+ )(
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_knowledge_permission(),
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_permission_workspace_manage_role(),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
- CompareConstants.AND),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
+ compare=CompareConstants.AND,
+ ),
)
else:
- is_permission = get_is_permissions(request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
- PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN
- )
+ is_permission = get_is_permissions(
+ request, workspace_id=self.data.get("workspace_id"), knowledge_id=self.data.get("knowledge_id")
+ )(PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN)
if not is_permission:
- raise AppUnauthorizedFailed(403, gettext('No permission to access'))
+ raise AppUnauthorizedFailed(403, gettext("No permission to access"))
ApplicationChatRecordImproveInstanceSerializer(data=instance).is_valid(raise_exception=True)
- chat_record_id = self.data.get('chat_record_id')
- chat_id = self.data.get('chat_id')
+ chat_record_id = self.data.get("chat_record_id")
+ chat_id = self.data.get("chat_id")
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first()
if chat_record is None:
- raise AppApiException(500, gettext('Conversation record does not exist'))
+ raise AppApiException(500, gettext("Conversation record does not exist"))
document_id = self.data.get("document_id")
knowledge_id = self.data.get("knowledge_id")
- max_position = Paragraph.objects.filter(document_id=document_id).aggregate(
- max_position=Max('position')
- )['max_position'] or 0
+ max_position = (
+ Paragraph.objects.filter(document_id=document_id).aggregate(max_position=Max("position"))["max_position"]
+ or 0
+ )
paragraph = Paragraph(
id=uuid.uuid7(),
document_id=document_id,
@@ -449,13 +522,17 @@ def improve(self, instance: Dict, request=None, scope='WORKSPACE', with_valid=Tr
position=max_position + 1,
chunks=text_to_chunk(instance.get("content", "")),
)
- problem_text = instance.get('problem_text') if instance.get(
- 'problem_text') is not None else chat_record.problem_text
+ problem_text = (
+ instance.get("problem_text") if instance.get("problem_text") is not None else chat_record.problem_text
+ )
problem, _ = QuerySet(Problem).get_or_create(content=problem_text, knowledge_id=knowledge_id)
- problem_paragraph_mapping = ProblemParagraphMapping(id=uuid.uuid7(), knowledge_id=knowledge_id,
- document_id=document_id,
- problem_id=problem.id,
- paragraph_id=paragraph.id)
+ problem_paragraph_mapping = ProblemParagraphMapping(
+ id=uuid.uuid7(),
+ knowledge_id=knowledge_id,
+ document_id=document_id,
+ problem_id=problem.id,
+ paragraph_id=paragraph.id,
+ )
# 插入段落
paragraph.save()
# 插入关联问题
@@ -464,14 +541,13 @@ def improve(self, instance: Dict, request=None, scope='WORKSPACE', with_valid=Tr
update_document_char_length(document_id)
# 添加标注
chat_record.save()
- ChatCountSerializer(data={'chat_id': chat_id}).update_chat()
+ ChatCountSerializer(data={"chat_id": chat_id}).update_chat()
return ChatRecordSerializerModel(chat_record).data, paragraph.id, knowledge_id
class Operate(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- chat_record_id = serializers.UUIDField(required=True,
- label=_("Conversation record id"))
+ chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id"))
knowledge_id = serializers.UUIDField(required=True, label=_("Knowledge base id"))
@@ -481,49 +557,63 @@ class Operate(serializers.Serializer):
workspace_id = serializers.CharField(required=True, label=_("Workspace ID"))
- def delete(self, request=None, scope='WORKSPACE', with_valid=True):
+ def delete(self, request=None, scope="WORKSPACE", with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- if scope == 'WORKSPACE':
- is_permission = get_is_permissions(request=request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
+ if scope == "WORKSPACE":
+ is_permission = get_is_permissions(
+ request=request,
+ workspace_id=self.data.get("workspace_id"),
+ knowledge_id=self.data.get("knowledge_id"),
+ )(
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_knowledge_permission(),
PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_permission_workspace_manage_role(),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
- CompareConstants.AND),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()],
+ compare=CompareConstants.AND,
+ ),
)
else:
- is_permission = get_is_permissions(request=request, workspace_id=self.data.get('workspace_id'),
- knowledge_id=self.data.get("knowledge_id"))(
- PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN
- )
+ is_permission = get_is_permissions(
+ request=request,
+ workspace_id=self.data.get("workspace_id"),
+ knowledge_id=self.data.get("knowledge_id"),
+ )(PermissionConstants.RESOURCE_KNOWLEDGE_DOCUMENT_EDIT, RoleConstants.ADMIN)
if not is_permission:
- raise AppUnauthorizedFailed(403, gettext('No permission to access'))
-
- workspace_id = self.data.get('workspace_id')
- chat_record_id = self.data.get('chat_record_id')
- chat_id = self.data.get('chat_id')
- knowledge_id = self.data.get('knowledge_id')
- document_id = self.data.get('document_id')
- paragraph_id = self.data.get('paragraph_id')
+ raise AppUnauthorizedFailed(403, gettext("No permission to access"))
+
+ workspace_id = self.data.get("workspace_id")
+ chat_record_id = self.data.get("chat_record_id")
+ chat_id = self.data.get("chat_id")
+ knowledge_id = self.data.get("knowledge_id")
+ document_id = self.data.get("document_id")
+ paragraph_id = self.data.get("paragraph_id")
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_id).first()
if chat_record is None:
- raise AppApiException(500, gettext('Conversation record does not exist'))
+ raise AppApiException(500, gettext("Conversation record does not exist"))
if not chat_record.improve_paragraph_id_list.__contains__(uuid.UUID(paragraph_id)):
message = lazy_format(
gettext(
- 'The paragraph id is wrong. The current conversation record does not exist. [{paragraph_id}] paragraph id'),
- paragraph_id=paragraph_id)
+ "The paragraph id is wrong. The current conversation record does not exist. [{paragraph_id}] paragraph id"
+ ),
+ paragraph_id=paragraph_id,
+ )
raise AppApiException(500, message.__str__())
- chat_record.improve_paragraph_id_list = [row for row in chat_record.improve_paragraph_id_list if
- str(row) != paragraph_id]
+ chat_record.improve_paragraph_id_list = [
+ row for row in chat_record.improve_paragraph_id_list if str(row) != paragraph_id
+ ]
chat_record.save()
o = ParagraphSerializers.Operate(
- data={"workspace_id": workspace_id, "knowledge_id": knowledge_id, 'document_id': document_id,
- "paragraph_id": paragraph_id})
+ data={
+ "workspace_id": workspace_id,
+ "knowledge_id": knowledge_id,
+ "document_id": document_id,
+ "paragraph_id": paragraph_id,
+ }
+ )
o.is_valid(raise_exception=True)
o.delete()
return True
diff --git a/apps/application/serializers/common.py b/apps/application/serializers/common.py
index b41ce79e470..a19b70760c4 100644
--- a/apps/application/serializers/common.py
+++ b/apps/application/serializers/common.py
@@ -1,19 +1,35 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: common.py
- @date:2025/6/9 13:42
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: common.py
+@date:2025/6/9 13:42
+@desc:
"""
+
from typing import List
+from application.models import Application, ApplicationTypeChoices, ApplicationVersion, Chat, ChatRecord, ChatUserType
+from application.serializers.application_chat import ChatCountSerializer
+from common.constants.cache_version import Cache_Version
+from common.database_model_manage.database_model_manage import DatabaseModelManage
+from common.exception.app_exception import ChatException
from django.core.cache import cache
from django.db.models import QuerySet
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
-from application.models import Application, ChatRecord, Chat, ApplicationVersion, ChatUserType, ApplicationTypeChoices
+from system_manage.models.chat_user_token_quota import ChatUserTokenQuota
+
+from application.models import (
+ Application,
+ ChatRecord,
+ Chat,
+ ApplicationVersion,
+ ChatUserType,
+ ApplicationTypeChoices,
+ ExecuteType,
+)
from application.serializers.application_chat import ChatCountSerializer
from common.constants.cache_version import Cache_Version
from common.database_model_manage.database_model_manage import DatabaseModelManage
@@ -26,12 +42,7 @@
class ToolExecute:
- def __init__(self, tool_id: str,
- tool_record_id: str,
- workspace_id: str,
- source_type,
- source_id,
- debug=False):
+ def __init__(self, tool_id: str, tool_record_id: str, workspace_id: str, source_type, source_id, debug=False):
self.tool_id = tool_id
self.workspace_id = workspace_id
self.source_type = source_type
@@ -42,8 +53,12 @@ def __init__(self, tool_id: str,
def get_record(self):
if self.tool_record_id:
if self.debug:
- return self.to_record(cache.get(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id),
- version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version()))
+ return self.to_record(
+ cache.get(
+ Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id),
+ version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version(),
+ )
+ )
else:
return QuerySet(ToolRecord).filter(tool_id=self.tool_id, id=self.tool_record_id).first()
return None
@@ -51,61 +66,74 @@ def get_record(self):
def to_record(self, tool_record_dict):
if tool_record_dict is None:
return None
- return ToolRecord(id=tool_record_dict.get('id'),
- tool_id=tool_record_dict.get('tool_id'),
- workspace_id=tool_record_dict.get('workspace_id'),
- source_type=tool_record_dict.get('source_type'),
- source_id=tool_record_dict.get('source_id'),
- meta=tool_record_dict.get('meta'),
- state=tool_record_dict.get('state'),
- run_time=tool_record_dict.get('run_time'))
+ return ToolRecord(
+ id=tool_record_dict.get("id"),
+ tool_id=tool_record_dict.get("tool_id"),
+ workspace_id=tool_record_dict.get("workspace_id"),
+ source_type=tool_record_dict.get("source_type"),
+ source_id=tool_record_dict.get("source_id"),
+ meta=tool_record_dict.get("meta"),
+ state=tool_record_dict.get("state"),
+ run_time=tool_record_dict.get("run_time"),
+ )
def to_dict(self, tool_record):
- return {'id': tool_record.id,
- 'tool_id': tool_record.tool_id,
- 'workspace_id': tool_record.workspace_id,
- 'source_type': tool_record.source_type,
- 'source_id': tool_record.source_id,
- 'meta': tool_record.meta,
- 'state': tool_record.state,
- 'run_time': tool_record.run_time}
+ return {
+ "id": tool_record.id,
+ "tool_id": tool_record.tool_id,
+ "workspace_id": tool_record.workspace_id,
+ "source_type": tool_record.source_type,
+ "source_id": tool_record.source_id,
+ "meta": tool_record.meta,
+ "state": tool_record.state,
+ "run_time": tool_record.run_time,
+ }
def set_record(self, tool_record):
- cache.set(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id), self.to_dict(tool_record),
- version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version(),
- timeout=60 * 30)
+ cache.set(
+ Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id),
+ self.to_dict(tool_record),
+ version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version(),
+ timeout=60 * 30,
+ )
if not self.debug:
- QuerySet(ToolRecord).update_or_create(id=tool_record.id,
- create_defaults={'id': tool_record.id,
- 'tool_id': tool_record.tool_id,
- 'state': tool_record.state,
- 'workspace_id': tool_record.workspace_id,
- "source_type": tool_record.source_type,
- 'source_id': tool_record.source_id,
- 'meta': tool_record.meta,
- 'run_time': tool_record.run_time},
- defaults={
- 'workspace_id': tool_record.workspace_id,
- 'tool_id': tool_record.tool_id,
- "source_type": tool_record.source_type,
- 'source_id': tool_record.source_id,
- 'state': tool_record.state,
- 'meta': tool_record.meta,
- 'run_time': tool_record.run_time
- })
+ QuerySet(ToolRecord).update_or_create(
+ id=tool_record.id,
+ create_defaults={
+ "id": tool_record.id,
+ "tool_id": tool_record.tool_id,
+ "state": tool_record.state,
+ "workspace_id": tool_record.workspace_id,
+ "source_type": tool_record.source_type,
+ "source_id": tool_record.source_id,
+ "meta": tool_record.meta,
+ "run_time": tool_record.run_time,
+ },
+ defaults={
+ "workspace_id": tool_record.workspace_id,
+ "tool_id": tool_record.tool_id,
+ "source_type": tool_record.source_type,
+ "source_id": tool_record.source_id,
+ "state": tool_record.state,
+ "meta": tool_record.meta,
+ "run_time": tool_record.run_time,
+ },
+ )
class ChatInfo:
- def __init__(self,
- chat_id: str,
- chat_user_id: str,
- chat_user_type: str,
- ip_address: str,
- source: {},
- knowledge_id_list: List[str],
- exclude_document_id_list: list[str],
- application_id: str,
- debug=False):
+ def __init__(
+ self,
+ chat_id: str,
+ chat_user_id: str,
+ chat_user_type: str,
+ ip_address: str,
+ source: {},
+ knowledge_id_list: List[str],
+ exclude_document_id_list: list[str],
+ application_id: str,
+ debug=False,
+ ):
"""
:param chat_id: 对话id
:param chat_user_id 对话用户id
@@ -133,36 +161,44 @@ def __init__(self,
@staticmethod
def get_no_references_setting(knowledge_setting, model_setting):
no_references_setting = knowledge_setting.get(
- 'no_references_setting', {
- 'status': 'ai_questioning',
- 'value': '{question}'})
- if no_references_setting.get('status') == 'ai_questioning':
- no_references_prompt = model_setting.get('no_references_prompt', '{question}')
- no_references_setting['value'] = no_references_prompt if len(no_references_prompt) > 0 else "{question}"
+ "no_references_setting", {"status": "ai_questioning", "value": "{question}"}
+ )
+ if no_references_setting.get("status") == "ai_questioning":
+ no_references_prompt = model_setting.get("no_references_prompt", "{question}")
+ no_references_setting["value"] = no_references_prompt if len(no_references_prompt) > 0 else "{question}"
return no_references_setting
def get_application(self):
if self.debug:
application = QuerySet(Application).filter(id=self.application_id).first()
if not application:
- raise ChatException(500, _('The application does not exist'))
+ raise ChatException(500, _("The application does not exist"))
else:
- application = QuerySet(ApplicationVersion).filter(application_id=self.application_id).order_by(
- '-create_time')[0:1].first()
+ application = (
+ QuerySet(ApplicationVersion)
+ .filter(application_id=self.application_id)
+ .order_by("-create_time")[0:1]
+ .first()
+ )
if not application:
raise ChatException(500, _("The application has not been published. Please use it after publishing."))
if application.type == ApplicationTypeChoices.SIMPLE.value:
- # 数据集id列表
- knowledge_id_list = [str(row.target_id) for row in
- QuerySet(ResourceMapping).filter(source_id=self.application_id,
- source_type='APPLICATION',
- target_type='KNOWLEDGE')]
+ # 数据集id列表 这里需要从application中获取知识库 不能从关联表获取
+ if self.debug:
+ knowledge_id_list = [
+ str(row.target_id)
+ for row in QuerySet(ResourceMapping).filter(
+ source_id=self.application_id, source_type="APPLICATION", target_type="KNOWLEDGE"
+ )
+ ]
+ else:
+ knowledge_id_list = application.knowledge_ids
# 需要排除的文档
- exclude_document_id_list = [str(document.id) for document in
- QuerySet(Document).filter(
- knowledge_id__in=knowledge_id_list,
- is_active=False)]
+ exclude_document_id_list = [
+ str(document.id)
+ for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
+ ]
self.knowledge_id_list = knowledge_id_list
self.exclude_document_id_list = exclude_document_id_list
self.application = application
@@ -171,41 +207,44 @@ def get_application(self):
def get_chat_user(self, asker=None):
if self.chat_user:
return self.chat_user
- chat_user_model = DatabaseModelManage.get_model("chat_user")
+ from system_manage.models import ChatUser
+
+ chat_user_model = ChatUser
if self.chat_user_type == ChatUserType.CHAT_USER.value and chat_user_model:
chat_user = QuerySet(chat_user_model).filter(id=self.chat_user_id).first()
return {
- 'id': str(chat_user.id),
- 'email': chat_user.email,
- 'phone': chat_user.phone,
- 'nick_name': chat_user.nick_name,
- 'username': chat_user.username,
- 'source': chat_user.source
+ "id": str(chat_user.id),
+ "email": chat_user.email,
+ "phone": chat_user.phone,
+ "nick_name": chat_user.nick_name,
+ "username": chat_user.username,
+ "source": chat_user.source,
}
else:
if asker:
if isinstance(asker, dict):
self.chat_user = asker
else:
- self.chat_user = {'username': asker}
+ self.chat_user = {"username": asker}
else:
- self.chat_user = {'username': '游客'}
+ self.chat_user = {"username": "游客"}
return self.chat_user
def get_chat_user_group(self, asker=None):
chat_user = self.get_chat_user(asker=asker)
- chat_user_id = chat_user.get('id')
+ chat_user_id = chat_user.get("id")
if not chat_user_id:
return []
user_group_relation_model = DatabaseModelManage.get_model("user_group_relation")
if user_group_relation_model:
- return [{
- 'id': user_group_relation.group_id,
- 'name': user_group_relation.group.name
- } for user_group_relation in
- QuerySet(user_group_relation_model).select_related('group').filter(user_id=chat_user_id)]
+ return [
+ {"id": user_group_relation.group_id, "name": user_group_relation.group.name}
+ for user_group_relation in QuerySet(user_group_relation_model)
+ .select_related("group")
+ .filter(user_id=chat_user_id)
+ ]
return []
def to_base_pipeline_manage_params(self):
@@ -217,66 +256,107 @@ def to_base_pipeline_manage_params(self):
model_params_setting = None
if model_id is not None:
model = QuerySet(Model).filter(id=model_id).first()
+ if model is None:
+ raise Exception(_("Model does not exist"))
credential = get_model_credential(model.provider, model.model_type, model.model_name)
model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
return {
- 'knowledge_id_list': self.knowledge_id_list,
- 'exclude_document_id_list': self.exclude_document_id_list,
- 'exclude_paragraph_id_list': [],
- 'top_n': 3 if knowledge_setting.get('top_n') is None else knowledge_setting.get('top_n'),
- 'similarity': 0.6 if knowledge_setting.get('similarity') is None else knowledge_setting.get('similarity'),
- 'max_paragraph_char_number': knowledge_setting.get('max_paragraph_char_number') or 5000,
- 'history_chat_record': self.chat_record_list,
- 'chat_id': self.chat_id,
- 'dialogue_number': self.application.dialogue_number,
- 'problem_optimization_prompt': self.application.problem_optimization_prompt if self.application.problem_optimization_prompt is not None and len(
- self.application.problem_optimization_prompt) > 0 else _(
- "() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the tag"),
- 'prompt': model_setting.get(
- 'prompt') if 'prompt' in model_setting and len(model_setting.get(
- 'prompt')) > 0 else Application.get_default_model_prompt(),
- 'system': model_setting.get(
- 'system', None),
- 'model_id': model_id,
- 'problem_optimization': self.application.problem_optimization,
- 'stream': True,
- 'model_setting': model_setting,
- 'model_params_setting': model_params_setting if self.application.model_params_setting is None or len(
- self.application.model_params_setting.keys()) == 0 else self.application.model_params_setting,
- 'search_mode': self.application.knowledge_setting.get('search_mode') or 'embedding',
- 'no_references_setting': self.get_no_references_setting(self.application.knowledge_setting, model_setting),
- 'workspace_id': self.application.workspace_id,
- 'application_id': self.application_id,
- 'mcp_enable': self.application.mcp_enable,
- 'mcp_tool_ids': self.application.mcp_tool_ids,
- 'mcp_servers': self.application.mcp_servers,
- 'mcp_source': self.application.mcp_source,
- 'tool_enable': self.application.tool_enable,
- 'tool_ids': self.application.tool_ids,
- 'application_enable': self.application.application_enable,
- 'application_ids': self.application.application_ids,
- 'skill_tool_ids': self.application.skill_tool_ids,
- 'mcp_output_enable': self.application.mcp_output_enable,
+ "knowledge_id_list": self.knowledge_id_list,
+ "exclude_document_id_list": self.exclude_document_id_list,
+ "exclude_paragraph_id_list": [],
+ "top_n": 3 if knowledge_setting.get("top_n") is None else knowledge_setting.get("top_n"),
+ "similarity": 0.6 if knowledge_setting.get("similarity") is None else knowledge_setting.get("similarity"),
+ "max_paragraph_char_number": knowledge_setting.get("max_paragraph_char_number") or 5000,
+ "history_chat_record": self.chat_record_list,
+ "chat_id": self.chat_id,
+ "dialogue_number": self.application.dialogue_number,
+ "problem_optimization_prompt": self.application.problem_optimization_prompt
+ if self.application.problem_optimization_prompt is not None
+ and len(self.application.problem_optimization_prompt) > 0
+ else _(
+ "() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the tag"
+ ),
+ "prompt": model_setting.get("prompt")
+ if "prompt" in model_setting and len(model_setting.get("prompt")) > 0
+ else Application.get_default_model_prompt(),
+ "system": model_setting.get("system", None),
+ "model_id": model_id,
+ "problem_optimization": self.application.problem_optimization,
+ "stream": True,
+ "model_setting": model_setting,
+ "model_params_setting": model_params_setting
+ if self.application.model_params_setting is None or len(self.application.model_params_setting.keys()) == 0
+ else self.application.model_params_setting,
+ "search_mode": self.application.knowledge_setting.get("search_mode") or "embedding",
+ "no_references_setting": self.get_no_references_setting(self.application.knowledge_setting, model_setting),
+ "workspace_id": self.application.workspace_id,
+ "application_id": self.application_id,
+ "mcp_enable": self.application.mcp_enable,
+ "mcp_tool_ids": self.application.mcp_tool_ids,
+ "mcp_servers": self.application.mcp_servers,
+ "mcp_source": self.application.mcp_source,
+ "tool_enable": self.application.tool_enable,
+ "tool_ids": self.application.tool_ids,
+ "application_enable": self.application.application_enable,
+ "application_ids": self.application.application_ids,
+ "skill_tool_ids": self.application.skill_tool_ids,
+ "mcp_output_enable": self.application.mcp_output_enable,
}
- def to_pipeline_manage_params(self, problem_text: str, post_response_handler,
- exclude_paragraph_id_list, chat_user_id: str, chat_user_type, ip_address, source,
- stream=True,
- form_data=None):
+ def to_pipeline_manage_params(
+ self,
+ problem_text: str,
+ post_response_handler,
+ exclude_paragraph_id_list,
+ chat_user_id: str,
+ chat_user_type,
+ ip_address,
+ source,
+ stream=True,
+ form_data=None,
+ ):
if form_data is None:
form_data = {}
params = self.to_base_pipeline_manage_params()
- return {**params, 'problem_text': problem_text, 'post_response_handler': post_response_handler,
- 'exclude_paragraph_id_list': exclude_paragraph_id_list, 'stream': stream, 'chat_user_id': chat_user_id,
- 'chat_user_type': chat_user_type, 'ip_address': ip_address, 'source': source, 'form_data': form_data}
+ return {
+ **params,
+ "problem_text": problem_text,
+ "post_response_handler": post_response_handler,
+ "exclude_paragraph_id_list": exclude_paragraph_id_list,
+ "stream": stream,
+ "chat_user_id": chat_user_id,
+ "chat_user_type": chat_user_type,
+ "ip_address": ip_address,
+ "source": source,
+ "form_data": form_data,
+ }
def set_chat(self, question):
if not self.debug:
if not QuerySet(Chat).filter(id=self.chat_id).exists():
- Chat(id=self.chat_id, application_id=self.application_id, abstract=question[0:1024],
- chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type,
- ip_address=self.ip_address, source=self.source,
- asker=self.get_chat_user()).save()
+ Chat(
+ id=self.chat_id,
+ application_id=self.application_id,
+ abstract=question[0:1024],
+ chat_user_id=self.chat_user_id,
+ chat_user_type=self.chat_user_type,
+ ip_address=self.ip_address,
+ source=self.source,
+ asker=self.get_chat_user(),
+ ).save()
+
+ def save_chat(self):
+ Chat(
+ id=self.chat_id,
+ application_id=self.application_id,
+ abstract="新建对话",
+ execute_type=ExecuteType.DEBUG if self.debug else ExecuteType.CHAT,
+ chat_user_id=self.chat_user_id,
+ chat_user_type=self.chat_user_type,
+ ip_address=self.ip_address,
+ source=self.source,
+ asker=self.get_chat_user(),
+ ).save()
def set_chat_variable(self, chat_context):
if not self.debug:
@@ -285,9 +365,12 @@ def set_chat_variable(self, chat_context):
chat.meta = {**(chat.meta if isinstance(chat.meta, dict) else {}), **chat_context}
chat.save()
else:
- cache.set(Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id), chat_context,
- version=Cache_Version.CHAT_VARIABLE.get_version(),
- timeout=60 * 30)
+ cache.set(
+ Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id),
+ chat_context,
+ version=Cache_Version.CHAT_VARIABLE.get_version(),
+ timeout=60 * 30,
+ )
def get_chat_variable(self):
if not self.debug:
@@ -296,8 +379,13 @@ def get_chat_variable(self):
return chat.meta
return {}
else:
- return cache.get(Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id),
- version=Cache_Version.CHAT_VARIABLE.get_version()) or {}
+ return (
+ cache.get(
+ Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id),
+ version=Cache_Version.CHAT_VARIABLE.get_version(),
+ )
+ or {}
+ )
def append_chat_record(self, chat_record: ChatRecord):
chat_record.problem_text = chat_record.problem_text[0:10240] if chat_record.problem_text is not None else ""
@@ -312,137 +400,172 @@ def append_chat_record(self, chat_record: ChatRecord):
break
if is_save:
self.chat_record_list.append(chat_record)
- if not self.debug:
- if not QuerySet(Chat).filter(id=self.chat_id).exists():
- Chat(id=self.chat_id, application_id=self.application_id, abstract=chat_record.problem_text[0:1024],
- chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type,
- ip_address=self.ip_address, source=self.source,
- asker=self.get_chat_user()).save()
- else:
- QuerySet(Chat).filter(id=self.chat_id).update(update_time=timezone.now())
- # 插入会话记录
- QuerySet(ChatRecord).update_or_create(id=chat_record.id,
- create_defaults={'id': chat_record.id,
- 'chat_id': chat_record.chat_id,
- "vote_status": chat_record.vote_status,
- 'problem_text': chat_record.problem_text,
- 'answer_text': chat_record.answer_text,
- 'answer_text_list': chat_record.answer_text_list,
- 'message_tokens': chat_record.message_tokens,
- 'answer_tokens': chat_record.answer_tokens,
- 'const': chat_record.const,
- 'details': chat_record.details,
- 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list,
- 'run_time': chat_record.run_time,
- 'source': chat_record.source,
- 'ip_address': chat_record.ip_address or '',
- 'index': chat_record.index},
- defaults={
- "vote_status": chat_record.vote_status,
- 'problem_text': chat_record.problem_text,
- 'answer_text': chat_record.answer_text,
- 'answer_text_list': chat_record.answer_text_list,
- 'message_tokens': chat_record.message_tokens,
- 'answer_tokens': chat_record.answer_tokens,
- 'const': chat_record.const,
- 'details': chat_record.details,
- 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list,
- 'run_time': chat_record.run_time,
- 'index': chat_record.index,
- 'source': chat_record.source,
- 'ip_address': chat_record.ip_address or '',
- })
- ChatCountSerializer(data={'chat_id': self.chat_id}).update_chat()
+ if not QuerySet(Chat).filter(id=self.chat_id).exists():
+ Chat(
+ id=self.chat_id,
+ application_id=self.application_id,
+ abstract=chat_record.problem_text[0:1024],
+ chat_user_id=self.chat_user_id,
+ chat_user_type=self.chat_user_type,
+ ip_address=self.ip_address,
+ source=self.source,
+ asker=self.get_chat_user(),
+ ).save()
+ else:
+ QuerySet(Chat).filter(id=self.chat_id).update(update_time=timezone.now())
+ # 记录Token消耗
+ total_tokens = (chat_record.message_tokens or 0) + (chat_record.answer_tokens or 0)
+ if total_tokens > 0:
+ ChatUserTokenQuota.consume(self.chat_user_id, total_tokens)
+ # 插入会话记录
+ QuerySet(ChatRecord).update_or_create(
+ id=chat_record.id,
+ create_defaults={
+ "id": chat_record.id,
+ "chat_id": chat_record.chat_id,
+ "vote_status": chat_record.vote_status,
+ "problem_text": chat_record.problem_text,
+ "answer_text": chat_record.answer_text,
+ "answer_text_list": chat_record.answer_text_list,
+ "message_tokens": chat_record.message_tokens,
+ "answer_tokens": chat_record.answer_tokens,
+ "const": chat_record.const,
+ "details": chat_record.details,
+ "improve_paragraph_id_list": chat_record.improve_paragraph_id_list,
+ "run_time": chat_record.run_time,
+ "source": chat_record.source,
+ "ip_address": chat_record.ip_address or "",
+ "index": chat_record.index,
+ },
+ defaults={
+ "vote_status": chat_record.vote_status,
+ "problem_text": chat_record.problem_text,
+ "answer_text": chat_record.answer_text,
+ "answer_text_list": chat_record.answer_text_list,
+ "message_tokens": chat_record.message_tokens,
+ "answer_tokens": chat_record.answer_tokens,
+ "const": chat_record.const,
+ "details": chat_record.details,
+ "improve_paragraph_id_list": chat_record.improve_paragraph_id_list,
+ "run_time": chat_record.run_time,
+ "index": chat_record.index,
+ "source": chat_record.source,
+ "ip_address": chat_record.ip_address or "",
+ },
+ )
+ ChatCountSerializer(data={"chat_id": self.chat_id}).update_chat()
def to_dict(self):
return {
- 'chat_id': self.chat_id,
- 'chat_user_id': self.chat_user_id,
- 'chat_user_type': self.chat_user_type,
- 'ip_address': self.ip_address,
- 'source': self.source,
- 'knowledge_id_list': self.knowledge_id_list,
- 'exclude_document_id_list': self.exclude_document_id_list,
- 'application_id': self.application_id,
- 'chat_record_list': [self.chat_record_to_map(c) for c in self.chat_record_list][-20:],
- 'debug': self.debug
+ "chat_id": self.chat_id,
+ "chat_user_id": self.chat_user_id,
+ "chat_user_type": self.chat_user_type,
+ "ip_address": self.ip_address,
+ "source": self.source,
+ "knowledge_id_list": self.knowledge_id_list,
+ "exclude_document_id_list": self.exclude_document_id_list,
+ "application_id": self.application_id,
+ "chat_record_list": [self.chat_record_to_map(c) for c in self.chat_record_list][-20:],
+ "debug": self.debug,
}
def chat_record_to_map(self, chat_record):
- return {'id': chat_record.id,
- 'chat_id': chat_record.chat_id,
- 'vote_status': chat_record.vote_status,
- 'problem_text': chat_record.problem_text,
- 'answer_text': chat_record.answer_text,
- 'answer_text_list': chat_record.answer_text_list,
- 'message_tokens': chat_record.message_tokens,
- 'answer_tokens': chat_record.answer_tokens,
- 'const': chat_record.const,
- 'details': chat_record.details,
- 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list,
- 'run_time': chat_record.run_time,
- 'source': chat_record.source,
- 'ip_address': chat_record.ip_address,
- 'index': chat_record.index}
+ return {
+ "id": chat_record.id,
+ "chat_id": chat_record.chat_id,
+ "vote_status": chat_record.vote_status,
+ "problem_text": chat_record.problem_text,
+ "answer_text": chat_record.answer_text,
+ "answer_text_list": chat_record.answer_text_list,
+ "message_tokens": chat_record.message_tokens,
+ "answer_tokens": chat_record.answer_tokens,
+ "const": chat_record.const,
+ "details": chat_record.details,
+ "improve_paragraph_id_list": chat_record.improve_paragraph_id_list,
+ "run_time": chat_record.run_time,
+ "source": chat_record.source,
+ "ip_address": chat_record.ip_address,
+ "index": chat_record.index,
+ }
@staticmethod
def map_to_chat_record(chat_record_dict):
- return ChatRecord(id=chat_record_dict.get('id'),
- chat_id=chat_record_dict.get('chat_id'),
- vote_status=chat_record_dict.get('vote_status'),
- problem_text=chat_record_dict.get('problem_text'),
- answer_text=chat_record_dict.get('answer_text'),
- answer_text_list=chat_record_dict.get('answer_text_list'),
- message_tokens=chat_record_dict.get('message_tokens'),
- answer_tokens=chat_record_dict.get('answer_tokens'),
- const=chat_record_dict.get('const'),
- details=chat_record_dict.get('details'),
- improve_paragraph_id_list=chat_record_dict.get('improve_paragraph_id_list'),
- run_time=chat_record_dict.get('run_time'),
- index=chat_record_dict.get('index'),
- source=chat_record_dict.get('source'),
- ip_address=chat_record_dict.get('ip_address'))
+ return ChatRecord(
+ id=chat_record_dict.get("id"),
+ chat_id=chat_record_dict.get("chat_id"),
+ vote_status=chat_record_dict.get("vote_status"),
+ problem_text=chat_record_dict.get("problem_text"),
+ answer_text=chat_record_dict.get("answer_text"),
+ answer_text_list=chat_record_dict.get("answer_text_list"),
+ message_tokens=chat_record_dict.get("message_tokens"),
+ answer_tokens=chat_record_dict.get("answer_tokens"),
+ const=chat_record_dict.get("const"),
+ details=chat_record_dict.get("details"),
+ improve_paragraph_id_list=chat_record_dict.get("improve_paragraph_id_list"),
+ run_time=chat_record_dict.get("run_time"),
+ index=chat_record_dict.get("index"),
+ source=chat_record_dict.get("source"),
+ ip_address=chat_record_dict.get("ip_address"),
+ )
def set_cache(self):
- cache.set(Cache_Version.CHAT.get_key(key=self.chat_id), self.to_dict(),
- version=Cache_Version.CHAT_INFO.get_version(),
- timeout=60 * 30)
+ cache.set(
+ Cache_Version.CHAT.get_key(key=self.chat_id),
+ self.to_dict(),
+ version=Cache_Version.CHAT_INFO.get_version(),
+ timeout=60 * 30,
+ )
@staticmethod
def map_to_chat_info(chat_info_dict):
- c = ChatInfo(chat_info_dict.get('chat_id'), chat_info_dict.get('chat_user_id'),
- chat_info_dict.get('chat_user_type'), chat_info_dict.get('ip_address'),
- chat_info_dict.get('source'),
- chat_info_dict.get('knowledge_id_list'),
- chat_info_dict.get('exclude_document_id_list'),
- chat_info_dict.get('application_id'),
- debug=chat_info_dict.get('debug'))
- c.chat_record_list = [ChatInfo.map_to_chat_record(c_r) for c_r in chat_info_dict.get('chat_record_list')]
+ c = ChatInfo(
+ chat_info_dict.get("chat_id"),
+ chat_info_dict.get("chat_user_id"),
+ chat_info_dict.get("chat_user_type"),
+ chat_info_dict.get("ip_address"),
+ chat_info_dict.get("source"),
+ chat_info_dict.get("knowledge_id_list"),
+ chat_info_dict.get("exclude_document_id_list"),
+ chat_info_dict.get("application_id"),
+ debug=chat_info_dict.get("debug"),
+ )
+ c.chat_record_list = [ChatInfo.map_to_chat_record(c_r) for c_r in chat_info_dict.get("chat_record_list")]
return c
@staticmethod
def get_cache(chat_id):
- chat_info_dict = cache.get(Cache_Version.CHAT.get_key(key=chat_id),
- version=Cache_Version.CHAT_INFO.get_version())
+ chat_info_dict = cache.get(
+ Cache_Version.CHAT.get_key(key=chat_id), version=Cache_Version.CHAT_INFO.get_version()
+ )
if chat_info_dict:
return ChatInfo.map_to_chat_info(chat_info_dict)
return None
def update_resource_mapping_by_application(application_id: str, other_resource_mapping=None):
- from application.flow.tools import get_instance_resource, save_workflow_mapping, \
- application_instance_field_call_dict
+ from application.flow.tools import (
+ application_instance_field_call_dict,
+ get_instance_resource,
+ save_workflow_mapping,
+ )
from system_manage.models.resource_mapping import ResourceType
+
if other_resource_mapping is None:
other_resource_mapping = []
application = QuerySet(Application).filter(id=application_id).first()
- instance_mapping = get_instance_resource(application, ResourceType.APPLICATION, str(application.id),
- application_instance_field_call_dict)
- if application.type == 'WORK_FLOW':
- save_workflow_mapping(application.work_flow, ResourceType.APPLICATION, str(application_id),
- instance_mapping + other_resource_mapping)
+ instance_mapping = get_instance_resource(
+ application, ResourceType.APPLICATION, str(application.id), application_instance_field_call_dict
+ )
+ if application.type == "WORK_FLOW":
+ save_workflow_mapping(
+ application.work_flow,
+ ResourceType.APPLICATION,
+ str(application_id),
+ instance_mapping + other_resource_mapping,
+ )
return
else:
- save_workflow_mapping({}, ResourceType.APPLICATION, str(application_id),
- instance_mapping + other_resource_mapping)
+ save_workflow_mapping(
+ {}, ResourceType.APPLICATION, str(application_id), instance_mapping + other_resource_mapping
+ )
diff --git a/apps/application/sql/list_application.sql b/apps/application/sql/list_application.sql
index 3b6e863fd97..c26218223d7 100644
--- a/apps/application/sql/list_application.sql
+++ b/apps/application/sql/list_application.sql
@@ -2,6 +2,7 @@ select *
from (select application."id"::text, application."name",
application."desc",
application."is_publish",
+ application."is_portal",
application."type",
'application' as "resource_type",
application."workspace_id",
diff --git a/apps/application/sql/list_application_user.sql b/apps/application/sql/list_application_user.sql
index ecffcd93daf..cc25dab288f 100644
--- a/apps/application/sql/list_application_user.sql
+++ b/apps/application/sql/list_application_user.sql
@@ -2,6 +2,7 @@ select *
from (select application."id"::text, application."name",
application."desc",
application."is_publish",
+ application."is_portal",
application."type",
'application' as "resource_type",
application."workspace_id",
@@ -16,5 +17,13 @@ from (select application."id"::text, application."name",
left join "user" on user_id = "user".id
where application."id"::text in (select target
from workspace_user_resource_permission ${workspace_user_resource_permission_query_set}
- and 'VIEW' = any (permission_list))) temp
-${application_query_set}
\ No newline at end of file
+ and 'VIEW' = any (permission_list)
+ union
+ select distinct target
+ from workspace_user_group_resource_permission
+ inner join system_user_group_relation
+ on system_user_group_relation.group_id =
+ workspace_user_group_resource_permission.user_group_id
+ ${workspace_user_group_resource_permission_query_set}
+ and 'VIEW' = any (permission_list))) temp
+${application_query_set}
diff --git a/apps/application/sql/list_application_user_ee.sql b/apps/application/sql/list_application_user_ee.sql
index 0fe61a1402c..da5c356d5e6 100644
--- a/apps/application/sql/list_application_user_ee.sql
+++ b/apps/application/sql/list_application_user_ee.sql
@@ -2,6 +2,7 @@ select *
from (select application."id"::text, application."name",
application."desc",
application."is_publish",
+ application."is_portal",
application."type",
'application' as "resource_type",
application."workspace_id",
@@ -33,5 +34,33 @@ from (select application."id"::text, application."name",
else
'VIEW' = any (permission_list)
- end)) temp
-${application_query_set}
\ No newline at end of file
+ end
+ union
+ select distinct target
+ from workspace_user_group_resource_permission
+ inner join system_user_group_relation
+ on system_user_group_relation.group_id =
+ workspace_user_group_resource_permission.user_group_id
+ ${workspace_user_group_resource_permission_query_set}
+ and (
+ 'VIEW' = any (permission_list)
+ or (
+ auth_type = 'ROLE'
+ and 'ROLE' = any (permission_list)
+ and 'APPLICATION:READ' in (select (case
+ when user_role_relation.role_id =
+ any (array['USER'])
+ then 'APPLICATION:READ'
+ else
+ role_permission.permission_id end)
+ from role_permission role_permission
+ right join user_role_relation user_role_relation
+ on user_role_relation.role_id =
+ role_permission.role_id
+ where user_role_relation.user_id =
+ system_user_group_relation.user_id
+ and user_role_relation.workspace_id =
+ workspace_user_group_resource_permission.workspace_id)
+ )
+ ))) temp
+${application_query_set}
diff --git a/apps/application/urls.py b/apps/application/urls.py
index 1c473b63935..05707a9f48f 100644
--- a/apps/application/urls.py
+++ b/apps/application/urls.py
@@ -43,5 +43,10 @@
path('workspace//application//play_demo_text', views.PlayDemoText.as_view()),
path('workspace//application//mcp_tools', views.McpServers.as_view()),
path('workspace//application//model//prompt_generate', views.PromptGenerateView.as_view()),
- path('chat_message/', views.ChatView.as_view()),
+ path('workspace//application//chat//chat_message', views.ChatView.as_view()),
+ path('workspace//application//chat//cancel_chat_message', views.CancelWorkflowView.as_view()),
+ path('workspace//application//chat//chat_record//resume_chat_message', views.ResumeStreamView.as_view()),
+ path('workspace//application//historical_conversation//', views.DebugHistoricalConversation.PageView.as_view()),
+ path('workspace//application//historical_conversation/', views.DebugHistoricalConversation.Operate.as_view()),
+ path('workspace//application//historical_conversation_record///', views.DebugHistoricalConversation.RecordPageView.as_view()),
]
diff --git a/apps/application/views/application.py b/apps/application/views/application.py
index 5f5058bca4d..7f2a28f20e9 100644
--- a/apps/application/views/application.py
+++ b/apps/application/views/application.py
@@ -23,7 +23,10 @@
from common import result
from common.auth import TokenAuth
from common.auth.authentication import has_permissions, get_is_permissions, check_batch_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
from common.log.log import log
from tools.api.tool import GetInternalToolAPI
@@ -151,7 +154,7 @@ class Export(APIView):
PermissionConstants.APPLICATION_EXPORT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="Export Application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
@@ -178,7 +181,7 @@ class Operate(APIView):
PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate='Deleting application',
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
@@ -204,7 +207,7 @@ def delete(self, request: Request, workspace_id: str, application_id: str):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="Modify the application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
@@ -230,7 +233,7 @@ def put(self, request: Request, workspace_id: str, application_id: str):
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
return result.success(ApplicationOperateSerializer(
@@ -254,7 +257,7 @@ class Move(APIView):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate='Move an application',
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
@@ -281,7 +284,7 @@ class Publish(APIView):
PermissionConstants.APPLICATION_PUBLISH.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate='Publishing an application',
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
@@ -333,17 +336,18 @@ def put(self, request: Request, workspace_id: str):
PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), workspace_id=workspace_id
)
+
@log(menu='Application', operate='Batch delete applications',
get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids))
- def inner(view,r, **kwargs):
+ def inner(view, r, **kwargs):
return ApplicationBatchOperateSerializer(
data={'workspace_id': workspace_id, 'user_id': request.user.id}
).batch_delete({'id_list': permitted_ids})
- return result.success(inner(self,request, workspace_id=workspace_id))
+ return result.success(inner(self, request, workspace_id=workspace_id))
class BatchMove(APIView):
authentication_classes = [TokenAuth]
@@ -370,19 +374,19 @@ def put(self, request: Request, workspace_id: str):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role()),
workspace_id=workspace_id
)
@log(menu='Application', operate='Batch move applications',
get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids))
- def inner(view,r, **kwargs):
+ def inner(view, r, **kwargs):
return ApplicationBatchOperateSerializer(
data={'workspace_id': workspace_id, 'user_id': request.user.id}
).batch_move({'id_list': permitted_ids, 'folder_id': request.data.get('folder_id')})
- return result.success(inner(self,request, workspace_id=workspace_id))
+ return result.success(inner(self, request, workspace_id=workspace_id))
class BatchCleanTime(APIView):
authentication_classes = [TokenAuth]
@@ -409,14 +413,14 @@ def put(self, request: Request, workspace_id: str):
PermissionConstants.APPLICATION_CHAT_LOG_CLEAR_POLICY.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role()),
workspace_id=workspace_id
)
@log(menu='Application', operate='Batch update application chat log clear policy',
get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids))
- def inner(view,r, **kwargs):
+ def inner(view, r, **kwargs):
return ApplicationBatchOperateSerializer(
data={'workspace_id': workspace_id, 'user_id': request.user.id}
).batch_clean_time({
@@ -425,7 +429,8 @@ def inner(view,r, **kwargs):
'file_clean_time': request.data.get('file_clean_time')
})
- return result.success(inner(self,request, workspace_id=workspace_id))
+ return result.success(inner(self, request, workspace_id=workspace_id))
+
class McpServers(APIView):
authentication_classes = [TokenAuth]
@@ -444,7 +449,7 @@ class McpServers(APIView):
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def post(self, request: Request, workspace_id, application_id: str):
return result.success(ApplicationOperateSerializer(
@@ -470,7 +475,7 @@ class SpeechToText(APIView):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def post(self, request: Request, workspace_id: str, application_id: str):
return result.success(
@@ -496,7 +501,7 @@ class TextToSpeech(APIView):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def post(self, request: Request, workspace_id: str, application_id: str):
byte_data = ApplicationOperateSerializer(
@@ -523,7 +528,7 @@ class PlayDemoText(APIView):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="trial listening",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
diff --git a/apps/application/views/application_access_token.py b/apps/application/views/application_access_token.py
index f7dc6bcaf83..ebf7da12e9f 100644
--- a/apps/application/views/application_access_token.py
+++ b/apps/application/views/application_access_token.py
@@ -18,7 +18,10 @@
from common import result
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
from common.log.log import log
def get_application_operation_object(application_id):
@@ -49,7 +52,7 @@ class AccessToken(APIView):
PermissionConstants.APPLICATION_OVERVIEW_ACCESS.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def put(self, request: Request, workspace_id: str, application_id: str):
return result.success(
@@ -68,7 +71,7 @@ def put(self, request: Request, workspace_id: str, application_id: str):
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role()
)
def get(self, request: Request, workspace_id: str, application_id: str):
diff --git a/apps/application/views/application_api_key.py b/apps/application/views/application_api_key.py
index 213c8fe7221..bd07d07a44f 100644
--- a/apps/application/views/application_api_key.py
+++ b/apps/application/views/application_api_key.py
@@ -9,7 +9,11 @@
from application.serializers.application_api_key import ApplicationKeySerializer
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
+
from common.log.log import log
from common.result import result, DefaultResultSerializer
@@ -42,8 +46,9 @@ class ApplicationKey(APIView):
@has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission(),
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ [
+ PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role()
)
def post(self, request: Request, workspace_id: str, application_id: str):
@@ -67,7 +72,7 @@ class Page(APIView):
PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int):
return result.success(ApplicationKeySerializer(
@@ -92,7 +97,7 @@ class Operate(APIView):
PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="Modify application API_KEY",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
@@ -118,7 +123,7 @@ def put(self, request: Request, workspace_id: str, application_id: str, api_key_
PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="Delete application API_KEY",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
diff --git a/apps/application/views/application_chat.py b/apps/application/views/application_chat.py
index d60206ded5a..4537025d581 100644
--- a/apps/application/views/application_chat.py
+++ b/apps/application/views/application_chat.py
@@ -1,179 +1,407 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application_chat.py
- @date:2025/6/10 11:00
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application_chat.py
+@date:2025/6/10 11:00
+@desc:
"""
-import uuid_utils.compat as uuid
-from django.db.models import QuerySet
+from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.views import APIView
-from application.api.application_chat import ApplicationChatQueryAPI, ApplicationChatQueryPageAPI, \
- ApplicationChatExportAPI
-from application.models import ChatUserType, Application
+from application.api.application_chat import (
+ ApplicationChatQueryAPI,
+ ApplicationChatQueryPageAPI,
+ ApplicationChatExportAPI,
+)
+from application.models import ChatUserType, Application, ChatSourceChoices
from application.serializers.application_chat import ApplicationChatQuerySerializers
-from chat.api.chat_api import ChatAPI, PromptGenerateAPI
+from chat.api.chat_api import ChatAPI, PromptGenerateAPI, PageHistoricalConversationAPI, HistoricalConversationRecordAPI
from chat.api.chat_authentication_api import ChatOpenAPI
-from chat.serializers.chat import OpenChatSerializers, ChatSerializers, DebugChatSerializers, PromptGenerateSerializer
+from chat.serializers.chat import OpenChatSerializers, DebugChatSerializers, PromptGenerateSerializer, ResumeSerializers
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
-from common.log.log import log
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
+from common.log.log import log, _get_ip_address
from common.result import result
from common.utils.common import query_params_to_single_dict
+
def get_application_operation_object(application_id):
application_model = QuerySet(model=Application).filter(id=application_id).first()
if application_model is not None:
- return {
- 'name': application_model.name
- }
+ return {"name": application_model.name}
return {}
+
class ApplicationChat(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the conversation list"),
summary=_("Get the conversation list"),
operation_id=_("Get the conversation list"), # type: ignore
request=ApplicationChatQueryAPI.get_request(),
parameters=ApplicationChatQueryAPI.get_parameters(),
responses=ApplicationChatQueryAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
- return result.success(ApplicationChatQuerySerializers(
- data={**query_params_to_single_dict(request.query_params), 'workspace_id': workspace_id,
- 'application_id': application_id,
- }).list())
+ return result.success(
+ ApplicationChatQuerySerializers(
+ data={
+ **query_params_to_single_dict(request.query_params),
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ }
+ ).list()
+ )
class Page(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the conversation list by page"),
summary=_("Get the conversation list by page"),
operation_id=_("Get the conversation list by page"), # type: ignore
request=ApplicationChatQueryPageAPI.get_request(),
parameters=ApplicationChatQueryPageAPI.get_parameters(),
responses=ApplicationChatQueryPageAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int):
- return result.success(ApplicationChatQuerySerializers(
- data={**query_params_to_single_dict(request.query_params), 'workspace_id': workspace_id,
- 'application_id': application_id,
- }).page(current_page=current_page,
- page_size=page_size))
+ return result.success(
+ ApplicationChatQuerySerializers(
+ data={
+ **query_params_to_single_dict(request.query_params),
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ }
+ ).page(current_page=current_page, page_size=page_size)
+ )
class Export(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("Export conversation"),
summary=_("Export conversation"),
operation_id=_("Export conversation"), # type: ignore
request=ApplicationChatExportAPI.get_request(),
parameters=ApplicationChatExportAPI.get_parameters(),
responses=ApplicationChatExportAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_EXPORT.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_EXPORT.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_EXPORT.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_EXPORT.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def post(self, request: Request, workspace_id: str, application_id: str):
return ApplicationChatQuerySerializers(
- data={**query_params_to_single_dict(request.query_params), 'workspace_id': workspace_id,
- 'application_id': application_id,
- }).export(request.data)
+ data={
+ **query_params_to_single_dict(request.query_params),
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ }
+ ).export(request.data)
class OpenView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get a temporary session id based on the application id"),
summary=_("Get a temporary session id based on the application id"),
operation_id=_("Get a temporary session id based on the application id"), # type: ignore
parameters=ChatOpenAPI.get_parameters(),
responses=None,
- tags=[_('Application')] # type: ignore
+ tags=[_("Application")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
- return result.success(OpenChatSerializers(
- data={'workspace_id': workspace_id, 'application_id': application_id,
- 'chat_user_id': str(uuid.uuid7()), 'chat_user_type': ChatUserType.ANONYMOUS_USER,
- 'debug': True}).open())
+ ip_address = _get_ip_address(request)
+ return result.success(
+ OpenChatSerializers(
+ data={
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_user_id": str(request.user.id),
+ "chat_user_type": ChatUserType.SYSTEM_USER,
+ "ip_address": ip_address,
+ "source": {"type": ChatSourceChoices.ONLINE.value},
+ "debug": True,
+ }
+ ).open()
+ )
class ChatView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("dialogue"),
summary=_("dialogue"),
operation_id=_("dialogue"), # type: ignore
request=ChatAPI.get_request(),
parameters=ChatAPI.get_parameters(),
responses=None,
- tags=[_('Application')] # type: ignore
+ tags=[_("Application")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- def post(self, request: Request, chat_id: str):
- return DebugChatSerializers(data={'chat_id': chat_id}).chat(request.data)
+ def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
+ # 携带 open 上下文:前端本地生成 chat_id 首次发消息时,缓存缺失则按该 id 现开会话。
+ return DebugChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_user_id": str(request.user.id),
+ "chat_user_type": ChatUserType.SYSTEM_USER,
+ "ip_address": _get_ip_address(request),
+ "source": {"type": ChatSourceChoices.ONLINE.value},
+ }
+ ).chat(request.data)
+
+
+class CancelWorkflowView(APIView):
+ authentication_classes = [TokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Cancel running workflow"),
+ summary=_("Cancel running workflow"),
+ operation_id=_("Cancel running workflow"), # type: ignore
+ tags=[_("Application")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
+ )
+ def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
+ from application.workflow.workflow_run_registry import WorkflowRunRegistry, CancelResult
+
+ result_enum = WorkflowRunRegistry.cancel_by_chat_id(chat_id)
+ if result_enum == CancelResult.CANCELLED:
+ return result.success({"status": "cancelled", "chat_id": chat_id})
+ elif result_enum == CancelResult.NOT_FOUND:
+ return result.success({"status": "not_found", "chat_id": chat_id})
+ else:
+ return result.error(_("Failed to cancel workflow"))
+
+
+class ResumeStreamView(APIView):
+ authentication_classes = [TokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Resume stream for workflow"),
+ summary=_("Resume stream for workflow"),
+ operation_id=_("Resume stream for workflow"), # type: ignore
+ tags=[_("Application")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
+ )
+ def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str):
+ return ResumeSerializers(data={"chat_id": chat_id, "chat_record_id": chat_record_id}).resume(request)
+
class PromptGenerateView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("generate prompt"),
summary=_("generate prompt"),
operation_id=_("generate prompt"), # type: ignore
request=PromptGenerateAPI.get_request(),
parameters=PromptGenerateAPI.get_parameters(),
responses=None,
- tags=[_('Application')] # type: ignore
+ tags=[_("Application")], # type: ignore
)
- @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
- @log(menu='Application', operate='Generate prompt',
- get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
- def post(self, request: Request, workspace_id: str, model_id:str, application_id: str):
- return PromptGenerateSerializer(data={'workspace_id': workspace_id, 'model_id': model_id, 'application_id': application_id}).generate_prompt(instance=request.data)
\ No newline at end of file
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
+ )
+ @log(
+ menu="Application",
+ operate="Generate prompt",
+ get_operation_object=lambda r, k: get_application_operation_object(k.get("application_id")),
+ )
+ def post(self, request: Request, workspace_id: str, model_id: str, application_id: str):
+ return PromptGenerateSerializer(
+ data={"workspace_id": workspace_id, "model_id": model_id, "application_id": application_id}
+ ).generate_prompt(instance=request.data)
+
+
+class DebugHistoricalConversation(APIView):
+ authentication_classes = [TokenAuth]
+
+ class PageView(APIView):
+ authentication_classes = [TokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation by page"),
+ summary=_("Get historical conversation by page"),
+ operation_id=_("Get historical conversation by page"), # type: ignore
+ parameters=PageHistoricalConversationAPI.get_parameters(),
+ responses=PageHistoricalConversationAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
+ )
+ def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int):
+ from chat.serializers.chat_record import HistoricalConversationSerializer
+
+ return result.success(
+ HistoricalConversationSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": str(request.user.id),
+ }
+ ).page(current_page, page_size)
+ )
+
+ class RecordPageView(APIView):
+ authentication_classes = [TokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation records"),
+ summary=_("Get historical conversation records"),
+ operation_id=_("Get historical conversation records"), # type: ignore
+ parameters=HistoricalConversationRecordAPI.get_parameters(),
+ responses=HistoricalConversationRecordAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
+ )
+ def get(
+ self,
+ request: Request,
+ workspace_id: str,
+ application_id: str,
+ chat_id: str,
+ current_page: int,
+ page_size: int,
+ ):
+ from chat.serializers.chat_record import HistoricalConversationRecordSerializer
+
+ serializer = HistoricalConversationRecordSerializer(
+ data={
+ "application_id": application_id,
+ "chat_id": chat_id,
+ "chat_user_id": str(request.user.id),
+ }
+ )
+ return result.success(serializer.page(current_page, page_size))
+
+ class Operate(APIView):
+ authentication_classes = [TokenAuth]
+
+ def delete(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
+ from django.db.models import QuerySet
+ from application.models import Chat
+
+ QuerySet(Chat).filter(id=chat_id, application_id=application_id).update(is_deleted=True)
+ return result.success(True)
+
+ def put(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
+ from django.db.models import QuerySet
+ from application.models import Chat
+
+ abstract = request.data.get("abstract", "")
+ QuerySet(Chat).filter(id=chat_id, application_id=application_id).update(abstract=abstract)
+ return result.success(True)
diff --git a/apps/application/views/application_chat_link.py b/apps/application/views/application_chat_link.py
index d410ef392a6..2ed46cbae2d 100644
--- a/apps/application/views/application_chat_link.py
+++ b/apps/application/views/application_chat_link.py
@@ -1,10 +1,11 @@
"""
- @project: MaxKB
- @Author: niu
- @file: application_chat_link.py
- @date: 2026/2/9 10:44
- @desc:
+@project: MaxKB
+@Author: niu
+@file: application_chat_link.py
+@date: 2026/2/9 10:44
+@desc:
"""
+
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
@@ -20,36 +21,32 @@ class ChatRecordLinkView(APIView):
authentication_classes = [ChatTokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("Generate share link"),
summary=_("Generate share link"),
operation_id=_("Generate share link"), # type: ignore
request=ChatRecordLinkAPI.get_request(),
parameters=ChatRecordLinkAPI.get_parameters(),
responses=ChatRecordLinkAPI.get_response(),
- tags=[_("Chat record link")] # type: ignore
+ tags=[_("Chat record link")], # type: ignore
)
-
def post(self, request: Request, application_id: str, chat_id: str):
- return result.success(ChatRecordShareLinkSerializer(data={
- "application_id": application_id,
- "chat_id": chat_id,
- "user_id": request.auth.chat_user_id
- }).generate_link(request.data))
+ return result.success(
+ ChatRecordShareLinkSerializer(
+ data={"application_id": application_id, "chat_id": chat_id, "user_id": request.user.id}
+ ).generate_link(request.data)
+ )
class ChatRecordDetailView(APIView):
-
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get chat record by share link"),
summary=_("Get chat record by share link"),
operation_id=_("Get chat record by share link"), # type: ignore
parameters=ChatRecordDetailShareAPI.get_parameters(),
responses=ChatRecordDetailShareAPI.get_response(),
- tags=[_("Chat record link")] # type: ignore
+ tags=[_("Chat record link")], # type: ignore
)
def get(self, request, link: str):
- return result.success(
- ChatShareLinkDetailSerializer(data={'link':link}).get_record_list()
- )
+ return result.success(ChatShareLinkDetailSerializer(data={"link": link}).get_record_list())
diff --git a/apps/application/views/application_chat_record.py b/apps/application/views/application_chat_record.py
index 0d59146b29d..91213efcc32 100644
--- a/apps/application/views/application_chat_record.py
+++ b/apps/application/views/application_chat_record.py
@@ -1,217 +1,313 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: application_chat_record.py
- @date:2025/6/10 15:08
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: application_chat_record.py
+@date:2025/6/10 15:08
+@desc:
"""
-from django.utils.translation import gettext_lazy as _
-from drf_spectacular.utils import extend_schema
-from rest_framework.request import Request
-from rest_framework.views import APIView
-from application.api.application_chat_record import ApplicationChatRecordQueryAPI, \
- ApplicationChatRecordImproveParagraphAPI, ApplicationChatRecordAddKnowledgeAPI
-from application.serializers.application_chat_record import ApplicationChatRecordQuerySerializers, \
- ApplicationChatRecordImproveSerializer, ChatRecordImproveSerializer, ApplicationChatRecordAddKnowledgeSerializer, \
- ChatRecordOperateSerializer
from common import result
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
from common.utils.common import query_params_to_single_dict
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from application.api.application_chat_record import (
+ ApplicationChatRecordAddKnowledgeAPI,
+ ApplicationChatRecordImproveParagraphAPI,
+ ApplicationChatRecordQueryAPI,
+)
+from application.serializers.application_chat_record import (
+ ApplicationChatRecordAddKnowledgeSerializer,
+ ApplicationChatRecordImproveSerializer,
+ ApplicationChatRecordQuerySerializers,
+ ChatRecordImproveSerializer,
+ ChatRecordOperateSerializer,
+)
class ApplicationChatRecord(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the conversation record list"),
summary=_("Get the conversation record list"),
operation_id=_("Get the conversation record list"), # type: ignore
request=ApplicationChatRecordQueryAPI.get_request(),
parameters=ApplicationChatRecordQueryAPI.get_parameters(),
responses=ApplicationChatRecordQueryAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str):
- return result.success(ApplicationChatRecordQuerySerializers(
- data={**query_params_to_single_dict(request.query_params), 'workspace_id': workspace_id,
- 'application_id': application_id,
- 'chat_id': chat_id
- }).list())
+ return result.success(
+ ApplicationChatRecordQuerySerializers(
+ data={
+ **query_params_to_single_dict(request.query_params),
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_id": chat_id,
+ }
+ ).list()
+ )
class Page(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the conversation record list by page"),
summary=_("Get the conversation record list by page"),
operation_id=_("Get the conversation record list by page"), # type: ignore
request=ApplicationChatRecordQueryAPI.get_request(),
parameters=ApplicationChatRecordQueryAPI.get_parameters(),
responses=ApplicationChatRecordQueryAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
- def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, current_page: int,
- page_size: int):
- return result.success(ApplicationChatRecordQuerySerializers(
- data={**query_params_to_single_dict(request.query_params), 'workspace_id': workspace_id,
- 'application_id': application_id,
- 'chat_id': chat_id}).page(
- current_page=current_page,
- page_size=page_size))
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND, ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
+ def get(
+ self,
+ request: Request,
+ workspace_id: str,
+ application_id: str,
+ chat_id: str,
+ current_page: int,
+ page_size: int,
+ ):
+ return result.success(
+ ApplicationChatRecordQuerySerializers(
+ data={
+ **query_params_to_single_dict(request.query_params),
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_id": chat_id,
+ }
+ ).page(current_page=current_page, page_size=page_size)
+ )
class ApplicationChatRecordOperateAPI(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get conversation record details"),
summary=_("Get conversation record details"),
operation_id=_("Get conversation record details"), # type: ignore
request=ApplicationChatRecordQueryAPI.get_request(),
parameters=ApplicationChatRecordQueryAPI.get_parameters(),
responses=ApplicationChatRecordQueryAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
+ PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(),
- PermissionConstants.APPLICATION_READ.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str):
- return result.success(ChatRecordOperateSerializer(
- data={
- 'workspace_id': workspace_id,
- 'application_id': application_id,
- 'chat_id': chat_id,
- 'chat_record_id': chat_record_id}).one(True))
+ return result.success(
+ ChatRecordOperateSerializer(
+ data={
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ }
+ ).one(True)
+ )
class ApplicationChatRecordAddKnowledge(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['POST'],
+ methods=["POST"],
description=_("Add to Knowledge Base"),
summary=_("Add to Knowledge Base"),
operation_id=_("Add to Knowledge Base"), # type: ignore
request=ApplicationChatRecordAddKnowledgeAPI.get_request(),
parameters=ApplicationChatRecordAddKnowledgeAPI.get_parameters(),
responses=ApplicationChatRecordAddKnowledgeAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_ADD_KNOWLEDGE.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_ADD_KNOWLEDGE.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ADD_KNOWLEDGE.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_ADD_KNOWLEDGE.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def post(self, request: Request, workspace_id: str, application_id: str):
- return result.success(ApplicationChatRecordAddKnowledgeSerializer(data = {'workspace_id': workspace_id, 'application_id': application_id, **request.data}).post_improve(
- {'workspace_id': workspace_id, 'application_id': application_id, **request.data}, request=request))
+ return result.success(
+ ApplicationChatRecordAddKnowledgeSerializer(
+ data={"workspace_id": workspace_id, "application_id": application_id, **request.data}
+ ).post_improve(
+ {"workspace_id": workspace_id, "application_id": application_id, **request.data}, request=request
+ )
+ )
class ApplicationChatRecordImprove(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['GET'],
+ methods=["GET"],
description=_("Get the list of marked paragraphs"),
summary=_("Get the list of marked paragraphs"),
operation_id=_("Get the list of marked paragraphs"), # type: ignore
request=ApplicationChatRecordQueryAPI.get_request(),
parameters=ApplicationChatRecordQueryAPI.get_parameters(),
responses=ApplicationChatRecordQueryAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str):
- return result.success(ChatRecordImproveSerializer(
- data={'workspace_id': workspace_id, 'application_id': application_id, 'chat_id': chat_id,
- 'chat_record_id': chat_record_id}).get())
+ return result.success(
+ ChatRecordImproveSerializer(
+ data={
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ }
+ ).get()
+ )
class ApplicationChatRecordImproveParagraph(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['PUT'],
+ methods=["PUT"],
description=_("Annotation"),
summary=_("Annotation"),
operation_id=_("Annotation"), # type: ignore
request=ApplicationChatRecordImproveParagraphAPI.get_request(),
parameters=ApplicationChatRecordImproveParagraphAPI.get_parameters(),
responses=ApplicationChatRecordImproveParagraphAPI.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
- def put(self, request: Request,
+ def put(
+ self,
+ request: Request,
workspace_id: str,
application_id: str,
chat_id: str,
chat_record_id: str,
knowledge_id: str,
- document_id: str):
- return result.success(ApplicationChatRecordImproveSerializer(
- data={'workspace_id': workspace_id, 'application_id': application_id, 'chat_id': chat_id,
- 'chat_record_id': chat_record_id,
- 'knowledge_id': knowledge_id, 'document_id': document_id}).improve(request.data, request=request))
+ document_id: str,
+ ):
+ return result.success(
+ ApplicationChatRecordImproveSerializer(
+ data={
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ "knowledge_id": knowledge_id,
+ "document_id": document_id,
+ }
+ ).improve(request.data, request=request)
+ )
class Operate(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
- methods=['DELETE'],
+ methods=["DELETE"],
description=_("Delete a Annotation"),
summary=_("Delete a Annotation"),
operation_id=_("Delete a Annotation"), # type: ignore
request=ApplicationChatRecordImproveParagraphAPI.Operate.get_request(),
parameters=ApplicationChatRecordImproveParagraphAPI.Operate.get_parameters(),
responses=ApplicationChatRecordImproveParagraphAPI.Operate.get_response(),
- tags=[_("Application/Conversation Log")] # type: ignore
+ tags=[_("Application/Conversation Log")], # type: ignore
+ )
+ @has_permissions(
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
+ PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
+ ViewPermission(
+ [RoleConstants.USER.get_workspace_role()],
+ [PermissionConstants.APPLICATION.get_workspace_application_permission()],
+ compare=CompareConstants.AND,
+ ),
+ RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
)
- @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_application_permission(),
- PermissionConstants.APPLICATION_CHAT_LOG_ANNOTATION.get_workspace_permission_workspace_manage_role(),
- ViewPermission([RoleConstants.USER.get_workspace_role()],
- [PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
- RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
- def delete(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str,
- knowledge_id: str,
- document_id: str, paragraph_id: str):
- return result.success(ApplicationChatRecordImproveSerializer.Operate(
- data={'chat_id': chat_id, 'chat_record_id': chat_record_id, 'workspace_id': workspace_id,
- 'application_id': application_id,
- 'knowledge_id': knowledge_id, 'document_id': document_id,
- 'paragraph_id': paragraph_id}).delete(request=request))
+ def delete(
+ self,
+ request: Request,
+ workspace_id: str,
+ application_id: str,
+ chat_id: str,
+ chat_record_id: str,
+ knowledge_id: str,
+ document_id: str,
+ paragraph_id: str,
+ ):
+ return result.success(
+ ApplicationChatRecordImproveSerializer.Operate(
+ data={
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ "workspace_id": workspace_id,
+ "application_id": application_id,
+ "knowledge_id": knowledge_id,
+ "document_id": document_id,
+ "paragraph_id": paragraph_id,
+ }
+ ).delete(request=request)
+ )
diff --git a/apps/application/views/application_stats.py b/apps/application/views/application_stats.py
index 4567156175e..c4fbe65bf9b 100644
--- a/apps/application/views/application_stats.py
+++ b/apps/application/views/application_stats.py
@@ -17,7 +17,10 @@
from django.utils.translation import gettext_lazy as _
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
class ApplicationStats(APIView):
@@ -36,7 +39,7 @@ class ApplicationStats(APIView):
PermissionConstants.APPLICATION_OVERVIEW_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
return result.success(
@@ -64,7 +67,7 @@ class TokenUsageStatistics(APIView):
PermissionConstants.APPLICATION_OVERVIEW_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
return result.success(
@@ -91,7 +94,7 @@ class TopQuestionsStatistics(APIView):
PermissionConstants.APPLICATION_OVERVIEW_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str):
return result.success(
diff --git a/apps/application/views/application_version.py b/apps/application/views/application_version.py
index 3a87a1533f0..ad3c3db9797 100644
--- a/apps/application/views/application_version.py
+++ b/apps/application/views/application_version.py
@@ -18,7 +18,10 @@
from common import result
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import ViewPermission
from common.log.log import log
@@ -38,7 +41,7 @@ class ApplicationVersionView(APIView):
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id, application_id: str):
return result.success(
@@ -62,7 +65,7 @@ class Page(APIView):
PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int):
return result.success(
@@ -87,7 +90,7 @@ class Operate(APIView):
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
def get(self, request: Request, workspace_id: str, application_id: str, application_version_id: str):
return result.success(
@@ -109,7 +112,7 @@ def get(self, request: Request, workspace_id: str, application_id: str, applicat
PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(),
ViewPermission([RoleConstants.USER.get_workspace_role()],
[PermissionConstants.APPLICATION.get_workspace_application_permission()],
- CompareConstants.AND),
+ compare=CompareConstants.AND),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role())
@log(menu='Application', operate="Modify application version information",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')),
diff --git a/apps/application/workflow/__init__.py b/apps/application/workflow/__init__.py
new file mode 100644
index 00000000000..c4f90ff4f38
--- /dev/null
+++ b/apps/application/workflow/__init__.py
@@ -0,0 +1,8 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/6/29 16:15
+ @desc:
+"""
diff --git a/apps/application/workflow/common.py b/apps/application/workflow/common.py
new file mode 100644
index 00000000000..b5bde9265f8
--- /dev/null
+++ b/apps/application/workflow/common.py
@@ -0,0 +1,212 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: workflow.py
+@date:2026/6/29 10:58
+@desc:
+"""
+
+from enum import Enum
+from typing import List, Dict
+
+from common.utils.common import group_by
+
+
+class Node:
+ def __init__(self, _id: str, _type: str, x: int, y: int, properties: dict, **kwargs):
+ """
+
+ @param _id: 节点id
+ @param _type: 类型
+ @param x: 节点x轴位置
+ @param y: 节点y轴位置
+ @param properties:
+ @param kwargs:
+ """
+ self.id = _id
+ self.type = _type
+ self.x = x
+ self.y = y
+ self.properties = properties
+ for keyword in kwargs:
+ self.__setattr__(keyword, kwargs.get(keyword))
+
+
+class Edge:
+ def __init__(self, _id: str, _type: str, sourceNodeId: str, targetNodeId: str, **keywords):
+ """
+ 线
+ @param _id: 线id
+ @param _type: 线类型
+ @param sourceNodeId:
+ @param targetNodeId:
+ @param keywords:
+ """
+ self.id = _id
+ self.type = _type
+ self.sourceNodeId = sourceNodeId
+ self.targetNodeId = targetNodeId
+ for keyword in keywords:
+ self.__setattr__(keyword, keywords.get(keyword))
+
+
+class EdgeNode:
+ edge: Edge
+ node: Node
+
+ def __init__(self, edge, node):
+ self.edge = edge
+ self.node = node
+
+
+def init_fields(workflow):
+ result = []
+ for node in workflow.nodes:
+ properties = node.properties
+ node_name = properties.get("stepName")
+ node_id = node.id
+ node_config = properties.get("config")
+ result.append(NodeField(node_id, node_name, "异常信息", "exception_message"))
+ if node_config is not None:
+ fields = node_config.get("fields")
+ if fields is not None:
+ for field in fields:
+ result.append(NodeField(node_id, node_name, field.get("label"), field.get("value")))
+ global_fields = node_config.get("globalFields")
+ if global_fields is not None:
+ for global_field in global_fields:
+ result.append(NodeField("global", "全局变量", global_field.get("label"), global_field.get("value")))
+ chat_fields = node_config.get("chatFields")
+ if chat_fields is not None:
+ for chat_field in chat_fields:
+ result.append(NodeField("chat", "chat", chat_field.get("label"), chat_field.get("value")))
+ result.sort(key=lambda f: len(f.node_name + f.value), reverse=True)
+ return result
+
+
+def get_node_parameters(node):
+ return node.properties.get("node_data", {})
+
+
+class NodeField:
+ def __init__(self, node_id, node_name, label, value):
+ self.node_id = node_id
+ self.node_name = node_name
+ self.label = label
+ self.value = value
+
+ def reset_variable(self, prompt: str):
+ userVariable = self.node_name + "." + self.value
+ systemVariable = f"context.get('{self.node_id}').get('{self.value}','')"
+ prompt = prompt.replace(userVariable, systemVariable)
+ # 全局变量:前端用 global.xxx 引用,也要能解析到 context['global']
+ if self.node_id == "global":
+ prompt = prompt.replace(f"global.{self.value}", systemVariable)
+ return prompt
+
+
+class Workflow:
+ """
+ 节点列表
+ """
+
+ nodes: List[Node]
+ """
+ 线列表
+ """
+ edges: List[Edge]
+ """
+ 节点id:node
+ """
+ node_map: Dict[str, Node]
+ """
+ 节点id:当前节点id上面的所有节点
+ """
+ up_node_map: Dict[str, List[EdgeNode]]
+ """
+ 节点id:当前节点id下面的所有节点
+ """
+ next_node_map: Dict[str, List[EdgeNode]]
+ """
+ 节点字段
+ """
+ node_field_list: List[NodeField]
+
+ def __init__(self, nodes: List[Node], edges: List[Edge]):
+ self.nodes = nodes
+ self.edges = edges
+ self.node_map = {node.id: node for node in nodes}
+
+ self.up_node_map = {
+ key: [EdgeNode(edge, self.node_map.get(edge.sourceNodeId)) for edge in edges]
+ for key, edges in group_by(edges, key=lambda edge: edge.targetNodeId).items()
+ }
+
+ self.next_node_map = {
+ key: [EdgeNode(edge, self.node_map.get(edge.targetNodeId)) for edge in edges]
+ for key, edges in group_by(edges, key=lambda edge: edge.sourceNodeId).items()
+ }
+ self.node_field_list = init_fields(self)
+
+ def get_node(self, node_id):
+ """
+ 根据node_id 获取节点信息
+ @param node_id: node_id
+ @return: 节点信息
+ """
+ return self.node_map.get(node_id)
+
+ def get_up_edge_nodes(self, node_id) -> List[EdgeNode]:
+ """
+ 根据节点id 获取当前连接前置节点和连线
+ @param node_id: 节点id
+ @return: 节点连线列表
+ """
+ return self.up_node_map.get(node_id)
+
+ def get_next_edge_nodes(self, node_id) -> List[EdgeNode]:
+ """
+ 根据节点id 获取当前连接目标节点和连线
+ @param node_id: 节点id
+ @return: 节点连线列表
+ """
+ return self.next_node_map.get(node_id)
+
+ def get_up_nodes(self, node_id) -> List[Node]:
+ """
+ 根据节点id 获取当前连接前置节点
+ @param node_id: 节点id
+ @return: 节点列表
+ """
+ return [en.node for en in self.up_node_map.get(node_id)]
+
+ def get_next_nodes(self, node_id) -> List[Node]:
+ """
+ 根据节点id 获取当前连接目标节点
+ @param node_id: 节点id
+ @return: 节点列表
+ """
+ return [en.node for en in self.next_node_map.get(node_id, [])]
+
+ def reset_prompt(self, prompt):
+ for node_field in self.node_field_list:
+ prompt = node_field.reset_variable(prompt)
+ return prompt
+
+
+class WorkflowType(Enum):
+ # 应用
+ APPLICATION = "APPLICATION"
+ # 知识库
+ KNOWLEDGE = "KNOWLEDGE"
+ # 工具
+ TOOL = "TOOL"
+
+
+def new_instance(flow_obj: Dict, workflow_type: WorkflowType = WorkflowType.APPLICATION):
+ nodes = flow_obj.get("nodes")
+ edges = flow_obj.get("edges")
+ nodes = [Node(node.get("id"), node.get("type"), **node) for node in nodes]
+ edges = [Edge(edge.get("id"), edge.get("type"), **edge) for edge in edges]
+ return Workflow(nodes, edges)
diff --git a/apps/application/workflow/compare/__init__.py b/apps/application/workflow/compare/__init__.py
new file mode 100644
index 00000000000..ce0c430e1ad
--- /dev/null
+++ b/apps/application/workflow/compare/__init__.py
@@ -0,0 +1,83 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: __init__.py.py
+ @date:2024/6/7 14:43
+ @desc:
+"""
+from typing import List
+
+from .contain_compare import ContainCompare
+from .end_with import EndWithCompare
+from .equal_compare import EqualCompare
+from .ge_compare import GECompare
+from .gt_compare import GTCompare
+from .is_not_null_compare import IsNotNullCompare
+from .is_not_true import IsNotTrueCompare
+from .is_null_compare import IsNullCompare
+from .is_true import IsTrueCompare
+from .le_compare import LECompare
+from .len_equal_compare import LenEqualCompare
+from .len_ge_compare import LenGECompare
+from .len_gt_compare import LenGTCompare
+from .len_le_compare import LenLECompare
+from .len_lt_compare import LenLTCompare
+from .lt_compare import LTCompare
+from .not_contain_compare import NotContainCompare
+from .not_equal_compare import NotEqualCompare
+from .regex_compare import RegexCompare
+from .start_with import StartWithCompare
+from .wildcard_compare import WildcardCompare
+
+_compare_handler_dict = {
+ 'is_null': IsNullCompare(),
+ 'is_not_null': IsNotNullCompare(),
+ 'contain': ContainCompare(),
+ 'not_contain': NotContainCompare(),
+ 'eq': EqualCompare(),
+ 'not_eq': NotEqualCompare(),
+ 'ge': GECompare(),
+ 'gt': GTCompare(),
+ 'le': LECompare(),
+ 'lt': LTCompare(),
+ 'len_eq': LenEqualCompare(),
+ 'len_ge': LenGECompare(),
+ 'len_gt': LenGTCompare(),
+ 'len_le': LenLECompare(),
+ 'len_lt': LenLTCompare(),
+ 'is_true': IsTrueCompare(),
+ 'is_not_true': IsNotTrueCompare(),
+ 'start_with': StartWithCompare(),
+ 'end_with': EndWithCompare(),
+ 'regex': RegexCompare(),
+ 'wildcard': WildcardCompare(),
+}
+
+
+def _compare(source_value, compare, target_value):
+ compare_handler = _compare_handler_dict.get(compare)
+ if compare_handler is None:
+ raise RuntimeError(f"Unknown compare handler '{compare}'")
+ return compare_handler.compare(source_value, compare, target_value)
+
+
+def _assertion(workflow_manage, field_list: List[str], compare: str, value):
+ try:
+ value = workflow_manage.generate_prompt(value)
+ except Exception:
+ pass
+ field_value = None
+ try:
+ field_value = workflow_manage.get_reference_field(field_list[0], field_list[1:])
+ except Exception:
+ pass
+ return _compare(field_value, compare, value)
+
+
+def do_assertion(workflow_manage, condition, condition_list):
+ b = False if condition == 'and' else True
+ for row in condition_list:
+ if _assertion(workflow_manage, row.get('field'), row.get('compare'), row.get('value')) is b:
+ return b
+ return not b
diff --git a/apps/application/workflow/compare/compare.py b/apps/application/workflow/compare/compare.py
new file mode 100644
index 00000000000..62eb4a7b910
--- /dev/null
+++ b/apps/application/workflow/compare/compare.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: compare.py
+ @date:2024/6/7 14:37
+ @desc:
+"""
+from abc import abstractmethod
+
+class Compare:
+
+ @abstractmethod
+ def compare(self, source_value, compare, target_value):
+ pass
diff --git a/apps/application/workflow/compare/contain_compare.py b/apps/application/workflow/compare/contain_compare.py
new file mode 100644
index 00000000000..cd50d543a3a
--- /dev/null
+++ b/apps/application/workflow/compare/contain_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: contain_compare.py
+ @date:2024/6/11 10:02
+ @desc:
+"""
+from .compare import Compare
+
+
+class ContainCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ target_value = str(target_value)
+
+ if isinstance(source_value, str):
+ return target_value in source_value
+ elif isinstance(source_value, list):
+ for item in source_value:
+ if str(item) == target_value:
+ return True
+ return False
+ else:
+ return target_value in str(source_value)
diff --git a/apps/application/workflow/compare/end_with.py b/apps/application/workflow/compare/end_with.py
new file mode 100644
index 00000000000..eae7e3a8a15
--- /dev/null
+++ b/apps/application/workflow/compare/end_with.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: start_with.py
+ @date:2025/10/20 10:37
+ @desc:
+"""
+from .compare import Compare
+
+
+class EndWithCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ source_value = str(source_value)
+ return source_value.endswith(str(target_value))
diff --git a/apps/application/workflow/compare/equal_compare.py b/apps/application/workflow/compare/equal_compare.py
new file mode 100644
index 00000000000..dad0cffa9fa
--- /dev/null
+++ b/apps/application/workflow/compare/equal_compare.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: equal_compare.py
+ @date:2024/6/7 14:44
+ @desc:
+"""
+from .compare import Compare
+
+
+class EqualCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ return str(source_value) == str(target_value)
diff --git a/apps/application/workflow/compare/ge_compare.py b/apps/application/workflow/compare/ge_compare.py
new file mode 100644
index 00000000000..e1cf2e7aac0
--- /dev/null
+++ b/apps/application/workflow/compare/ge_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 大于比较器
+"""
+from .compare import Compare
+
+
+class GECompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ if source_value is None:
+ return target_value is None
+
+ try:
+ return float(source_value) >= float(target_value)
+ except Exception:
+ try:
+ return str(source_value) >= str(target_value)
+ except Exception:
+ pass
+ return False
diff --git a/apps/application/workflow/compare/gt_compare.py b/apps/application/workflow/compare/gt_compare.py
new file mode 100644
index 00000000000..fab86c4bc8c
--- /dev/null
+++ b/apps/application/workflow/compare/gt_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 大于比较器
+"""
+from .compare import Compare
+
+
+class GTCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ if source_value is None:
+ return False
+
+ try:
+ return float(source_value) > float(target_value)
+ except Exception:
+ try:
+ return str(source_value) > str(target_value)
+ except Exception:
+ pass
+ return False
diff --git a/apps/application/workflow/compare/is_not_null_compare.py b/apps/application/workflow/compare/is_not_null_compare.py
new file mode 100644
index 00000000000..37fd4a72ea4
--- /dev/null
+++ b/apps/application/workflow/compare/is_not_null_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: is_not_null_compare.py
+ @date:2024/6/28 10:45
+ @desc:
+"""
+from .compare import Compare
+
+
+class IsNotNullCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return source_value is not None and len(source_value) > 0
+ except Exception:
+ return True
diff --git a/apps/application/workflow/compare/is_not_true.py b/apps/application/workflow/compare/is_not_true.py
new file mode 100644
index 00000000000..fabeec2cc41
--- /dev/null
+++ b/apps/application/workflow/compare/is_not_true.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎
+ @file: is_not_true.py
+ @date:2025/4/7 13:44
+ @desc:
+"""
+from .compare import Compare
+
+
+class IsNotTrueCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return source_value is False
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/is_null_compare.py b/apps/application/workflow/compare/is_null_compare.py
new file mode 100644
index 00000000000..240403ea605
--- /dev/null
+++ b/apps/application/workflow/compare/is_null_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: is_null_compare.py
+ @date:2024/6/28 10:45
+ @desc:
+"""
+from .compare import Compare
+
+
+class IsNullCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return source_value is None or len(source_value) == 0
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/is_true.py b/apps/application/workflow/compare/is_true.py
new file mode 100644
index 00000000000..8cb4a45a2a5
--- /dev/null
+++ b/apps/application/workflow/compare/is_true.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎
+ @file: IsTrue.py
+ @date:2025/4/7 13:38
+ @desc:
+"""
+from .compare import Compare
+
+
+class IsTrueCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return source_value is True
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/le_compare.py b/apps/application/workflow/compare/le_compare.py
new file mode 100644
index 00000000000..0ebdb394857
--- /dev/null
+++ b/apps/application/workflow/compare/le_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 小于比较器
+"""
+from .compare import Compare
+
+
+class LECompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ if source_value is None:
+ return target_value is None
+
+ try:
+ return float(source_value) <= float(target_value)
+ except Exception:
+ try:
+ return str(source_value) <= str(target_value)
+ except Exception:
+ pass
+ return False
diff --git a/apps/application/workflow/compare/len_equal_compare.py b/apps/application/workflow/compare/len_equal_compare.py
new file mode 100644
index 00000000000..98a5314a292
--- /dev/null
+++ b/apps/application/workflow/compare/len_equal_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: equal_compare.py
+ @date:2024/6/7 14:44
+ @desc:
+"""
+from .compare import Compare
+
+
+class LenEqualCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return len(source_value) == int(target_value)
+ except Exception as e:
+ return False
diff --git a/apps/application/workflow/compare/len_ge_compare.py b/apps/application/workflow/compare/len_ge_compare.py
new file mode 100644
index 00000000000..06dd566cf24
--- /dev/null
+++ b/apps/application/workflow/compare/len_ge_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 大于比较器
+"""
+from .compare import Compare
+
+
+class LenGECompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return len(source_value) >= int(target_value)
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/len_gt_compare.py b/apps/application/workflow/compare/len_gt_compare.py
new file mode 100644
index 00000000000..fae2668e0ba
--- /dev/null
+++ b/apps/application/workflow/compare/len_gt_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 大于比较器
+"""
+from .compare import Compare
+
+
+class LenGTCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return len(source_value) > int(target_value)
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/len_le_compare.py b/apps/application/workflow/compare/len_le_compare.py
new file mode 100644
index 00000000000..41b9ee9f709
--- /dev/null
+++ b/apps/application/workflow/compare/len_le_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 小于比较器
+"""
+from .compare import Compare
+
+
+class LenLECompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return len(source_value) <= int(target_value)
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/len_lt_compare.py b/apps/application/workflow/compare/len_lt_compare.py
new file mode 100644
index 00000000000..4a9b11654ca
--- /dev/null
+++ b/apps/application/workflow/compare/len_lt_compare.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 小于比较器
+"""
+from .compare import Compare
+
+
+class LenLTCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ try:
+ return len(source_value) < int(target_value)
+ except Exception:
+ return False
diff --git a/apps/application/workflow/compare/lt_compare.py b/apps/application/workflow/compare/lt_compare.py
new file mode 100644
index 00000000000..ecf8d549bfd
--- /dev/null
+++ b/apps/application/workflow/compare/lt_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: lt_compare.py
+ @date:2024/6/11 9:52
+ @desc: 小于比较器
+"""
+from .compare import Compare
+
+
+class LTCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ if source_value is None:
+ return False
+
+ try:
+ return float(source_value) < float(target_value)
+ except Exception:
+ try:
+ return str(source_value) < str(target_value)
+ except Exception:
+ pass
+ return False
diff --git a/apps/application/workflow/compare/not_contain_compare.py b/apps/application/workflow/compare/not_contain_compare.py
new file mode 100644
index 00000000000..99194e70364
--- /dev/null
+++ b/apps/application/workflow/compare/not_contain_compare.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:虎
+ @file: contain_compare.py
+ @date:2024/6/11 10:02
+ @desc:
+"""
+from .compare import Compare
+
+
+class NotContainCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ target_value = str(target_value)
+
+ if isinstance(source_value, str):
+ return target_value not in source_value
+ elif isinstance(source_value, list):
+ for item in source_value:
+ if str(item) == target_value:
+ return False
+ return True
+ else:
+ return target_value not in str(source_value)
diff --git a/apps/application/workflow/compare/not_equal_compare.py b/apps/application/workflow/compare/not_equal_compare.py
new file mode 100644
index 00000000000..f53057ebfa9
--- /dev/null
+++ b/apps/application/workflow/compare/not_equal_compare.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:wangliang181230
+ @file: not_equal_compare.py
+ @date:2026/3/17 9:41
+ @desc:
+"""
+from .compare import Compare
+
+
+class NotEqualCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ return str(source_value) != str(target_value)
diff --git a/apps/application/workflow/compare/regex_compare.py b/apps/application/workflow/compare/regex_compare.py
new file mode 100644
index 00000000000..613300e6589
--- /dev/null
+++ b/apps/application/workflow/compare/regex_compare.py
@@ -0,0 +1,35 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:wangliang181230
+ @file: regex_compare.py
+ @date:2026/3/30 12:11
+ @desc:
+"""
+import re
+
+from .compare import Compare
+from common.cache.mem_cache import MemCache
+
+match_cache = MemCache('regex', {
+ 'TIMEOUT': 3600, # 缓存有效期为 1 小时
+ 'OPTIONS': {
+ 'MAX_ENTRIES': 500, # 最多缓存 500 个条目
+ 'CULL_FREQUENCY': 10, # 达到上限时,删除约 1/10 的缓存
+ },
+})
+
+
+def compile_and_cache(regex):
+ match = match_cache.get(regex)
+ if not match:
+ match = re.compile(regex).fullmatch
+ match_cache.set(regex, match)
+ return match
+
+
+class RegexCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ match = compile_and_cache(str(target_value))
+ return bool(match(str(source_value)))
diff --git a/apps/application/workflow/compare/start_with.py b/apps/application/workflow/compare/start_with.py
new file mode 100644
index 00000000000..054ea9bd6cb
--- /dev/null
+++ b/apps/application/workflow/compare/start_with.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: start_with.py
+ @date:2025/10/20 10:37
+ @desc:
+"""
+from .compare import Compare
+
+
+class StartWithCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ source_value = str(source_value)
+ return source_value.startswith(str(target_value))
diff --git a/apps/application/workflow/compare/wildcard_compare.py b/apps/application/workflow/compare/wildcard_compare.py
new file mode 100644
index 00000000000..43c903a9360
--- /dev/null
+++ b/apps/application/workflow/compare/wildcard_compare.py
@@ -0,0 +1,38 @@
+# coding=utf-8
+"""
+ @project: maxkb
+ @Author:wangliang181230
+ @file: wildcard_compare.py
+ @date:2026/3/30 12:11
+ @desc:
+"""
+import fnmatch
+import re
+
+from .compare import Compare
+from common.cache.mem_cache import MemCache
+
+
+match_cache = MemCache('wildcard_to_regex', {
+ 'TIMEOUT': 3600, # 缓存有效期为 1 小时
+ 'OPTIONS': {
+ 'MAX_ENTRIES': 500, # 最多缓存 500 个条目
+ 'CULL_FREQUENCY': 10, # 达到上限时,删除约 1/10 的缓存
+ },
+})
+
+
+def translate_and_compile_and_cache(wildcard):
+ match = match_cache.get(wildcard)
+ if not match:
+ regex = fnmatch.translate(wildcard)
+ match = re.compile(regex).match
+ match_cache.set(wildcard, match)
+ return match
+
+class WildcardCompare(Compare):
+
+ def compare(self, source_value, compare, target_value):
+ # 转成正则,性能更高
+ match = translate_and_compile_and_cache(str(target_value))
+ return bool(match(str(source_value)))
diff --git a/apps/application/workflow/content_type.py b/apps/application/workflow/content_type.py
new file mode 100644
index 00000000000..1920ec132a8
--- /dev/null
+++ b/apps/application/workflow/content_type.py
@@ -0,0 +1,19 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: content_type.py
+ @date:2026/6/30 15:57
+ @desc:
+"""
+from enum import Enum
+
+
+class ContentType(Enum):
+ TEXT = "TEXT"
+ REASONING = "REASONING"
+ FAILURE = "FAILURE"
+ TOOL = "TOOL"
+ CONTINUE = "CONTINUE"
+ BREAK = "BREAK"
+ FORM = "FORM"
diff --git a/apps/application/workflow/i_node.py b/apps/application/workflow/i_node.py
new file mode 100644
index 00000000000..19431851073
--- /dev/null
+++ b/apps/application/workflow/i_node.py
@@ -0,0 +1,247 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: i_node.py
+@date:2026/6/29 16:41
+@desc:
+"""
+
+import time
+from enum import Enum
+from typing import Optional, Type, Callable
+
+from rest_framework import serializers
+
+from application.workflow.common import Node
+from application.workflow.message.struct.content import Content
+from application.workflow.status import Status
+from common.utils.logger import maxkb_logger
+
+
+class CancelledException(Exception):
+ """工作流取消异常"""
+
+ pass
+
+
+class Signal(str, Enum):
+ BREAK = "BREAK"
+ CONTINUE = "CONTINUE"
+ FORM = "FORM"
+ CANCELLED = "CANCELLED"
+
+
+class INode:
+ # 当前节点支持的工作流类型
+ supported_workflow_type_list = []
+ # 节点类型
+ type = None
+ # 序列化校验器
+ serializer_class: Optional[Type[serializers.Serializer]] = None
+
+ @staticmethod
+ def is_valid(data):
+ INode.serializer_class(data=data).is_valid(raise_exception=True)
+
+ def __init__(self, node, workflow_manage, get_node_parameters: Callable[[Node], dict]):
+ self.node = node
+ self.status = Status.BEFORE_RUNNING
+ self.workflow_manage = workflow_manage
+ # 节点参数
+ self.parameters = get_node_parameters(node)
+ # 节点运行时产生的数据
+ self.data = {}
+ self._completed = False
+ # ---- 锚点构造,全项目唯一的拼接点 ----
+
+ def anchor(self, *parts):
+ """
+ 通用锚点: anchor('right') → '{id}_right',
+ anchor(branch_id, 'right') → '{id}_{branch_id}_right'
+ """
+ return "_".join([self.node.id, *map(str, parts)])
+
+ def success_anchor(self):
+ """
+ 成功锚点
+ @return: 成功锚点
+ """
+ return self.anchor("right")
+
+ def fail_anchor(self):
+ """
+ 失败锚点
+ @return: 失败锚点
+ """
+ return self.branch_anchor("exception")
+
+ def branch_anchor(self, branch_id):
+ """
+ 自定义锚点
+ @param branch_id: 自定义分支id
+ @return: 自定义锚点
+ """
+ return self.anchor(branch_id, "right")
+
+ def execute(self):
+ pass
+
+ def run(self):
+ """
+ 运行节点
+ @return: 不响应数据
+ """
+ self.data["start_time"] = time.time()
+ self.status = Status.RUNNING
+ try:
+ self._run()
+ except CancelledException:
+ self.complete(Status.CANCELLED)
+ except Exception as e:
+ self.complete(Status.FAIL, error=e)
+
+ def _run(self):
+ """
+ 执行节点
+ @return:
+ """
+ self.execute()
+ self.complete(Status.SUCCESS)
+
+ def complete(self, status, anchors=None, error=None, signal: Optional[Signal] = None):
+ """
+ 节点结束调用函数
+
+ @param status: 状态
+ @param anchors 锚点信息
+ @param error: 错误信息
+ @param signal: 信号
+ @return:
+ """
+ if self._completed:
+ return
+ self._completed = True
+ self.status = status
+ if error:
+ self.data["error"] = str(error)
+ self.data["run_time"] = time.time() - self.data["start_time"]
+ if signal:
+ self.workflow_manage.signal = signal
+ anchors = []
+ if anchors is None:
+ anchors = [
+ self.success_anchor() if [Status.SUCCESS, Status.CANCELLED].__contains__(status) else self.fail_anchor()
+ ]
+ self._dispatch(anchors)
+ self.workflow_manage.assertion_end(error)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ """
+ 获取节点运行详情
+ @param index: 节点索引
+ @param position: 位置信息,用于表单节点等断点续跑场景
+ @param old_details: 旧的详情数据,用于表单节点等断点续跑场景
+ @return: 节点详情字典
+ """
+ return {
+ "node_id": self.node.id,
+ "name": self.get_node_name(),
+ "index": index,
+ "run_time": self.data.get("run_time"),
+ "type": self.type,
+ "status": self.status.value if self.status else None,
+ "error": self.data.get("error"),
+ }
+
+ def _dispatch(self, anchors):
+ """
+ 根据锚点执行下一个节点
+ @param anchors: 锚点列表
+ @return:不返回
+ """
+ edge_node_list = self.workflow_manage.workflow.get_next_edge_nodes(self.node.id) or []
+ known = {en.edge.sourceAnchorId for en in edge_node_list}
+ unknown = set(anchors) - known
+ if unknown and known:
+ maxkb_logger.warning(f"node {self.node.id}: anchors {unknown} matched no edges, known={known}")
+ self.workflow_manage.next_nodes([en.node for en in edge_node_list if en.edge.sourceAnchorId in anchors])
+
+ def get_node_id(self):
+ """
+ 获取节点id
+ @return: 节点id
+ """
+ return self.node.id
+
+ def get_node_name(self):
+ """
+ 获取节点名称
+ @return: 节点名称
+ """
+ return self.node.properties.get("stepName")
+
+ def write_context(self, key, value, append=False):
+ """
+ 将数据写入节点上下文
+ @param key: 数据key
+ @param value: 数据value
+ @param append: 是否追加
+ @return: None
+ """
+ self.workflow_manage.write_context(self.node.id, key, value, append)
+
+ def get_context(self, key):
+ """
+ 获取上下文数据 根据key
+ @param key: key
+ @return: 数据
+ """
+ return self.workflow_manage.get_context(self.node.id, key)
+
+ def get_workflow_type(self):
+ """
+ 获取工作流类型
+ @return: 工作流类型
+ """
+ return self.workflow_manage.workflow_type
+
+ def get_workflow_parameters(self):
+ """
+ 获取工作流body
+ @return: 工作流body数据
+ """
+ return self.workflow_manage.get_parameters()
+
+ def get_parameters(self):
+ """
+ 获取节点参数数据
+ @return: 节点参数数据
+ """
+ return self.parameters
+
+ def get_next_nodes(self, wf):
+ """
+ 获取下n个基点
+ @param wf: 工作流对象
+ @return: 下n个节点
+ """
+ return wf.get_next_nodes(self.get_node_id())
+
+ def write(self, message: Content):
+ self.workflow_manage.write(message)
+
+ def cancel(self):
+ """
+ 取消运行
+ @return:
+ """
+ self.status = Status.CANCELLED
+
+ def _check_cancelled(self):
+ """
+ 检查是否已取消,如果已取消则抛出 CancelledException
+ @return:
+ """
+ if self.status == Status.CANCELLED or self.workflow_manage.signal == Signal.CANCELLED:
+ raise CancelledException()
diff --git a/apps/application/workflow/loop_workflow_manage.py b/apps/application/workflow/loop_workflow_manage.py
new file mode 100644
index 00000000000..5952c995376
--- /dev/null
+++ b/apps/application/workflow/loop_workflow_manage.py
@@ -0,0 +1,57 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: loop_workflow_manage.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from typing import Dict, Optional, Callable
+
+from application.workflow.common import Workflow, WorkflowType, Node
+from application.workflow.i_node import INode
+from application.workflow.workflow_manage import WorkflowManage, CallBack
+
+
+class LoopWorkFlowManage(WorkflowManage):
+
+ def __init__(self,
+ workflow: Workflow,
+ parameters: Dict,
+ workflow_type: WorkflowType,
+ call_back: CallBack,
+ get_start_node: Callable[[Workflow, WorkflowManage], INode],
+ parent_workflow_manage: WorkflowManage,
+ loop_context: Dict = None):
+ self.parent_workflow_manage = parent_workflow_manage
+ self.loop_context = loop_context or {}
+ super().__init__(workflow, parameters, workflow_type, call_back, get_start_node)
+
+ def get_parameters(self):
+ return {**self.parameters, **self.loop_context}
+
+ def get_parent_context(self, node_id, key):
+ return self.parent_workflow_manage.get_context(node_id, key)
+
+ def generate_prompt(self, prompt):
+ prompt = self.workflow.reset_prompt(prompt)
+ prompt = self.parent_workflow_manage.workflow.reset_prompt(prompt)
+ context = {**self.context, **self.parent_workflow_manage.context}
+ from langchain_core.prompts import PromptTemplate
+ prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
+ return prompt_template.format(context=context)
+
+ def get_reference_field(self, node_id, fields):
+ """
+ 获取引用字段,先从当前工作流获取,获取不到再从父工作流获取
+ @param node_id: 节点id
+ @param fields: 字段
+ @return: 引用数据
+ """
+ # 先从当前工作流获取
+ result = super().get_reference_field(node_id, fields)
+ if result is not None:
+ return result
+
+ # 从父工作流获取
+ return self.parent_workflow_manage.get_reference_field(node_id, fields)
diff --git a/apps/application/workflow/message/aggregator/__init__.py b/apps/application/workflow/message/aggregator/__init__.py
new file mode 100644
index 00000000000..82b549143e2
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/__init__.py
@@ -0,0 +1,12 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @date:2026/7/22 16:24
+ @desc: 内容聚合器模块
+"""
+from application.workflow.message.aggregator.content_aggregator import ContentAggregator
+from application.workflow.message.aggregator.aggregator_factory import AggregatorFactory
+from application.workflow.message.aggregator.aggregation_manager import AggregationManager
+
+__all__ = ['ContentAggregator', 'AggregatorFactory', 'AggregationManager']
diff --git a/apps/application/workflow/message/aggregator/aggregation_manager.py b/apps/application/workflow/message/aggregator/aggregation_manager.py
new file mode 100644
index 00000000000..149345c9788
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/aggregation_manager.py
@@ -0,0 +1,59 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: aggregation_manager.py
+ @date:2026/7/22 16:24
+ @desc: 聚合管理器
+"""
+from typing import Dict, List
+
+from application.workflow.message.struct.content import Content
+from application.workflow.message.aggregator.aggregator_factory import AggregatorFactory
+
+
+class AggregationManager:
+ """
+ 聚合管理器
+ 管理内容块的聚合,将相同id和类型的内容合并
+ """
+
+ def __init__(self):
+ self._key_to_index: Dict[str, int] = {}
+ self._contents: List[Content] = []
+
+ @property
+ def contents(self) -> List[Content]:
+ """获取聚合后的内容列表"""
+ return self._contents
+
+ def aggregate(self, chunk: Content) -> None:
+ """
+ 聚合内容块
+
+ @param chunk: 内容块
+ """
+ key = f"{chunk.id}_{chunk.type.value if hasattr(chunk.type, 'value') else chunk.type}"
+
+ idx = self._key_to_index.get(key)
+ if idx is None:
+ # 新key
+ self._key_to_index[key] = len(self._contents)
+ self._contents.append(chunk)
+ else:
+ # 已存在,聚合
+ prev = self._contents[idx]
+ aggregator = AggregatorFactory.get_aggregator(type(prev))
+ self._contents[idx] = aggregator.aggregate(prev, chunk)
+
+ def clear(self) -> None:
+ """清空聚合器"""
+ self._contents.clear()
+ self._key_to_index.clear()
+
+ def get_contents(self) -> List[Dict]:
+ """
+ 获取所有聚合后的内容(字典格式)
+
+ @return: 内容字典列表
+ """
+ return [content.to_dict() for content in self._contents]
diff --git a/apps/application/workflow/message/aggregator/aggregator_factory.py b/apps/application/workflow/message/aggregator/aggregator_factory.py
new file mode 100644
index 00000000000..18a96461447
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/aggregator_factory.py
@@ -0,0 +1,53 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: aggregator_factory.py
+ @date:2026/7/22 16:24
+ @desc: 聚合器工厂
+"""
+from typing import Dict, Type, Optional
+
+from application.workflow.message.struct.content import Content
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.message.struct.reasoning_content import ReasoningContent
+from application.workflow.message.struct.tool_content import ToolContent
+from application.workflow.message.aggregator.content_aggregator import ContentAggregator
+from application.workflow.message.aggregator.impl.text_aggregator import TextAggregator
+from application.workflow.message.aggregator.impl.reasoning_aggregator import ReasoningAggregator
+from application.workflow.message.aggregator.impl.tool_aggregator import ToolAggregator
+
+
+class AggregatorFactory:
+ """
+ 聚合器工厂
+ 根据内容类型获取对应的聚合器
+ """
+ _aggregators: Dict[Type[Content], ContentAggregator] = {
+ TextContent: TextAggregator(),
+ ReasoningContent: ReasoningAggregator(),
+ ToolContent: ToolAggregator(),
+ }
+
+ @classmethod
+ def get_aggregator(cls, content_class: Type[Content]) -> ContentAggregator:
+ """
+ 获取聚合器
+
+ @param content_class: 内容类型
+ @return: 聚合器实例
+ @raises ValueError: 如果找不到对应的聚合器
+ """
+ aggregator = cls._aggregators.get(content_class)
+ if aggregator is None:
+ raise ValueError(f"No aggregator found for class: {content_class.__name__}")
+ return aggregator
+
+ @classmethod
+ def get_aggregator_optional(cls, content_class: Type[Content]) -> Optional[ContentAggregator]:
+ """
+ 获取聚合器(可选)
+
+ @param content_class: 内容类型
+ @return: 聚合器实例或None
+ """
+ return cls._aggregators.get(content_class)
diff --git a/apps/application/workflow/message/aggregator/content_aggregator.py b/apps/application/workflow/message/aggregator/content_aggregator.py
new file mode 100644
index 00000000000..fb3def1f997
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/content_aggregator.py
@@ -0,0 +1,45 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: content_aggregator.py
+ @date:2026/7/22 16:24
+ @desc: 内容聚合器接口
+"""
+from abc import ABC, abstractmethod
+from typing import TypeVar, Generic
+
+from application.workflow.message.struct.content import Content
+
+T = TypeVar('T', bound=Content)
+
+
+class ContentAggregator(ABC, Generic[T]):
+ """
+ 内容聚合器接口
+ 用于合并相同类型的流式内容块
+ """
+
+ @abstractmethod
+ def aggregate(self, prev: T, chunk: T) -> T:
+ """
+ 聚合两个内容块
+
+ @param prev: 之前的内容
+ @param chunk: 新的内容块
+ @return: 合并后的内容
+ """
+ pass
+
+ def merge_base_fields(self, prev: T, chunk: T, result: T) -> None:
+ """
+ 合并基础字段
+
+ @param prev: 之前的内容
+ @param chunk: 新的内容块
+ @param result: 结果对象
+ """
+ result.id = chunk.id if chunk.id else prev.id
+ result.status = chunk.status if chunk.status else prev.status
+ result.node_info = chunk.node_info if chunk.node_info else prev.node_info
+ result.position = chunk.position if chunk.position else prev.position
+ result.extra = chunk.extra if chunk.extra else prev.extra
diff --git a/apps/application/workflow/message/aggregator/impl/__init__.py b/apps/application/workflow/message/aggregator/impl/__init__.py
new file mode 100644
index 00000000000..5b6b3cfe902
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/impl/__init__.py
@@ -0,0 +1,12 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @date:2026/7/22 16:24
+ @desc: 聚合器实现模块
+"""
+from application.workflow.message.aggregator.impl.text_aggregator import TextAggregator
+from application.workflow.message.aggregator.impl.reasoning_aggregator import ReasoningAggregator
+from application.workflow.message.aggregator.impl.tool_aggregator import ToolAggregator
+
+__all__ = ['TextAggregator', 'ReasoningAggregator', 'ToolAggregator']
diff --git a/apps/application/workflow/message/aggregator/impl/reasoning_aggregator.py b/apps/application/workflow/message/aggregator/impl/reasoning_aggregator.py
new file mode 100644
index 00000000000..c278f6a6887
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/impl/reasoning_aggregator.py
@@ -0,0 +1,44 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: reasoning_aggregator.py
+ @date:2026/7/22 16:24
+ @desc: ReasoningContent 聚合器
+"""
+from application.workflow.message.aggregator.content_aggregator import ContentAggregator
+from application.workflow.message.struct.reasoning_content import ReasoningContent
+
+
+class ReasoningAggregator(ContentAggregator[ReasoningContent]):
+ """
+ 推理内容聚合器
+ 用于合并流式推理内容块
+ """
+
+ def aggregate(self, prev: ReasoningContent, chunk: ReasoningContent) -> ReasoningContent:
+ """
+ 聚合推理内容
+
+ @param prev: 之前的内容
+ @param chunk: 新的内容块
+ @return: 合并后的内容
+ """
+ if prev is None:
+ return chunk
+
+ # 合并 content
+ prev_content = prev.content if prev.content else ""
+ chunk_content = chunk.content if chunk.content else ""
+ merged_content = prev_content + chunk_content
+
+ # 合并 status: 优先使用 chunk 的,否则使用 prev 的
+ merged_status = chunk.status if chunk.status else prev.status
+
+ # 合并基础字段
+ merged_id = chunk.id if chunk.id else prev.id
+ merged_node_info = chunk.node_info if chunk.node_info else prev.node_info
+ merged_position = chunk.position if chunk.position else prev.position
+
+ result = ReasoningContent(merged_id, merged_content, merged_status, merged_node_info, merged_position)
+
+ return result
diff --git a/apps/application/workflow/message/aggregator/impl/text_aggregator.py b/apps/application/workflow/message/aggregator/impl/text_aggregator.py
new file mode 100644
index 00000000000..af519bad38e
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/impl/text_aggregator.py
@@ -0,0 +1,44 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: text_aggregator.py
+ @date:2026/7/22 16:24
+ @desc: TextContent 聚合器
+"""
+from application.workflow.message.aggregator.content_aggregator import ContentAggregator
+from application.workflow.message.struct.text_content import TextContent
+
+
+class TextAggregator(ContentAggregator[TextContent]):
+ """
+ 文本内容聚合器
+ 用于合并流式文本内容块
+ """
+
+ def aggregate(self, prev: TextContent, chunk: TextContent) -> TextContent:
+ """
+ 聚合文本内容
+
+ @param prev: 之前的内容
+ @param chunk: 新的内容块
+ @return: 合并后的内容
+ """
+ if prev is None:
+ return chunk
+
+ # 合并 content
+ prev_content = prev.content if prev.content else ""
+ chunk_content = chunk.content if chunk.content else ""
+ merged_content = prev_content + chunk_content
+
+ # 合并 status: 优先使用 chunk 的,否则使用 prev 的
+ merged_status = chunk.status if chunk.status else prev.status
+
+ # 合并基础字段
+ merged_id = chunk.id if chunk.id else prev.id
+ merged_node_info = chunk.node_info if chunk.node_info else prev.node_info
+ merged_position = chunk.position if chunk.position else prev.position
+
+ result = TextContent(merged_id, merged_content, merged_status, merged_node_info, merged_position)
+
+ return result
diff --git a/apps/application/workflow/message/aggregator/impl/tool_aggregator.py b/apps/application/workflow/message/aggregator/impl/tool_aggregator.py
new file mode 100644
index 00000000000..029ddc0224b
--- /dev/null
+++ b/apps/application/workflow/message/aggregator/impl/tool_aggregator.py
@@ -0,0 +1,53 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: tool_aggregator.py
+ @date:2026/7/22 16:24
+ @desc: ToolContent 聚合器
+"""
+from application.workflow.message.aggregator.content_aggregator import ContentAggregator
+from application.workflow.message.struct.tool_content import ToolContent
+
+
+class ToolAggregator(ContentAggregator[ToolContent]):
+ """
+ 工具内容聚合器
+ 用于合并流式工具调用内容块
+ """
+
+ def aggregate(self, prev: ToolContent, chunk: ToolContent) -> ToolContent:
+ """
+ 聚合工具内容
+
+ @param prev: 之前的内容
+ @param chunk: 新的内容块
+ @return: 合并后的内容
+ """
+ if prev is None:
+ return chunk
+
+ # 合并 content (tool_name)
+ prev_content = prev.content if prev.content else ""
+ chunk_content = chunk.content if chunk.content else ""
+ merged_content = chunk_content if chunk_content else prev_content
+
+ # 合并 arguments
+ prev_arguments = prev.arguments if prev.arguments else ""
+ chunk_arguments = chunk.arguments if chunk.arguments else ""
+ merged_arguments = prev_arguments + chunk_arguments
+
+ # 合并 result
+ prev_result = prev.result if prev.result else ""
+ chunk_result = chunk.result if chunk.result else ""
+ merged_result = prev_result + chunk_result
+
+ # 合并基础字段
+ merged_id = chunk.id if chunk.id else prev.id
+ merged_status = chunk.status if chunk.status else prev.status
+ merged_node_info = chunk.node_info if chunk.node_info else prev.node_info
+ merged_position = chunk.position if chunk.position else prev.position
+
+ result = ToolContent(merged_id, merged_content, merged_arguments, merged_result,
+ merged_status, merged_node_info, merged_position)
+
+ return result
diff --git a/apps/application/workflow/message/struct/content.py b/apps/application/workflow/message/struct/content.py
new file mode 100644
index 00000000000..ed57f6bd8a3
--- /dev/null
+++ b/apps/application/workflow/message/struct/content.py
@@ -0,0 +1,63 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: content.py
+ @date:2026/6/30 15:38
+ @desc:
+"""
+from enum import Enum
+from typing import Optional
+
+from application.workflow.content_type import ContentType
+from application.workflow.status import Status
+
+
+class NodeInfo:
+ def __init__(self, _id: str, name: str, status: Status):
+ self.id = _id
+ self.name = name
+ self.status = status
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'name': self.name,
+ 'status': self.status.value if hasattr(self.status, 'value') else str(self.status),
+ }
+
+
+class Position:
+ def __init__(self, _id: str, index: Optional[int] = None, children: Optional['Position'] = None):
+ self.id = _id
+ self.index = index
+ self.children = children
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'index': self.index,
+ 'children': self.children.to_dict() if self.children else None,
+ }
+
+
+class Content:
+ def __init__(self, _id, status: Status, _type: ContentType, node_info: NodeInfo, position: Position, **kwargs):
+ self.id = _id
+ self.status = status
+ self.type = _type
+ self.node_info = node_info
+ self.position = position
+ self.extra = kwargs
+
+ def to_dict(self):
+ result = {
+ 'id': self.id,
+ 'type': self.type.value if hasattr(self.type, 'value') else str(self.type),
+ 'status': self.status.value if hasattr(self.status, 'value') else str(self.status),
+ 'node_info': self.node_info.to_dict() if self.node_info else None,
+ 'position': self.position.to_dict() if self.position else None,
+ }
+ if self.extra:
+ result.update(self.extra)
+ return result
diff --git a/apps/application/workflow/message/struct/failure_content.py b/apps/application/workflow/message/struct/failure_content.py
new file mode 100644
index 00000000000..b867c21c967
--- /dev/null
+++ b/apps/application/workflow/message/struct/failure_content.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: failure_content.py
+ @date:2026/7/27 11:14
+ @desc:
+"""
+from typing import Optional
+
+from application.workflow.content_type import ContentType
+from application.workflow.message.struct.content import Content, NodeInfo, Position
+from application.workflow.status import Status
+
+
+class FailureContent(Content):
+ def __init__(self, _id, content: str, status: Status, node_info: Optional[NodeInfo], position: Optional[Position],
+ **kwargs):
+ self.content = content
+ super().__init__(_id, status, ContentType.FAILURE, node_info, position, **kwargs)
+
+ def to_dict(self):
+ result = super().to_dict()
+ result['content'] = self.content
+ return result
diff --git a/apps/application/workflow/message/struct/form_content.py b/apps/application/workflow/message/struct/form_content.py
new file mode 100644
index 00000000000..080837d409c
--- /dev/null
+++ b/apps/application/workflow/message/struct/form_content.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: form_content.py
+ @date:2026/7/6 15:30
+ @desc:
+"""
+from typing import List, Dict, Optional
+
+from application.workflow.content_type import ContentType
+from application.workflow.message.struct.content import Content, NodeInfo, Position
+from application.workflow.status import Status
+
+
+class FormContent(Content):
+ def __init__(self, _id, form_field_list: List[Dict], form_content_format: str,
+ is_submit: bool, status: Status, node_info: NodeInfo, position: Position,
+ form_data: Optional[Dict] = None, **kwargs):
+ self.form_field_list = form_field_list
+ self.form_content_format = form_content_format
+ self.is_submit = is_submit
+ self.form_data = form_data or {}
+ super().__init__(_id, status, ContentType.FORM, node_info, position, **kwargs)
+
+ def to_dict(self):
+ result = super().to_dict()
+ result['form_field_list'] = self.form_field_list
+ result['form_content_format'] = self.form_content_format
+ result['is_submit'] = self.is_submit
+ result['form_data'] = self.form_data
+ return result
diff --git a/apps/application/workflow/message/struct/reasoning_content.py b/apps/application/workflow/message/struct/reasoning_content.py
new file mode 100644
index 00000000000..e7903d2e188
--- /dev/null
+++ b/apps/application/workflow/message/struct/reasoning_content.py
@@ -0,0 +1,22 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: reasoning_content.py
+ @date:2026/6/30 16:07
+ @desc:
+"""
+from application.workflow.content_type import ContentType
+from application.workflow.message.struct.content import Content, NodeInfo, Position
+from application.workflow.status import Status
+
+
+class ReasoningContent(Content):
+ def __init__(self, _id, content: str, status: Status, node_info: NodeInfo, position: Position, **kwargs):
+ self.content = content
+ super().__init__(_id, status, ContentType.REASONING, node_info, position, **kwargs)
+
+ def to_dict(self):
+ result = super().to_dict()
+ result['content'] = self.content
+ return result
diff --git a/apps/application/workflow/message/struct/text_content.py b/apps/application/workflow/message/struct/text_content.py
new file mode 100644
index 00000000000..a2691778ac4
--- /dev/null
+++ b/apps/application/workflow/message/struct/text_content.py
@@ -0,0 +1,22 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: text_content.py
+ @date:2026/6/30 16:03
+ @desc:
+"""
+from application.workflow.content_type import ContentType
+from application.workflow.message.struct.content import Content, NodeInfo, Position
+from application.workflow.status import Status
+
+
+class TextContent(Content):
+ def __init__(self, _id, content: str, status: Status, node_info: NodeInfo, position: Position, **kwargs):
+ self.content = content
+ super().__init__(_id, status, ContentType.TEXT, node_info, position, **kwargs)
+
+ def to_dict(self):
+ result = super().to_dict()
+ result['content'] = self.content
+ return result
diff --git a/apps/application/workflow/message/struct/tool_content.py b/apps/application/workflow/message/struct/tool_content.py
new file mode 100644
index 00000000000..3f89ea6d04b
--- /dev/null
+++ b/apps/application/workflow/message/struct/tool_content.py
@@ -0,0 +1,27 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: tool_content.py
+ @date:2026/6/30 16:17
+ @desc:
+"""
+from application.workflow.content_type import ContentType
+from application.workflow.message.struct.content import Content, NodeInfo, Position
+from application.workflow.status import Status
+
+
+class ToolContent(Content):
+ def __init__(self, _id, tool_name: str, arguments: str, result: str, status: Status, node_info: NodeInfo,
+ position: Position, **kwargs):
+ self.content = tool_name
+ self.arguments = arguments
+ self.result = result
+ super().__init__(_id, status, ContentType.TOOL, node_info, position, **kwargs)
+
+ def to_dict(self):
+ result = super().to_dict()
+ result['content'] = self.content
+ result['arguments'] = self.arguments
+ result['result'] = self.result
+ return result
diff --git a/apps/application/workflow/message_queue.py b/apps/application/workflow/message_queue.py
new file mode 100644
index 00000000000..6a31b565ed5
--- /dev/null
+++ b/apps/application/workflow/message_queue.py
@@ -0,0 +1,606 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: message_queue.py
+ @date:2026/7/27 10:10
+ @desc: 消息队列管理,用于流式响应的消息存储和消费
+ 支持多消费者、断线重连、消息持久化
+"""
+import bisect
+import fnmatch
+import json
+import socket
+import threading
+import time
+from abc import ABC, abstractmethod
+from enum import Enum
+from typing import Any, Callable, List, Optional, Tuple
+
+from common.utils.logger import maxkb_logger
+
+DEFAULT_TTL = 3600
+DEFAULT_BATCH_SIZE = 100
+DEFAULT_BLOCK_TIMEOUT_MS = 5000
+MAX_CONSECUTIVE_ERRORS = 5
+# XREAD block 窗口相对 socket_timeout 的安全比例,保证服务端先返回、客户端后超时
+BLOCK_SAFETY_RATIO = 0.6
+
+
+def _benign_timeouts() -> tuple:
+ """
+ 阻塞读读空窗口时的超时属于正常现象,不能计入故障预算。
+ redis.exceptions.TimeoutError 与内建 TimeoutError/socket.timeout 都要覆盖。
+ """
+ candidates = [TimeoutError, socket.timeout]
+ try:
+ from redis.exceptions import TimeoutError as RedisTimeoutError
+ candidates.append(RedisTimeoutError)
+ except ImportError:
+ pass
+ return tuple({c for c in candidates if isinstance(c, type)})
+
+
+BENIGN_TIMEOUTS = _benign_timeouts()
+
+
+class MessageQueueError(Exception):
+ """
+ 队列后端不可用。
+
+ 刻意与"队列不存在"区分开:Redis 抖动不应该被上层误判成会话已失效。
+ """
+
+
+class MessageStatus(str, Enum):
+ RUNNING = "RUNNING"
+ SUCCESS = "SUCCESS"
+ FAIL = "FAIL"
+ CANCELLED = "CANCELLED"
+
+
+def parse_stream_id(value: Any) -> Tuple[int, int]:
+ """
+ 把 Redis Stream ID ("1699999999999-0") 解析成可比较的元组。
+ 非法值一律退化成 (0, 0),即"从头开始"。
+ """
+ if value is None:
+ return 0, 0
+ if isinstance(value, (bytes, bytearray)):
+ value = value.decode("utf-8")
+ value = str(value)
+ if not value or value == "0":
+ return 0, 0
+ if value == "$":
+ return (1 << 63) - 1, 0
+ parts = value.split("-", 1)
+ try:
+ ms = int(parts[0])
+ seq = int(parts[1]) if len(parts) > 1 and parts[1] else 0
+ return ms, seq
+ except (TypeError, ValueError):
+ return 0, 0
+
+
+class IMessageQueue(ABC):
+ """消息队列接口"""
+
+ @abstractmethod
+ def exists(self, queue_id: str) -> bool:
+ pass
+
+ @abstractmethod
+ def produce(self, queue_id: str, message: Any, ttl: int = None) -> None:
+ pass
+
+ @abstractmethod
+ def produce_done(self, queue_id: str, ttl: int = None) -> None:
+ pass
+
+ @abstractmethod
+ def is_done(self, queue_id: str) -> bool:
+ pass
+
+ @abstractmethod
+ def consume(
+ self,
+ queue_id: str,
+ start_id: str = "0",
+ on_message: Optional[Callable[[str, str], None]] = None,
+ on_done: Optional[Callable[[], None]] = None,
+ timeout: float = 300,
+ should_stop: Optional[Callable[[], bool]] = None,
+ ) -> None:
+ """
+ 消费消息,阻塞直到队列结束 / 超时 / 被取消。
+
+ @param start_id: 起始消息ID,"0" 表示从头;语义为"返回 ID 严格大于 start_id 的消息"
+ @param on_message: 回调 (message_id, message_data)
+ @param on_done: 结束回调,保证有且只调用一次
+ @param timeout: 最长消费时间(秒)
+ @param should_stop: 取消钩子,返回 True 则立即结束消费
+ """
+ pass
+
+ @abstractmethod
+ def get_messages(self, queue_id: str, start_id: str = "0", count: int = 100) -> list:
+ """拉取 ID 严格大于 start_id 的历史消息,用于断线重连补发。"""
+ pass
+
+ @abstractmethod
+ def delete(self, queue_id: str) -> None:
+ pass
+
+ @abstractmethod
+ def clear_by_pattern(self, pattern: str) -> int:
+ pass
+
+
+_instances: dict[str, IMessageQueue] = {}
+_instances_lock = threading.Lock()
+
+
+class InMemoryMessageQueue(IMessageQueue):
+ """内存消息队列,仅适用于单进程环境(多 worker 下生产者和消费者可能不在同一进程)"""
+
+ def __init__(self, default_ttl: int = DEFAULT_TTL):
+ # queue_id -> (sort_keys, items),两个列表下标一一对应,便于 bisect 定位游标
+ self._sort_keys: dict[str, List[Tuple[int, int]]] = {}
+ self._items: dict[str, List[Tuple[str, str]]] = {}
+ self._done_flags: dict[str, bool] = {}
+ self._last_id: dict[str, Tuple[int, int]] = {}
+ self._expire_at: dict[str, float] = {}
+ self._default_ttl = default_ttl
+ # 用 RLock,避免 clear_by_pattern -> delete 这类内部复用造成自死锁
+ self._cond = threading.Condition(threading.RLock())
+
+ # ---------- 内部工具 ----------
+
+ def _next_id(self, queue_id: str) -> str:
+ """生成与 Redis Stream 同构的 ID,保证两种实现的 start_id 可以互换。"""
+ now = int(time.time() * 1000)
+ last_ms, last_seq = self._last_id.get(queue_id, (0, 0))
+ new_id = (now, 0) if now > last_ms else (last_ms, last_seq + 1)
+ self._last_id[queue_id] = new_id
+ return f"{new_id[0]}-{new_id[1]}"
+
+ def _drop(self, queue_id: str) -> None:
+ """调用方必须已持有锁。"""
+ self._sort_keys.pop(queue_id, None)
+ self._items.pop(queue_id, None)
+ self._done_flags.pop(queue_id, None)
+ self._last_id.pop(queue_id, None)
+ self._expire_at.pop(queue_id, None)
+
+ def _purge_if_expired(self, queue_id: str) -> None:
+ """调用方必须已持有锁。"""
+ expire_at = self._expire_at.get(queue_id)
+ if expire_at is not None and expire_at <= time.time():
+ self._drop(queue_id)
+
+ def _read_after(self, cursor: Tuple[int, int], queue_id: str) -> Tuple[List[Tuple[str, str]], bool]:
+ """
+ 原子地返回 (游标之后的消息, 是否已结束)。
+
+ 两个值必须在同一次加锁内读取:生产者是先 produce 再 produce_done,
+ 所以只要读到 done=True,就说明所有消息在本次快照里已经全部可见,
+ 不存在"最后一条消息还没写进来就判定结束"的竞态。
+ """
+ with self._cond:
+ self._purge_if_expired(queue_id)
+ keys = self._sort_keys.get(queue_id)
+ done = self._done_flags.get(queue_id, False)
+ if not keys:
+ return [], done
+ start = bisect.bisect_right(keys, cursor)
+ return list(self._items[queue_id][start:]), done
+
+ def purge_expired(self) -> int:
+ """惰性清理兜底:建议由定时任务周期调用,防止用户关页面后队列常驻内存。"""
+ now = time.time()
+ with self._cond:
+ expired = [k for k, exp in self._expire_at.items() if exp <= now]
+ for k in expired:
+ self._drop(k)
+ return len(expired)
+
+ # ---------- 接口实现 ----------
+
+ def exists(self, queue_id: str) -> bool:
+ with self._cond:
+ self._purge_if_expired(queue_id)
+ return queue_id in self._items
+
+ def produce(self, queue_id: str, message: Any, ttl: int = None) -> None:
+ data = message if isinstance(message, str) else json.dumps(message, ensure_ascii=False)
+ with self._cond:
+ self._purge_if_expired(queue_id)
+ if queue_id not in self._items:
+ self._items[queue_id] = []
+ self._sort_keys[queue_id] = []
+ msg_id = self._next_id(queue_id)
+ self._sort_keys[queue_id].append(parse_stream_id(msg_id))
+ self._items[queue_id].append((msg_id, data))
+ # 滑动过期,长会话不会中途被清掉
+ self._expire_at[queue_id] = time.time() + (ttl or self._default_ttl)
+ self._cond.notify_all()
+
+ def produce_done(self, queue_id: str, ttl: int = None) -> None:
+ with self._cond:
+ self._done_flags[queue_id] = True
+ self._expire_at[queue_id] = time.time() + (ttl or self._default_ttl)
+ self._cond.notify_all()
+
+ def is_done(self, queue_id: str) -> bool:
+ with self._cond:
+ return self._done_flags.get(queue_id, False)
+
+ def consume(
+ self,
+ queue_id: str,
+ start_id: str = "0",
+ on_message: Optional[Callable[[str, str], None]] = None,
+ on_done: Optional[Callable[[], None]] = None,
+ timeout: float = 300,
+ should_stop: Optional[Callable[[], bool]] = None,
+ ) -> None:
+ deadline = time.monotonic() + timeout
+ cursor = parse_stream_id(start_id)
+ try:
+ while True:
+ if should_stop is not None and should_stop():
+ break
+
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ maxkb_logger.warning(f"MessageQueue consume timeout: {queue_id}")
+ break
+
+ batch, done = self._read_after(cursor, queue_id)
+ if batch:
+ for msg_id, msg_data in batch:
+ if on_message:
+ on_message(msg_id, msg_data)
+ cursor = parse_stream_id(msg_id)
+ continue
+ if done:
+ break
+
+ # 等待生产者唤醒,而不是固定 sleep,降低首字延迟
+ with self._cond:
+ self._cond.wait(min(0.05, remaining))
+ finally:
+ if on_done:
+ on_done()
+
+ def get_messages(self, queue_id: str, start_id: str = "0", count: int = 100) -> list:
+ batch, _ = self._read_after(parse_stream_id(start_id), queue_id)
+ return [{"id": mid, "data": data} for mid, data in batch[:count]]
+
+ def delete(self, queue_id: str) -> None:
+ with self._cond:
+ self._drop(queue_id)
+ self._cond.notify_all()
+
+ def clear_by_pattern(self, pattern: str) -> int:
+ with self._cond:
+ keys = [k for k in self._items if fnmatch.fnmatch(k, pattern)]
+ for k in keys:
+ self._drop(k)
+ self._cond.notify_all()
+ return len(keys)
+
+
+class RedisStreamMessageQueue(IMessageQueue):
+ """
+ Redis Stream 消息队列
+ 支持多消费者、断线重连、消息持久化
+ """
+
+ def __init__(self, namespace: str = "mq", redis_client=None, alias: str = "default"):
+ self._namespace = namespace
+ self._redis = redis_client
+ self._alias = alias
+ self._resolved = None
+ self._default_ttl = DEFAULT_TTL
+ self._batch_size = DEFAULT_BATCH_SIZE
+ self._block_timeout = DEFAULT_BLOCK_TIMEOUT_MS
+ self._block_limit = None
+
+ # ---------- 连接 ----------
+
+ def _resolve_redis(self):
+ """
+ django-redis 的 cache.client 是 DefaultClient 包装层,没有 xadd/xread,
+ 必须取出底层 redis-py 连接。
+ """
+ try:
+ from django_redis import get_redis_connection
+ return get_redis_connection(self._alias)
+ except ImportError:
+ pass
+ except Exception as e:
+ maxkb_logger.warning(f"get_redis_connection({self._alias}) failed: {e}")
+
+ from django.core.cache import cache
+ client = getattr(cache, "client", None)
+ if client is not None and hasattr(client, "get_client"):
+ return client.get_client(write=True)
+ if client is not None and hasattr(client, "xadd"):
+ return client
+ raise MessageQueueError("当前 CACHES 后端不是 django-redis,无法使用 RedisStreamMessageQueue")
+
+ def _get_redis(self):
+ if self._redis is not None:
+ return self._redis
+ if self._resolved is None:
+ self._resolved = self._resolve_redis()
+ return self._resolved
+
+ def ping(self) -> bool:
+ """真实探活,不吞异常,供 create_message_queue 判断是否降级。"""
+ return bool(self._get_redis().ping())
+
+ def _block_limit_ms(self, redis) -> int:
+ """
+ XREAD 的 block 是让服务端挂起的时长,而客户端等响应用的是连接的 socket_timeout。
+ 一旦 block >= socket_timeout,空窗口必然先触发 "Timeout reading from socket"
+ 并导致 redis-py 断连重建。这里按连接实际配置反推一个安全上限。
+ """
+ if self._block_limit is not None:
+ return self._block_limit
+
+ limit = self._block_timeout
+ try:
+ kwargs = getattr(getattr(redis, "connection_pool", None), "connection_kwargs", None) or {}
+ socket_timeout = kwargs.get("socket_timeout")
+ if socket_timeout:
+ safe = int(float(socket_timeout) * 1000 * BLOCK_SAFETY_RATIO)
+ limit = max(100, min(limit, safe))
+ if limit < self._block_timeout:
+ maxkb_logger.info(
+ f"MessageQueue: socket_timeout={socket_timeout}s,XREAD block 收敛到 {limit}ms"
+ )
+ except Exception as e:
+ maxkb_logger.warning(f"MessageQueue: 无法读取 socket_timeout,沿用默认 block: {e}")
+
+ self._block_limit = limit
+ return limit
+
+ # ---------- 编解码 ----------
+
+ def _key(self, queue_id: str) -> str:
+ return f"{self._namespace}:{queue_id}"
+
+ def _done_key(self, queue_id: str) -> str:
+ return f"{self._namespace}:{queue_id}:done"
+
+ def _encode(self, value: Any) -> str:
+ if isinstance(value, str):
+ return value
+ return json.dumps(value, ensure_ascii=False)
+
+ @staticmethod
+ def _decode_bytes(data: Any) -> str:
+ return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data)
+
+ def _decode_field(self, fields: dict, name: str = "data") -> str:
+ """同时兼容 decode_responses=True / False 两种 client 配置。"""
+ if not fields:
+ return ""
+ val = fields.get(name)
+ if val is None:
+ val = fields.get(name.encode("utf-8"))
+ if val is None:
+ return ""
+ return self._decode_bytes(val)
+
+ def _emit(self, messages, on_message) -> Optional[str]:
+ last_id = None
+ for msg_id_raw, fields in messages:
+ msg_id = self._decode_bytes(msg_id_raw)
+ if on_message:
+ on_message(msg_id, self._decode_field(fields))
+ last_id = msg_id
+ return last_id
+
+ # ---------- 接口实现 ----------
+
+ def exists(self, queue_id: str) -> bool:
+ try:
+ return self._get_redis().exists(self._key(queue_id)) > 0
+ except Exception as e:
+ raise MessageQueueError(f"exists({queue_id}) failed: {e}") from e
+
+ def produce(self, queue_id: str, message: Any, ttl: int = None) -> None:
+ key = self._key(queue_id)
+ try:
+ # pipeline 合并 xadd + expire,流式场景每个 token 少一次 RTT;
+ # 同时每次都续期,避免长会话中途整条 stream 过期
+ pipe = self._get_redis().pipeline(transaction=False)
+ pipe.xadd(key, {"data": self._encode(message)})
+ pipe.expire(key, ttl or self._default_ttl)
+ pipe.execute()
+ except Exception as e:
+ maxkb_logger.error(f"MessageQueue produce error [{queue_id}]: {e}")
+ raise MessageQueueError(f"produce({queue_id}) failed: {e}") from e
+
+ def produce_done(self, queue_id: str, ttl: int = None) -> None:
+ try:
+ self._get_redis().set(self._done_key(queue_id), "1", ex=ttl or self._default_ttl)
+ except Exception as e:
+ maxkb_logger.error(f"MessageQueue produce_done error [{queue_id}]: {e}")
+ raise MessageQueueError(f"produce_done({queue_id}) failed: {e}") from e
+
+ def is_done(self, queue_id: str) -> bool:
+ try:
+ return self._get_redis().exists(self._done_key(queue_id)) > 0
+ except Exception as e:
+ raise MessageQueueError(f"is_done({queue_id}) failed: {e}") from e
+
+ def consume(
+ self,
+ queue_id: str,
+ start_id: str = "0",
+ on_message: Optional[Callable[[str, str], None]] = None,
+ on_done: Optional[Callable[[], None]] = None,
+ timeout: float = 300,
+ should_stop: Optional[Callable[[], bool]] = None,
+ ) -> None:
+ key = self._key(queue_id)
+ deadline = time.monotonic() + timeout
+ current_id = start_id or "0"
+ errors = 0
+
+ try:
+ redis = self._get_redis()
+ while True:
+ if should_stop is not None and should_stop():
+ break
+
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ maxkb_logger.warning(f"MessageQueue consume timeout: {queue_id}")
+ break
+
+ try:
+ # block 必须 >= 1:block=0 在 Redis 里是"无限阻塞",会挂死消费线程
+ block_ms = max(1, min(int(remaining * 1000), self._block_limit_ms(redis)))
+ result = redis.xread({key: current_id}, count=self._batch_size, block=block_ms)
+ errors = 0
+ except BENIGN_TIMEOUTS:
+ # 阻塞窗口内没有新消息而已,不是故障:不计入熔断预算,也不退避。
+ # 游标未推进,Stream 可重复读,不会丢消息。
+ result = None
+ except Exception as e:
+ errors += 1
+ maxkb_logger.error(
+ f"MessageQueue consume error [{queue_id}] ({errors}/{MAX_CONSECUTIVE_ERRORS}): {e}"
+ )
+ if errors >= MAX_CONSECUTIVE_ERRORS:
+ break
+ time.sleep(min(0.1 * errors, 1.0))
+ continue
+
+ if result:
+ for _, messages in result:
+ last_id = self._emit(messages, on_message)
+ if last_id:
+ current_id = last_id
+ continue
+
+ # 阻塞窗口内没有新消息,检查是否已结束
+ try:
+ finished = self.is_done(queue_id)
+ except MessageQueueError:
+ continue
+ if finished:
+ # done 标记是在所有 produce 之后写的,这里把尾部残留全部读干净
+ self._flush_remaining(redis, key, current_id, on_message)
+ break
+ except MessageQueueError as e:
+ maxkb_logger.error(f"MessageQueue consume aborted [{queue_id}]: {e}")
+ finally:
+ if on_done:
+ on_done()
+
+ def _flush_remaining(self, redis, key: str, current_id: str, on_message: Optional[Callable]) -> str:
+ """
+ 用非阻塞 xread 循环读完尾部消息。
+ 不传 block 参数即为非阻塞,且 xread 天然是"ID 严格大于游标"的语义,
+ 无需依赖 Redis 6.2+ 的 "(" 排他区间写法。
+ """
+ cursor = current_id
+ while True:
+ try:
+ result = redis.xread({key: cursor}, count=self._batch_size)
+ except Exception as e:
+ maxkb_logger.error(f"MessageQueue flush error [{key}]: {e}")
+ break
+ if not result:
+ break
+ total = 0
+ for _, messages in result:
+ total += len(messages)
+ last_id = self._emit(messages, on_message)
+ if last_id:
+ cursor = last_id
+ if total < self._batch_size:
+ break
+ return cursor
+
+ def get_messages(self, queue_id: str, start_id: str = "0", count: int = 100) -> list:
+ try:
+ result = self._get_redis().xread({self._key(queue_id): start_id or "0"}, count=count)
+ except Exception as e:
+ raise MessageQueueError(f"get_messages({queue_id}) failed: {e}") from e
+ return [
+ {"id": self._decode_bytes(mid), "data": self._decode_field(fields)}
+ for _, messages in (result or [])
+ for mid, fields in messages
+ ]
+
+ def delete(self, queue_id: str) -> None:
+ try:
+ self._get_redis().delete(self._key(queue_id), self._done_key(queue_id))
+ except Exception as e:
+ maxkb_logger.error(f"MessageQueue delete error [{queue_id}]: {e}")
+ raise MessageQueueError(f"delete({queue_id}) failed: {e}") from e
+
+ def clear_by_pattern(self, pattern: str) -> int:
+ """返回删除的 stream 数量;对应的 :done 标记也会一并清理。"""
+ try:
+ redis = self._get_redis()
+ count = 0
+ for full_pattern, counted in (
+ (f"{self._namespace}:{pattern}", True),
+ (f"{self._namespace}:{pattern}:done", False),
+ ):
+ cursor = 0
+ while True:
+ cursor, keys = redis.scan(cursor, match=full_pattern, count=500)
+ if keys:
+ redis.delete(*keys)
+ if counted:
+ count += len(keys)
+ if cursor == 0:
+ break
+ return count
+ except Exception as e:
+ maxkb_logger.error(f"MessageQueue clear_by_pattern error [{pattern}]: {e}")
+ return 0
+
+
+def create_message_queue(namespace: str = "mq", use_redis: bool = True) -> IMessageQueue:
+ """
+ 创建消息队列实例
+ @param namespace: 命名空间
+ @param use_redis: 是否使用Redis(False则使用内存实现)
+ """
+ if use_redis:
+ try:
+ queue = RedisStreamMessageQueue(namespace=namespace)
+ queue.ping()
+ return queue
+ except Exception as e:
+ maxkb_logger.warning(
+ f"Redis 不可用,降级为 InMemoryMessageQueue(多 worker 部署下跨进程消费将失效): {e}"
+ )
+ return InMemoryMessageQueue()
+
+
+def get_message_queue(namespace: str = "chat") -> IMessageQueue:
+ """进程内按 namespace 复用队列实例。禁止在业务代码里直接调 create_message_queue。"""
+ instance = _instances.get(namespace) # 快路径,GIL 下 dict.get 原子
+ if instance is not None:
+ return instance
+ with _instances_lock:
+ instance = _instances.get(namespace) # 双检
+ if instance is None:
+ from django.conf import settings
+ instance = create_message_queue(
+ namespace=namespace,
+ use_redis=getattr(settings, "MESSAGE_QUEUE_USE_REDIS", True),
+ )
+ _instances[namespace] = instance
+ return instance
diff --git a/apps/application/workflow/nodes/__init__.py b/apps/application/workflow/nodes/__init__.py
new file mode 100644
index 00000000000..3f6eeb92644
--- /dev/null
+++ b/apps/application/workflow/nodes/__init__.py
@@ -0,0 +1,78 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py.py
+ @date:2026/6/29 16:15
+ @desc:
+"""
+import pkgutil
+import importlib
+import inspect
+from pathlib import Path
+
+from application.workflow.i_node import INode
+
+node_list: list[type[INode]] = []
+_seen: set[type] = set()
+
+for _, module_name, _ in pkgutil.iter_modules([str(Path(__file__).parent)]):
+ module = importlib.import_module(f".{module_name}", __package__)
+ for _, obj in inspect.getmembers(module, inspect.isclass):
+ if (
+ issubclass(obj, INode)
+ and obj is not INode
+ and obj.__module__.startswith(__package__)
+ and obj not in _seen
+ ):
+ _seen.add(obj)
+ node_list.append(obj)
+
+if not node_list:
+ raise RuntimeError(f"未发现任何节点,检查各子包 __init__.py 是否导出了 INode 子类: {Path(__file__).parent}")
+
+node_map = {n.type: {workflow_type: n for workflow_type in n.supported_workflow_type_list} for n in node_list}
+
+
+def get_node_class(_type, workflow_type):
+ """
+ 根据节点类型 获取此类型的处理器
+ @param _type: 节点类型
+ @param workflow_type: 工作流类型
+ @return: 节点处理器
+ """
+ node_class = node_map.get(_type, {}).get(workflow_type)
+ if node_class is None:
+ raise ValueError(f"节点不存在: type={_type}, workflow_type={workflow_type}")
+ return node_class
+
+
+def get_start_node(workflow, workflow_manage, workflow_type, position=None):
+ """
+ 获取开始节点实例
+ @param workflow: 工作流对象
+ @param workflow_manage 工作流管理器
+ @param workflow_type: 工作流类型
+ @param position: 位置信息(可选)
+ @return: 开始节点实例
+ """
+ # 如果有 position,根据 position 确定开始节点
+ if position and position.get('id'):
+ node_id = position.get('id')
+ node = workflow.get_node(node_id)
+ if node:
+ node_class = get_node_class(node.type, workflow_type)
+ def get_node_parameters(n):
+ return n.properties.get('node_data', {})
+ return node_class(node, workflow_manage, get_node_parameters)
+
+ # 默认返回开始节点
+ start_node = workflow.get_node('start-node')
+ if start_node is None:
+ raise ValueError("开始节点不存在")
+ node_class = get_node_class(start_node.type, workflow_type)
+
+ def get_node_parameters(node):
+ return node.properties.get('node_data', {})
+
+ return node_class(start_node, workflow_manage, get_node_parameters)
diff --git a/apps/application/workflow/nodes/ai_chat_node/__init__.py b/apps/application/workflow/nodes/ai_chat_node/__init__.py
new file mode 100644
index 00000000000..0682df52ea0
--- /dev/null
+++ b/apps/application/workflow/nodes/ai_chat_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/1 16:59
+ @desc:
+"""
+from .ai_chat_node import AIChatNode
diff --git a/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py b/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py
new file mode 100644
index 00000000000..2b4583324d4
--- /dev/null
+++ b/apps/application/workflow/nodes/ai_chat_node/ai_chat_node.py
@@ -0,0 +1,615 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: ai_chat_node.py
+@date:2026/7/1 16:59
+@desc:
+"""
+
+import base64
+import json
+import re
+from functools import reduce
+
+import uuid_utils.compat as uuid
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
+from rest_framework import serializers
+
+from application.flow.tools import get_tools, mcp_response_generator
+from application.models import Application, ApplicationAccessToken, ApplicationApiKey
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.reasoning_content import ReasoningContent
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.message.struct.tool_content import ToolContent
+from application.workflow.status import Status
+from application.workflow.tools import Reasoning
+from common.exception.app_exception import AppApiException
+from common.utils.common import guess_image_format
+from common.utils.rsa_util import rsa_long_decrypt
+from common.utils.shared_resource_auth import filter_authorized_ids
+from common.utils.tool_code import ToolExecutor
+from knowledge.models import File
+from models_provider.models import Model
+from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id
+from tools.models import Tool, ToolType
+
+
+class ChatNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ system = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Role Setting"))
+ prompt = serializers.CharField(required=True, label=_("Prompt word"))
+ dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+ model_setting = serializers.DictField(required=False, label="Model settings")
+ dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Context Type"))
+ mcp_servers = serializers.JSONField(required=False, label=_("MCP Server"))
+ mcp_tool_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("MCP Tool ID"))
+ mcp_tool_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ allow_empty=True,
+ label=_("MCP Tool IDs"),
+ )
+ mcp_source = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("MCP Source"))
+ tool_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ allow_empty=True,
+ label=_("Tool IDs"),
+ )
+ application_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ allow_empty=True,
+ label=_("App IDs"),
+ )
+ skill_tool_ids = serializers.ListField(
+ child=serializers.UUIDField(),
+ required=False,
+ allow_empty=True,
+ label=_("Skill IDs"),
+ )
+ mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output"))
+ video_list = serializers.ListField(required=False, label=_("video"))
+ image_list = serializers.ListField(required=False, label=_("picture"))
+ vision = serializers.BooleanField(required=False, default=False, label=_("vision"))
+
+
+def _get_default_model_params_setting(model_id):
+ model = QuerySet(Model).filter(id=model_id).first()
+ credential = get_model_credential(model.provider, model.model_type, model.model_name)
+ model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
+ return model_params_setting
+
+
+def _get_node_message(chat_record, runtime_node_id):
+ node_details = chat_record.get_node_details_runtime_node_id(runtime_node_id)
+ if node_details is None:
+ return []
+ return [HumanMessage(node_details.get("question")), AIMessage(node_details.get("answer"))]
+
+
+def _get_workflow_message(chat_record):
+ return [chat_record.get_human_message(), chat_record.get_ai_message()]
+
+
+def _get_message(chat_record, dialogue_type, runtime_node_id):
+ if dialogue_type == "NODE":
+ return _get_node_message(chat_record, runtime_node_id)
+ return _get_workflow_message(chat_record)
+
+
+def _get_history_message(history_chat_record, dialogue_number, dialogue_type, runtime_node_id):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ _get_message(history_chat_record[index], dialogue_type, runtime_node_id)
+ for index in range(max(start_index, 0), len(history_chat_record))
+ ],
+ [],
+ )
+ for message in history_message:
+ if isinstance(message.content, str):
+ message.content = re.sub(r".*?", "", message.content, flags=re.DOTALL)
+ return history_message
+
+
+def _process_images(image):
+ images = []
+ if isinstance(image, str) and image.startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": image}})
+ elif image is not None and len(image) > 0:
+ for img in image:
+ if "file_id" in img:
+ file_id = img["file_id"]
+ file = QuerySet(File).filter(id=file_id).first()
+ image_bytes = file.get_bytes()
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
+ image_format = guess_image_format(image_bytes)
+ images.append(
+ {"type": "image_url", "image_url": {"url": f"data:image/{image_format};base64,{base64_image}"}}
+ )
+ elif "url" in img and img["url"].startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": img["url"]}})
+ return images
+
+
+def _process_videos(video, video_model):
+ videos = []
+ if isinstance(video, str) and video.startswith("http"):
+ videos.append({"type": "video_url", "video_url": {"url": video}})
+ elif video is not None and len(video) > 0:
+ for v in video:
+ if "file_id" in v:
+ file_id = v["file_id"]
+ file = QuerySet(File).filter(id=file_id).first()
+ url = video_model.upload_file_and_get_url(file.get_bytes(), file.file_name)
+ videos.append({"type": "video_url", "video_url": {"url": url}})
+ elif "url" in v and v["url"].startswith("http"):
+ videos.append({"type": "video_url", "video_url": {"url": v["url"]}})
+ return videos
+
+
+class AIChatNode(INode):
+ serializer_class = ChatNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "ai-chat-node"
+
+ def execute(self):
+ workflow_params = self.get_workflow_parameters()
+ node_params = self.get_parameters()
+ reasoning_content_id = str(uuid.uuid7())
+ text_content_id = str(uuid.uuid7())
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ model_params_setting = node_params.get("model_params_setting")
+ model_setting = node_params.get("model_setting")
+ system = node_params.get("system", "")
+ prompt = node_params.get("prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ dialogue_type = node_params.get("dialogue_type", "WORKFLOW") or "WORKFLOW"
+ is_result = node_params.get("is_result", False)
+ vision = node_params.get("vision", False)
+ image_list = node_params.get("image_list")
+ video_list = node_params.get("video_list")
+ stream = node_params.get("stream", True)
+
+ mcp_servers = node_params.get("mcp_servers")
+ mcp_tool_id = node_params.get("mcp_tool_id")
+ mcp_tool_ids = node_params.get("mcp_tool_ids")
+ mcp_source = node_params.get("mcp_source")
+ tool_ids = node_params.get("tool_ids")
+ application_ids = node_params.get("application_ids")
+ skill_tool_ids = node_params.get("skill_tool_ids")
+ mcp_output_enable = node_params.get("mcp_output_enable", True)
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ chat_id = None
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ chat_id = workflow_params.get("chat_id")
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ if model_params_setting is None and model_id:
+ model_params_setting = _get_default_model_params_setting(model_id)
+
+ if model_setting is None:
+ model_setting = {
+ "reasoning_content_enable": False,
+ "reasoning_content_end": "",
+ "reasoning_content_start": "",
+ }
+ self.write_context("model_setting", model_setting)
+
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = _get_history_message(history_chat_record, dialogue_number, dialogue_type, self.get_node_id())
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message or [])],
+ )
+
+ question = self._generate_prompt_question(prompt, chat_model, vision, image_list, video_list)
+ self.write_context("question", question.content)
+
+ system = self.workflow_manage.generate_prompt(system)
+ self.write_context("system", system)
+
+ message_list = [*history_message, question]
+
+ all_tool_ids = list(
+ set(
+ (mcp_tool_ids or [])
+ + (tool_ids or [])
+ + (skill_tool_ids or [])
+ + ([mcp_tool_id] if mcp_tool_id else [])
+ )
+ )
+ authorized_set = set(filter_authorized_ids("tool", all_tool_ids, workspace_id))
+ mcp_tool_ids = [i for i in (mcp_tool_ids or []) if i in authorized_set]
+ tool_ids = [i for i in (tool_ids or []) if i in authorized_set]
+ skill_tool_ids = [i for i in (skill_tool_ids or []) if i in authorized_set]
+ mcp_tool_id = mcp_tool_id if (mcp_tool_id and mcp_tool_id in authorized_set) else None
+
+ mcp_handled = self._handle_mcp(
+ mcp_source,
+ mcp_servers,
+ mcp_tool_id,
+ mcp_tool_ids,
+ tool_ids,
+ application_ids,
+ skill_tool_ids,
+ mcp_output_enable,
+ chat_model,
+ SystemMessage(system),
+ message_list,
+ history_message,
+ question,
+ chat_id,
+ workspace_id,
+ workflow_type,
+ reasoning_content_id,
+ text_content_id,
+ is_result,
+ )
+ if not mcp_handled:
+ message_list_with_system = [SystemMessage(system)] + message_list
+
+ if stream:
+ r = chat_model.stream(message_list_with_system)
+ self._stream_response(
+ r, chat_model, message_list_with_system, question.content, reasoning_content_id, text_content_id
+ )
+ else:
+ r = chat_model.invoke(message_list_with_system)
+ self._invoke_response(
+ r, chat_model, message_list_with_system, question.content, is_result, text_content_id
+ )
+
+ def _generate_prompt_question(self, prompt, model, vision, image_list, video_list):
+ images = []
+ videos = []
+ if vision:
+ if image_list:
+ image = self.workflow_manage.get_reference_field(image_list[0], image_list[1:])
+ images = _process_images(image)
+ if video_list:
+ video = self.workflow_manage.get_reference_field(video_list[0], video_list[1:])
+ videos = _process_videos(video, model)
+ return HumanMessage(
+ content=[*videos, *images, {"type": "text", "text": self.workflow_manage.generate_prompt(prompt)}]
+ )
+
+ def _stream_response(self, response, chat_model, message_list, question, reasoning_content_id, text_content_id):
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.RUNNING)
+ model_setting = self.get_context("model_setting") or {}
+ reasoning = Reasoning(
+ model_setting.get("reasoning_content_start", ""),
+ model_setting.get("reasoning_content_end", ""),
+ )
+ answer = ""
+ reasoning_content = ""
+ response_reasoning_content = False
+
+ for chunk in response:
+ self._check_cancelled()
+ reasoning_chunk = reasoning.get_reasoning_content(chunk)
+ content_chunk = reasoning_chunk.get("content")
+ if "reasoning_content" in chunk.additional_kwargs:
+ response_reasoning_content = True
+ reasoning_content_chunk = chunk.additional_kwargs.get("reasoning_content", "")
+ else:
+ reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
+ answer += content_chunk
+ if reasoning_content_chunk is None:
+ reasoning_content_chunk = ""
+ reasoning_content += reasoning_content_chunk
+ reasoning_end = False
+ if content_chunk:
+ if not reasoning_end:
+ self.write(
+ ReasoningContent(
+ reasoning_content_id, "", Status.SUCCESS, node_info, Position(self.get_node_id())
+ )
+ )
+ self.write(
+ TextContent(text_content_id, content_chunk, Status.RUNNING, node_info, Position(self.get_node_id()))
+ )
+ if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
+ self.write(
+ ReasoningContent(
+ reasoning_content_id,
+ reasoning_content_chunk,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+
+ reasoning_end = reasoning.get_end_reasoning_content()
+ answer += reasoning_end.get("content")
+ reasoning_content_chunk = ""
+ if not response_reasoning_content:
+ reasoning_content_chunk = reasoning_end.get("reasoning_content")
+ if reasoning_end.get("content"):
+ self.write(
+ TextContent(
+ text_content_id,
+ reasoning_end.get("content"),
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+ if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
+ self.write(
+ ReasoningContent(
+ reasoning_content_id,
+ reasoning_content_chunk,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+
+ self._write_final_context(chat_model, message_list, question, answer, reasoning_content)
+
+ def _invoke_response(self, response, chat_model, message_list, question, is_result=False, text_content_id=None):
+ model_setting = self.get_context("model_setting") or {}
+ reasoning = Reasoning(
+ model_setting.get("reasoning_content_start", ""),
+ model_setting.get("reasoning_content_end", ""),
+ )
+ reasoning_result = reasoning.get_reasoning_content(response)
+ reasoning_result_end = reasoning.get_end_reasoning_content()
+ content = reasoning_result.get("content") + reasoning_result_end.get("content")
+ meta = {**response.response_metadata, **response.additional_kwargs}
+ if "reasoning_content" in meta:
+ reasoning_content = meta.get("reasoning_content", "") or ""
+ else:
+ reasoning_content = (reasoning_result.get("reasoning_content") or "") + (
+ reasoning_result_end.get("reasoning_content") or ""
+ )
+ self._write_final_context(chat_model, message_list, question, content, reasoning_content)
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(TextContent(text_content_id, content, Status.SUCCESS, node_info, Position(self.get_node_id())))
+
+ def _write_final_context(self, chat_model, message_list, question, answer, reasoning_content):
+ message_tokens = chat_model.get_num_tokens_from_messages(message_list)
+ answer_tokens = chat_model.get_num_tokens(answer)
+ self.write_context("message_tokens", message_tokens)
+ self.write_context("answer_tokens", answer_tokens)
+ self.write_context("answer", answer)
+ self.write_context("question", question)
+ self.write_context("reasoning_content", reasoning_content)
+
+ def _handle_mcp(
+ self,
+ mcp_source,
+ mcp_servers,
+ mcp_tool_id,
+ mcp_tool_ids,
+ tool_ids,
+ application_ids,
+ skill_tool_ids,
+ mcp_output_enable,
+ chat_model,
+ system_prompt,
+ message_list,
+ history_message,
+ question,
+ chat_id,
+ workspace_id,
+ workflow_type,
+ reasoning_content_id,
+ text_content_id,
+ is_result=False,
+ ):
+ mcp_servers_config = {}
+
+ if mcp_source is None:
+ mcp_source = "custom"
+ if not mcp_tool_ids:
+ mcp_tool_ids = []
+ if mcp_tool_id:
+ mcp_tool_ids = list(set(mcp_tool_ids + [mcp_tool_id]))
+
+ if mcp_source == "custom" and mcp_servers:
+ mcp_servers_config = json.loads(mcp_servers)
+ mcp_servers_config = self._handle_variables(mcp_servers_config)
+ elif mcp_tool_ids:
+ mcp_tools = QuerySet(Tool).filter(id__in=mcp_tool_ids).values()
+ for mcp_tool in mcp_tools:
+ if mcp_tool and mcp_tool["is_active"]:
+ mcp_servers_config = {**mcp_servers_config, **json.loads(mcp_tool["code"])}
+ mcp_servers_config = self._handle_variables(mcp_servers_config)
+
+ ToolExecutor().validate_mcp_transport(json.dumps(mcp_servers_config))
+
+ tool_init_params = {}
+ if workflow_type == WorkflowType.KNOWLEDGE:
+ source_id = self.get_workflow_parameters().get("knowledge_id")
+ source_type = "KNOWLEDGE"
+ elif workflow_type == WorkflowType.TOOL:
+ source_id = self.get_workflow_parameters().get("tool_id")
+ source_type = "TOOL"
+ else:
+ source_id = self.get_workflow_parameters().get("application_id")
+ source_type = "APPLICATION"
+
+ tools = get_tools(source_type, chat_id, tool_ids, workspace_id)
+ if tool_ids and len(tool_ids) > 0:
+ self.write_context("tool_ids", tool_ids)
+ custom_tools_map = {
+ str(t.id): t for t in QuerySet(Tool).filter(id__in=tool_ids, tool_type=ToolType.CUSTOM, is_active=True)
+ }
+ for tool_id in tool_ids:
+ tool = custom_tools_map.get(str(tool_id))
+ if tool is None:
+ continue
+ executor = ToolExecutor()
+ init_params_default_value = {i["field"]: i.get("default_value") for i in tool.init_field_list}
+ if tool.init_params is not None:
+ tool_init_params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
+ else:
+ tool_init_params = init_params_default_value
+ tool_config = executor.get_tool_mcp_config(tool, tool_init_params)
+ mcp_servers_config[str(tool.id)] = tool_config
+
+ if application_ids and len(application_ids) > 0:
+ self.write_context("application_ids", application_ids)
+ apps_map = {str(a.id): a for a in QuerySet(Application).filter(id__in=application_ids, is_publish=True)}
+ app_keys_map = {
+ str(ak.application_id): ak
+ for ak in QuerySet(ApplicationApiKey).filter(application_id__in=application_ids, is_active=True)
+ }
+ app_access_tokens_map = {
+ str(at.application_id): at
+ for at in QuerySet(ApplicationAccessToken).filter(application_id__in=application_ids)
+ }
+ for application_id in application_ids:
+ app = apps_map.get(str(application_id))
+ if app is None:
+ continue
+ app_key = app_keys_map.get(str(application_id))
+ if app_key is not None:
+ api_key = app_key.secret_key
+ application_access_token = app_access_tokens_map.get(str(app_key.application_id))
+ if application_access_token is not None and application_access_token.authentication:
+ raise AppApiException(
+ 500,
+ _("Agent 【{name}】 access token authentication is not supported for agent tool").format(
+ name=app.name
+ ),
+ )
+ else:
+ raise AppApiException(
+ 500, _("Agent Key is required for agent tool 【{name}】").format(name=app.name)
+ )
+ executor = ToolExecutor()
+ app_config = executor.get_app_mcp_config(api_key)
+ mcp_servers_config[app.name] = app_config
+
+ if skill_tool_ids and len(skill_tool_ids) > 0:
+ self.write_context("skill_tool_ids", skill_tool_ids)
+ skill_file_items = []
+ skill_tools_map = {str(t.id): t for t in QuerySet(Tool).filter(id__in=skill_tool_ids, is_active=True)}
+ for tool_id in skill_tool_ids:
+ tool = skill_tools_map.get(str(tool_id))
+ if tool is None:
+ continue
+ init_params_default_value = {i["field"]: i.get("default_value") for i in tool.init_field_list}
+ if tool.init_params is not None:
+ params = init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params))
+ else:
+ params = init_params_default_value
+ skill_file_items.append({"tool_id": str(tool.id), "file_id": tool.code, "params": params})
+ mcp_servers_config["skills"] = skill_file_items
+
+ if len(mcp_servers_config) > 0 or len(tools) > 0:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.RUNNING)
+ tool_content_id = str(uuid.uuid7())
+ r = mcp_response_generator(
+ chat_model,
+ system_prompt,
+ message_list,
+ json.dumps(mcp_servers_config),
+ mcp_output_enable,
+ tool_init_params,
+ source_id,
+ source_type,
+ chat_id,
+ tools,
+ )
+ answer = ""
+ tool_calls_map = {}
+ for chunk in r:
+ self._check_cancelled()
+ if isinstance(chunk, ToolMessage):
+ tool_call = tool_calls_map.get(chunk.tool_call_id, {})
+ self.write(
+ ToolContent(
+ tool_content_id,
+ tool_call.get("name", getattr(chunk, "name", "")),
+ json.dumps(tool_call.get("args", {}), ensure_ascii=False),
+ chunk.content,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+ continue
+
+ if hasattr(chunk, "tool_calls") and chunk.tool_calls:
+ for tool_call in chunk.tool_calls:
+ tool_calls_map[tool_call.get("id", "")] = tool_call
+
+ answer += chunk.content if hasattr(chunk, "content") else str(chunk)
+ if chunk.content:
+ self.write(
+ TextContent(
+ text_content_id, chunk.content, Status.RUNNING, node_info, Position(self.get_node_id())
+ )
+ )
+ self._write_final_context(chat_model, message_list, question.content, answer, "")
+ return True
+
+ return False
+
+ def _handle_variables(self, tool_params):
+ for k, v in tool_params.items():
+ if isinstance(v, str):
+ tool_params[k] = self.workflow_manage.generate_prompt(v)
+ elif isinstance(v, dict):
+ self._handle_variables(v)
+ elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], str):
+ tool_params[k] = self._get_reference_content(v)
+ return tool_params
+
+ def _get_reference_content(self, fields):
+ if fields:
+ return str(self.workflow_manage.get_reference_field(fields[0], fields[1:]))
+ return ""
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "reasoning_content": self.get_context("reasoning_content"),
+ "message_tokens": self.get_context("message_tokens"),
+ "answer_tokens": self.get_context("answer_tokens"),
+ "history_message": self.get_context("history_message"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/condition_node/__init__.py b/apps/application/workflow/nodes/condition_node/__init__.py
new file mode 100644
index 00000000000..c1c44f4f7ca
--- /dev/null
+++ b/apps/application/workflow/nodes/condition_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from .condition_node import ConditionNode
diff --git a/apps/application/workflow/nodes/condition_node/condition_node.py b/apps/application/workflow/nodes/condition_node/condition_node.py
new file mode 100644
index 00000000000..d04b1fddcd1
--- /dev/null
+++ b/apps/application/workflow/nodes/condition_node/condition_node.py
@@ -0,0 +1,72 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: condition_node.py
+@date:2026/7/2 10:00
+@desc:
+"""
+
+from typing import List
+
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.compare import do_assertion
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.status import Status
+
+
+class ConditionSerializer(serializers.Serializer):
+ compare = serializers.CharField(required=True, label=_("Comparator"))
+ value = serializers.CharField(required=True, label=_("value"))
+ field = serializers.ListField(required=True, label=_("Fields"))
+
+
+class ConditionBranchSerializer(serializers.Serializer):
+ id = serializers.CharField(required=True, label=_("Branch id"))
+ type = serializers.CharField(required=True, label=_("Branch Type"))
+ condition = serializers.CharField(required=True, label=_("Condition or|and"))
+ conditions = ConditionSerializer(many=True)
+
+
+class ConditionNodeSerializer(serializers.Serializer):
+ branch = ConditionBranchSerializer(many=True)
+
+
+class ConditionNode(INode):
+ serializer_class = ConditionNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "condition-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ branch_list = node_params.get("branch", [])
+ branch = self._evaluate_branches(branch_list)
+ branch_id = branch.get("id")
+ branch_name = branch.get("type")
+
+ self.write_context("branch_id", branch_id)
+ self.write_context("branch_name", branch_name)
+
+ self.complete(Status.SUCCESS, [self.branch_anchor(branch_id)])
+
+ def _evaluate_branches(self, branch_list: List):
+ for branch in branch_list:
+ if self._branch_assertion(branch):
+ return branch
+ return branch_list[-1] if branch_list else {}
+
+ def _branch_assertion(self, branch):
+ return do_assertion(self.workflow_manage, branch.get("condition"), branch.get("conditions"))
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "branch_id": self.get_context("branch_id"),
+ "branch_name": self.get_context("branch_name"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/form_node/__init__.py b/apps/application/workflow/nodes/form_node/__init__.py
new file mode 100644
index 00000000000..261db05100c
--- /dev/null
+++ b/apps/application/workflow/nodes/form_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/6 15:30
+ @desc:
+"""
+from .form_node import FormNode
diff --git a/apps/application/workflow/nodes/form_node/form_node.py b/apps/application/workflow/nodes/form_node/form_node.py
new file mode 100644
index 00000000000..0ec16624e43
--- /dev/null
+++ b/apps/application/workflow/nodes/form_node/form_node.py
@@ -0,0 +1,199 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: form_node.py
+@date:2026/7/6 15:30
+@desc:
+"""
+
+import copy
+import re
+
+import uuid_utils.compat as uuid
+from rest_framework import serializers
+
+from django.utils.translation import gettext_lazy as _
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode, Signal
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.form_content import FormContent
+from application.workflow.status import Status
+
+_TEMPLATE_RE = re.compile(r"\{\{([^.\s}]+)\.([^.\s}]+)\}\}")
+
+_MULTI_SELECT_TYPES = {"MultiSelect", "MultiRow"}
+
+
+def _get_default_option(option_list, _type, value_field):
+ try:
+ if option_list and isinstance(option_list, list) and len(option_list) > 0:
+ default_value_list = [o.get(value_field) for o in option_list if o.get("default")]
+ if len(default_value_list) == 0:
+ return (
+ [option_list[0].get(value_field)]
+ if _type in _MULTI_SELECT_TYPES
+ else option_list[0].get(value_field)
+ )
+ else:
+ return default_value_list if _type in _MULTI_SELECT_TYPES else default_value_list[0]
+ except Exception:
+ pass
+ return []
+
+
+class FormNodeSerializer(serializers.Serializer):
+ form_field_list = serializers.ListField(required=True, label=_("Form Configuration"))
+ form_content_format = serializers.CharField(required=True, label=_("Form output content"))
+ form_data = serializers.DictField(required=False, allow_null=True, label=_("Form Data"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+
+
+class FormNode(INode):
+ serializer_class = FormNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "form-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.workflow_manage.get_parameters()
+
+ # 判断是否是表单提交
+ position = workflow_params.get("position") or {}
+ is_form_submit = position.get("id") == self.node.id
+
+ form_field_list = node_params.get("form_field_list", [])
+ form_content_format = node_params.get("form_content_format", "")
+
+ if is_form_submit:
+ # 表单提交:从 workflow_params 获取前端提交的 form_data
+ form_data = workflow_params.get("form_data") or {}
+ is_submit = True
+ # 复用前端传来的 chunk_id
+ chunk_id = workflow_params.get("chunk_id") or str(uuid.uuid7())
+ else:
+ # 首次执行:从节点参数获取
+ form_data = node_params.get("form_data")
+ is_submit = form_data is not None
+ # 生成新 chunk_id
+ chunk_id = str(uuid.uuid7())
+
+ # 写入 context
+ self.write_context("is_submit", is_submit)
+ self.write_context("form_content_format", form_content_format)
+
+ if is_submit:
+ self.write_context("form_data", form_data)
+ for key in form_data:
+ self.write_context(key, form_data.get(key))
+
+ form_field_list = [self._reset_field(field) for field in form_field_list]
+ self.write_context("form_field_list", form_field_list)
+
+ # 输出表单内容
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(
+ FormContent(
+ chunk_id,
+ form_field_list,
+ form_content_format,
+ is_submit,
+ Status.SUCCESS,
+ node_info,
+ Position(self.get_node_id()),
+ form_data=form_data,
+ )
+ )
+
+ # 如果未提交,中断工作流等待用户提交
+ if not is_submit:
+ self.complete(Status.SUCCESS, signal=Signal.FORM)
+ return
+
+ # 已提交,继续执行后续节点
+ self.complete(Status.SUCCESS)
+
+ def _generate_prompt(self, prompt):
+ try:
+ return self.workflow_manage.generate_prompt(prompt)
+ except Exception:
+ return prompt
+
+ def _reset_field(self, field):
+ field = copy.copy(field)
+ for f in ["field", "label", "default_value"]:
+ _value = field.get(f)
+ if _value is None:
+ continue
+ if isinstance(_value, str):
+ field[f] = self._generate_prompt(_value)
+ elif f == "label" and isinstance(_value, dict):
+ _label_value = _value.get("label")
+ _value["label"] = self._generate_prompt(_label_value)
+ tooltip = _value.get("attrs", {}).get("tooltip")
+ if tooltip is not None:
+ _value["attrs"]["tooltip"] = self._generate_prompt(tooltip)
+
+ input_type = field.get("input_type")
+ if input_type in {"SingleSelect", "MultiSelect", "RadioCard", "RadioRow", "MultiRow"}:
+ if field.get("assignment_method") == "ref_variables":
+ option_list_ref = field.get("option_list")
+ if option_list_ref and len(option_list_ref) >= 2:
+ option_list = self.workflow_manage.get_reference_field(option_list_ref[0], option_list_ref[1:])
+ option_list = option_list if isinstance(option_list, list) else []
+ field["option_list"] = option_list
+ field["default_value"] = _get_default_option(option_list, input_type, field.get("value_field"))
+
+ if input_type == "JsonInput":
+ if field.get("default_value_assignment_method") == "ref_variables":
+ default_ref = field.get("default_value")
+ if default_ref and isinstance(default_ref, list) and len(default_ref) >= 2:
+ field["default_value"] = self.workflow_manage.get_reference_field(default_ref[0], default_ref[1:])
+
+ self._reset_visibility_rules(field)
+ return field
+
+ def _reset_visibility_rules(self, field):
+ visibility_rules = field.get("visibility_rules")
+ if not visibility_rules or not isinstance(visibility_rules.get("conditions"), list):
+ return
+ for cond in visibility_rules["conditions"]:
+ cond_field = cond.get("field")
+ if not cond_field or len(cond_field) < 2 or not cond_field[0] or not cond_field[1]:
+ continue
+ if cond_field[0] != self.node.id:
+ cond["_left"] = self.workflow_manage.get_reference_field(cond_field[0], cond_field[1:])
+ cond_value = cond.get("value")
+ if isinstance(cond_value, str) and _TEMPLATE_RE.search(cond_value):
+ cond["value"] = self._render_cond_value(cond_value)
+
+ def _render_cond_value(self, value):
+ def replacer(match):
+ node_display = match.group(1)
+ field_name = match.group(2)
+ workflow = self.workflow_manage.workflow
+ for f in workflow.node_field_list:
+ if f.node_name == node_display and f.value == field_name:
+ if f.node_id == self.node.id:
+ return match.group(0)
+ ref = self.workflow_manage.get_reference_field(f.node_id, [field_name])
+ return str(ref) if ref is not None else ""
+ return match.group(0)
+
+ try:
+ return _TEMPLATE_RE.sub(replacer, value)
+ except Exception:
+ return value
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "form_field_list": self.get_context("form_field_list"),
+ "form_data": self.get_context("form_data"),
+ "is_submit": self.get_context("is_submit"),
+ "form_content_format": self.get_context("form_content_format"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/image_generate_node/__init__.py b/apps/application/workflow/nodes/image_generate_node/__init__.py
new file mode 100644
index 00000000000..b915a23112f
--- /dev/null
+++ b/apps/application/workflow/nodes/image_generate_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/6 16:00
+ @desc:
+"""
+from .image_generate_node import ImageGenerateNode
diff --git a/apps/application/workflow/nodes/image_generate_node/image_generate_node.py b/apps/application/workflow/nodes/image_generate_node/image_generate_node.py
new file mode 100644
index 00000000000..9611864b37c
--- /dev/null
+++ b/apps/application/workflow/nodes/image_generate_node/image_generate_node.py
@@ -0,0 +1,211 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: image_generate_node.py
+@date:2026/7/6 16:00
+@desc:
+"""
+
+import base64
+from functools import reduce
+
+import requests
+import uuid_utils.compat as uuid
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import HumanMessage, AIMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from common.utils.common import bytes_to_uploaded_file
+from knowledge.models import FileSourceType
+from models_provider.tools import get_model_instance_by_model_workspace_id
+from oss.serializers.file import FileSerializer
+
+
+class ImageGenerateNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ prompt = serializers.CharField(required=True, label=_("Prompt word (positive)"))
+ negative_prompt = serializers.CharField(
+ required=False, label=_("Prompt word (negative)"), allow_null=True, allow_blank=True
+ )
+ dialogue_number = serializers.IntegerField(
+ required=False, default=0, label=_("Number of multi-round conversations")
+ )
+ dialogue_type = serializers.CharField(required=False, default="NODE", label=_("Conversation storage type"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model parameter settings"))
+
+
+class ImageGenerateNode(INode):
+ serializer_class = ImageGenerateNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "image-generate-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ prompt = node_params.get("prompt", "")
+ negative_prompt = node_params.get("negative_prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ dialogue_type = node_params.get("dialogue_type", "NODE")
+ is_result = node_params.get("is_result", False)
+ model_params_setting = node_params.get("model_params_setting")
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ workspace_id = workflow_params.get("workspace_id")
+ tti_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = self._get_history_message(history_chat_record, dialogue_number)
+ self.write_context("history_message", [{"content": m.content, "role": m.type} for m in (history_message or [])])
+
+ question = self.workflow_manage.generate_prompt(prompt)
+ self.write_context("question", question)
+ self.write_context("negative_prompt", self.workflow_manage.generate_prompt(negative_prompt or ""))
+ self.write_context("dialogue_type", dialogue_type)
+
+ self._check_cancelled()
+ image_urls = tti_model.generate_image(question, negative_prompt)
+
+ file_urls = []
+ for image_url in image_urls:
+ file_name = "generated_image.png"
+ if isinstance(image_url, str):
+ if image_url.startswith("http"):
+ image_url = requests.get(image_url).content
+ elif image_url.startswith("data:image"):
+ header, encoded = image_url.split(",", 1)
+ image_url = base64.b64decode(encoded)
+ else:
+ image_url = base64.b64decode(image_url)
+ file = bytes_to_uploaded_file(image_url, file_name)
+ file_url = self._upload_file(file, workflow_params, workflow_type)
+ file_urls.append(file_url)
+
+ image_list = [{"file_id": path.split("/")[-1], "url": path} for path in file_urls]
+ self.write_context("image_list", image_list)
+
+ answer = " ".join([f"" for path in file_urls])
+ self.write_context("answer", answer)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(
+ TextContent(str(uuid.uuid7()), answer, Status.SUCCESS, node_info, position=Position(self.get_node_id()))
+ )
+
+ def _get_history_message(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ return reduce(
+ lambda x, y: [*x, *y],
+ [
+ [
+ self._generate_history_human_message(history_chat_record[index]),
+ self._generate_history_ai_message(history_chat_record[index]),
+ ]
+ for index in range(max(start_index, 0), len(history_chat_record))
+ ],
+ [],
+ )
+
+ def _generate_history_human_message(self, chat_record):
+ for data in chat_record.details.values():
+ if self.node.id == data.get("node_id") and "image_list" in data:
+ image_list = data["image_list"]
+ if len(image_list) == 0 or data.get("dialogue_type") == "WORKFLOW":
+ return HumanMessage(content=chat_record.problem_text)
+ return HumanMessage(content=data.get("question", chat_record.problem_text))
+ return HumanMessage(content=chat_record.problem_text)
+
+ def _generate_history_ai_message(self, chat_record):
+ for val in chat_record.details.values():
+ if self.node.id == val.get("node_id") and "image_list" in val:
+ if val.get("dialogue_type") == "WORKFLOW":
+ return chat_record.get_ai_message()
+ image_list = val["image_list"]
+ return AIMessage(
+ content=[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]
+ )
+ return chat_record.get_ai_message()
+
+ def _upload_file(self, file, workflow_params, workflow_type):
+ if workflow_type == WorkflowType.KNOWLEDGE:
+ return self._upload_knowledge_file(file, workflow_params)
+ if workflow_type == WorkflowType.TOOL:
+ return self._upload_tool_file(file, workflow_params)
+ return self._upload_application_file(file, workflow_params)
+
+ def _upload_knowledge_file(self, file, workflow_params):
+ knowledge_id = workflow_params.get("knowledge_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "knowledge_id": knowledge_id},
+ "source_id": knowledge_id,
+ "source_type": FileSourceType.KNOWLEDGE.value,
+ }
+ ).upload()
+
+ def _upload_tool_file(self, file, workflow_params):
+ tool_id = workflow_params.get("tool_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "tool_id": tool_id},
+ "source_id": tool_id,
+ "source_type": FileSourceType.TOOL.value,
+ }
+ ).upload()
+
+ def _upload_application_file(self, file, workflow_params):
+ application_id = workflow_params.get("application_id")
+ chat_id = workflow_params.get("chat_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "chat_id": chat_id, "application_id": application_id},
+ "source_id": application_id,
+ "source_type": FileSourceType.APPLICATION.value,
+ }
+ ).upload()
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "image_list": self.get_context("image_list"),
+ "negative_prompt": self.get_context("negative_prompt"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/image_to_video_node/__init__.py b/apps/application/workflow/nodes/image_to_video_node/__init__.py
new file mode 100644
index 00000000000..2a6d404093b
--- /dev/null
+++ b/apps/application/workflow/nodes/image_to_video_node/__init__.py
@@ -0,0 +1,2 @@
+# coding=utf-8
+from .image_to_video_node import ImageToVideoNode
diff --git a/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py b/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py
new file mode 100644
index 00000000000..31df25c47b6
--- /dev/null
+++ b/apps/application/workflow/nodes/image_to_video_node/image_to_video_node.py
@@ -0,0 +1,266 @@
+# coding=utf-8
+import base64
+import uuid_utils.compat as uuid
+import requests
+from functools import reduce
+from typing import List
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _, gettext
+from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from common.utils.common import bytes_to_uploaded_file
+from knowledge.models import FileSourceType, File
+from oss.serializers.file import FileSerializer, mime_types
+from models_provider.tools import get_model_instance_by_model_workspace_id
+from common.utils.logger import maxkb_logger
+
+
+class ImageToVideoNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ prompt = serializers.CharField(required=True, label=_("Prompt word (positive)"))
+ negative_prompt = serializers.CharField(
+ required=False, label=_("Prompt word (negative)"), allow_null=True, allow_blank=True
+ )
+ dialogue_number = serializers.IntegerField(
+ required=False, default=0, label=_("Number of multi-round conversations")
+ )
+ dialogue_type = serializers.CharField(required=False, default="NODE", label=_("Conversation storage type"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model parameter settings"))
+ first_frame_url = serializers.ListField(required=True, label=_("First frame url"))
+ last_frame_url = serializers.ListField(required=False, label=_("Last frame url"))
+
+
+class ImageToVideoNode(INode):
+ serializer_class = ImageToVideoNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "image-to-video-node"
+
+ def execute(self):
+ maxkb_logger.info(f"[ImageToVideoNode] execute START, node_id={self.get_node_id()}")
+ workflow_params = self.get_workflow_parameters()
+ node_params = self.get_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ prompt = node_params.get("prompt", "")
+ negative_prompt = node_params.get("negative_prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ dialogue_type = node_params.get("dialogue_type", "NODE")
+ is_result = node_params.get("is_result", False)
+ model_params_setting = node_params.get("model_params_setting")
+ first_frame_url_ref = node_params.get("first_frame_url")
+ last_frame_url_ref = node_params.get("last_frame_url")
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ chat_id = None
+ chat_record_id = None
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ chat_id = workflow_params.get("chat_id")
+ chat_record_id = workflow_params.get("chat_record_id")
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ if first_frame_url_ref is None or first_frame_url_ref == []:
+ raise ValueError(_("First frame url cannot be empty"))
+
+ first_frame_url = self.workflow_manage.get_reference_field(first_frame_url_ref[0], first_frame_url_ref[1:])
+
+ last_frame_url = None
+ if last_frame_url_ref is not None and last_frame_url_ref != []:
+ last_frame_url = self.workflow_manage.get_reference_field(last_frame_url_ref[0], last_frame_url_ref[1:])
+
+ ttv_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = self._get_history_message(history_chat_record, dialogue_number)
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message or [])],
+ )
+
+ question = self.workflow_manage.generate_prompt(prompt)
+ self.write_context("question", question)
+
+ message_list = [*history_message, question]
+ self.write_context("message_list", message_list)
+ self.write_context("dialogue_type", dialogue_type)
+ self.write_context("negative_prompt", self.workflow_manage.generate_prompt(negative_prompt))
+ self.write_context("first_frame_url", first_frame_url)
+ self.write_context("last_frame_url", last_frame_url)
+
+ first_frame_url = self._get_file_base64(first_frame_url)
+ last_frame_url = self._get_file_base64(last_frame_url)
+
+ self._check_cancelled()
+ video_urls = ttv_model.generate_video(question, negative_prompt, first_frame_url, last_frame_url)
+ maxkb_logger.info(
+ f"[ImageToVideoNode] generate_video result: {video_urls is not None}, node_id={self.get_node_id()}"
+ )
+
+ if video_urls is None or video_urls == "":
+ raise Exception(gettext("Failed to generate video"))
+
+ file_name = "generated_video.mp4"
+ if isinstance(video_urls, str) and video_urls.startswith("http"):
+ video_urls = requests.get(video_urls).content
+
+ file = bytes_to_uploaded_file(video_urls, file_name)
+ file_url = self._upload_file(file, workflow_type, workflow_params)
+
+ video_label = f''
+ video_list = [{"file_id": file_url.split("/")[-1], "file_name": file_name, "url": file_url}]
+
+ self.write_context("answer", video_label)
+ self.write_context("video", video_list)
+ self.write_context("chat_model", ttv_model)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(
+ TextContent(str(uuid.uuid7()), video_label, Status.SUCCESS, node_info, Position(self.get_node_id()))
+ )
+
+ def _get_file_base64(self, image_url):
+ try:
+ if isinstance(image_url, list):
+ image_url = image_url[0].get("file_id") if "file_id" in image_url[0] else image_url[0].get("url")
+ if isinstance(image_url, str) and not image_url.startswith("http"):
+ file = QuerySet(File).filter(id=image_url).first()
+ file_bytes = file.get_bytes()
+ file_type = file.file_name.split(".")[-1].lower()
+ content_type = mime_types.get(file_type, "application/octet-stream")
+ encoded_bytes = base64.b64encode(file_bytes)
+ return f"data:{content_type};base64,{encoded_bytes.decode()}"
+ return image_url
+ except Exception as e:
+ raise ValueError(gettext("Failed to obtain the image"))
+
+ def _upload_file(self, file, workflow_type, workflow_params):
+ if workflow_type == WorkflowType.KNOWLEDGE:
+ return self._upload_knowledge_file(file, workflow_params)
+ if workflow_type == WorkflowType.TOOL:
+ return self._upload_tool_file(file, workflow_params)
+ return self._upload_application_file(file, workflow_params)
+
+ def _upload_knowledge_file(self, file, workflow_params):
+ knowledge_id = workflow_params.get("knowledge_id")
+ meta = {"debug": False, "knowledge_id": knowledge_id}
+ file_url = FileSerializer(
+ data={"file": file, "meta": meta, "source_id": knowledge_id, "source_type": FileSourceType.KNOWLEDGE.value}
+ ).upload()
+ return file_url
+
+ def _upload_tool_file(self, file, workflow_params):
+ tool_id = workflow_params.get("tool_id")
+ meta = {
+ "debug": False,
+ "tool_id": tool_id,
+ }
+ file_url = FileSerializer(
+ data={"file": file, "meta": meta, "source_id": tool_id, "source_type": FileSourceType.TOOL.value}
+ ).upload()
+ return file_url
+
+ def _upload_application_file(self, file, workflow_params):
+ application_id = workflow_params.get("application_id")
+ chat_id = workflow_params.get("chat_id")
+ debug = workflow_params.get("debug", False)
+ meta = {
+ "debug": debug,
+ "chat_id": chat_id,
+ "application_id": application_id,
+ }
+ file_url = FileSerializer(
+ data={
+ "file": file,
+ "meta": meta,
+ "source_id": application_id,
+ "source_type": FileSourceType.APPLICATION.value,
+ }
+ ).upload()
+ return file_url
+
+ def _generate_history_ai_message(self, chat_record):
+ for val in chat_record.details.values():
+ if self.node.id == val["node_id"] and "image_list" in val:
+ if val["dialogue_type"] == "WORKFLOW":
+ return chat_record.get_ai_message()
+ image_list = val["image_list"]
+ return AIMessage(
+ content=[*[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]]
+ )
+ return chat_record.get_ai_message()
+
+ def _get_history_message(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [
+ self._generate_history_human_message(history_chat_record[index]),
+ self._generate_history_ai_message(history_chat_record[index]),
+ ]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+ return history_message
+
+ def _generate_history_human_message(self, chat_record):
+ for data in chat_record.details.values():
+ if self.node.id == data["node_id"] and "image_list" in data:
+ image_list = data["image_list"]
+ if len(image_list) == 0 or data["dialogue_type"] == "WORKFLOW":
+ return HumanMessage(content=chat_record.problem_text)
+ return HumanMessage(content=data["question"])
+ return HumanMessage(content=chat_record.problem_text)
+
+ @staticmethod
+ def reset_message_list(message_list: List[BaseMessage], answer_text):
+ result = [
+ {"role": "user" if isinstance(message, HumanMessage) else "ai", "content": message.content}
+ for message in message_list
+ ]
+ result.append({"role": "ai", "content": answer_text})
+ return result
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "video": self.get_context("video"),
+ "first_frame_url": self.get_context("first_frame_url"),
+ "last_frame_url": self.get_context("last_frame_url"),
+ "negative_prompt": self.get_context("negative_prompt"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/image_understand_node/__init__.py b/apps/application/workflow/nodes/image_understand_node/__init__.py
new file mode 100644
index 00000000000..d6242aeec4b
--- /dev/null
+++ b/apps/application/workflow/nodes/image_understand_node/__init__.py
@@ -0,0 +1,2 @@
+# coding=utf-8
+from .image_understand_node import ImageUnderstandNode
diff --git a/apps/application/workflow/nodes/image_understand_node/image_understand_node.py b/apps/application/workflow/nodes/image_understand_node/image_understand_node.py
new file mode 100644
index 00000000000..6882d3e5fc3
--- /dev/null
+++ b/apps/application/workflow/nodes/image_understand_node/image_understand_node.py
@@ -0,0 +1,377 @@
+# coding=utf-8
+import base64
+import uuid_utils.compat as uuid
+from functools import reduce
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.reasoning_content import ReasoningContent
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from application.workflow.tools import Reasoning
+from common.utils.common import guess_image_format
+from knowledge.models import File
+from models_provider.tools import get_model_instance_by_model_workspace_id
+
+
+class ImageUnderstandNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ system = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Role Setting"))
+ prompt = serializers.CharField(required=True, label=_("Prompt word"))
+ dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
+ dialogue_type = serializers.CharField(required=True, label=_("Conversation storage type"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ image_list = serializers.ListField(required=False, label=_("picture"))
+ model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model parameter settings"))
+ model_setting = serializers.DictField(required=False, label="Model settings")
+
+
+class ImageUnderstandNode(INode):
+ serializer_class = ImageUnderstandNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "image-understand-node"
+
+ def execute(self):
+ workflow_params = self.get_workflow_parameters()
+ node_params = self.get_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ system = node_params.get("system", "")
+ prompt = node_params.get("prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ dialogue_type = node_params.get("dialogue_type", "WORKFLOW")
+ is_result = node_params.get("is_result", False)
+ image_list_ref = node_params.get("image_list")
+ model_params_setting = node_params.get("model_params_setting")
+ model_setting = node_params.get("model_setting")
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ chat_id = None
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ chat_id = workflow_params.get("chat_id")
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_setting is None:
+ model_setting = {
+ "reasoning_content_enable": False,
+ "reasoning_content_end": "",
+ "reasoning_content_start": "",
+ }
+ self.write_context("model_setting", model_setting)
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ image = None
+ if image_list_ref:
+ image = self.workflow_manage.get_reference_field(image_list_ref[0], image_list_ref[1:])
+
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message_for_details = self._get_history_message_for_details(history_chat_record, dialogue_number)
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message_for_details or [])],
+ )
+
+ question = self.workflow_manage.generate_prompt(prompt)
+ self.write_context("question", question)
+
+ system = self.workflow_manage.generate_prompt(system)
+ self.write_context("system", system)
+
+ history_message = self._get_history_message(history_chat_record, dialogue_number)
+ message_list = self._generate_message_list(chat_model, system, prompt, history_message, image)
+ self.write_context("message_list", message_list)
+
+ self._generate_context_image(image)
+ self.write_context("dialogue_type", dialogue_type)
+
+ reasoning_content_id = str(uuid.uuid7())
+ text_content_id = str(uuid.uuid7())
+
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.RUNNING)
+
+ r = chat_model.stream(message_list)
+ self._stream_response(
+ r, chat_model, message_list, question, reasoning_content_id, text_content_id, node_info, is_result
+ )
+
+ def _stream_response(
+ self, response, chat_model, message_list, question, reasoning_content_id, text_content_id, node_info, is_result
+ ):
+ model_setting = self.get_context("model_setting") or {}
+ reasoning = Reasoning(
+ model_setting.get("reasoning_content_start", ""),
+ model_setting.get("reasoning_content_end", ""),
+ )
+ answer = ""
+ reasoning_content = ""
+ response_reasoning_content = False
+
+ for chunk in response:
+ self._check_cancelled()
+ reasoning_chunk = reasoning.get_reasoning_content(chunk)
+ content_chunk = reasoning_chunk.get("content")
+ if "reasoning_content" in chunk.additional_kwargs:
+ response_reasoning_content = True
+ reasoning_content_chunk = chunk.additional_kwargs.get("reasoning_content", "")
+ else:
+ reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
+ answer += content_chunk
+ if reasoning_content_chunk is None:
+ reasoning_content_chunk = ""
+ reasoning_content += reasoning_content_chunk
+
+ if is_result:
+ if isinstance(chunk.content, list):
+ for chunk_item in chunk.content:
+ text = chunk_item.get("text", "")
+ if text:
+ self.write(
+ TextContent(
+ text_content_id, text, Status.RUNNING, node_info, Position(self.get_node_id())
+ )
+ )
+ if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
+ self.write(
+ ReasoningContent(
+ reasoning_content_id,
+ reasoning_content_chunk,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+ else:
+ if content_chunk:
+ self.write(
+ TextContent(
+ text_content_id, content_chunk, Status.RUNNING, node_info, Position(self.get_node_id())
+ )
+ )
+ if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
+ self.write(
+ ReasoningContent(
+ reasoning_content_id,
+ reasoning_content_chunk,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+
+ reasoning_end = reasoning.get_end_reasoning_content()
+ answer += reasoning_end.get("content")
+ reasoning_content_chunk = ""
+ if not response_reasoning_content:
+ reasoning_content_chunk = reasoning_end.get("reasoning_content")
+ if is_result:
+ if reasoning_end.get("content"):
+ self.write(
+ TextContent(
+ text_content_id,
+ reasoning_end.get("content"),
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+ if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
+ self.write(
+ ReasoningContent(
+ reasoning_content_id,
+ reasoning_content_chunk,
+ Status.RUNNING,
+ node_info,
+ Position(self.get_node_id()),
+ )
+ )
+
+ self._write_final_context(chat_model, message_list, question, answer, reasoning_content)
+
+ def _write_final_context(self, chat_model, message_list, question, answer, reasoning_content):
+ message_tokens = chat_model.get_num_tokens_from_messages(message_list)
+ answer_tokens = chat_model.get_num_tokens(answer)
+ self.write_context("message_tokens", message_tokens)
+ self.write_context("answer_tokens", answer_tokens)
+ self.write_context("answer", answer)
+ self.write_context("question", question)
+ self.write_context("reasoning_content", reasoning_content)
+
+ def _generate_context_image(self, image):
+ if isinstance(image, str) and image.startswith("http"):
+ self.write_context("image_list", [{"url": image}])
+ elif image is not None and len(image) > 0:
+ self.write_context("image_list", image)
+
+ def _get_history_message_for_details(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [
+ self._generate_history_human_message_for_details(history_chat_record[index]),
+ self._generate_history_ai_message(history_chat_record[index]),
+ ]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+ return history_message
+
+ def _generate_history_ai_message(self, chat_record):
+ for val in chat_record.details.values():
+ if self.node.id == val["node_id"] and "image_list" in val:
+ if val["dialogue_type"] == "WORKFLOW":
+ return chat_record.get_ai_message()
+ return AIMessage(content=val["answer"])
+ return chat_record.get_ai_message()
+
+ def _generate_history_human_message_for_details(self, chat_record):
+ for data in chat_record.details.values():
+ if self.node.id == data["node_id"] and "image_list" in data:
+ image_list = data["image_list"] or []
+ if len(image_list) == 0 or data["dialogue_type"] == "WORKFLOW":
+ return HumanMessage(content=chat_record.problem_text)
+ file_id_list = []
+ url_list = []
+ for image in image_list:
+ if "file_id" in image:
+ file_id_list.append(image.get("file_id"))
+ elif "url" in image:
+ url_list.append(image.get("url"))
+ return HumanMessage(
+ content=[
+ {"type": "text", "text": data["question"]},
+ *[
+ {"type": "image_url", "image_url": {"url": f"./oss/file/{file_id}"}}
+ for file_id in file_id_list
+ ],
+ *[{"type": "image_url", "image_url": {"url": url}} for url in url_list],
+ ]
+ )
+ return HumanMessage(content=chat_record.problem_text)
+
+ def _get_history_message(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [
+ self._generate_history_human_message(history_chat_record[index]),
+ self._generate_history_ai_message(history_chat_record[index]),
+ ]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+ return history_message
+
+ def _generate_history_human_message(self, chat_record):
+ for data in chat_record.details.values():
+ if self.node.id == data["node_id"] and "image_list" in data:
+ image_list = data["image_list"] or []
+ if len(image_list) == 0 or data["dialogue_type"] == "WORKFLOW":
+ return HumanMessage(content=chat_record.problem_text)
+ file_id_list = []
+ url_list = []
+ for image in image_list:
+ if "file_id" in image:
+ file_id_list.append(image.get("file_id"))
+ elif "url" in image:
+ url_list.append(image.get("url"))
+ image_base64_list = [self._file_id_to_base64(file_id) for file_id in file_id_list]
+ return HumanMessage(
+ content=[
+ {"type": "text", "text": data["question"]},
+ *[
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/{base64_image[1]};base64,{base64_image[0]}"},
+ }
+ for base64_image in image_base64_list
+ ],
+ *[{"type": "image_url", "image_url": {"url": url}} for url in url_list],
+ ]
+ )
+ return HumanMessage(content=chat_record.problem_text)
+
+ @staticmethod
+ def _file_id_to_base64(file_id: str):
+ file = QuerySet(File).filter(id=file_id).first()
+ file_bytes = file.get_bytes()
+ base64_image = base64.b64encode(file_bytes).decode("utf-8")
+ return [base64_image, guess_image_format(file_bytes, file.file_name)]
+
+ def _process_images(self, image):
+ images = []
+ if isinstance(image, str) and image.startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": image}})
+ elif image is not None and len(image) > 0:
+ for img in image:
+ if "file_id" in img:
+ file_id = img["file_id"]
+ file = QuerySet(File).filter(id=file_id).first()
+ image_bytes = file.get_bytes()
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
+ image_format = guess_image_format(image_bytes, file.file_name)
+ images.append(
+ {"type": "image_url", "image_url": {"url": f"data:image/{image_format};base64,{base64_image}"}}
+ )
+ elif "url" in img and img["url"].startswith("http"):
+ images.append({"type": "image_url", "image_url": {"url": img["url"]}})
+ return images
+
+ def _generate_message_list(self, image_model, system: str, prompt: str, history_message, image):
+ prompt_text = self.workflow_manage.generate_prompt(prompt)
+ images = self._process_images(image)
+
+ if images:
+ messages = [HumanMessage(content=[{"type": "text", "text": prompt_text}, *images])]
+ else:
+ messages = [HumanMessage(prompt_text)]
+
+ if system is not None and len(system) > 0:
+ return [SystemMessage(system), *history_message, *messages]
+ else:
+ return [*history_message, *messages]
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "image_list": self.get_context("image_list"),
+ "reasoning_content": self.get_context("reasoning_content"),
+ "message_tokens": self.get_context("message_tokens"),
+ "answer_tokens": self.get_context("answer_tokens"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/intent_node/__init__.py b/apps/application/workflow/nodes/intent_node/__init__.py
new file mode 100644
index 00000000000..87587caeb24
--- /dev/null
+++ b/apps/application/workflow/nodes/intent_node/__init__.py
@@ -0,0 +1,2 @@
+# coding=utf-8
+from .intent_node import IntentNode
diff --git a/apps/application/workflow/nodes/intent_node/intent_node.py b/apps/application/workflow/nodes/intent_node/intent_node.py
new file mode 100644
index 00000000000..0137b12b3a8
--- /dev/null
+++ b/apps/application/workflow/nodes/intent_node/intent_node.py
@@ -0,0 +1,238 @@
+# coding=utf-8
+import json
+import re
+from typing import List, Dict, Any
+from functools import reduce
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import HumanMessage, SystemMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.status import Status
+from models_provider.models import Model
+from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential
+from .prompt_template import PROMPT_TEMPLATE
+
+
+class IntentBranchSerializer(serializers.Serializer):
+ id = serializers.CharField(required=True, label=_("Branch id"))
+ content = serializers.CharField(required=True, label=_("content"))
+ isOther = serializers.BooleanField(required=True, label=_("Branch Type"))
+
+
+class IntentNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ content_list = serializers.ListField(required=True, label=_("Text content"))
+ dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+ branch = IntentBranchSerializer(many=True)
+
+
+def _get_default_model_params_setting(model_id):
+ model = QuerySet(Model).filter(id=model_id).first()
+ credential = get_model_credential(model.provider, model.model_type, model.model_name)
+ model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
+ return model_params_setting
+
+
+class IntentNode(INode):
+ serializer_class = IntentNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "intent-node"
+
+ def execute(self):
+ workflow_params = self.get_workflow_parameters()
+ node_params = self.get_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ content_list_ref = node_params.get("content_list")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ model_params_setting = node_params.get("model_params_setting")
+ branch = node_params.get("branch", [])
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ if model_params_setting is None and model_id:
+ model_params_setting = _get_default_model_params_setting(model_id)
+
+ user_input = self.workflow_manage.get_reference_field(content_list_ref[0], content_list_ref[1:])
+
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = self._get_history_message(history_chat_record, dialogue_number)
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message or [])],
+ )
+
+ self.write_context("user_input", str(user_input))
+
+ prompt = self._build_classification_prompt(str(user_input), branch)
+ system = self._build_system_prompt()
+ message_list = self._generate_message_list(system, prompt, history_message)
+ self.write_context(
+ "message_list",
+ [{"content": m.content, "role": m.type} for m in message_list],
+ )
+
+ try:
+ self._check_cancelled()
+ r = chat_model.invoke(message_list)
+ classification_result = r.content.strip()
+ matched_branch = self._parse_classification_result(classification_result, branch)
+
+ message_tokens = chat_model.get_num_tokens_from_messages(message_list)
+ answer_tokens = chat_model.get_num_tokens(r.content)
+ self.write_context("message_tokens", message_tokens)
+ self.write_context("answer_tokens", answer_tokens)
+ self.write_context("answer", r.content)
+ self.write_context("branch_id", matched_branch["id"])
+ self.write_context("reason", self._parse_result_reason(r.content))
+ self.write_context("category", matched_branch.get("content", matched_branch["id"]))
+
+ self.complete(Status.SUCCESS, [self.branch_anchor(matched_branch["id"])])
+
+ except Exception as e:
+ other_branch = self._find_other_branch(branch)
+ if other_branch:
+ self.write_context("branch_id", other_branch["id"])
+ self.write_context("category", other_branch.get("content", other_branch["id"]))
+ self.write_context("error", str(e))
+ self.complete(Status.SUCCESS, [self.branch_anchor(other_branch["id"])])
+ else:
+ raise Exception(f"error: {str(e)}")
+
+ def _get_history_message(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+
+ for message in history_message:
+ if isinstance(message.content, str):
+ message.content = re.sub(r".*?", "", message.content, flags=re.DOTALL)
+ return history_message
+
+ def _build_system_prompt(self) -> str:
+ return "你是一个专业的意图识别助手,请根据用户输入和意图选项,准确识别用户的真实意图。"
+
+ def _build_classification_prompt(self, user_input: str, branch: List[Dict]) -> str:
+ classification_list = []
+ other_branch = self._find_other_branch(branch)
+ if other_branch:
+ classification_list.append({"classificationId": 0, "content": other_branch.get("content")})
+ classification_id = 1
+ for b in branch:
+ if not b.get("isOther"):
+ classification_list.append({"classificationId": classification_id, "content": b["content"]})
+ classification_id += 1
+
+ return PROMPT_TEMPLATE.format(classification_list=classification_list, user_input=user_input)
+
+ def _generate_message_list(self, system: str, prompt: str, history_message):
+ if system is None or len(system) == 0:
+ return [*history_message, HumanMessage(self.workflow_manage.generate_prompt(prompt))]
+ else:
+ return [
+ SystemMessage(self.workflow_manage.generate_prompt(system)),
+ *history_message,
+ HumanMessage(self.workflow_manage.generate_prompt(prompt)),
+ ]
+
+ def _parse_classification_result(self, result: str, branch: List[Dict]) -> Dict[str, Any]:
+ other_branch = self._find_other_branch(branch)
+ normal_intents = [b for b in branch if not b.get("isOther")]
+
+ def get_branch_by_id(category_id: int):
+ if category_id == 0:
+ return other_branch
+ elif 1 <= category_id <= len(normal_intents):
+ return normal_intents[category_id - 1]
+ return None
+
+ try:
+ result_json = json.loads(result)
+ classification_id = result_json.get("classificationId")
+ matched_branch = get_branch_by_id(classification_id)
+ if matched_branch:
+ return matched_branch
+ except Exception as e:
+ numbers = re.findall(r'"classificationId":\s*(\d+)', result)
+ if numbers:
+ classification_id = int(numbers[0])
+ matched_branch = get_branch_by_id(classification_id)
+ if matched_branch:
+ return matched_branch
+
+ return other_branch or (normal_intents[0] if normal_intents else {"id": "unknown", "content": "unknown"})
+
+ def _parse_result_reason(self, result: str):
+ try:
+ result_json = json.loads(result)
+ return result_json.get("reason", "")
+ except Exception as e:
+ reason_patterns = [
+ r'"reason":\s*"([^"]*)"',
+ r'"reason":\s*"([^"]*)',
+ r'"reason":\s*([^,}\n]*)',
+ ]
+ for pattern in reason_patterns:
+ match = re.search(pattern, result, re.DOTALL)
+ if match:
+ reason = match.group(1).strip()
+ reason = re.sub(r'["\s]*$', "", reason)
+ return reason
+ return ""
+
+ def _find_other_branch(self, branch: List[Dict]) -> Dict[str, Any] | None:
+ for b in branch:
+ if b.get("isOther"):
+ return b
+ return None
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "user_input": self.get_context("user_input"),
+ "answer": self.get_context("answer"),
+ "branch_id": self.get_context("branch_id"),
+ "category": self.get_context("category"),
+ "reason": self.get_context("reason"),
+ "message_tokens": self.get_context("message_tokens"),
+ "answer_tokens": self.get_context("answer_tokens"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/intent_node/prompt_template.py b/apps/application/workflow/nodes/intent_node/prompt_template.py
new file mode 100644
index 00000000000..1bcfd61743e
--- /dev/null
+++ b/apps/application/workflow/nodes/intent_node/prompt_template.py
@@ -0,0 +1,29 @@
+
+
+PROMPT_TEMPLATE = """# Role
+You are an intention classification expert, good at being able to judge which classification the user's input belongs to.
+
+## Skills
+Skill 1: Clearly determine which of the following intention classifications the user's input belongs to.
+Intention classification list:
+{classification_list}
+
+Note:
+- Please determine the match between the user's input content and the Intention classification list content, without judging or categorizing the match with the classification ID.
+- **When classifying, you must give higher weight to the context and intent continuity shown in the historical conversation. Do not rely solely on the literal meaning of the current input; instead, prioritize the most consistent classification with the previous dialogue flow.**
+
+## User Input
+{user_input}
+
+## Reply requirements
+- The answer must be returned in JSON format.
+- Strictly ensure that the output is in a valid JSON format.
+- Do not add prefix ```json or suffix ```
+- The answer needs to include the following fields such as:
+{{
+"classificationId": 0,
+"reason": ""
+}}
+
+## Limit
+- Please do not reply in text."""
diff --git a/apps/application/workflow/nodes/loop_break_node/__init__.py b/apps/application/workflow/nodes/loop_break_node/__init__.py
new file mode 100644
index 00000000000..8bc0177353d
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_break_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/6 15:00
+ @desc:
+"""
+from .loop_break_node import LoopBreakNode
diff --git a/apps/application/workflow/nodes/loop_break_node/loop_break_node.py b/apps/application/workflow/nodes/loop_break_node/loop_break_node.py
new file mode 100644
index 00000000000..4e19ed8901b
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_break_node/loop_break_node.py
@@ -0,0 +1,55 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: loop_break_node.py
+@date:2026/7/6 15:00
+@desc:
+"""
+
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.compare import do_assertion
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode, Signal
+from application.workflow.status import Status
+
+
+class ConditionSerializer(serializers.Serializer):
+ compare = serializers.CharField(required=True, label=_("Comparator"))
+ value = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("value"))
+ field = serializers.ListField(required=True, label=_("Fields"))
+
+
+class LoopBreakNodeSerializer(serializers.Serializer):
+ condition = serializers.CharField(required=True, label=_("Condition or|and"))
+ condition_list = ConditionSerializer(many=True)
+
+
+class LoopBreakNode(INode):
+ serializer_class = LoopBreakNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "loop-break-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ condition = node_params.get("condition")
+ condition_list = node_params.get("condition_list", [])
+
+ is_break = do_assertion(self.workflow_manage, condition, condition_list)
+ self.write_context("is_break", is_break)
+
+ if is_break:
+ self.complete(Status.SUCCESS, signal=Signal.BREAK)
+ return
+ self.complete(Status.SUCCESS)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "is_break": self.get_context("is_break"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/loop_continue_node/__init__.py b/apps/application/workflow/nodes/loop_continue_node/__init__.py
new file mode 100644
index 00000000000..a7733ae87d4
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_continue_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/6 15:10
+ @desc:
+"""
+from .loop_continue_node import LoopContinueNode
diff --git a/apps/application/workflow/nodes/loop_continue_node/loop_continue_node.py b/apps/application/workflow/nodes/loop_continue_node/loop_continue_node.py
new file mode 100644
index 00000000000..aabab104314
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_continue_node/loop_continue_node.py
@@ -0,0 +1,55 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: loop_continue_node.py
+@date:2026/7/6 15:10
+@desc:
+"""
+
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.compare import do_assertion
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode, Signal
+from application.workflow.status import Status
+
+
+class ConditionSerializer(serializers.Serializer):
+ compare = serializers.CharField(required=True, label=_("Comparator"))
+ value = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("value"))
+ field = serializers.ListField(required=True, label=_("Fields"))
+
+
+class LoopContinueNodeSerializer(serializers.Serializer):
+ condition = serializers.CharField(required=True, label=_("Condition or|and"))
+ condition_list = ConditionSerializer(many=True)
+
+
+class LoopContinueNode(INode):
+ serializer_class = LoopContinueNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "loop-continue-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ condition = node_params.get("condition")
+ condition_list = node_params.get("condition_list", [])
+
+ is_continue = do_assertion(self.workflow_manage, condition, condition_list)
+ self.write_context("is_continue", is_continue)
+
+ if is_continue:
+ self.complete(Status.SUCCESS, signal=Signal.CONTINUE)
+ return
+ self.complete(Status.SUCCESS)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "is_continue": self.get_context("is_continue"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/loop_node/__init__.py b/apps/application/workflow/nodes/loop_node/__init__.py
new file mode 100644
index 00000000000..826ece19728
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from .loop_node import LoopNode
diff --git a/apps/application/workflow/nodes/loop_node/loop_node.py b/apps/application/workflow/nodes/loop_node/loop_node.py
new file mode 100644
index 00000000000..7697daa3e63
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_node/loop_node.py
@@ -0,0 +1,254 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: loop_node.py
+@date:2026/7/2 10:00
+@desc:
+"""
+
+import time
+
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType, new_instance
+from application.workflow.i_node import INode, Signal
+
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+
+from common.exception.app_exception import AppApiException
+
+MAX_LOOP_COUNT = 500
+
+
+class LoopNodeSerializer(serializers.Serializer):
+ loop_type = serializers.CharField(required=True, label=_("loop_type"))
+ array = serializers.ListField(required=False, allow_null=True, label=_("array"))
+ number = serializers.IntegerField(required=False, allow_null=True, label=_("number"))
+ loop_body = serializers.DictField(required=True, label="循环体")
+
+ def is_valid(self, *, raise_exception=False):
+ super().is_valid(raise_exception=True)
+ loop_type = self.data.get("loop_type")
+ if loop_type == "ARRAY":
+ array = self.data.get("array")
+ if array is None or len(array) == 0:
+ message = _("{field}, this field is required.", field="array")
+ raise AppApiException(500, message)
+ elif loop_type == "NUMBER":
+ number = self.data.get("number")
+ if number is None:
+ message = _("{field}, this field is required.", field="number")
+ raise AppApiException(500, message)
+
+
+def _generate_loop_number(number, start_index=0):
+ return iter([(i, i) for i in range(start_index, number)])
+
+
+def _generate_loop_array(array, start_index=0):
+ return iter([(item, i) for i, item in enumerate(array) if i >= start_index])
+
+
+def _generate_while_loop(number, start_index=0):
+ return iter([(i, i) for i in range(start_index, number)])
+
+
+class LoopNode(INode):
+ serializer_class = LoopNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "loop-node"
+
+ def _run(self):
+ self.execute()
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ loop_type = node_params.get("loop_type")
+ array = node_params.get("array")
+ number = node_params.get("number")
+ loop_body = node_params.get("loop_body")
+
+ # 从 position 获取 start_index
+ position = workflow_params.get("position") or {}
+ start_index = position.get("index") or 0 if position.get("id") == self.node.id else 0
+
+ if loop_type == "ARRAY" and isinstance(array, list) and len(array) >= 2:
+ array = self.workflow_manage.get_reference_field(array[0], array[1:])
+
+ self.write_context("params", {"loop_type": loop_type, "array": array, "number": number})
+
+ # 根据 start_index 构建迭代器
+ if loop_type == "ARRAY":
+ iterator = _generate_loop_array(array, start_index=start_index)
+ elif loop_type == "LOOP":
+ iterator = _generate_while_loop(number or MAX_LOOP_COUNT, start_index=start_index)
+ else:
+ iterator = _generate_loop_number(number, start_index=start_index)
+
+ self._loop_node_data = self.get_context("loop_node_data") or []
+ self._loop_answer_data = self.get_context("loop_answer_data") or []
+ self._answer_text = self.get_context("answer") or ""
+ self._workflow_params = workflow_params
+ self._loop_body = loop_body
+ self._iterator = iterator
+
+ self._run_next()
+
+ def _run_next(self):
+ try:
+ item, index = next(self._iterator)
+ except StopIteration:
+ self.write_context("answer", self._answer_text)
+ self.write_context("run_time", time.time() - self.data.get("start_time", time.time()))
+ self.complete(Status.SUCCESS)
+ return
+ loop_context = {"index": index, "item": item}
+ workflow = new_instance(self._loop_body, self.get_workflow_type())
+
+ chunk_list = []
+
+ def on_next(wf_manage, content):
+ chunk_list.append(content)
+ if hasattr(content, "content"):
+ self._answer_text += content.content
+ content.position = Position(self.get_node_id(), index, content.position)
+ self.write(content)
+
+ def on_complete(wf_manage, error):
+ self._loop_node_data.append(wf_manage.context)
+ self._loop_answer_data.append(chunk_list)
+ self.write_context("loop_node_data", self._loop_node_data)
+ self.write_context("loop_answer_data", self._loop_answer_data)
+ self.write_context("index", index)
+ self.write_context("item", item)
+
+ if wf_manage.signal == Signal.BREAK or wf_manage.signal == Signal.FORM:
+ self.write_context("answer", self._answer_text)
+ self.write_context("run_time", time.time() - self.data.get("start_time", time.time()))
+ self.complete(Status.SUCCESS)
+ return
+
+ if wf_manage.signal == Signal.CONTINUE:
+ self._run_next()
+ return
+
+ if error:
+ self.write_context("error_message", str(error))
+ return
+
+ self._run_next()
+
+ from application.workflow.workflow_manage import CallBack
+ from application.workflow.loop_workflow_manage import LoopWorkFlowManage
+ from application.workflow.nodes import get_node_class
+
+ call_back = CallBack(on_next, on_complete)
+
+ loop_start_class = get_node_class("loop-start-node", self.get_workflow_type())
+
+ # 获取 position,当前 index 和 position.index 一致时传入 children
+ position = self._workflow_params.get("position") or {}
+ child_position = None
+ if position.get("id") == self.node.id and position.get("index") == index:
+ child_position = position.get("children")
+
+ def get_start_node_fn(wf, wf_manage):
+ # 如果有 child_position,从指定节点开始
+ if child_position and child_position.get("id"):
+ node_id = child_position.get("id")
+ node = wf.get_node(node_id)
+ if node:
+ node_class = get_node_class(node.type, self.get_workflow_type())
+ return node_class(node, wf_manage, lambda n: n.properties.get("node_data", {}))
+
+ # 默认从 loop-start-node 开始
+ start_node = wf.get_node("loop-start-node")
+ return loop_start_class(start_node, wf_manage, lambda n: n.properties.get("node_data", {}))
+
+ # 构建子工作流参数,第一次迭代传入 child_position
+ loop_workflow_params = dict(self._workflow_params)
+ if child_position:
+ loop_workflow_params["position"] = child_position
+ else:
+ loop_workflow_params.pop("position", None)
+
+ loop_manage = LoopWorkFlowManage(
+ workflow=workflow,
+ parameters=loop_workflow_params,
+ workflow_type=self.get_workflow_type(),
+ call_back=call_back,
+ get_start_node=get_start_node_fn,
+ parent_workflow_manage=self.workflow_manage,
+ loop_context=loop_context,
+ )
+ loop_manage.start_node.workflow_manage = loop_manage
+ loop_manage.run()
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "params": self.get_context("params"),
+ "index": self.get_context("index"),
+ "item": self.get_context("item"),
+ "answer": self.get_context("answer"),
+ }
+ )
+
+ loop_node_data = self.get_context("loop_node_data") or []
+ loop_details = []
+ position_index = 0
+
+ # 从 old_details 获取之前的迭代详情
+ old_iteration_details = []
+ if old_details and isinstance(old_details, dict):
+ old_iteration_details = old_details.get("iteration_details", [])
+
+ # 1. 先把 old_iteration_details 全部复制过来,记录断点位置
+ if old_iteration_details and position and position.id == self.node.id:
+ for i, value in enumerate(old_iteration_details):
+ loop_details.append(value)
+ if position.index == i:
+ position_index = i
+
+ # 2. 遍历当前新执行的迭代
+ for new_iter_index, iteration_context in enumerate(loop_node_data):
+ iteration_result = []
+
+ # 第一个新迭代且是断点:传入旧详情
+ if new_iter_index == 0 and position and position.id == self.node.id:
+ # 续跑迭代:先复制旧详情
+ if position_index < len(old_iteration_details):
+ for old_item in old_iteration_details[position_index]:
+ iteration_result.append(old_item)
+
+ # 遍历当前迭代的节点 context
+ child_position = (
+ position.children if new_iter_index == 0 and position and position.id == self.node.id else None
+ )
+ child_position_index = 0
+ for node_id, node_context in iteration_context.items():
+ node_details = {
+ "node_id": node_id,
+ **node_context,
+ }
+ # 断点节点:插入到 child_position_index 位置
+ if child_position and child_position.id == node_id:
+ iteration_result.insert(child_position_index, node_details)
+ else:
+ iteration_result.append(node_details)
+
+ # 第一个新迭代且是断点:插入到 position_index,否则追加
+ if new_iter_index == 0 and position and position.id == self.node.id:
+ loop_details.insert(position_index, iteration_result)
+ else:
+ loop_details.append(iteration_result)
+
+ details["iteration_details"] = loop_details
+ return details
diff --git a/apps/application/workflow/nodes/loop_start_node/__init__.py b/apps/application/workflow/nodes/loop_start_node/__init__.py
new file mode 100644
index 00000000000..7d1dbb5f046
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_start_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from .loop_start_node import LoopStartNode
diff --git a/apps/application/workflow/nodes/loop_start_node/loop_start_node.py b/apps/application/workflow/nodes/loop_start_node/loop_start_node.py
new file mode 100644
index 00000000000..53943087a54
--- /dev/null
+++ b/apps/application/workflow/nodes/loop_start_node/loop_start_node.py
@@ -0,0 +1,41 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: loop_start_node.py
+@date:2026/7/2 10:00
+@desc:
+"""
+
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+
+
+class LoopStartNodeSerializer(serializers.Serializer):
+ pass
+
+
+class LoopStartNode(INode):
+ serializer_class = LoopStartNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "loop-start-node"
+
+ def execute(self):
+ loop_context = getattr(self.workflow_manage, "loop_context", {})
+ index = loop_context.get("index", 0)
+ item = loop_context.get("item", None)
+
+ self.write_context("index", index)
+ self.write_context("item", item)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "index": self.get_context("index"),
+ "item": self.get_context("item"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/mcp_node/__init__.py b/apps/application/workflow/nodes/mcp_node/__init__.py
new file mode 100644
index 00000000000..cd0b0387c40
--- /dev/null
+++ b/apps/application/workflow/nodes/mcp_node/__init__.py
@@ -0,0 +1,2 @@
+# coding=utf-8
+from .mcp_node import McpNode
diff --git a/apps/application/workflow/nodes/mcp_node/mcp_node.py b/apps/application/workflow/nodes/mcp_node/mcp_node.py
new file mode 100644
index 00000000000..0155d36ee98
--- /dev/null
+++ b/apps/application/workflow/nodes/mcp_node/mcp_node.py
@@ -0,0 +1,101 @@
+# coding=utf-8
+import asyncio
+import json
+from typing import List, Dict, Any
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_mcp_adapters.client import MultiServerMCPClient
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.status import Status
+from common.utils.tool_code import ToolExecutor
+from tools.models import Tool
+
+
+class McpNodeSerializer(serializers.Serializer):
+ mcp_servers = serializers.JSONField(required=True, label=_("Mcp servers"))
+ mcp_server = serializers.CharField(required=True, label=_("Mcp server"))
+ mcp_tool = serializers.CharField(required=True, label=_("Mcp tool"))
+ mcp_tool_id = serializers.CharField(required=False, label=_("Mcp tool"), allow_null=True, allow_blank=True)
+ mcp_source = serializers.CharField(required=False, label=_("Mcp source"), allow_blank=True, allow_null=True)
+ tool_params = serializers.DictField(required=True, label=_("Tool parameters"))
+
+
+class McpNode(INode):
+ serializer_class = McpNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "mcp-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+
+ mcp_servers = node_params.get("mcp_servers")
+ mcp_server = node_params.get("mcp_server")
+ mcp_tool = node_params.get("mcp_tool")
+ mcp_tool_id = node_params.get("mcp_tool_id")
+ mcp_source = node_params.get("mcp_source")
+ tool_params = node_params.get("tool_params", {})
+
+ if mcp_source == "referencing":
+ if not mcp_tool_id:
+ raise ValueError("MCP tool ID is required when mcp_source is 'referencing'.")
+ tool = QuerySet(Tool).filter(id=mcp_tool_id).first()
+ if not tool:
+ raise ValueError(f"Tool with ID {mcp_tool_id} not found.")
+ if not tool.is_active:
+ raise ValueError(f"Tool with ID {mcp_tool_id} is inactive.")
+ servers = json.loads(tool.code)
+ else:
+ servers = json.loads(mcp_servers) if isinstance(mcp_servers, str) else mcp_servers
+
+ servers = self._handle_variables(servers)
+ ToolExecutor().validate_mcp_transport(json.dumps(servers))
+
+ params = json.loads(json.dumps(tool_params))
+ params = self._handle_variables(params)
+
+ self._check_cancelled()
+
+ async def call_tool(t, a):
+ client = MultiServerMCPClient(servers)
+ async with client.session(mcp_server) as s:
+ return await s.call_tool(t, a)
+
+ res = asyncio.run(call_tool(mcp_tool, params))
+ result = [content.text for content in res.content]
+
+ self.write_context("result", result)
+ self.write_context("tool_params", params)
+ self.write_context("mcp_tool", mcp_tool)
+
+ def _handle_variables(self, tool_params: Any) -> Any:
+ if isinstance(tool_params, dict):
+ for k, v in tool_params.items():
+ tool_params[k] = self._handle_variables(v)
+ return tool_params
+ elif isinstance(tool_params, list):
+ if len(tool_params) > 0 and isinstance(tool_params[0], str):
+ return self._get_reference_content(tool_params)
+ return [self._handle_variables(item) for item in tool_params]
+ elif isinstance(tool_params, str):
+ return self.workflow_manage.generate_prompt(tool_params)
+ return tool_params
+
+ def _get_reference_content(self, fields: List[str]) -> Any:
+ if fields:
+ return self.workflow_manage.get_reference_field(fields[0], fields[1:])
+ return None
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "mcp_tool": self.get_context("mcp_tool"),
+ "tool_params": self.get_context("tool_params"),
+ "result": self.get_context("result"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/parameter_extraction_node/__init__.py b/apps/application/workflow/nodes/parameter_extraction_node/__init__.py
new file mode 100644
index 00000000000..e695c85c365
--- /dev/null
+++ b/apps/application/workflow/nodes/parameter_extraction_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .parameter_extraction_node import ParameterExtractionNode
diff --git a/apps/application/workflow/nodes/parameter_extraction_node/parameter_extraction_node.py b/apps/application/workflow/nodes/parameter_extraction_node/parameter_extraction_node.py
new file mode 100644
index 00000000000..a5847b945a2
--- /dev/null
+++ b/apps/application/workflow/nodes/parameter_extraction_node/parameter_extraction_node.py
@@ -0,0 +1,157 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: parameter_extraction_node.py
+@desc:
+"""
+
+import json
+import re
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import HumanMessage
+from langchain_core.prompts import PromptTemplate
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from models_provider.models import Model
+from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential
+
+prompt = """
+Please strictly process the text according to the following requirements:
+**Task**:
+Extract specified field information from given text
+
+**Enter text**:
+{{question}}
+
+**Extract configuration**:
+{{properties}}
+
+**Rule**:
+- Strictly follow the data and field of Extract configuration
+- If not found, use null value
+- Only return pure JSON without additional text
+- Keep the string format neat
+"""
+
+
+class ParameterExtractionNodeSerializer(serializers.Serializer):
+ input_variable = serializers.ListField(required=True, label=_("input variable"))
+ variable_list = serializers.ListField(required=True, label=_("Split variables"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+
+
+def _get_default_model_params_setting(model_id):
+ model = QuerySet(Model).filter(id=model_id).first()
+ credential = get_model_credential(model.provider, model.model_type, model.model_name)
+ model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
+ return model_params_setting
+
+
+def _generate_properties(variable_list):
+ return {
+ variable["field"]: {
+ "type": variable["parameter_type"],
+ "description": (variable.get("desc") or ""),
+ "title": variable["label"],
+ }
+ for variable in variable_list
+ }
+
+
+def _generate_example(variable_list):
+ return {variable["field"]: None for variable in variable_list}
+
+
+def _generate_content(input_variable, variable_list):
+ properties = _generate_properties(variable_list)
+ prompt_template = PromptTemplate.from_template(prompt, template_format="jinja2")
+ value = prompt_template.format(properties=properties, question=input_variable)
+ return value
+
+
+def _json_loads(response, variable_list):
+ if not response or not isinstance(response, str):
+ return _generate_example(variable_list)
+
+ cleaned = response.strip()
+
+ extraction_strategies = [
+ lambda: json.loads(cleaned),
+ lambda: json.loads(re.search(r"```(?:json)?\s*(\{.*?\})\s*```", cleaned, re.DOTALL).group(1)),
+ lambda: json.loads(re.search(r"(\{.*\})", cleaned, flags=re.DOTALL).group(1)),
+ ]
+ for strategy in extraction_strategies:
+ try:
+ result = strategy()
+ return result
+ except:
+ continue
+ return _generate_example(variable_list)
+
+
+class ParameterExtractionNode(INode):
+ serializer_class = ParameterExtractionNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "parameter-extraction-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ model_params_setting = node_params.get("model_params_setting")
+ input_variable_ref = node_params.get("input_variable")
+ variable_list = node_params.get("variable_list")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ if model_params_setting is None and model_id:
+ model_params_setting = _get_default_model_params_setting(model_id)
+
+ workspace_id = workflow_params.get("workspace_id")
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ input_variable = self.workflow_manage.get_reference_field(input_variable_ref[0], input_variable_ref[1:])
+
+ input_variable_str = str(input_variable)
+ self.write_context("request", input_variable_str)
+
+ content = _generate_content(input_variable_str, variable_list)
+ self._check_cancelled()
+ response = chat_model.invoke([HumanMessage(content=content)])
+ result = _json_loads(response.content, variable_list)
+
+ self.write_context("result", result)
+ for key, value in result.items():
+ self.write_context(key, value)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "request": self.get_context("request"),
+ "result": self.get_context("result"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/question_node/__init__.py b/apps/application/workflow/nodes/question_node/__init__.py
new file mode 100644
index 00000000000..e8ddbf43989
--- /dev/null
+++ b/apps/application/workflow/nodes/question_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .question_node import QuestionNode
diff --git a/apps/application/workflow/nodes/question_node/question_node.py b/apps/application/workflow/nodes/question_node/question_node.py
new file mode 100644
index 00000000000..861eae9d594
--- /dev/null
+++ b/apps/application/workflow/nodes/question_node/question_node.py
@@ -0,0 +1,154 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: question_node.py
+@desc:
+"""
+
+import re
+from functools import reduce
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from langchain_core.messages import HumanMessage, SystemMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from models_provider.models import Model
+from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential
+
+
+class QuestionNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ system = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Role Setting"))
+ prompt = serializers.CharField(required=True, label=_("Prompt word"))
+ dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+
+
+def _get_default_model_params_setting(model_id):
+ model = QuerySet(Model).filter(id=model_id).first()
+ credential = get_model_credential(model.provider, model.model_type, model.model_name)
+ model_params_setting = credential.get_model_params_setting_form(model.model_name).get_default_form_data()
+ return model_params_setting
+
+
+def _get_history_message(history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+ for message in history_message:
+ if isinstance(message.content, str):
+ message.content = re.sub(r".*?", "", message.content, flags=re.DOTALL)
+ return history_message
+
+
+class QuestionNode(INode):
+ serializer_class = QuestionNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "question-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ model_params_setting = node_params.get("model_params_setting")
+ system = node_params.get("system", "")
+ prompt = node_params.get("prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ is_result = node_params.get("is_result", False)
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ if model_params_setting is None and model_id:
+ model_params_setting = _get_default_model_params_setting(model_id)
+
+ chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = _get_history_message(history_chat_record, dialogue_number)
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message or [])],
+ )
+
+ question = HumanMessage(self.workflow_manage.generate_prompt(prompt))
+ self.write_context("question", question.content)
+
+ system = self.workflow_manage.generate_prompt(system)
+ self.write_context("system", system)
+
+ if system and len(system) > 0:
+ message_list = [SystemMessage(system), *history_message, question]
+ else:
+ message_list = [*history_message, question]
+ self.write_context(
+ "message_list",
+ [{"content": m.content, "role": m.type} for m in message_list],
+ )
+
+ response = chat_model.stream(message_list)
+ answer = ""
+
+ for chunk in response:
+ self._check_cancelled()
+ answer += chunk.content
+
+ message_tokens = chat_model.get_num_tokens_from_messages(message_list)
+ answer_tokens = chat_model.get_num_tokens(answer)
+ self.write_context("message_tokens", message_tokens)
+ self.write_context("answer_tokens", answer_tokens)
+ self.write_context("answer", answer)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(TextContent(self.get_node_id(), answer, Status.SUCCESS, node_info, Position(self.get_node_id())))
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "system": self.get_context("system"),
+ "message_tokens": self.get_context("message_tokens"),
+ "answer_tokens": self.get_context("answer_tokens"),
+ "history_message": self.get_context("history_message"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/reply_node/__init__.py b/apps/application/workflow/nodes/reply_node/__init__.py
new file mode 100644
index 00000000000..5bdd7388bc7
--- /dev/null
+++ b/apps/application/workflow/nodes/reply_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from .reply_node import ReplyNode
diff --git a/apps/application/workflow/nodes/reply_node/reply_node.py b/apps/application/workflow/nodes/reply_node/reply_node.py
new file mode 100644
index 00000000000..f40b8f53373
--- /dev/null
+++ b/apps/application/workflow/nodes/reply_node/reply_node.py
@@ -0,0 +1,71 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: reply_node.py
+@date:2026/7/2 10:00
+@desc:
+"""
+
+from typing import List
+import uuid_utils.compat as uuid
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+
+
+class ReplyNodeSerializer(serializers.Serializer):
+ reply_type = serializers.CharField(required=True, label=_("Response Type"))
+ fields = serializers.ListField(required=False, label=_("Reference Field"))
+ content = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Direct answer content"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+
+
+class ReplyNode(INode):
+ serializer_class = ReplyNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "reply-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ chunk_id = uuid.uuid7()
+
+ reply_type = node_params.get("reply_type")
+ fields = node_params.get("fields")
+ content = node_params.get("content")
+ is_result = node_params.get("is_result", False)
+
+ if reply_type == "referencing":
+ result = self._get_reference_content(fields)
+ else:
+ result = self._generate_reply_content(content)
+
+ self.write_context("answer", result)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(TextContent(str(chunk_id), result, Status.SUCCESS, node_info, Position(self.get_node_id())))
+
+ def _generate_reply_content(self, prompt):
+ if prompt is None:
+ return ""
+ return self.workflow_manage.generate_prompt(prompt)
+
+ def _get_reference_content(self, fields: List[str]):
+ if fields and len(fields) >= 2:
+ return str(self.workflow_manage.get_reference_field(fields[0], fields[1:]))
+ return ""
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "answer": self.get_context("answer"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/reranker_node/__init__.py b/apps/application/workflow/nodes/reranker_node/__init__.py
new file mode 100644
index 00000000000..e6adc8ee183
--- /dev/null
+++ b/apps/application/workflow/nodes/reranker_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .reranker_node import RerankerNode
diff --git a/apps/application/workflow/nodes/reranker_node/reranker_node.py b/apps/application/workflow/nodes/reranker_node/reranker_node.py
new file mode 100644
index 00000000000..e830bd8499e
--- /dev/null
+++ b/apps/application/workflow/nodes/reranker_node/reranker_node.py
@@ -0,0 +1,188 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: reranker_node.py
+@desc:
+"""
+
+from typing import List
+
+from django.utils.translation import gettext_lazy as _
+from langchain_core.documents import Document
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from models_provider.tools import get_model_instance_by_model_workspace_id
+
+
+class RerankerSettingSerializer(serializers.Serializer):
+ top_n = serializers.IntegerField(required=True, label=_("Reference segment number"))
+ similarity = serializers.FloatField(required=True, max_value=2, min_value=0, label=_("Reference segment number"))
+ max_paragraph_char_number = serializers.IntegerField(
+ required=True, label=_("Maximum number of words in a quoted segment")
+ )
+
+
+class RerankerNodeSerializer(serializers.Serializer):
+ reranker_setting = RerankerSettingSerializer(required=True)
+ question_reference_address = serializers.ListField(required=True)
+ reranker_model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True)
+ reranker_model_id_type = serializers.CharField(required=False, default="custom")
+ reranker_model_id_reference = serializers.ListField(required=False, child=serializers.CharField(), allow_empty=True)
+ reranker_reference_list = serializers.ListField(required=True, child=serializers.ListField(required=True))
+ show_knowledge = serializers.BooleanField(
+ required=True, label=_("The results are displayed in the knowledge sources")
+ )
+
+
+def _merge_reranker_list(reranker_list, result=None):
+ if result is None:
+ result = []
+ for document in reranker_list:
+ if isinstance(document, list):
+ _merge_reranker_list(document, result)
+ elif isinstance(document, dict):
+ content = document.get("title", "") + document.get("content", "")
+ title = document.get("title")
+ result.append(
+ Document(
+ page_content=str(document) if len(content) == 0 else content, metadata={"title": title, **document}
+ )
+ )
+ else:
+ result.append(Document(page_content=str(document), metadata={}))
+ return result
+
+
+def _filter_result(document_list: List[Document], max_paragraph_char_number, top_n, similarity):
+ use_len = 0
+ result = []
+ for index in range(len(document_list)):
+ document = document_list[index]
+ if (
+ use_len >= max_paragraph_char_number
+ or index >= top_n
+ or document.metadata.get("relevance_score") < similarity
+ ):
+ break
+ content = document.page_content[0 : max_paragraph_char_number - use_len]
+ use_len = use_len + len(content)
+ result.append({"page_content": content, "metadata": document.metadata})
+ return result
+
+
+def _reset_result_list(result_list: List[Document], document_list: List[Document]):
+ r = []
+ document_list = document_list.copy()
+ for result in result_list:
+ filter_result_list = [document for document in document_list if document.page_content == result.page_content]
+ if len(filter_result_list) > 0:
+ item = filter_result_list[0]
+ document_list.remove(item)
+ r.append(
+ Document(
+ page_content=item.page_content,
+ metadata={**item.metadata, "relevance_score": result.metadata.get("relevance_score")},
+ )
+ )
+ else:
+ r.append(result)
+ return r
+
+
+def _reset_metadata(metadata):
+ meta = metadata.get("meta")
+ if isinstance(metadata.get("meta"), dict):
+ if not meta.get("allow_download", False):
+ metadata["meta"] = {"allow_download": False}
+ return metadata
+
+
+class RerankerNode(INode):
+ serializer_class = RerankerNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.TOOL]
+ type = "reranker-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ question_ref = node_params.get("question_reference_address")
+ reranker_reference_list = node_params.get("reranker_reference_list")
+ reranker_setting = node_params.get("reranker_setting")
+ reranker_model_id = node_params.get("reranker_model_id")
+ reranker_model_id_type = node_params.get("reranker_model_id_type", "custom")
+ reranker_model_id_reference = node_params.get("reranker_model_id_reference")
+ show_knowledge = node_params.get("show_knowledge", False)
+
+ question = self.workflow_manage.get_reference_field(question_ref[0], question_ref[1:])
+ question = str(question)
+
+ reranker_list = [self.workflow_manage.get_reference_field(ref[0], ref[1:]) for ref in reranker_reference_list]
+
+ if reranker_model_id_type == "reference" and reranker_model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ reranker_model_id_reference[0],
+ reranker_model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ reranker_model_id = reference_data.get(
+ "reranker_model_id", reference_data.get("model_id", reranker_model_id)
+ )
+
+ if not reranker_model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ self.write_context("show_knowledge", show_knowledge)
+
+ documents = _merge_reranker_list(reranker_list)
+ documents = [d for d in documents if d.page_content and len(d.page_content) > 0]
+
+ if len(documents) == 0:
+ self.write_context("document_list", [])
+ self.write_context("question", question)
+ self.write_context("result_list", [])
+ self.write_context("result", "")
+ return
+
+ top_n = reranker_setting.get("top_n", 3)
+ self.write_context(
+ "document_list",
+ [
+ {"page_content": document.page_content, "metadata": _reset_metadata(document.metadata)}
+ for document in documents
+ ],
+ )
+ self.write_context("question", question)
+
+ workspace_id = workflow_params.get("workspace_id")
+ reranker_model = get_model_instance_by_model_workspace_id(reranker_model_id, workspace_id, top_n=top_n)
+
+ self._check_cancelled()
+ result = reranker_model.compress_documents(documents, question)
+
+ similarity = reranker_setting.get("similarity", 0.6)
+ max_paragraph_char_number = reranker_setting.get("max_paragraph_char_number", 5000)
+
+ result = _reset_result_list(result, documents)
+ r = _filter_result(result, max_paragraph_char_number, top_n, similarity)
+
+ self.write_context("result_list", r)
+ self.write_context("result", "".join([item.get("page_content") for item in r]))
+ self.write_context(
+ "is_hit_handling_method_list", [row for row in r if row.get("metadata").get("is_hit_handling_method")]
+ )
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "result_list": self.get_context("result_list"),
+ "result": self.get_context("result"),
+ "document_list": self.get_context("document_list"),
+ "show_knowledge": self.get_context("show_knowledge"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/search_document_node/__init__.py b/apps/application/workflow/nodes/search_document_node/__init__.py
new file mode 100644
index 00000000000..822ff4d9c8d
--- /dev/null
+++ b/apps/application/workflow/nodes/search_document_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .search_document_node import SearchDocumentNode
diff --git a/apps/application/workflow/nodes/search_document_node/search_document_node.py b/apps/application/workflow/nodes/search_document_node/search_document_node.py
new file mode 100644
index 00000000000..cd6280de78d
--- /dev/null
+++ b/apps/application/workflow/nodes/search_document_node/search_document_node.py
@@ -0,0 +1,237 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: search_document_node.py
+@desc:
+"""
+
+from typing import List
+
+import jieba
+from django.db.models import Q
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from common.auth.constants.role_constants import RoleConstants
+from common.database_model_manage.database_model_manage import DatabaseModelManage
+from common.utils.shared_resource_auth import filter_authorized_ids
+from knowledge.models import Document, DocumentTag, Knowledge
+
+
+class SearchDocumentNodeSerializer(serializers.Serializer):
+ knowledge_id_list = serializers.ListField(
+ required=False, child=serializers.UUIDField(required=True), label=_("knowledge id list"), default=list
+ )
+ search_mode = serializers.ChoiceField(
+ required=False, choices=["auto", "custom"], label=_("search mode"), default="auto"
+ )
+ search_scope_type = serializers.ChoiceField(
+ required=False,
+ choices=["custom", "referencing"],
+ label=_("search scope type"),
+ allow_null=True,
+ default="custom",
+ )
+ search_scope_source = serializers.ChoiceField(
+ required=False, choices=["document", "knowledge"], label=_("search scope variable type"), default="knowledge"
+ )
+ search_scope_reference = serializers.ListField(required=False, label=_("search scope variable"), default=list)
+ question_reference = serializers.ListField(required=False, label=_("question reference address"), default=list)
+ search_condition_type = serializers.ChoiceField(
+ required=False, choices=["AND", "OR"], label=_("search condition type"), default="AND"
+ )
+ search_condition_list = serializers.ListField(required=False, label=_("search condition list"), default=list)
+
+
+def _handle_auto_tags(workflow_manage, document_id_list, question_reference):
+ question = (
+ workflow_manage.get_reference_field(question_reference[0], question_reference[1:]) if question_reference else ""
+ )
+ keywords = jieba.lcut(str(question))
+ if not keywords:
+ return set()
+
+ q_objects = Q()
+ for keyword in keywords:
+ q_objects |= Q(tag__value__icontains=keyword)
+
+ matched_doc_ids = set(
+ QuerySet(DocumentTag)
+ .filter(document_id__in=document_id_list)
+ .filter(q_objects)
+ .values_list("document_id", flat=True)
+ .distinct()
+ )
+ return matched_doc_ids
+
+
+def _handle_custom_tags(workflow_manage, document_id_list, search_condition_list, search_condition_type):
+ if not search_condition_list:
+ return set(document_id_list)
+
+ if search_condition_type == "AND":
+ matched_doc_ids = set(document_id_list)
+ for condition in search_condition_list:
+ tag_key = condition["key"]
+ field_value = workflow_manage.generate_prompt(condition["value"])
+ compare_type = condition["compare"]
+
+ if not field_value or field_value == "None" or len(field_value) == 0:
+ continue
+
+ if compare_type == "not_contain":
+ exclude_docs = set(
+ QuerySet(DocumentTag)
+ .filter(document_id__in=matched_doc_ids, tag__key=tag_key, tag__value__icontains=field_value)
+ .values_list("document_id", flat=True)
+ .distinct()
+ )
+ matched_doc_ids = matched_doc_ids - exclude_docs
+ else:
+ if compare_type == "contain":
+ q_filter = Q(tag__key=tag_key, tag__value__icontains=field_value)
+ elif compare_type == "eq":
+ q_filter = Q(tag__key=tag_key, tag__value=field_value)
+ else:
+ continue
+
+ tag_docs = set(
+ QuerySet(DocumentTag)
+ .filter(document_id__in=matched_doc_ids)
+ .filter(q_filter)
+ .values_list("document_id", flat=True)
+ .distinct()
+ )
+ matched_doc_ids = matched_doc_ids.intersection(tag_docs)
+
+ return matched_doc_ids
+ else:
+ matched_docs = set()
+ for condition in search_condition_list:
+ tag_key = condition["key"]
+ field_value = workflow_manage.generate_prompt(condition["value"])
+ compare_type = condition["compare"]
+
+ if not field_value or field_value == "None" or len(field_value) == 0:
+ continue
+
+ if compare_type == "not_contain":
+ exclude_docs = set(
+ QuerySet(DocumentTag)
+ .filter(document_id__in=document_id_list, tag__key=tag_key, tag__value__icontains=field_value)
+ .values_list("document_id", flat=True)
+ .distinct()
+ )
+ matched_docs = matched_docs.union(set(document_id_list) - exclude_docs)
+ else:
+ if compare_type == "contain":
+ q_filter = Q(tag__key=tag_key, tag__value__icontains=field_value)
+ elif compare_type == "eq":
+ q_filter = Q(tag__key=tag_key, tag__value=field_value)
+ else:
+ continue
+
+ docs = set(
+ QuerySet(DocumentTag)
+ .filter(document_id__in=document_id_list)
+ .filter(q_filter)
+ .values_list("document_id", flat=True)
+ .distinct()
+ )
+ matched_docs = matched_docs.union(docs)
+
+ return matched_docs
+
+
+class SearchDocumentNode(INode):
+ serializer_class = SearchDocumentNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.TOOL]
+ type = "search-document-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ knowledge_id_list = node_params.get("knowledge_id_list", [])
+ search_mode = node_params.get("search_mode", "auto")
+ search_scope_type = node_params.get("search_scope_type", "custom")
+ search_scope_source = node_params.get("search_scope_source", "knowledge")
+ search_scope_reference = node_params.get("search_scope_reference", [])
+ question_reference = node_params.get("question_reference", [])
+ search_condition_type = node_params.get("search_condition_type", "AND")
+ search_condition_list = node_params.get("search_condition_list", [])
+
+ workspace_id = workflow_params.get("workspace_id")
+
+ if search_scope_type == "custom":
+ knowledge_id_list = filter_authorized_ids("knowledge", knowledge_id_list, workspace_id)
+ document_id_list = list(
+ QuerySet(Document).filter(knowledge_id__in=knowledge_id_list).values_list("id", flat=True)
+ )
+ else:
+ if search_scope_source == "document":
+ document_id_list = (
+ self.workflow_manage.get_reference_field(search_scope_reference[0], search_scope_reference[1:])
+ if search_scope_reference
+ else []
+ )
+ else:
+ ref_knowledge_ids = (
+ self.workflow_manage.get_reference_field(search_scope_reference[0], search_scope_reference[1:])
+ if search_scope_reference
+ else []
+ )
+ ref_knowledge_ids = filter_authorized_ids("knowledge", ref_knowledge_ids, workspace_id)
+ document_id_list = list(
+ QuerySet(Document).filter(knowledge_id__in=ref_knowledge_ids).values_list("id", flat=True)
+ )
+
+ get_knowledge_list_of_authorized = DatabaseModelManage.get_model("get_knowledge_list_of_authorized")
+ chat_user_type = workflow_params.get("chat_user_type")
+
+ if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
+ actual_knowledge_ids = list(
+ QuerySet(Document).filter(id__in=document_id_list).values_list("knowledge_id", flat=True).distinct()
+ )
+ authorized_knowledge_ids = get_knowledge_list_of_authorized(
+ workflow_params.get("chat_user_id"), [str(k_id) for k_id in actual_knowledge_ids]
+ )
+ document_id_list = list(
+ QuerySet(Document)
+ .filter(id__in=document_id_list, knowledge_id__in=authorized_knowledge_ids)
+ .values_list("id", flat=True)
+ )
+
+ if search_mode == "auto":
+ matched_doc_ids = _handle_auto_tags(self.workflow_manage, document_id_list, question_reference)
+ final_document_ids = list(matched_doc_ids)
+ else:
+ matched_document_ids = _handle_custom_tags(
+ self.workflow_manage, document_id_list, search_condition_list, search_condition_type
+ )
+ final_document_ids = list(matched_document_ids)
+
+ final_document_ids = [str(doc_id) for doc_id in final_document_ids]
+ document_items = list(QuerySet(Document).filter(id__in=final_document_ids).values())
+ final_knowledge_ids = list(set(str(doc["knowledge_id"]) for doc in document_items))
+ knowledge_items = list(QuerySet(Knowledge).filter(id__in=final_knowledge_ids).values())
+
+ self.write_context("document_list", final_document_ids)
+ self.write_context("document_items", document_items)
+ self.write_context("knowledge_list", final_knowledge_ids)
+ self.write_context("knowledge_items", knowledge_items)
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "document_list": self.get_context("document_list"),
+ "document_items": self.get_context("document_items"),
+ "knowledge_list": self.get_context("knowledge_list"),
+ "knowledge_items": self.get_context("knowledge_items"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/search_knowledge_node/__init__.py b/apps/application/workflow/nodes/search_knowledge_node/__init__.py
new file mode 100644
index 00000000000..2ce5f1602b2
--- /dev/null
+++ b/apps/application/workflow/nodes/search_knowledge_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: __init__.py
+ @date:2026/7/2 10:00
+ @desc:
+"""
+from .search_knowledge_node import SearchKnowledgeNode
diff --git a/apps/application/workflow/nodes/search_knowledge_node/search_knowledge_node.py b/apps/application/workflow/nodes/search_knowledge_node/search_knowledge_node.py
new file mode 100644
index 00000000000..cf6cda2304a
--- /dev/null
+++ b/apps/application/workflow/nodes/search_knowledge_node/search_knowledge_node.py
@@ -0,0 +1,282 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: search_knowledge_node.py
+@date:2026/7/2 10:00
+@desc:
+"""
+
+import os
+import re
+from typing import List, Dict
+
+from django.core import validators
+from django.db import connection
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from common.auth.constants.role_constants import RoleConstants
+from common.config.embedding_config import VectorStore
+from common.database_model_manage.database_model_manage import DatabaseModelManage
+from common.db.search import native_search
+from common.utils.common import flat_map, get_file_content
+from common.utils.shared_resource_auth import filter_authorized_ids
+from knowledge.models import Document, Paragraph, Knowledge, SearchMode
+from maxkb.conf import PROJECT_DIR
+from models_provider.tools import get_model_instance_by_model_workspace_id
+
+
+class DatasetSettingSerializer(serializers.Serializer):
+ top_n = serializers.IntegerField(required=True, label=_("Reference segment number"))
+ similarity = serializers.FloatField(required=True, max_value=2, min_value=0, label=_("similarity"))
+ search_mode = serializers.CharField(
+ required=True,
+ validators=[
+ validators.RegexValidator(
+ regex=re.compile("^embedding|keywords|blend$"),
+ message=_("The type only supports embedding|keywords|blend"),
+ code=500,
+ )
+ ],
+ label=_("Retrieval Mode"),
+ )
+ max_paragraph_char_number = serializers.IntegerField(
+ required=True, label=_("Maximum number of words in a quoted segment")
+ )
+
+
+class SearchKnowledgeNodeSerializer(serializers.Serializer):
+ knowledge_id_list = serializers.ListField(
+ required=True, child=serializers.UUIDField(required=True), label=_("Dataset id list")
+ )
+ knowledge_setting = DatasetSettingSerializer(required=True)
+ question_reference_address = serializers.ListField(required=True)
+ show_knowledge = serializers.BooleanField(
+ required=True, label=_("The results are displayed in the knowledge sources")
+ )
+ search_scope_type = serializers.ChoiceField(
+ required=False,
+ choices=["custom", "referencing"],
+ label=_("search scope type"),
+ allow_null=True,
+ default="custom",
+ )
+ search_scope_source = serializers.ChoiceField(
+ required=False, choices=["document", "knowledge"], label=_("search scope variable type"), default="knowledge"
+ )
+ search_scope_reference = serializers.ListField(required=False, label=_("search scope variable"), default=list)
+
+
+def _get_paragraph_list(chat_record, node_id):
+ return flat_map(
+ [
+ chat_record.details[key].get("paragraph_list", [])
+ for key in chat_record.details
+ if (chat_record.details[key].get("type", "") == "search-dataset-node")
+ and chat_record.details[key].get("paragraph_list", []) is not None
+ and key == node_id
+ ]
+ )
+
+
+def _get_embedding_id(dataset_id_list):
+ dataset_list = QuerySet(Knowledge).filter(id__in=dataset_id_list)
+ if len(set([dataset.embedding_model_id for dataset in dataset_list])) > 1:
+ raise Exception("关联知识库的向量模型不一致,无法召回分段。")
+ if len(dataset_list) == 0:
+ raise Exception("知识库设置错误,请重新设置知识库")
+ return dataset_list[0].embedding_model_id
+
+
+def _reset_title(title):
+ if title is None or len(title.strip()) == 0:
+ return ""
+ else:
+ return f"#### {title}\n"
+
+
+def _reset_meta(meta):
+ if not meta.get("allow_download", False):
+ return {"allow_download": False}
+ return meta
+
+
+def _reset_paragraph(paragraph: Dict, embedding_list: List):
+ filter_embedding_list = [
+ embedding for embedding in embedding_list if str(embedding.get("paragraph_id")) == str(paragraph.get("id"))
+ ]
+ if filter_embedding_list is not None and len(filter_embedding_list) > 0:
+ find_embedding = filter_embedding_list[-1]
+ return {
+ **paragraph,
+ "similarity": find_embedding.get("similarity"),
+ "is_hit_handling_method": find_embedding.get("similarity") > paragraph.get("directly_return_similarity")
+ and paragraph.get("hit_handling_method") == "directly_return",
+ "update_time": paragraph.get("update_time").strftime("%Y-%m-%d %H:%M:%S"),
+ "create_time": paragraph.get("create_time").strftime("%Y-%m-%d %H:%M:%S"),
+ "id": str(paragraph.get("id")),
+ "knowledge_id": str(paragraph.get("knowledge_id")),
+ "document_id": str(paragraph.get("document_id")),
+ "meta": _reset_meta(paragraph.get("meta")),
+ }
+
+
+def _list_paragraph(embedding_list: List, vector):
+ paragraph_id_list = [row.get("paragraph_id") for row in embedding_list]
+ if paragraph_id_list is None or len(paragraph_id_list) == 0:
+ return []
+ paragraph_list = native_search(
+ QuerySet(Paragraph).filter(id__in=paragraph_id_list),
+ get_file_content(
+ os.path.join(PROJECT_DIR, "apps", "application", "sql", "list_knowledge_paragraph_by_paragraph_id.sql")
+ ),
+ with_table_name=True,
+ )
+ if len(paragraph_list) != len(paragraph_id_list):
+ exist_paragraph_list = [row.get("id") for row in paragraph_list]
+ for paragraph_id in paragraph_id_list:
+ if paragraph_id not in exist_paragraph_list:
+ vector.delete_by_paragraph_id(paragraph_id)
+ return paragraph_list
+
+
+class SearchKnowledgeNode(INode):
+ serializer_class = SearchKnowledgeNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.TOOL]
+ type = "search-knowledge-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ knowledge_id_list = node_params.get("knowledge_id_list", [])
+ knowledge_setting = node_params.get("knowledge_setting", {})
+ question_reference_address = node_params.get("question_reference_address", [])
+ show_knowledge = node_params.get("show_knowledge", False)
+ search_scope_type = node_params.get("search_scope_type", "custom")
+ search_scope_source = node_params.get("search_scope_source", "knowledge")
+ search_scope_reference = node_params.get("search_scope_reference", [])
+
+ question = str(
+ self.workflow_manage.get_reference_field(question_reference_address[0], question_reference_address[1:])
+ )
+
+ exclude_paragraph_id_list = []
+ if workflow_params.get("re_chat", False):
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ paragraph_id_list = [
+ p.get("id")
+ for p in flat_map(
+ [
+ _get_paragraph_list(chat_record, self.get_node_id())
+ for chat_record in history_chat_record
+ if chat_record.problem_text == question
+ ]
+ )
+ ]
+ exclude_paragraph_id_list = list(set(paragraph_id_list))
+
+ self.write_context("question", question)
+ self.write_context("show_knowledge", show_knowledge)
+
+ document_id_list = None
+ if search_scope_type == "referencing":
+ if search_scope_source == "knowledge":
+ knowledge_id_list = self._get_reference_content(search_scope_reference)
+ else:
+ document_id_list = self._get_reference_content(search_scope_reference)
+ knowledge_id_list = [
+ str(k)
+ for k in QuerySet(Document)
+ .filter(id__in=document_id_list)
+ .values_list("knowledge_id", flat=True)
+ .distinct()
+ ]
+
+ get_knowledge_list_of_authorized = DatabaseModelManage.get_model("get_knowledge_list_of_authorized")
+ chat_user_type = workflow_params.get("chat_user_type")
+ if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
+ knowledge_id_list = get_knowledge_list_of_authorized(workflow_params.get("chat_user_id"), knowledge_id_list)
+
+ workspace_id = workflow_params.get("workspace_id")
+ knowledge_id_list = filter_authorized_ids("knowledge", knowledge_id_list, workspace_id)
+
+ if len(knowledge_id_list) == 0:
+ self._write_empty_result(question)
+ return
+
+ model_id = _get_embedding_id(knowledge_id_list)
+ self._check_cancelled()
+ embedding_model = get_model_instance_by_model_workspace_id(model_id, workspace_id)
+ embedding_value = embedding_model.embed_query(question)
+ vector = VectorStore.get_embedding_vector()
+
+ exclude_document_id_list = [
+ str(document.id)
+ for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
+ ]
+
+ self._check_cancelled()
+ embedding_list = vector.query(
+ question,
+ embedding_value,
+ knowledge_id_list,
+ document_id_list,
+ exclude_document_id_list,
+ exclude_paragraph_id_list,
+ True,
+ knowledge_setting.get("top_n"),
+ knowledge_setting.get("similarity"),
+ SearchMode(knowledge_setting.get("search_mode")),
+ )
+
+ connection.close()
+
+ if embedding_list is None:
+ self._write_empty_result(question)
+ return
+
+ paragraph_list = _list_paragraph(embedding_list, vector)
+ result = [_reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list]
+ result = sorted(result, key=lambda p: p.get("similarity"), reverse=True)
+
+ self.write_context("paragraph_list", result)
+ self.write_context("is_hit_handling_method_list", [row for row in result if row.get("is_hit_handling_method")])
+ self.write_context(
+ "data",
+ "\n".join(
+ [f"{_reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in result]
+ )[0 : knowledge_setting.get("max_paragraph_char_number", 5000)],
+ )
+ self.write_context(
+ "directly_return",
+ "\n".join([paragraph.get("content") for paragraph in result if paragraph.get("is_hit_handling_method")]),
+ )
+
+ def _write_empty_result(self, question):
+ self.write_context("paragraph_list", [])
+ self.write_context("is_hit_handling_method_list", [])
+ self.write_context("data", "")
+ self.write_context("directly_return", "")
+ self.write_context("question", question)
+
+ def _get_reference_content(self, fields: List[str]):
+ if fields:
+ return self.workflow_manage.get_reference_field(fields[0], fields[1:])
+ return None
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "paragraph_list": self.get_context("paragraph_list"),
+ "data": self.get_context("data"),
+ "show_knowledge": self.get_context("show_knowledge"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/speech_to_text_node/__init__.py b/apps/application/workflow/nodes/speech_to_text_node/__init__.py
new file mode 100644
index 00000000000..ffbe9f1673b
--- /dev/null
+++ b/apps/application/workflow/nodes/speech_to_text_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .speech_to_text_node import SpeechToTextNode
diff --git a/apps/application/workflow/nodes/speech_to_text_node/speech_to_text_node.py b/apps/application/workflow/nodes/speech_to_text_node/speech_to_text_node.py
new file mode 100644
index 00000000000..0bf86e0a955
--- /dev/null
+++ b/apps/application/workflow/nodes/speech_to_text_node/speech_to_text_node.py
@@ -0,0 +1,128 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: speech_to_text_node.py
+@desc:
+"""
+
+import os
+import tempfile
+from concurrent.futures import ThreadPoolExecutor
+
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from common.utils.common import split_and_transcribe, any_to_mp3
+from knowledge.models import File
+from models_provider.tools import get_model_instance_by_model_workspace_id
+
+
+class SpeechToTextNodeSerializer(serializers.Serializer):
+ stt_model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ stt_model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ stt_model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ audio_list = serializers.ListField(required=True, label=_("The audio file cannot be empty"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+
+
+def _process_audio_item(audio_item, model):
+ file = QuerySet(File).filter(id=audio_item["file_id"]).first()
+ file_format = file.file_name.split(".")[-1]
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_format}") as temp_file:
+ temp_file.write(file.get_bytes())
+ temp_file_path = temp_file.name
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_amr_file:
+ temp_mp3_path = temp_amr_file.name
+ any_to_mp3(temp_file_path, temp_mp3_path)
+ try:
+ transcription = split_and_transcribe(temp_mp3_path, model)
+ return {file.file_name: transcription}
+ finally:
+ os.remove(temp_file_path)
+ os.remove(temp_mp3_path)
+
+
+def _process_audio_items(audio_list, model):
+ with ThreadPoolExecutor(max_workers=5) as executor:
+ results = list(executor.map(lambda item: _process_audio_item(item, model), audio_list))
+ return results
+
+
+class SpeechToTextNode(INode):
+ serializer_class = SpeechToTextNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "speech-to-text-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+
+ stt_model_id = node_params.get("stt_model_id")
+ stt_model_id_type = node_params.get("stt_model_id_type", "custom")
+ stt_model_id_reference = node_params.get("stt_model_id_reference")
+ model_params_setting = node_params.get("model_params_setting")
+ audio_list_ref = node_params.get("audio_list")
+ is_result = node_params.get("is_result", False)
+
+ audio_list = self.workflow_manage.get_reference_field(audio_list_ref[0], audio_list_ref[1:])
+ for audio in audio_list:
+ if "file_id" not in audio:
+ raise ValueError(
+ _("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails")
+ )
+
+ if stt_model_id_type == "reference" and stt_model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ stt_model_id_reference[0],
+ stt_model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ stt_model_id = reference_data.get("stt_model_id", reference_data.get("model_id", stt_model_id))
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not stt_model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ workspace_id = workflow_params.get("workspace_id")
+ stt_model = get_model_instance_by_model_workspace_id(stt_model_id, workspace_id, **(model_params_setting or {}))
+
+ self.write_context("audio_list", audio_list)
+
+ self._check_cancelled()
+ result = _process_audio_items(audio_list, stt_model)
+ content = []
+ result_content = []
+ for item in result:
+ for key, value in item.items():
+ content.append(f"### {key}\n{value}")
+ result_content.append(value)
+
+ answer = "\n".join(result_content)
+ self.write_context("answer", answer)
+ self.write_context("result", answer)
+ self.write_context("content", content)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(TextContent(self.get_node_id(), answer, Status.SUCCESS, node_info, Position(self.get_node_id())))
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "answer": self.get_context("answer"),
+ "result": self.get_context("result"),
+ "content": self.get_context("content"),
+ "audio_list": self.get_context("audio_list"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/start_node/__init__.py b/apps/application/workflow/nodes/start_node/__init__.py
new file mode 100644
index 00000000000..21960a776ac
--- /dev/null
+++ b/apps/application/workflow/nodes/start_node/__init__.py
@@ -0,0 +1,9 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: __init__.py.py
+ @date:2026/6/29 10:59
+ @desc:
+"""
+from .start_node import StarNode
diff --git a/apps/application/workflow/nodes/start_node/start_node.py b/apps/application/workflow/nodes/start_node/start_node.py
new file mode 100644
index 00000000000..cffee289ef9
--- /dev/null
+++ b/apps/application/workflow/nodes/start_node/start_node.py
@@ -0,0 +1,118 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: start_node.py
+@date:2026/7/1 16:59
+@desc:
+"""
+
+import time
+from typing import List
+
+from django.db.models import QuerySet
+from django.utils import timezone
+from rest_framework import serializers
+
+from application.models.application_chat import ApplicationLongTermMemory
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.status import Status
+
+
+def get_default_global_variable(input_field_list: List):
+ return {
+ item.get("variable") or item.get("field"): item.get("default_value")
+ for item in input_field_list
+ if item.get("default_value", None) is not None
+ }
+
+
+class ApplicationSerializer(serializers.Serializer):
+ chat_id = serializers.UUIDField(required=True, label="对话id")
+ user_id = serializers.UUIDField(required=True, label="用户id")
+ chat_record_id = serializers.UUIDField(required=True, label="对话记录id")
+ messages = serializers.ListField(required=True, label="上下文数据")
+
+
+class StarNode(INode):
+ serializer_class = ApplicationSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION]
+ type = "start-node"
+
+ def execute(self):
+ workflow_params = self.get_workflow_parameters()
+ base_node = self.workflow_manage.workflow.get_node("base-node")
+
+ user_input_field_list = base_node.properties.get("user_input_field_list", []) if base_node else []
+ api_input_field_list = base_node.properties.get("api_input_field_list", []) if base_node else []
+ default_global = get_default_global_variable(user_input_field_list)
+ default_api_global = get_default_global_variable(api_input_field_list)
+
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ history_context = [{"question": r.problem_text, "answer": r.answer_text} for r in history_chat_record]
+
+ chat_id = workflow_params.get("chat_id")
+ chat_user_id = workflow_params.get("chat_user_id")
+
+ memory = ""
+ if chat_user_id:
+ long_term_memory = (
+ QuerySet(ApplicationLongTermMemory)
+ .filter(chat_user_id=chat_user_id, application_id=workflow_params.get("application_id"))
+ .first()
+ )
+ if long_term_memory:
+ memory = long_term_memory.memory
+
+ workflow_variable = {
+ **default_global,
+ **default_api_global,
+ "time": timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S"),
+ "start_time": time.time(),
+ "history_context": history_context,
+ "chat_id": str(chat_id) if chat_id else None,
+ "chat_user_id": chat_user_id,
+ "chat_user_type": workflow_params.get("chat_user_type"),
+ "chat_user": workflow_params.get("chat_user"),
+ "chat_user_group": workflow_params.get("chat_user_group"),
+ "memory": memory,
+ }
+
+ question = workflow_params.get("question", "")
+ node_variable = {
+ "question": question,
+ "image": workflow_params.get("image_list", []),
+ "document": workflow_params.get("document_list", []),
+ "audio": workflow_params.get("audio_list", []),
+ "video": workflow_params.get("video_list", []),
+ "other": workflow_params.get("other_list", []),
+ "memory": memory,
+ }
+
+ for key, value in node_variable.items():
+ self.write_context(key, value)
+
+ # 全局变量统一放进 context['global'],与 reset_variable / get_reference_field 的引用约定一致
+ for key, value in workflow_variable.items():
+ self.workflow_manage.write_context("global", key, value)
+
+ config = self.node.properties.get("config", {})
+ if config:
+ for field in config.get("globalFields", []):
+ key = field.get("value")
+ if key:
+ self.workflow_manage.write_context("global", key, workflow_variable.get(key, ""))
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "image": self.get_context("image"),
+ "document": self.get_context("document"),
+ "audio": self.get_context("audio"),
+ "video": self.get_context("video"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/text_to_speech_node/__init__.py b/apps/application/workflow/nodes/text_to_speech_node/__init__.py
new file mode 100644
index 00000000000..37c432059c0
--- /dev/null
+++ b/apps/application/workflow/nodes/text_to_speech_node/__init__.py
@@ -0,0 +1,7 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: __init__.py
+ @desc:
+"""
+from .text_to_speech_node import TextToSpeechNode
diff --git a/apps/application/workflow/nodes/text_to_speech_node/text_to_speech_node.py b/apps/application/workflow/nodes/text_to_speech_node/text_to_speech_node.py
new file mode 100644
index 00000000000..cede4303beb
--- /dev/null
+++ b/apps/application/workflow/nodes/text_to_speech_node/text_to_speech_node.py
@@ -0,0 +1,186 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@file: text_to_speech_node.py
+@desc:
+"""
+
+import io
+import mimetypes
+
+from django.core.files.uploadedfile import InMemoryUploadedFile
+from django.utils.translation import gettext_lazy as _
+from pydub import AudioSegment
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from common.utils.common import _remove_empty_lines
+from knowledge.models import FileSourceType
+from models_provider.tools import get_model_instance_by_model_workspace_id
+from oss.serializers.file import FileSerializer
+
+
+class TextToSpeechNodeSerializer(serializers.Serializer):
+ tts_model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ tts_model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ tts_model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ content_list = serializers.ListField(required=True, label=_("Text content"))
+ model_params_setting = serializers.DictField(required=False, label=_("Model parameter settings"))
+
+
+def _bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
+ content_type, _ = mimetypes.guess_type(file_name)
+ if content_type is None:
+ content_type = "application/octet-stream"
+ file_stream = io.BytesIO(file_bytes)
+ file_size = len(file_bytes)
+ uploaded_file = InMemoryUploadedFile(
+ file=file_stream,
+ field_name=None,
+ name=file_name,
+ content_type=content_type,
+ size=file_size,
+ charset=None,
+ )
+ return uploaded_file
+
+
+class TextToSpeechNode(INode):
+ serializer_class = TextToSpeechNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "text-to-speech-node"
+
+ def execute(self):
+ node_params = self.get_parameters()
+ workflow_params = self.get_workflow_parameters()
+ workflow_type = self.get_workflow_type()
+
+ tts_model_id = node_params.get("tts_model_id")
+ tts_model_id_type = node_params.get("tts_model_id_type", "custom")
+ tts_model_id_reference = node_params.get("tts_model_id_reference")
+ model_params_setting = node_params.get("model_params_setting")
+ content_list_ref = node_params.get("content_list")
+ is_result = node_params.get("is_result", False)
+
+ content = (
+ self.workflow_manage.get_reference_field(content_list_ref[0], content_list_ref[1:])
+ if content_list_ref
+ else ""
+ )
+
+ if tts_model_id_type == "reference" and tts_model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ tts_model_id_reference[0],
+ tts_model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ tts_model_id = reference_data.get("tts_model_id", reference_data.get("model_id", tts_model_id))
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not tts_model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ content = _remove_empty_lines(str(content))
+ max_length = 1024
+ content_chunks = [content[i : i + max_length] for i in range(0, len(content), max_length)]
+
+ audio_segments = []
+ temp_files = []
+
+ for chunk in content_chunks:
+ self._check_cancelled()
+ self.write_context("content", chunk)
+ workspace_id = workflow_params.get("workspace_id")
+ model = get_model_instance_by_model_workspace_id(tts_model_id, workspace_id, **(model_params_setting or {}))
+ audio_byte = model.text_to_speech(chunk)
+ temp_file = io.BytesIO(audio_byte)
+ audio_segment = AudioSegment.from_file(temp_file)
+ audio_segments.append(audio_segment)
+ temp_files.append(temp_file)
+
+ combined_audio = AudioSegment.empty()
+ for segment in audio_segments:
+ combined_audio += segment
+
+ output_buffer = io.BytesIO()
+ combined_audio.export(output_buffer, format="mp3")
+ combined_bytes = output_buffer.getvalue()
+ file_name = "combined_audio.mp3"
+ file = _bytes_to_uploaded_file(combined_bytes, file_name)
+ file_url = self._upload_file(file, workflow_params, workflow_type)
+
+ file_id = file_url.split("/")[-1]
+ audio_list = [{"file_id": file_id, "file_name": file_name, "url": file_url}]
+
+ for temp_file in temp_files:
+ temp_file.close()
+ output_buffer.close()
+
+ audio_label = f''
+ self.write_context("answer", audio_label)
+ self.write_context("result", audio_list)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(
+ TextContent(self.get_node_id(), audio_label, Status.SUCCESS, node_info, Position(self.get_node_id()))
+ )
+
+ def _upload_file(self, file, workflow_params, workflow_type):
+ if workflow_type == WorkflowType.KNOWLEDGE:
+ return self._upload_knowledge_file(file, workflow_params)
+ if workflow_type == WorkflowType.TOOL:
+ return self._upload_tool_file(file, workflow_params)
+ return self._upload_application_file(file, workflow_params)
+
+ def _upload_knowledge_file(self, file, workflow_params):
+ knowledge_id = workflow_params.get("knowledge_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "knowledge_id": knowledge_id},
+ "source_id": knowledge_id,
+ "source_type": FileSourceType.KNOWLEDGE.value,
+ }
+ ).upload()
+
+ def _upload_tool_file(self, file, workflow_params):
+ tool_id = workflow_params.get("tool_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "tool_id": tool_id},
+ "source_id": tool_id,
+ "source_type": FileSourceType.TOOL.value,
+ }
+ ).upload()
+
+ def _upload_application_file(self, file, workflow_params):
+ application_id = workflow_params.get("application_id")
+ chat_id = workflow_params.get("chat_id")
+ return FileSerializer(
+ data={
+ "file": file,
+ "meta": {"debug": False, "chat_id": chat_id, "application_id": application_id},
+ "source_id": application_id,
+ "source_type": FileSourceType.APPLICATION.value,
+ }
+ ).upload()
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "content": self.get_context("content"),
+ "answer": self.get_context("answer"),
+ "result": self.get_context("result"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/nodes/text_to_video_node/__init__.py b/apps/application/workflow/nodes/text_to_video_node/__init__.py
new file mode 100644
index 00000000000..3c4dac4c599
--- /dev/null
+++ b/apps/application/workflow/nodes/text_to_video_node/__init__.py
@@ -0,0 +1,2 @@
+# coding=utf-8
+from .text_to_video_node import TextToVideoNode
diff --git a/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py b/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py
new file mode 100644
index 00000000000..0b8afe3b094
--- /dev/null
+++ b/apps/application/workflow/nodes/text_to_video_node/text_to_video_node.py
@@ -0,0 +1,229 @@
+# coding=utf-8
+import uuid_utils.compat as uuid
+import requests
+from functools import reduce
+from typing import List
+
+from django.utils.translation import gettext_lazy as _, gettext
+from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
+from rest_framework import serializers
+
+from application.workflow.common import WorkflowType
+from application.workflow.i_node import INode
+from application.workflow.message.struct.content import NodeInfo, Position
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.status import Status
+from common.utils.common import bytes_to_uploaded_file
+from knowledge.models import FileSourceType
+from oss.serializers.file import FileSerializer
+from models_provider.tools import get_model_instance_by_model_workspace_id
+from common.utils.logger import maxkb_logger
+
+
+class TextToVideoNodeSerializer(serializers.Serializer):
+ model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
+ model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
+ model_id_reference = serializers.ListField(
+ required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
+ )
+ prompt = serializers.CharField(required=True, label=_("Prompt word (positive)"))
+ negative_prompt = serializers.CharField(
+ required=False, label=_("Prompt word (negative)"), allow_null=True, allow_blank=True
+ )
+ dialogue_number = serializers.IntegerField(
+ required=False, default=0, label=_("Number of multi-round conversations")
+ )
+ dialogue_type = serializers.CharField(required=False, default="NODE", label=_("Conversation storage type"))
+ is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
+ model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model parameter settings"))
+
+
+class TextToVideoNode(INode):
+ serializer_class = TextToVideoNodeSerializer
+ supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
+ type = "text-to-video-node"
+
+ def execute(self):
+ maxkb_logger.info(f"[TextToVideoNode] execute START, node_id={self.get_node_id()}")
+ workflow_params = self.get_workflow_parameters()
+ node_params = self.get_parameters()
+
+ model_id = node_params.get("model_id")
+ model_id_type = node_params.get("model_id_type", "custom")
+ model_id_reference = node_params.get("model_id_reference")
+ prompt = node_params.get("prompt", "")
+ negative_prompt = node_params.get("negative_prompt", "")
+ dialogue_number = node_params.get("dialogue_number", 0)
+ dialogue_type = node_params.get("dialogue_type", "NODE")
+ is_result = node_params.get("is_result", False)
+ model_params_setting = node_params.get("model_params_setting")
+
+ workflow_type = self.get_workflow_type()
+ if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
+ history_chat_record = []
+ chat_id = None
+ chat_record_id = None
+ workspace_id = workflow_params.get("workspace_id")
+ else:
+ history_chat_record = workflow_params.get("history_chat_record", [])
+ chat_id = workflow_params.get("chat_id")
+ chat_record_id = workflow_params.get("chat_record_id")
+ workspace_id = workflow_params.get("workspace_id")
+
+ if model_id_type == "reference" and model_id_reference:
+ reference_data = self.workflow_manage.get_reference_field(
+ model_id_reference[0],
+ model_id_reference[1:],
+ )
+ if reference_data and isinstance(reference_data, dict):
+ model_id = reference_data.get("model_id", model_id)
+ model_params_setting = reference_data.get("model_params_setting")
+
+ if not model_id:
+ raise Exception(_("Model is not allowed to be empty"))
+
+ ttv_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
+
+ history_message = self._get_history_message(history_chat_record, dialogue_number)
+ self.write_context(
+ "history_message",
+ [{"content": message.content, "role": message.type} for message in (history_message or [])],
+ )
+
+ question = self.workflow_manage.generate_prompt(prompt)
+ self.write_context("question", question)
+
+ message_list = [*history_message, question]
+ self.write_context("message_list", message_list)
+ self.write_context("dialogue_type", dialogue_type)
+ self.write_context("negative_prompt", self.workflow_manage.generate_prompt(negative_prompt))
+
+ self._check_cancelled()
+ video_urls = ttv_model.generate_video(question, negative_prompt)
+ maxkb_logger.info(
+ f"[TextToVideoNode] generate_video result: {video_urls is not None}, node_id={self.get_node_id()}"
+ )
+
+ if video_urls is None or video_urls == "":
+ raise Exception(gettext("Failed to generate video"))
+
+ file_name = "generated_video.mp4"
+ if isinstance(video_urls, str) and video_urls.startswith("http"):
+ video_urls = requests.get(video_urls).content
+
+ file = bytes_to_uploaded_file(video_urls, file_name)
+ file_url = self._upload_file(file, workflow_type, workflow_params)
+
+ video_label = f''
+ video_list = [{"file_id": file_url.split("/")[-1], "file_name": file_name, "url": file_url}]
+
+ self.write_context("answer", video_label)
+ self.write_context("video", video_list)
+ self.write_context("chat_model", ttv_model)
+
+ if is_result:
+ node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.SUCCESS)
+ self.write(
+ TextContent(str(uuid.uuid7()), video_label, Status.SUCCESS, node_info, Position(self.get_node_id()))
+ )
+
+ def _upload_file(self, file, workflow_type, workflow_params):
+ if workflow_type == WorkflowType.KNOWLEDGE:
+ return self._upload_knowledge_file(file, workflow_params)
+ if workflow_type == WorkflowType.TOOL:
+ return self._upload_tool_file(file, workflow_params)
+ return self._upload_application_file(file, workflow_params)
+
+ def _upload_knowledge_file(self, file, workflow_params):
+ knowledge_id = workflow_params.get("knowledge_id")
+ meta = {"debug": False, "knowledge_id": knowledge_id}
+ file_url = FileSerializer(
+ data={"file": file, "meta": meta, "source_id": knowledge_id, "source_type": FileSourceType.KNOWLEDGE.value}
+ ).upload()
+ return file_url
+
+ def _upload_tool_file(self, file, workflow_params):
+ tool_id = workflow_params.get("tool_id")
+ meta = {
+ "debug": False,
+ "tool_id": tool_id,
+ }
+ file_url = FileSerializer(
+ data={"file": file, "meta": meta, "source_id": tool_id, "source_type": FileSourceType.TOOL.value}
+ ).upload()
+ return file_url
+
+ def _upload_application_file(self, file, workflow_params):
+ application_id = workflow_params.get("application_id")
+ chat_id = workflow_params.get("chat_id")
+ debug = workflow_params.get("debug", False)
+ meta = {
+ "debug": debug,
+ "chat_id": chat_id,
+ "application_id": application_id,
+ }
+ file_url = FileSerializer(
+ data={
+ "file": file,
+ "meta": meta,
+ "source_id": application_id,
+ "source_type": FileSourceType.APPLICATION.value,
+ }
+ ).upload()
+ return file_url
+
+ def _generate_history_ai_message(self, chat_record):
+ for val in chat_record.details.values():
+ if self.node.id == val["node_id"] and "image_list" in val:
+ if val["dialogue_type"] == "WORKFLOW":
+ return chat_record.get_ai_message()
+ image_list = val["image_list"]
+ return AIMessage(
+ content=[*[{"type": "image_url", "image_url": {"url": f"{file_url}"}} for file_url in image_list]]
+ )
+ return chat_record.get_ai_message()
+
+ def _get_history_message(self, history_chat_record, dialogue_number):
+ start_index = len(history_chat_record) - dialogue_number
+ history_message = reduce(
+ lambda x, y: [*x, *y],
+ [
+ [
+ self._generate_history_human_message(history_chat_record[index]),
+ self._generate_history_ai_message(history_chat_record[index]),
+ ]
+ for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
+ ],
+ [],
+ )
+ return history_message
+
+ def _generate_history_human_message(self, chat_record):
+ for data in chat_record.details.values():
+ if self.node.id == data["node_id"] and "image_list" in data:
+ image_list = data["image_list"]
+ if len(image_list) == 0 or data["dialogue_type"] == "WORKFLOW":
+ return HumanMessage(content=chat_record.problem_text)
+ return HumanMessage(content=data["question"])
+ return HumanMessage(content=chat_record.problem_text)
+
+ @staticmethod
+ def reset_message_list(message_list: List[BaseMessage], answer_text):
+ result = [
+ {"role": "user" if isinstance(message, HumanMessage) else "ai", "content": message.content}
+ for message in message_list
+ ]
+ result.append({"role": "ai", "content": answer_text})
+ return result
+
+ def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
+ details = super().get_details(index, position, old_details, **kwargs)
+ details.update(
+ {
+ "question": self.get_context("question"),
+ "answer": self.get_context("answer"),
+ "video": self.get_context("video"),
+ "negative_prompt": self.get_context("negative_prompt"),
+ }
+ )
+ return details
diff --git a/apps/application/workflow/status.py b/apps/application/workflow/status.py
new file mode 100644
index 00000000000..3bb2b0db594
--- /dev/null
+++ b/apps/application/workflow/status.py
@@ -0,0 +1,22 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: status.py
+ @date:2026/6/30 15:47
+ @desc:
+"""
+from enum import Enum
+
+
+class Status(Enum):
+ # 成功
+ SUCCESS = "SUCCESS"
+ # 失败
+ FAIL = "FAIL"
+ # 运行中
+ RUNNING = "RUNNING"
+ # 运行前
+ BEFORE_RUNNING = "BEFORE_RUNNING"
+ # 取消
+ CANCELLED = "CANCELLED"
diff --git a/apps/application/workflow/tools.py b/apps/application/workflow/tools.py
new file mode 100644
index 00000000000..c5e6db6fb87
--- /dev/null
+++ b/apps/application/workflow/tools.py
@@ -0,0 +1,94 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: tools.py
+ @date:2026/6/29 18:44
+ @desc:
+"""
+
+
+class Reasoning:
+ def __init__(self, reasoning_content_start, reasoning_content_end):
+ self.content = ""
+ self.reasoning_content = ""
+ self.all_content = ""
+ self.reasoning_content_start_tag = reasoning_content_start
+ self.reasoning_content_end_tag = reasoning_content_end
+ self.reasoning_content_start_tag_len = len(
+ reasoning_content_start) if reasoning_content_start is not None else 0
+ self.reasoning_content_end_tag_len = len(reasoning_content_end) if reasoning_content_end is not None else 0
+ self.reasoning_content_end_tag_prefix = reasoning_content_end[
+ 0] if self.reasoning_content_end_tag_len > 0 else ''
+ self.reasoning_content_is_start = False
+ self.reasoning_content_is_end = False
+ self.reasoning_content_chunk = ""
+
+ def get_end_reasoning_content(self):
+ if not self.reasoning_content_is_start and not self.reasoning_content_is_end:
+ r = {'content': self.all_content, 'reasoning_content': ''}
+ self.reasoning_content_chunk = ""
+ return r
+ if self.reasoning_content_is_start and not self.reasoning_content_is_end:
+ r = {'content': '', 'reasoning_content': self.reasoning_content_chunk}
+ self.reasoning_content_chunk = ""
+ return r
+ return {'content': '', 'reasoning_content': ''}
+
+ def get_reasoning_content(self, chunk):
+ # 如果没有开始思考过程标签那么就全是结果
+ if self.reasoning_content_start_tag is None or len(self.reasoning_content_start_tag) == 0:
+ self.content += chunk.content
+ return {'content': chunk.content, 'reasoning_content': ''}
+ # 如果没有结束思考过程标签那么就全部是思考过程
+ if self.reasoning_content_end_tag is None or len(self.reasoning_content_end_tag) == 0:
+ return {'content': '', 'reasoning_content': chunk.content}
+ self.all_content += chunk.content
+ if not self.reasoning_content_is_start and len(self.all_content) >= self.reasoning_content_start_tag_len:
+ if self.all_content.startswith(self.reasoning_content_start_tag):
+ self.reasoning_content_is_start = True
+ self.reasoning_content_chunk = self.all_content[self.reasoning_content_start_tag_len:]
+ else:
+ if not self.reasoning_content_is_end:
+ self.reasoning_content_is_end = True
+ self.content += self.all_content
+ return {'content': self.all_content, 'reasoning_content': ''}
+ else:
+ if self.reasoning_content_is_start:
+ self.reasoning_content_chunk += chunk.content
+ reasoning_content_end_tag_prefix_index = self.reasoning_content_chunk.find(
+ self.reasoning_content_end_tag_prefix)
+ if self.reasoning_content_is_end:
+ self.content += chunk.content
+ return {'content': chunk.content, 'reasoning_content': ''}
+ # 是否包含结束
+ if reasoning_content_end_tag_prefix_index > -1:
+ if len(self.reasoning_content_chunk) - reasoning_content_end_tag_prefix_index >= self.reasoning_content_end_tag_len:
+ reasoning_content_end_tag_index = self.reasoning_content_chunk.find(self.reasoning_content_end_tag)
+ if reasoning_content_end_tag_index > -1:
+ reasoning_content_chunk = self.reasoning_content_chunk[0:reasoning_content_end_tag_index]
+ content_chunk = self.reasoning_content_chunk[
+ reasoning_content_end_tag_index + self.reasoning_content_end_tag_len:]
+ self.reasoning_content += reasoning_content_chunk
+ self.content += content_chunk
+ self.reasoning_content_chunk = ""
+ self.reasoning_content_is_end = True
+ return {'content': content_chunk, 'reasoning_content': reasoning_content_chunk}
+ else:
+ reasoning_content_chunk = self.reasoning_content_chunk[0:reasoning_content_end_tag_prefix_index + 1]
+ self.reasoning_content_chunk = self.reasoning_content_chunk.replace(reasoning_content_chunk, '')
+ self.reasoning_content += reasoning_content_chunk
+ return {'content': '', 'reasoning_content': reasoning_content_chunk}
+ else:
+ return {'content': '', 'reasoning_content': ''}
+
+ else:
+ if self.reasoning_content_is_end:
+ self.content += chunk.content
+ return {'content': chunk.content, 'reasoning_content': ''}
+ else:
+ # aaa
+ result = {'content': '', 'reasoning_content': self.reasoning_content_chunk}
+ self.reasoning_content += self.reasoning_content_chunk
+ self.reasoning_content_chunk = ""
+ return result
diff --git a/apps/application/workflow/workflow_manage.py b/apps/application/workflow/workflow_manage.py
new file mode 100644
index 00000000000..a4294b4313c
--- /dev/null
+++ b/apps/application/workflow/workflow_manage.py
@@ -0,0 +1,291 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: workflow_manage.py
+@date:2026/6/29 10:30
+@desc:
+"""
+
+from __future__ import annotations
+
+import threading
+from typing import List, Dict, Optional, Callable
+
+from langchain_core.prompts import PromptTemplate
+
+from application.workflow.common import Workflow, WorkflowType, Node, get_node_parameters
+from application.workflow.i_node import INode, Signal
+from application.workflow.message.struct.content import Content, Position
+
+from application.workflow.status import Status
+
+
+class CallBack:
+ def __init__(
+ self,
+ on_next: Callable[[WorkflowManage, Content], None],
+ on_complete: Callable[[WorkflowManage, Optional[Exception]], None],
+ ):
+ self.on_next = on_next
+ self.on_complete = on_complete
+
+
+class WorkflowManage:
+ # 工作流节点数据
+ context: Dict[Dict[str, any]]
+ # 运行的节点
+ nodes: List[INode]
+ # 是否结束
+ done: bool
+
+ def __init__(
+ self,
+ workflow: Workflow,
+ parameters: Dict,
+ workflow_type: WorkflowType,
+ call_back: CallBack,
+ get_start_node: Callable[[Workflow, WorkflowManage], INode],
+ ):
+ """
+
+ @param workflow: 工作流对象
+ @param workflow_type: 工作流类型
+ @param parameters: 工作流使用到的其他数据
+ """
+ self._lock = threading.Lock()
+ self.done = False
+ self.call_back = call_back
+ self.workflow = workflow
+ self.workflow_type = workflow_type
+ self.parameters = parameters
+ self.context = {}
+ self.nodes = []
+ self.node_dict = {}
+ self.signal = None
+ self.details = {"position": {}, "details": {}}
+ self.start_node = get_start_node(workflow, self)
+
+ def run(self):
+ """
+ 工作流执行
+ @return: None
+ """
+ self.nodes.append(self.start_node)
+ self.node_dict = {node.node.id: node for node in self.nodes}
+ self._run_async(self.start_node)
+
+ def _run(self, node):
+ node.run()
+
+ def next_nodes(self, nodes: Optional[List[Node]]):
+ """
+ 继续执行下面的节点
+ @param nodes: 执行下面要执行的节点
+ @return:
+ """
+ if [Signal.FORM, Signal.CANCELLED].__contains__(self.signal):
+ return
+ if nodes is None or len(nodes) == 0:
+ return
+ # 需要校验是否可执行
+ for n in nodes:
+ condition = n.properties.get("condition")
+ if condition == "AND":
+ up_nodes = self.workflow.get_up_nodes(n.id)
+ # 如果是AND就是前面所有节点都执行结束
+ unfinished = {Status.BEFORE_RUNNING, Status.RUNNING}
+ end = all(
+ [
+ self.node_dict.get(node.id) and self.node_dict.get(node.id).status not in unfinished
+ for node in up_nodes
+ ]
+ )
+ if not end:
+ return
+
+ with self._lock:
+ from application.workflow.nodes import get_node_class
+
+ instances = [get_node_class(n.type, self.workflow_type)(n, self, get_node_parameters) for n in nodes]
+ self.nodes.extend(instances)
+ for node in instances:
+ self.node_dict[node.node.id] = node
+ for inst in instances:
+ self._run_async(inst)
+
+ def assertion_end(self, error=None):
+ with self._lock:
+ if self.done:
+ return
+ if not self.is_end():
+ return
+ self.done = True # 锁内抢占,保证只有一个线程能往下走
+ self.end(error) # 回调放到锁外,避免回调里再触碰本 manage 造成重入/死锁
+
+ def is_end(self):
+ """
+ 工作流是否执行结束
+ @return: 是否执行结束
+ """
+ unfinished = {Status.BEFORE_RUNNING, Status.RUNNING}
+ return not any(node.status in unfinished for node in self.nodes)
+
+ def write_context(self, node_id, key, value, append=False):
+ """
+ 写入上下文
+ @param node_id: 节点id
+ @param key: 数据key
+ @param value: 数据value
+ @param append: 是否追加
+ @return: None
+ """
+ node_context = self.context.setdefault(node_id, {})
+ if append and key in node_context:
+ node_context[key] += value
+ else:
+ node_context[key] = value
+
+ def get_context(self, node_id, key):
+ """
+ 获取节点上下文的指定key的内容
+ @param node_id: 节点id
+ @param key: key
+ @return: 数据
+ """
+ node_context = self.context.get(node_id)
+ if node_context is None:
+ return None
+ return node_context.get(key)
+
+ def _run_async(self, node):
+ t = threading.Thread(target=lambda: self._run(node))
+ t.start()
+ return t
+
+ def invoke(self):
+ """
+ 非流式响应
+ @return: 没个节点的 块数据
+ """
+ self.run()
+
+ def write(self, message: Content):
+ """
+ 写入数据
+ @param message: 节点输出内容
+ @return: None
+ """
+ self.call_back.on_next(self, message)
+
+ def end(self, error=None):
+ """
+ 工作流输出结束的时候调用
+ @return: None
+ """
+ self.call_back.on_complete(self, error)
+
+ def get_parameters(self):
+ """
+ 获取工作流的参数信息
+ @return: 工作流参数信息
+ """
+ return self.parameters
+
+ def get_details(self, position: Dict = None, old_details=None):
+ """
+ 获取所有节点的运行详情
+ @param position: 位置信息,用于表单节点等需要断点续跑的场景
+ @param old_details: 旧的详情数据,用于表单节点等断点续跑场景
+ @return: 包含position和details的字典
+ """
+ details_result = []
+ position_index = 0
+ if old_details and position:
+ for index, value in enumerate(old_details):
+ details_result.append(value)
+ if position.get("id") == value.get("node_id"):
+ position_index = index
+ for index, node in enumerate(self.nodes):
+ if position is not None and node.node.id == position.get("id") and index == 0:
+ details = node.get_details(
+ index + position_index, position=position, old_details=old_details[position_index]
+ )
+ details_result.insert(position_index, details)
+ else:
+ details = node.get_details(index + position_index)
+ details_result.append(details)
+ return details_result
+
+ def generate_prompt(self, prompt):
+ """
+ 处理提示词
+ @param prompt: 提示词
+ @return: 处理后的提示词
+ """
+ prompt = self.workflow.reset_prompt(prompt)
+ prompt_template = PromptTemplate.from_template(prompt, template_format="jinja2")
+ return prompt_template.format(context=self.context)
+
+ def get_reference_field(self, node_id, fields):
+ """
+ 获取引用字段
+ @param node_id: 节点id
+ @param fields: 字段
+ @return: 引用数据
+ """
+ node_context = self.context.get(node_id)
+ if node_context is None:
+ return None
+ obj = node_context
+ for field in fields:
+ if isinstance(obj, dict):
+ obj = obj.get(field)
+ else:
+ return None
+ return obj
+
+ @classmethod
+ def from_context(cls, chat_record_id, workflow, parameters, workflow_type, call_back, get_start_node):
+ """从历史 context 恢复 WorkflowManage"""
+ from application.models import ChatRecord
+ from django.core.cache import cache
+ from common.constants.cache_version import Cache_Version
+
+ try:
+ context_data = None
+
+ # 先从 Redis 查(调试模式)
+ cache_key = Cache_Version.DEBUG_WORKFLOW_CONTEXT.get_key(chat_record_id=str(chat_record_id))
+ context_data = cache.get(cache_key)
+
+ # Redis 没有,从数据库查
+ if not context_data:
+ chat_record = ChatRecord.objects.filter(id=chat_record_id).first()
+ if not chat_record or not chat_record.workflow_context:
+ return None
+ context_data = chat_record.workflow_context
+
+ # 创建 WorkflowManage 实例
+ instance = cls(
+ workflow=workflow,
+ parameters=parameters,
+ workflow_type=workflow_type,
+ call_back=call_back,
+ get_start_node=get_start_node,
+ )
+
+ # 恢复全局 context
+ instance.context = context_data
+
+ return instance
+ except Exception as e:
+ import traceback
+
+ traceback.print_exc()
+ return None
+
+ def cancel(self):
+ self.signal = Signal.CANCELLED
+ for node in self.nodes:
+ node.cancel()
diff --git a/apps/application/workflow/workflow_run_registry.py b/apps/application/workflow/workflow_run_registry.py
new file mode 100644
index 00000000000..5e240eb84c4
--- /dev/null
+++ b/apps/application/workflow/workflow_run_registry.py
@@ -0,0 +1,168 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @file: workflow_run_registry.py
+ @desc: 工作流运行注册表,用于管理和取消正在运行的工作流实例
+"""
+import threading
+from enum import Enum
+
+from common.utils.logger import maxkb_logger
+
+
+class CancelResult(Enum):
+ """取消操作结果"""
+ CANCELLED = "CANCELLED"
+ NOT_FOUND = "NOT_FOUND"
+ FAILED = "FAILED"
+
+
+class WorkflowRunRegistry:
+ _lock = threading.Lock()
+ _running = {} # {chat_record_id: WorkflowManage}
+ _chat_to_records = {} # {chat_id: set[chat_record_id]}
+
+ @classmethod
+ def register(cls, chat_record_id: str, chat_id: str, workflow_manage) -> None:
+ """
+ 注册一个正在运行的工作流实例
+ @param chat_record_id: 聊天记录ID
+ @param chat_id: 聊天ID
+ @param workflow_manage: WorkflowManage 实例
+ """
+ if not chat_record_id or not workflow_manage:
+ return
+ with cls._lock:
+ cls._running[str(chat_record_id)] = workflow_manage
+ if chat_id:
+ if chat_id not in cls._chat_to_records:
+ cls._chat_to_records[chat_id] = set()
+ cls._chat_to_records[chat_id].add(str(chat_record_id))
+ maxkb_logger.debug(f"Workflow registered: {chat_record_id}, total running: {len(cls._running)}")
+
+ @classmethod
+ def unregister(cls, chat_record_id: str, chat_id: str = None) -> None:
+ """
+ 注销一个工作流实例(无论成功/失败/取消都应调用)
+ @param chat_record_id: 聊天记录ID
+ @param chat_id: 聊天ID
+ """
+ if not chat_record_id:
+ return
+ with cls._lock:
+ removed = cls._running.pop(str(chat_record_id), None)
+ if chat_id and chat_id in cls._chat_to_records:
+ cls._chat_to_records[chat_id].discard(str(chat_record_id))
+ if not cls._chat_to_records[chat_id]:
+ del cls._chat_to_records[chat_id]
+ if removed is not None:
+ maxkb_logger.debug(f"Workflow unregistered: {chat_record_id}, total running: {len(cls._running)}")
+
+ @classmethod
+ def cancel_by_chat_id(cls, chat_id: str) -> CancelResult:
+ """
+ 取消某个聊天下所有运行中的工作流
+ @param chat_id: 聊天ID
+ @return: CancelResult
+ """
+ if not chat_id:
+ return CancelResult.NOT_FOUND
+
+ with cls._lock:
+ record_ids = list(cls._chat_to_records.get(chat_id, set()))
+
+ if not record_ids:
+ maxkb_logger.info(f"Cancel requested but no running workflow found for chat: {chat_id}")
+ return CancelResult.NOT_FOUND
+
+ cancelled_count = 0
+ failed_count = 0
+ for record_id in record_ids:
+ with cls._lock:
+ wm = cls._running.get(record_id)
+ if wm:
+ try:
+ wm.cancel()
+ cancelled_count += 1
+ maxkb_logger.info(f"Cancel signal sent to workflow: {record_id}")
+ except Exception as e:
+ failed_count += 1
+ maxkb_logger.error(f"Failed to cancel workflow: {record_id}, error: {e}")
+
+ if failed_count > 0 and cancelled_count == 0:
+ return CancelResult.FAILED
+ return CancelResult.CANCELLED
+
+ @classmethod
+ def cancel_by_record_id(cls, chat_record_id: str) -> CancelResult:
+ """
+ 取消某个特定的工作流
+ @param chat_record_id: 聊天记录ID
+ @return: CancelResult
+ """
+ if not chat_record_id:
+ return CancelResult.NOT_FOUND
+
+ with cls._lock:
+ wm = cls._running.get(str(chat_record_id))
+
+ if wm is None:
+ maxkb_logger.info(f"Cancel requested but workflow not found (may already finished): {chat_record_id}")
+ return CancelResult.NOT_FOUND
+
+ try:
+ wm.cancel()
+ maxkb_logger.info(f"Cancel signal sent to workflow: {chat_record_id}")
+ return CancelResult.CANCELLED
+ except Exception as e:
+ maxkb_logger.error(f"Failed to cancel workflow: {chat_record_id}, error: {e}")
+ return CancelResult.FAILED
+
+ @classmethod
+ def get(cls, chat_record_id: str):
+ """
+ 获取正在运行的工作流实例
+ @param chat_record_id: 聊天记录ID
+ @return: WorkflowManage 实例或 None
+ """
+ if not chat_record_id:
+ return None
+ return cls._running.get(str(chat_record_id))
+
+ @classmethod
+ def is_running(cls, chat_record_id: str) -> bool:
+ """
+ 检查工作流是否正在运行
+ @param chat_record_id: 聊天记录ID
+ @return: 是否正在运行
+ """
+ return chat_record_id is not None and str(chat_record_id) in cls._running
+
+ @classmethod
+ def is_chat_running(cls, chat_id: str) -> bool:
+ """
+ 检查某个聊天是否有正在运行的工作流
+ @param chat_id: 聊天ID
+ @return: 是否有正在运行的工作流
+ """
+ if not chat_id:
+ return False
+ with cls._lock:
+ return chat_id in cls._chat_to_records and len(cls._chat_to_records[chat_id]) > 0
+
+ @classmethod
+ def running_count(cls) -> int:
+ """
+ 获取正在运行的工作流数量
+ @return: 数量
+ """
+ return len(cls._running)
+
+ @classmethod
+ def running_ids(cls) -> list:
+ """
+ 获取所有正在运行的工作流ID列表
+ @return: ID列表
+ """
+ with cls._lock:
+ return list(cls._running.keys())
diff --git a/apps/chat/api/chat_authentication_api.py b/apps/chat/api/chat_authentication_api.py
index 6f6b1b1a835..c344931a1bc 100644
--- a/apps/chat/api/chat_authentication_api.py
+++ b/apps/chat/api/chat_authentication_api.py
@@ -1,10 +1,10 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: chat_authentication_api.py
- @date:2025/6/6 19:59
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: chat_authentication_api.py
+@date:2025/6/6 19:59
+@desc:
"""
from django.utils.translation import gettext_lazy as _
@@ -25,31 +25,62 @@ def get_request():
class ChatAuthenticationAPI(APIMixin):
@staticmethod
def get_request():
- return AnonymousAuthenticationSerializer
+ return None
@staticmethod
def get_parameters():
- pass
+ return [
+ OpenApiParameter(
+ name="application_id",
+ description=_("Application ID"),
+ type=OpenApiTypes.UUID,
+ location="query",
+ required=False,
+ )
+ ]
@staticmethod
def get_response():
pass
-class ChatAuthenticationProfileAPI(APIMixin):
+class ChatAuthenticationProfileAPIV2(APIMixin):
+ @staticmethod
+ def get_parameters():
+ return [
+ OpenApiParameter(
+ name="access_token",
+ description=_("access_token"),
+ type=OpenApiTypes.STR,
+ location="query",
+ required=True,
+ )
+ ]
+
+class ChatAuthenticationProfileAPI(APIMixin):
@staticmethod
def get_parameters():
- return [OpenApiParameter(
- name="access_token",
- description=_("access_token"),
- type=OpenApiTypes.STR,
- location='query',
- required=True,
- )]
+ return [
+ OpenApiParameter(
+ name="application_id",
+ description=_("Application ID"),
+ type=OpenApiTypes.UUID,
+ location="query",
+ required=True,
+ )
+ ]
class ChatOpenAPI(APIMixin):
@staticmethod
def get_parameters():
- return []
+ return [
+ OpenApiParameter(
+ name="application_id",
+ description=_("Application ID"),
+ type=OpenApiTypes.UUID,
+ location=OpenApiParameter.PATH,
+ required=True,
+ )
+ ]
diff --git a/apps/chat/api/portal_api.py b/apps/chat/api/portal_api.py
new file mode 100644
index 00000000000..db9b1d6fb03
--- /dev/null
+++ b/apps/chat/api/portal_api.py
@@ -0,0 +1,128 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:MaxKB
+@file: portal.py
+@date:2026/8/14
+@desc: 门户API文档
+"""
+
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import OpenApiParameter
+
+from common.mixins.api_mixin import APIMixin
+from common.result import DefaultResultSerializer
+from users.serializers.login import LoginRequest
+
+
+class PortalAPI(APIMixin):
+ class Get(APIMixin):
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Save(APIMixin):
+ @staticmethod
+ def get_request():
+ return {
+ "multipart/form-data": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "门户名称"},
+ "description": {"type": "string", "description": "门户描述"},
+ "logo": {"type": "string", "format": "binary", "description": "门户Logo"},
+ "tab_logo": {"type": "string", "format": "binary", "description": "浏览器Tab Logo"},
+ "enable_public_access": {"type": "boolean", "description": "是否开启公开访问"},
+ "enable_api": {"type": "boolean", "description": "是否开启API服务"},
+ "enable_auth": {"type": "boolean", "description": "是否开启身份认证"},
+ "auth_config": {"type": "object", "description": "身份认证配置"},
+ "enable_cors": {"type": "boolean", "description": "是否开启跨域设置"},
+ "cors_config": {"type": "object", "description": "跨域配置"},
+ },
+ }
+ }
+
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Application(APIMixin):
+ @staticmethod
+ def get_parameters():
+ return [
+ OpenApiParameter(
+ name="current_page",
+ description="当前页码",
+ type=OpenApiTypes.INT,
+ location="path",
+ required=True,
+ ),
+ OpenApiParameter(
+ name="page_size",
+ description="每页数量",
+ type=OpenApiTypes.INT,
+ location="path",
+ required=True,
+ ),
+ OpenApiParameter(
+ name="name",
+ description="应用名称搜索",
+ type=OpenApiTypes.STR,
+ location="query",
+ required=False,
+ ),
+ ]
+
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Login(APIMixin):
+ @staticmethod
+ def get_request():
+ return LoginRequest
+
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Info(APIMixin):
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Logout(APIMixin):
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
+
+ class Conversation(APIMixin):
+ @staticmethod
+ def get_parameters():
+ return [
+ OpenApiParameter(
+ name="current_page",
+ description="当前页码",
+ type=OpenApiTypes.INT,
+ location="path",
+ required=True,
+ ),
+ OpenApiParameter(
+ name="page_size",
+ description="每页数量",
+ type=OpenApiTypes.INT,
+ location="path",
+ required=True,
+ ),
+ OpenApiParameter(
+ name="name",
+ description="应用名称搜索",
+ type=OpenApiTypes.STR,
+ location="query",
+ required=False,
+ ),
+ ]
+
+ @staticmethod
+ def get_response():
+ return DefaultResultSerializer
diff --git a/apps/chat/mcp/tools.py b/apps/chat/mcp/tools.py
index 4a3ff972388..f33dec73f80 100644
--- a/apps/chat/mcp/tools.py
+++ b/apps/chat/mcp/tools.py
@@ -1,87 +1,174 @@
+import base64
import json
import re
import uuid_utils.compat as uuid
+from application.models import Application, ApplicationApiKey, ChatSourceChoices, ChatUserType
from django.db.models import QuerySet
+from django.utils import timezone
-from application.models import ApplicationApiKey, Application, ChatUserType, ChatSourceChoices
from chat.serializers.chat import ChatSerializers
+CHAT_FILE_LIST_FIELDS = ("image_list", "document_list", "audio_list", "video_list", "other_list")
+
+CHAT_FILE_TYPE_LABELS = {
+ "image_list": "image",
+ "document_list": "document",
+ "audio_list": "audio",
+ "video_list": "video",
+ "other_list": "file",
+}
+
class MCPToolHandler:
- def __init__(self, auth_header):
+ def __init__(self, auth_header, chat_files_header=None, form_data=None):
app_key = QuerySet(ApplicationApiKey).filter(secret_key=auth_header, is_active=True).first()
if not app_key:
raise PermissionError("Invalid API Key")
+ if app_key.is_permanent is False and app_key.expire_time < timezone.now():
+ raise PermissionError("API Key is expired")
self.application = QuerySet(Application).filter(id=app_key.application_id, is_publish=True).first()
if not self.application:
raise PermissionError("Application is not found or not published")
+ self.chat_files = self.decode_chat_files(chat_files_header)
+ self.form_data = self.decode_form_data(form_data)
+
+ @staticmethod
+ def decode_chat_files(chat_files_header):
+ """
+ 解析上层应用透传过来的文件列表
+ """
+ if not chat_files_header:
+ return {}
+ try:
+ chat_files = json.loads(base64.b64decode(chat_files_header).decode("utf-8"))
+ except Exception:
+ return {}
+ if not isinstance(chat_files, dict):
+ return {}
+ return {
+ key: value
+ for key, value in chat_files.items()
+ if key in CHAT_FILE_LIST_FIELDS and isinstance(value, list) and len(value) > 0
+ }
+
+ @staticmethod
+ def decode_form_data(form_data):
+ """
+ 解析上层应用透传过来的表单数据
+ """
+ if not form_data:
+ return {}
+ try:
+ form_data = json.loads(base64.b64decode(form_data).decode("utf-8"))
+ except Exception:
+ return {}
+ if not isinstance(form_data, dict):
+ return {}
+ return form_data
def initialize(self):
return {
"protocolVersion": "2025-06-18",
- "serverInfo": {
- "name": "maxkb-mcp",
- "version": "1.0.0"
- },
- "capabilities": {
- "tools": {}
- }
+ "serverInfo": {"name": "maxkb-mcp", "version": "1.0.0"},
+ "capabilities": {"tools": {}},
}
+ def build_description(self):
+ """
+ 工具描述中带上当前对话已上传的文件, 否则上层模型不知道子应用可以处理这些文件
+ """
+ description = f"{self.application.name} {self.application.desc}"
+ file_desc_list = []
+ for field, file_list in self.chat_files.items():
+ name_list = [
+ str(file.get("name") or file.get("file_id"))
+ for file in file_list
+ if isinstance(file, dict) and (file.get("name") or file.get("file_id"))
+ ]
+ if name_list:
+ file_desc_list.append(f"{CHAT_FILE_TYPE_LABELS.get(field, 'file')}: {', '.join(name_list)}")
+ if not file_desc_list:
+ return description
+ return (
+ f"{description}\n"
+ "The user has attached the following files to the current conversation. "
+ "They are forwarded to this AI automatically, so it can read and process them directly "
+ "and you do NOT need to pass them as arguments: "
+ f"{'; '.join(file_desc_list)}."
+ )
+
def list_tools(self):
return {
"tools": [
{
- "name": f'agent_{str(self.application.id)[:8]}',
- "description": f'{self.application.name} {self.application.desc}',
+ "name": f"agent_{str(self.application.id)[:8]}",
+ "description": self.build_description(),
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "The message to send to the AI."},
},
- "required": ["message"]
- }
+ "required": ["message"],
+ },
}
]
}
def _get_chat_id(self):
from application.models import ChatUserType
- from chat.serializers.chat import OpenChatSerializers
from common.init import init_template
+ from chat.serializers.chat import OpenChatSerializers
+
init_template.run()
- return OpenChatSerializers(data={
- 'application_id': self.application.id,
- 'chat_user_id': str(uuid.uuid7()),
- 'chat_user_type': ChatUserType.ANONYMOUS_USER,
- 'ip_address': '-',
- 'source': {"type": ChatSourceChoices.ONLINE.value},
- 'debug': False
- }).open()
+ return OpenChatSerializers(
+ data={
+ "application_id": self.application.id,
+ "chat_user_id": str(uuid.uuid7()),
+ "chat_user_type": ChatUserType.ANONYMOUS_USER,
+ "ip_address": "-",
+ "source": {"type": ChatSourceChoices.ONLINE.value},
+ "debug": False,
+ }
+ ).open()
+
+ def build_form_data(self, message):
+ """
+ 合并父应用透传参数与提示词中的 JSON 参数,提示词参数优先。
+ """
+ try:
+ message_form_data = json.loads(message or "{}")
+ except (TypeError, json.JSONDecodeError):
+ message_form_data = {}
+ if not isinstance(message_form_data, dict):
+ message_form_data = {}
+ return {**self.form_data, **message_form_data}
def call_tool(self, params):
- name = params["name"]
args = params.get("arguments", {})
- # print(params)
+ message = args.get("message")
payload = {
- 'message': args.get('message'),
- 'stream': True,
- 're_chat': False
+ "message": message,
+ "stream": True,
+ "re_chat": False,
+ "form_data": self.build_form_data(message),
+ **self.chat_files,
}
- resp = ChatSerializers(data={
- 'chat_id': self._get_chat_id(),
- 'chat_user_id': str(uuid.uuid7()),
- 'chat_user_type': ChatUserType.ANONYMOUS_USER,
- 'application_id': self.application.id,
- 'ip_address': '-',
- 'source': {"type": ChatSourceChoices.ONLINE.value},
- 'debug': False,
- }).chat(payload)
+ resp = ChatSerializers(
+ data={
+ "chat_id": self._get_chat_id(),
+ "chat_user_id": str(uuid.uuid7()),
+ "chat_user_type": ChatUserType.ANONYMOUS_USER,
+ "application_id": self.application.id,
+ "ip_address": "-",
+ "source": {"type": ChatSourceChoices.ONLINE.value},
+ "debug": False,
+ }
+ ).chat(payload)
chunks = []
for raw_line in resp:
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
@@ -99,7 +186,7 @@ def call_tool(self, params):
if event.get("is_end"):
break
- data = ''.join(chunks)
+ data = "".join(chunks)
# 排除标签
- data = re.sub(r'.*?', '', data, flags=re.DOTALL)
+ data = re.sub(r".*?", "", data, flags=re.DOTALL)
return {"content": [{"type": "text", "text": data}]}
diff --git a/apps/chat/serializers/chat.py b/apps/chat/serializers/chat.py
index 58f23136638..918041823c6 100644
--- a/apps/chat/serializers/chat.py
+++ b/apps/chat/serializers/chat.py
@@ -1,47 +1,74 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: chat.py
- @date:2025/6/9 11:23
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: chat.py
+@date:2025/6/9 11:23
+@desc:
"""
+
import json
import os
+import queue as thread_queue
+import threading
from gettext import gettext
from typing import List, Dict
+import uuid_utils
import uuid_utils.compat as uuid
from django.db.models import QuerySet
+from django.http import StreamingHttpResponse
from django.utils.translation import gettext_lazy as _
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from rest_framework import serializers
+from rest_framework.request import Request
from application.chat_pipeline.pipeline_manage import PipelineManage
from application.chat_pipeline.step.chat_step.i_chat_step import PostResponseHandler
from application.chat_pipeline.step.chat_step.impl.base_chat_step import BaseChatStep
-from application.chat_pipeline.step.generate_human_message_step.impl.base_generate_human_message_step import \
- BaseGenerateHumanMessageStep
+from application.chat_pipeline.step.generate_human_message_step.impl.base_generate_human_message_step import (
+ BaseGenerateHumanMessageStep,
+)
from application.chat_pipeline.step.reset_problem_step.impl.base_reset_problem_step import BaseResetProblemStep
from application.chat_pipeline.step.search_dataset_step.impl.base_search_dataset_step import BaseSearchDatasetStep
-from application.flow.common import Answer, Workflow
-from application.flow.i_step_node import WorkFlowPostHandler
+from application.flow.common import Answer
from application.flow.tools import to_stream_response_simple
-from application.flow.workflow_manage import WorkflowManage
-from application.models import Application, ApplicationTypeChoices, \
- ChatUserType, ApplicationChatUserStats, ApplicationAccessToken, ChatRecord, Chat, ApplicationVersion
+from application.models import (
+ Application,
+ ApplicationTypeChoices,
+ ChatUserType,
+ ApplicationChatUserStats,
+ ApplicationAccessToken,
+ ChatRecord,
+ Chat,
+ ApplicationVersion,
+)
from application.serializers.application import ApplicationOperateSerializer
from application.serializers.common import ChatInfo
+from application.workflow.common import WorkflowType, new_instance
+from application.workflow.message.aggregator import AggregationManager
+from application.workflow.message.struct.failure_content import FailureContent
+from application.workflow.message.struct.form_content import FormContent
+from application.workflow.message.struct.reasoning_content import ReasoningContent
+from application.workflow.message.struct.text_content import TextContent
+from application.workflow.message.struct.tool_content import ToolContent
+from application.workflow.message_queue import get_message_queue
+from application.workflow.nodes import get_start_node
+from application.workflow.workflow_manage import WorkflowManage, CallBack
+from application.workflow.workflow_run_registry import WorkflowRunRegistry
+from common import result
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import AppApiException, AppChatNumOutOfBoundsFailed, ChatException
from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.openai_to_response import OpenaiToResponse
from common.handle.impl.response.system_to_response import SystemToResponse
from common.utils.common import flat_map, get_file_content, is_valid_uuid
+from common.utils.logger import maxkb_logger
from knowledge.models import Document, Paragraph
from maxkb.conf import PROJECT_DIR
from models_provider.models import Model, Status
from models_provider.tools import get_model_instance_by_model_workspace_id
+from system_manage.models.chat_user_token_quota import ChatUserTokenQuota
from system_manage.models.resource_mapping import ResourceMapping
@@ -62,69 +89,74 @@ def is_valid(self, *, raise_exception=False):
raise AppApiException(400, _("Too many messages"))
for index in range(len(messages)):
- role = messages[index].get('role')
- if role == 'ai' and index % 2 != 1:
+ role = messages[index].get("role")
+ if role == "ai" and index % 2 != 1:
raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
- if role == 'user' and index % 2 != 0:
+ if role == "user" and index % 2 != 0:
raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
- if role not in ['user', 'ai']:
+ if role not in ["user", "ai"]:
raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
class ChatMessageSerializers(serializers.Serializer):
- message = serializers.CharField(required=True, label=_("User Questions"))
- stream = serializers.BooleanField(required=True,
- label=_("Is the answer in streaming mode"))
+ message = serializers.DictField(required=True, label=_("User Questions"))
+ stream = serializers.BooleanField(required=True, label=_("Is the answer in streaming mode"))
re_chat = serializers.BooleanField(required=True, label=_("Do you want to reply again"))
- chat_record_id = serializers.UUIDField(required=False, allow_null=True,
- label=_("Conversation record id"))
+ chat_record_id = serializers.UUIDField(required=False, allow_null=True, label=_("Conversation record id"))
- node_id = serializers.CharField(required=False, allow_null=True, allow_blank=True,
- label=_("Node id"))
+ node_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Node id"))
- runtime_node_id = serializers.CharField(required=False, allow_null=True, allow_blank=True,
- label=_("Runtime node id"))
+ runtime_node_id = serializers.CharField(
+ required=False, allow_null=True, allow_blank=True, label=_("Runtime node id")
+ )
- node_data = serializers.DictField(required=False, allow_null=True,
- label=_("Node parameters"))
+ node_data = serializers.DictField(required=False, allow_null=True, label=_("Node parameters"))
form_data = serializers.DictField(required=False, label=_("Global variables"))
- image_list = serializers.ListField(required=False, label=_("picture"))
- document_list = serializers.ListField(required=False, label=_("document"))
- audio_list = serializers.ListField(required=False, label=_("Audio"))
- other_list = serializers.ListField(required=False, label=_("Other"))
- child_node = serializers.DictField(required=False, allow_null=True,
- label=_("Child Nodes"))
+ child_node = serializers.DictField(required=False, allow_null=True, label=_("Child Nodes"))
def get_post_handler(chat_info: ChatInfo):
class PostHandler(PostResponseHandler):
-
- def handler(self,
- chat_id,
- chat_record_id,
- paragraph_list: List[Paragraph],
- problem_text: str,
- answer_text,
- manage: PipelineManage,
- step: BaseChatStep,
- padding_problem_text: str = None,
- **kwargs):
- answer_list = [[Answer(answer_text, 'ai-chat-node', 'ai-chat-node', 'ai-chat-node', {}, 'ai-chat-node',
- kwargs.get('reasoning_content', '')).to_dict()]]
- chat_record = ChatRecord(id=chat_record_id,
- chat_id=chat_id,
- problem_text=problem_text,
- answer_text=answer_text,
- details=manage.get_details(),
- message_tokens=manage.context['message_tokens'],
- answer_tokens=manage.context['answer_tokens'],
- answer_text_list=answer_list,
- run_time=manage.context['run_time'],
- index=len(chat_info.chat_record_list) + 1,
- ip_address=chat_info.ip_address,
- source=chat_info.source
- )
+ def handler(
+ self,
+ chat_id,
+ chat_record_id,
+ paragraph_list: List[Paragraph],
+ problem_text: str,
+ answer_text,
+ manage: PipelineManage,
+ step: BaseChatStep,
+ padding_problem_text: str = None,
+ **kwargs,
+ ):
+ answer_list = [
+ [
+ Answer(
+ answer_text,
+ "ai-chat-node",
+ "ai-chat-node",
+ "ai-chat-node",
+ {},
+ "ai-chat-node",
+ kwargs.get("reasoning_content", ""),
+ ).to_dict()
+ ]
+ ]
+ chat_record = ChatRecord(
+ id=chat_record_id,
+ chat_id=chat_id,
+ problem_text=problem_text,
+ answer_text=answer_text,
+ details=manage.get_details(),
+ message_tokens=manage.context["message_tokens"],
+ answer_tokens=manage.context["answer_tokens"],
+ answer_text_list=answer_list,
+ run_time=manage.context["run_time"],
+ index=len(chat_info.chat_record_list) + 1,
+ ip_address=chat_info.ip_address,
+ source=chat_info.source,
+ )
chat_info.append_chat_record(chat_record)
# 重新设置缓存
chat_info.set_cache()
@@ -134,77 +166,107 @@ def handler(self,
class DebugChatSerializers(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
+ # 以下字段用于「缓存缺失时按前端提供的 chat_id 现开会话」(open-if-missing)
+ workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
+ application_id = serializers.UUIDField(required=False, allow_null=True, label=_("Application ID"))
+ chat_user_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Client id"))
+ chat_user_type = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Client Type"))
+ ip_address = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("IP Address"))
+ source = serializers.JSONField(required=False, allow_null=True, label=_("Source"))
def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToResponse()):
self.is_valid(raise_exception=True)
- chat_id = self.data.get('chat_id')
+ chat_id = self.data.get("chat_id")
chat_info: ChatInfo = ChatInfo.get_cache(chat_id)
+ if chat_info is None:
+ # 前端本地生成的 chat_id 首次发消息时,缓存里还没有会话,按该 id 现开一个 debug 会话。
+ OpenChatSerializers(
+ data={
+ "workspace_id": self.data.get("workspace_id"),
+ "application_id": self.data.get("application_id"),
+ "chat_user_id": self.data.get("chat_user_id"),
+ "chat_user_type": self.data.get("chat_user_type"),
+ "ip_address": self.data.get("ip_address"),
+ "source": self.data.get("source"),
+ "debug": True,
+ }
+ ).open(chat_id=str(chat_id))
+ chat_info = ChatInfo.get_cache(chat_id)
application = QuerySet(Application).filter(id=chat_info.application_id).first()
chat_info.application = application
- return ChatSerializers(data={
- 'chat_id': chat_id, "chat_user_id": chat_info.chat_user_id,
- "chat_user_type": chat_info.chat_user_type,
- "application_id": chat_info.application.id, "debug": True
- }).chat(instance, base_to_response)
+ return ChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "chat_user_id": chat_info.chat_user_id,
+ "chat_user_type": chat_info.chat_user_type,
+ "application_id": chat_info.application.id,
+ "debug": True,
+ }
+ ).chat(instance, base_to_response)
-SYSTEM_ROLE = get_file_content(os.path.join(PROJECT_DIR, "apps", "chat", 'template', 'generate_prompt_system'))
+SYSTEM_ROLE = get_file_content(os.path.join(PROJECT_DIR, "apps", "chat", "template", "generate_prompt_system"))
class PromptGenerateSerializer(serializers.Serializer):
- workspace_id = serializers.CharField(required=False, label=_('Workspace ID'))
+ workspace_id = serializers.CharField(required=False, label=_("Workspace ID"))
model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model"))
application_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Application"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- query_set = QuerySet(Application).filter(id=self.data.get('application_id'))
+ workspace_id = self.data.get("workspace_id")
+ query_set = QuerySet(Application).filter(id=self.data.get("application_id"))
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
application = query_set.first()
if application is None:
- raise AppApiException(500, _('Application id does not exist'))
+ raise AppApiException(500, _("Application id does not exist"))
return application
def generate_prompt(self, instance: dict):
application = self.is_valid(raise_exception=True)
GeneratePromptSerializers(data=instance).is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- model_id = self.data.get('model_id')
- prompt = instance.get('prompt')
- messages = instance.get('messages')
+ workspace_id = self.data.get("workspace_id")
+ model_id = self.data.get("model_id")
+ prompt = instance.get("prompt")
+ messages = instance.get("messages")
- message = messages[-1]['content']
+ message = messages[-1]["content"]
q = prompt.replace("{userInput}", message)
- messages[-1]['content'] = q
+ messages[-1]["content"] = q
SUPPORTED_MODEL_TYPES = ["LLM", "IMAGE"]
- model_exist = QuerySet(Model).filter(
- id=model_id,
- model_type__in=SUPPORTED_MODEL_TYPES
- ).exists()
+ model_exist = QuerySet(Model).filter(id=model_id, model_type__in=SUPPORTED_MODEL_TYPES).exists()
if not model_exist:
raise Exception(_("Model does not exists or is not an LLM model"))
def process():
- model = get_model_instance_by_model_workspace_id(model_id=model_id, workspace_id=workspace_id,
- **application.model_params_setting)
+ model = get_model_instance_by_model_workspace_id(
+ model_id=model_id, workspace_id=workspace_id, **application.model_params_setting
+ )
try:
- for r in model.stream([SystemMessage(content=SYSTEM_ROLE),
- *[HumanMessage(content=m.get('content')) if m.get(
- 'role') == 'user' else AIMessage(
- content=m.get('content')) for m in messages]]):
- yield 'data: ' + json.dumps({'content': r.content}) + '\n\n'
+ for r in model.stream(
+ [
+ SystemMessage(content=SYSTEM_ROLE),
+ *[
+ HumanMessage(content=m.get("content"))
+ if m.get("role") == "user"
+ else AIMessage(content=m.get("content"))
+ for m in messages
+ ],
+ ]
+ ):
+ yield "data: " + json.dumps({"content": r.content}) + "\n\n"
except Exception as e:
- yield 'data: ' + json.dumps({'error': str(e)}) + '\n\n'
+ yield "data: " + json.dumps({"error": str(e)}) + "\n\n"
return to_stream_response_simple(process())
class OpenAIMessage(serializers.Serializer):
- content = serializers.CharField(required=True, label=_('content'))
- role = serializers.CharField(required=True, label=_('Role'))
+ content = serializers.CharField(required=True, label=_("content"))
+ role = serializers.CharField(required=True, label=_("Role"))
class OpenAIInstanceSerializer(serializers.Serializer):
@@ -223,26 +285,27 @@ class OpenAIChatSerializer(serializers.Serializer):
@staticmethod
def get_message(instance):
- return instance.get('messages')[-1].get('content')
+ return instance.get("messages")[-1].get("content")
@staticmethod
def generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type, ip_address, source):
if chat_id is None:
chat_id = str(uuid.uuid1())
- chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], [],
- application_id)
+ chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], [], application_id)
chat_info.set_cache()
else:
chat_info = ChatInfo.get_cache(chat_id)
if chat_info is None:
- open_chat = ChatSerializers(data={
- 'chat_id': chat_id,
- 'chat_user_id': chat_user_id,
- 'chat_user_type': chat_user_type,
- 'application_id': application_id,
- 'ip_address': ip_address,
- 'source': source,
- })
+ open_chat = ChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "chat_user_id": chat_user_id,
+ "chat_user_type": chat_user_type,
+ "application_id": application_id,
+ "ip_address": ip_address,
+ "source": source,
+ }
+ )
open_chat.is_valid(raise_exception=True)
chat_info = open_chat.re_open_chat(chat_id)
chat_info.set_cache()
@@ -252,42 +315,45 @@ def chat(self, instance: Dict, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
OpenAIInstanceSerializer(data=instance).is_valid(raise_exception=True)
- chat_id = instance.get('chat_id')
+ chat_id = instance.get("chat_id")
message = self.get_message(instance)
- re_chat = instance.get('re_chat', False)
- stream = instance.get('stream', False)
- application_id = self.data.get('application_id')
- chat_user_id = self.data.get('chat_user_id')
- chat_user_type = self.data.get('chat_user_type')
- ip_address = self.data.get('ip_address')
- source = self.data.get('source')
+ re_chat = instance.get("re_chat", False)
+ stream = instance.get("stream", False)
+ application_id = self.data.get("application_id")
+ chat_user_id = self.data.get("chat_user_id")
+ chat_user_type = self.data.get("chat_user_type")
+ ip_address = self.data.get("ip_address")
+ source = self.data.get("source")
chat_id = self.generate_chat(chat_id, application_id, message, chat_user_id, chat_user_type, ip_address, source)
return ChatSerializers(
data={
- 'chat_id': chat_id,
- 'chat_user_id': chat_user_id,
- 'chat_user_type': chat_user_type,
- 'application_id': application_id,
- 'ip_address': ip_address,
- 'source': source,
+ "chat_id": chat_id,
+ "chat_user_id": chat_user_id,
+ "chat_user_type": chat_user_type,
+ "application_id": application_id,
+ "ip_address": ip_address,
+ "source": source,
}
- ).chat({'message': message,
- 're_chat': re_chat,
- 'stream': stream,
- 'form_data': instance.get('form_data', {}),
- 'image_list': instance.get('image_list', []),
- 'document_list': instance.get('document_list', []),
- 'audio_list': instance.get('audio_list', []),
- 'other_list': instance.get('other_list', [])},
- base_to_response=OpenaiToResponse())
+ ).chat(
+ {
+ "message": message,
+ "re_chat": re_chat,
+ "stream": stream,
+ "form_data": instance.get("form_data", {}),
+ "image_list": instance.get("image_list", []),
+ "document_list": instance.get("document_list", []),
+ "audio_list": instance.get("audio_list", []),
+ "other_list": instance.get("other_list", []),
+ },
+ base_to_response=OpenaiToResponse(),
+ )
class ChatSerializers(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
chat_user_id = serializers.CharField(required=True, label=_("Client id"))
chat_user_type = serializers.CharField(required=True, label=_("Client Type"))
- application_id = serializers.UUIDField(required=True, allow_null=True,
- label=_("Application ID"))
+ application_id = serializers.UUIDField(required=True, allow_null=True, label=_("Application ID"))
debug = serializers.BooleanField(required=False, label=_("Debug"))
ip_address = serializers.CharField(required=False, label=_("IP Address"), allow_null=True, allow_blank=True)
source = serializers.JSONField(required=False, label=_("Source"))
@@ -296,27 +362,34 @@ def is_valid_application_workflow(self, *, raise_exception=False):
self.is_valid_intraday_access_num()
def is_valid_chat_id(self, chat_info: ChatInfo):
- if self.data.get('application_id') is not None and self.data.get('application_id') != str(
- chat_info.application_id):
+ if self.data.get("application_id") is not None and self.data.get("application_id") != str(
+ chat_info.application_id
+ ):
raise ChatException(500, _("Conversation does not exist"))
def is_valid_intraday_access_num(self):
- if not self.data.get('debug') and [ChatUserType.ANONYMOUS_USER.value,
- ChatUserType.CHAT_USER.value].__contains__(
- self.data.get('chat_user_type')):
- access_client = QuerySet(ApplicationChatUserStats).filter(chat_user_id=self.data.get('chat_user_id'),
- application_id=self.data.get(
- 'application_id')).first()
+ if not self.data.get("debug") and [
+ ChatUserType.ANONYMOUS_USER.value,
+ ChatUserType.CHAT_USER.value,
+ ].__contains__(self.data.get("chat_user_type")):
+ access_client = (
+ QuerySet(ApplicationChatUserStats)
+ .filter(chat_user_id=self.data.get("chat_user_id"), application_id=self.data.get("application_id"))
+ .first()
+ )
if access_client is None:
- access_client = ApplicationChatUserStats(chat_user_id=self.data.get('chat_user_id'),
- chat_user_type=self.data.get('chat_user_type'),
- application_id=self.data.get('application_id'),
- access_num=0,
- intraday_access_num=0)
+ access_client = ApplicationChatUserStats(
+ chat_user_id=self.data.get("chat_user_id"),
+ chat_user_type=self.data.get("chat_user_type"),
+ application_id=self.data.get("application_id"),
+ access_num=0,
+ intraday_access_num=0,
+ )
access_client.save()
- application_access_token = QuerySet(ApplicationAccessToken).filter(
- application_id=self.data.get('application_id')).first()
+ application_access_token = (
+ QuerySet(ApplicationAccessToken).filter(application_id=self.data.get("application_id")).first()
+ )
if application_access_token.access_num <= access_client.intraday_access_num:
raise AppChatNumOutOfBoundsFailed(1002, _("The number of visits exceeds today's visits"))
@@ -335,118 +408,363 @@ def is_valid_application_simple(self, *, chat_info: ChatInfo, raise_exception=Fa
return chat_info
def chat_simple(self, chat_info: ChatInfo, instance, base_to_response):
- message = instance.get('message')
- re_chat = instance.get('re_chat')
- stream = instance.get('stream')
- chat_user_id = self.data.get('chat_user_id')
- chat_user_type = self.data.get('chat_user_type')
- ip_address = self.data.get('ip_address')
- source = self.data.get('source')
+ message_dict = instance.get("message")
+ message = message_dict.get("content", "") if isinstance(message_dict, dict) else message_dict
+ re_chat = instance.get("re_chat")
+ stream = instance.get("stream")
+ chat_user_id = self.data.get("chat_user_id")
+ chat_user_type = self.data.get("chat_user_type")
+ ip_address = self.data.get("ip_address")
+ source = self.data.get("source")
form_data = instance.get("form_data")
- chat_record_id = instance.get('chat_record_id')
+ chat_record_id = instance.get("chat_record_id")
pipeline_manage_builder = PipelineManage.builder()
# 如果开启了问题优化,则添加上问题优化步骤
if chat_info.application.problem_optimization:
pipeline_manage_builder.append_step(BaseResetProblemStep)
# 构建流水线管理器
- pipeline_message = (pipeline_manage_builder.append_step(BaseSearchDatasetStep)
- .append_step(BaseGenerateHumanMessageStep)
- .append_step(BaseChatStep)
- .add_base_to_response(base_to_response)
- .add_debug(self.data.get('debug', False))
- .build())
+ pipeline_message = (
+ pipeline_manage_builder.append_step(BaseSearchDatasetStep)
+ .append_step(BaseGenerateHumanMessageStep)
+ .append_step(BaseChatStep)
+ .add_base_to_response(base_to_response)
+ .add_debug(self.data.get("debug", False))
+ .build()
+ )
exclude_paragraph_id_list = []
# 相同问题是否需要排除已经查询到的段落
if re_chat:
paragraph_id_list = flat_map(
- [[paragraph.get('id') for paragraph in chat_record.details['search_step']['paragraph_list']] for
- chat_record in chat_info.chat_record_list if
- chat_record.problem_text == message and 'search_step' in chat_record.details and 'paragraph_list' in
- chat_record.details['search_step']])
+ [
+ [paragraph.get("id") for paragraph in chat_record.details["search_step"]["paragraph_list"]]
+ for chat_record in chat_info.chat_record_list
+ if chat_record.problem_text == message
+ and "search_step" in chat_record.details
+ and "paragraph_list" in chat_record.details["search_step"]
+ ]
+ )
exclude_paragraph_id_list = list(set(paragraph_id_list))
# 构建运行参数
- params = chat_info.to_pipeline_manage_params(message, get_post_handler(chat_info), exclude_paragraph_id_list,
- chat_user_id, chat_user_type, ip_address, source, stream,
- form_data)
+ params = chat_info.to_pipeline_manage_params(
+ message,
+ get_post_handler(chat_info),
+ exclude_paragraph_id_list,
+ chat_user_id,
+ chat_user_type,
+ ip_address,
+ source,
+ stream,
+ form_data,
+ )
if chat_record_id:
- params['chat_record_id'] = chat_record_id
+ params["chat_record_id"] = chat_record_id
chat_info.set_chat(message)
# 运行流水线作业
pipeline_message.run(params)
- return pipeline_message.context['chat_result']
+ return pipeline_message.context["chat_result"]
@staticmethod
def get_chat_record(chat_info, chat_record_id):
if chat_info is not None:
- chat_record_list = [chat_record for chat_record in chat_info.chat_record_list if
- str(chat_record.id) == str(chat_record_id)]
+ chat_record_list = [
+ chat_record for chat_record in chat_info.chat_record_list if str(chat_record.id) == str(chat_record_id)
+ ]
if chat_record_list is not None and len(chat_record_list):
return chat_record_list[-1]
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id, chat_id=chat_info.chat_id).first()
if chat_record is None:
- raise ChatException(500, _("Conversation record does not exist"))
-
- return chat_record
+ if not is_valid_uuid(chat_record_id):
+ raise ChatException(500, _("Conversation record does not exist"))
chat_record = QuerySet(ChatRecord).filter(id=chat_record_id).first()
return chat_record
def chat_work_flow(self, chat_info: ChatInfo, instance: dict, base_to_response):
- message = instance.get('message')
- re_chat = instance.get('re_chat')
- stream = instance.get('stream')
+ import queue
+
+ message_dict = instance.get("message")
+ message = message_dict.get("content", "") if isinstance(message_dict, dict) else message_dict
+ re_chat = instance.get("re_chat")
+ stream = instance.get("stream")
chat_user_id = self.data.get("chat_user_id")
- chat_user_type = self.data.get('chat_user_type')
- ip_address = self.data.get('ip_address')
- source = self.data.get('source')
- form_data = instance.get('form_data')
- image_list = instance.get('image_list')
- video_list = instance.get('video_list')
- document_list = instance.get('document_list')
- audio_list = instance.get('audio_list')
- other_list = instance.get('other_list')
+ chat_user_type = self.data.get("chat_user_type")
+ ip_address = self.data.get("ip_address")
+ source = self.data.get("source")
+ form_data = instance.get("form_data")
+ image_list = message_dict.get("image_list", []) if isinstance(message_dict, dict) else []
+ video_list = message_dict.get("video_list", []) if isinstance(message_dict, dict) else []
+ document_list = message_dict.get("document_list", []) if isinstance(message_dict, dict) else []
+ audio_list = message_dict.get("audio_list", []) if isinstance(message_dict, dict) else []
+ other_list = message_dict.get("other_list", []) if isinstance(message_dict, dict) else []
workspace_id = chat_info.application.workspace_id
- chat_record_id = instance.get('chat_record_id')
- debug = self.data.get('debug', False)
- chat_record = None
+ chat_record_id = instance.get("chat_record_id")
+ position = instance.get("position")
+ chunk_id = instance.get("chunk_id")
+ debug = self.data.get("debug", False)
history_chat_record = chat_info.chat_record_list
if chat_record_id is not None:
chat_record = self.get_chat_record(chat_info, chat_record_id)
if chat_record:
history_chat_record = [r for r in chat_info.chat_record_list if str(r.id) != chat_record_id]
+
work_flow = chat_info.application.work_flow
- work_flow_manage = WorkflowManage(Workflow.new_instance(work_flow),
- {'history_chat_record': history_chat_record, 'question': message,
- 'chat_id': chat_info.chat_id, 'chat_record_id': str(
- uuid.uuid7()) if chat_record_id is None else str(chat_record_id),
- 'stream': stream,
- 're_chat': re_chat,
- 'chat_user_id': chat_user_id,
- 'chat_user_type': chat_user_type,
- 'ip_address': ip_address,
- 'source': source,
- 'workspace_id': workspace_id,
- 'debug': debug,
- 'chat_user': chat_info.get_chat_user(),
- 'chat_user_group': chat_info.get_chat_user_group(),
- 'application_id': str(chat_info.application_id)},
- WorkFlowPostHandler(chat_info),
- base_to_response, form_data, image_list, document_list, audio_list,
- video_list,
- other_list,
- instance.get('runtime_node_id'),
- instance.get('node_data'), chat_record, instance.get('child_node'))
+ workflow = new_instance(work_flow, WorkflowType.APPLICATION)
+
+ chat_record_id_str = str(uuid.uuid7()) if chat_record_id is None else str(chat_record_id)
+
+ parameters = {
+ "history_chat_record": history_chat_record,
+ "question": message,
+ "chat_id": chat_info.chat_id,
+ "chat_record_id": chat_record_id_str,
+ "stream": stream,
+ "re_chat": re_chat,
+ "chat_user_id": chat_user_id,
+ "chat_user_type": chat_user_type,
+ "ip_address": ip_address,
+ "source": source,
+ "workspace_id": workspace_id,
+ "debug": debug,
+ "chat_user": chat_info.get_chat_user(),
+ "chat_user_group": chat_info.get_chat_user_group(),
+ "application_id": str(chat_info.application_id),
+ "form_data": form_data or {},
+ "position": position,
+ "chunk_id": chunk_id,
+ "image_list": image_list or [],
+ "document_list": document_list or [],
+ "audio_list": audio_list or [],
+ "video_list": video_list or [],
+ "other_list": other_list or [],
+ }
+
+ result_queue = queue.Queue()
+
+ aggregation = AggregationManager()
+ self.save_chat_record(chat_info, chat_info.chat_id, chat_record_id_str, message_dict)
+
+ def on_next(wf_manage, content):
+ aggregation.aggregate(content)
+ message_queue = get_message_queue()
+ message_queue.produce(chat_record_id_str, content.to_dict())
+ if isinstance(content, TextContent):
+ result_queue.put(
+ (
+ "chunk",
+ {
+ "content": [
+ {
+ "id": content.id,
+ "type": "TEXT",
+ "content": content.content,
+ }
+ ]
+ },
+ )
+ )
+ elif isinstance(content, ReasoningContent):
+ result_queue.put(
+ (
+ "chunk",
+ {
+ "content": [
+ {
+ "id": content.id,
+ "type": "REASONING",
+ "content": content.content,
+ "status": content.status.value if content.status else None,
+ }
+ ]
+ },
+ )
+ )
+ elif isinstance(content, ToolContent):
+ result_queue.put(
+ (
+ "chunk",
+ {
+ "content": [
+ {
+ "id": content.id,
+ "type": "TOOL",
+ "content": content.content,
+ "arguments": content.arguments,
+ "result": content.result,
+ "status": content.status.value if content.status else None,
+ }
+ ]
+ },
+ )
+ )
+ elif isinstance(content, FormContent):
+
+ def position_to_dict(pos):
+ if pos is None:
+ return None
+ return {"id": pos.id, "index": pos.index, "children": position_to_dict(pos.children)}
+
+ result_queue.put(
+ (
+ "chunk",
+ {
+ "content": [
+ {
+ "id": content.id,
+ "type": "FORM",
+ "form_field_list": content.form_field_list,
+ "form_content_format": content.form_content_format,
+ "is_submit": content.is_submit,
+ "form_data": content.form_data,
+ "status": content.status.value if content.status else None,
+ "position": position_to_dict(content.position),
+ "chat_record_id": chat_record_id_str,
+ }
+ ]
+ },
+ )
+ )
+
+ def on_complete(wf_manage, error):
+ # 注销工作流实例
+ WorkflowRunRegistry.unregister(chat_record_id_str, str(chat_info.chat_id))
+ message_queue = get_message_queue()
+ if error:
+ result_queue.put(("error", error))
+ message_queue.produce(
+ chat_record_id_str,
+ FailureContent(str(uuid_utils.uuid7()), str(error), Status.SUCCESS, None, None).to_dict(),
+ )
+ QuerySet(ChatRecord).filter(id=chat_record_id).update()
+ self.update_chat_record(
+ chat_info, chat_info.chat_id, chat_record_id_str, wf_manage.context, aggregation.get_contents()
+ )
+ result_queue.put(("done", None))
+ message_queue.produce_done(chat_record_id_str)
+
+ call_back = CallBack(on_next, on_complete)
+
+ def get_start_node_fn(wf, wm):
+ return get_start_node(wf, wm, WorkflowType.APPLICATION, position)
+
+ # 判断是否是 Form 提交(有 position 和 chat_record_id)
+ if position and chat_record_id:
+ # 从历史 context 恢复
+ work_flow_manage = WorkflowManage.from_context(
+ chat_record_id=chat_record_id,
+ workflow=workflow,
+ parameters=parameters,
+ workflow_type=WorkflowType.APPLICATION,
+ call_back=call_back,
+ get_start_node=get_start_node_fn,
+ )
+ if work_flow_manage is None:
+ # 恢复失败,回退到正常流程
+ work_flow_manage = WorkflowManage(
+ workflow, parameters, WorkflowType.APPLICATION, call_back, get_start_node_fn
+ )
+ else:
+ # 正常创建新的 WorkflowManage
+ work_flow_manage = WorkflowManage(
+ workflow, parameters, WorkflowType.APPLICATION, call_back, get_start_node_fn
+ )
+
+ work_flow_manage.start_node.workflow_manage = work_flow_manage
+
+ # 注册工作流实例到注册表
+ WorkflowRunRegistry.register(chat_record_id_str, str(chat_info.chat_id), work_flow_manage)
+
chat_info.set_chat(message)
- r = work_flow_manage.run()
- return r
+
+ if stream:
+
+ def generate():
+ work_flow_manage.run()
+ while True:
+ msg_type, data = result_queue.get()
+ if msg_type == "done":
+ yield "data: [DONE]\n\n"
+ break
+ if msg_type == "error":
+ yield (
+ "data: "
+ + json.dumps(
+ {
+ "chat_id": str(chat_info.chat_id),
+ "chat_record_id": chat_record_id_str,
+ "content": [{"type": "FAILURE", "content": str(data)}],
+ },
+ ensure_ascii=False,
+ )
+ + "\n\n"
+ )
+ yield "data: [DONE]\n\n"
+ break
+ if msg_type == "chunk":
+ data["chat_id"] = str(chat_info.chat_id)
+ data["chat_record_id"] = chat_record_id_str
+ yield "data: " + json.dumps(data, ensure_ascii=False) + "\n\n"
+
+ return to_stream_response_simple(generate())
+ else:
+ work_flow_manage.run()
+ while True:
+ msg_type, data = result_queue.get()
+ if msg_type == "done":
+ break
+ if msg_type == "error":
+ raise data
+ return base_to_response.to_block_response(chat_info.chat_id, chat_record_id_str, "", True, 0, 0)
+
+ @staticmethod
+ def save_chat_record(chat_info, chat_id, chat_record_id, question):
+ chat_record = ChatRecord(
+ id=chat_record_id,
+ chat_id=chat_id,
+ problem_text="",
+ answer_text="",
+ details={},
+ message_tokens=0,
+ answer_tokens=0,
+ answer_text_list=[[]],
+ run_time=0,
+ index=len(chat_info.chat_record_list) + 1,
+ ip_address=chat_info.ip_address,
+ source=chat_info.source,
+ workflow_context={},
+ question=question,
+ messages=[],
+ )
+ chat_info.append_chat_record(chat_record)
+ chat_info.set_cache()
+
+ @staticmethod
+ def update_chat_record(chat_info, chat_id, chat_record_id, workflow_context, messages):
+ message_tokens = sum(
+ v.get("message_tokens", 0)
+ for v in workflow_context.values()
+ if isinstance(v, dict) and "message_tokens" in v
+ )
+ answer_tokens = sum(
+ v.get("answer_tokens", 0) for v in workflow_context.values() if isinstance(v, dict) and "answer_tokens" in v
+ )
+ ChatUserTokenQuota.consume(chat_info.chat_user_id, message_tokens + answer_tokens)
+ QuerySet(ChatRecord).filter(id=chat_record_id).update(
+ workflow_context=workflow_context,
+ messages=messages,
+ message_tokens=message_tokens,
+ answer_tokens=answer_tokens,
+ )
def is_valid_chat_user(self):
- chat_user_id = self.data.get('chat_user_id')
- application_id = self.data.get('application_id')
- chat_user_type = self.data.get('chat_user_type')
+ chat_user_id = self.data.get("chat_user_id")
+ application_id = self.data.get("application_id")
+ chat_user_type = self.data.get("chat_user_type")
is_auth_chat_user = DatabaseModelManage.get_model("is_auth_chat_user")
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
- if application_access_token and application_access_token.authentication and application_access_token.authentication_value.get(
- 'type') == 'login':
+ if (
+ application_access_token
+ and application_access_token.authentication
+ and application_access_token.authentication_value.get("type") == "login"
+ ):
if chat_user_type == ChatUserType.ANONYMOUS_USER.value:
raise ChatException(500, _("The chat user is not authorized."))
if chat_user_type == ChatUserType.CHAT_USER.value and is_auth_chat_user:
@@ -459,10 +777,11 @@ def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToRespon
ChatMessageSerializers(data=instance).is_valid(raise_exception=True)
chat_info = self.get_chat_info()
chat_info.get_application()
- chat_info.get_chat_user(asker=(instance.get('form_data') or {}).get('asker'))
+ chat_info.get_chat_user(asker=(instance.get("form_data") or {}).get("asker"))
self.is_valid_chat_id(chat_info)
- if not self.data.get('debug'):
+ if not self.data.get("debug"):
self.is_valid_chat_user()
+ ChatUserTokenQuota.consume(chat_info.chat_user_id, 0) # 触发周期重置 + 配额预校验
if chat_info.application.type == ApplicationTypeChoices.SIMPLE:
self.is_valid_application_simple(raise_exception=True, chat_info=chat_info)
return self.chat_simple(chat_info, instance, base_to_response)
@@ -472,7 +791,7 @@ def chat(self, instance: dict, base_to_response: BaseToResponse = SystemToRespon
def get_chat_info(self):
self.is_valid(raise_exception=True)
- chat_id = self.data.get('chat_id')
+ chat_id = self.data.get("chat_id")
chat_info: ChatInfo = ChatInfo.get_cache(chat_id)
if chat_info is None:
chat_info: ChatInfo = self.re_open_chat(chat_id)
@@ -486,8 +805,9 @@ def re_open_chat(self, chat_id: str):
application = QuerySet(Application).filter(id=chat.application_id).first()
if application is None:
raise ChatException(500, _("Application does not exist"))
- application_version = QuerySet(ApplicationVersion).filter(application_id=application.id).order_by(
- '-create_time')[0:1].first()
+ application_version = (
+ QuerySet(ApplicationVersion).filter(application_id=application.id).order_by("-create_time")[0:1].first()
+ )
if application_version is None:
raise ChatException(500, _("The application has not been published. Please use it after publishing."))
if application.type == ApplicationTypeChoices.SIMPLE:
@@ -496,39 +816,198 @@ def re_open_chat(self, chat_id: str):
return self.re_open_chat_work_flow(chat_id, application)
def re_open_chat_simple(self, chat_id, application):
- # 数据集id列表
- knowledge_id_list = [str(row.target_id) for row in
- QuerySet(ResourceMapping).filter(source_id=str(application.id),
- source_type='APPLICATION',
- target_type='KNOWLEDGE')]
+ if self.data.get("debug"):
+ # 数据集id列表
+ knowledge_id_list = [
+ str(row.target_id)
+ for row in QuerySet(ResourceMapping).filter(
+ source_id=str(application.id), source_type="APPLICATION", target_type="KNOWLEDGE"
+ )
+ ]
+ else:
+ application_version = (
+ QuerySet(ApplicationVersion).filter(application_id=application.id).order_by("-create_time")[0:1].first()
+ )
+ knowledge_id_list = application_version.knowledge_ids
# 需要排除的文档
- exclude_document_id_list = [str(document.id) for document in
- QuerySet(Document).filter(
- knowledge_id__in=knowledge_id_list,
- is_active=False)]
- chat_info = ChatInfo(chat_id, self.data.get('chat_user_id'), self.data.get('chat_user_type'),
- self.data.get('ip_address'),
- self.data.get('source'), knowledge_id_list,
- exclude_document_id_list, application.id)
- chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by('-create_time')[0:5])
+ exclude_document_id_list = [
+ str(document.id)
+ for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
+ ]
+ chat_info = ChatInfo(
+ chat_id,
+ self.data.get("chat_user_id"),
+ self.data.get("chat_user_type"),
+ self.data.get("ip_address"),
+ self.data.get("source"),
+ knowledge_id_list,
+ exclude_document_id_list,
+ application.id,
+ )
+ chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by("-create_time")[0:5])
chat_record_list.sort(key=lambda r: r.create_time)
for chat_record in chat_record_list:
chat_info.chat_record_list.append(chat_record)
return chat_info
def re_open_chat_work_flow(self, chat_id, application):
- chat_info = ChatInfo(chat_id, self.data.get('chat_user_id'), self.data.get('chat_user_type'),
- self.data.get('ip_address'),
- self.data.get('source'), [], [],
- application.id)
- chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by('-create_time')[0:5])
+ chat_info = ChatInfo(
+ chat_id,
+ self.data.get("chat_user_id"),
+ self.data.get("chat_user_type"),
+ self.data.get("ip_address"),
+ self.data.get("source"),
+ [],
+ [],
+ application.id,
+ )
+ chat_record_list = list(QuerySet(ChatRecord).filter(chat_id=chat_id).order_by("-create_time")[0:5])
chat_record_list.sort(key=lambda r: r.create_time)
for chat_record in chat_record_list:
chat_info.chat_record_list.append(chat_record)
return chat_info
+# consume 桥接队列的上限:满了会反压 pump 线程,防止慢客户端把消息全堆进内存
+_BRIDGE_MAXSIZE = 1000
+# 消费上限(秒),与桥接 get 的超时保持一致的量级
+_CONSUME_TIMEOUT = 300
+
+
+class ResumeSerializers(serializers.Serializer):
+ chat_id = serializers.UUIDField(required=True)
+ chat_record_id = serializers.UUIDField(required=True)
+
+ def resume(self, request):
+ self.is_valid(raise_exception=True)
+ from application.workflow.message_queue import get_message_queue
+ from application.models import ChatRecord
+
+ chat_record_id = self.data.get("chat_record_id")
+ mq = get_message_queue()
+
+ start_id = self._resolve_start_id(request)
+
+ is_running = mq.exists(chat_record_id) and not mq.is_done(chat_record_id)
+
+ if is_running:
+ generator = self._stream_from_queue(mq, chat_record_id, start_id)
+ else:
+ chat_record = ChatRecord.objects.filter(id=chat_record_id).first()
+ if not chat_record:
+ return result.error(_("Chat record not found"))
+ generator = self._stream_from_db(chat_record, start_id)
+
+ response = StreamingHttpResponse(
+ generator,
+ content_type="text/event-stream;charset=utf-8",
+ )
+ response["Cache-Control"] = "no-cache"
+ response["X-Accel-Buffering"] = "no"
+ return response
+
+ @staticmethod
+ def _resolve_start_id(request: Request) -> str:
+ """
+ 优先取 SSE 标准的 Last-Event-ID 头(浏览器 EventSource 断线重连会自动带上),
+ 兼容 body / query 里显式传的 last_event_id。取不到则从头开始。
+ """
+ candidate = (
+ request.META.get("HTTP_LAST_EVENT_ID")
+ or (request.data.get("last_event_id") if hasattr(request, "data") else None)
+ or request.query_params.get("last_event_id")
+ )
+ candidate = (candidate or "").strip()
+ return candidate or "0"
+
+ @staticmethod
+ def _sse(msg_id: str, msg_data: str) -> str:
+ """
+ 带 id 字段的 SSE 帧:浏览器会把最后收到的 id 存进 Last-Event-ID,
+ 下次重连自动回传,从而实现断点续传。
+ """
+ return f"id: {msg_id}\ndata: {msg_data}\n\n"
+
+ def _stream_from_queue(self, mq, chat_record_id: str, start_id: str):
+ """
+ 用后台线程跑阻塞式 consume,把回调桥接成 generator。
+ 复用 consume 已经处理好的 done 标记 / 尾部残留竞态,视图层不再重写收尾。
+ """
+ bridge: thread_queue.Queue = thread_queue.Queue(maxsize=_BRIDGE_MAXSIZE)
+ done_sentinel = object()
+ stop_event = threading.Event()
+
+ def pump():
+ try:
+ mq.consume(
+ queue_id=chat_record_id,
+ start_id=start_id,
+ # bridge.put 无 timeout:队列满时在此反压,等 generator 消费腾位
+ on_message=lambda mid, data: bridge.put((mid, data)),
+ on_done=lambda: bridge.put(done_sentinel), # 契约保证有且仅一次
+ timeout=_CONSUME_TIMEOUT,
+ should_stop=stop_event.is_set, # 客户端断开时提前结束,省掉空转
+ )
+ except Exception as e:
+ maxkb_logger.error(f"ResumeStream pump error [{chat_record_id}]: {e}")
+ # 兜底:即使 consume 内部异常也要放哨兵,避免 generator 永久阻塞
+ try:
+ bridge.put_nowait(done_sentinel)
+ except thread_queue.Full:
+ pass
+
+ worker = threading.Thread(target=pump, name=f"resume-{chat_record_id}", daemon=True)
+ worker.start()
+
+ try:
+ while True:
+ try:
+ # 略大于 consume timeout:正常情况下哨兵会先到,这里只防线程异常挂死
+ item = bridge.get(timeout=_CONSUME_TIMEOUT + 5)
+ except thread_queue.Empty:
+ maxkb_logger.warning(f"ResumeStream bridge idle timeout [{chat_record_id}]")
+ break
+ if item is done_sentinel:
+ break
+ msg_id, msg_data = item
+ yield self._sse(msg_id, msg_data)
+ yield "data: [DONE]\n\n"
+ finally:
+ # 客户端提前关闭连接会在 yield 处抛 GeneratorExit,落到这里;
+ # 通知 consume 线程停止,不必再等到 300s 超时
+ stop_event.set()
+
+ def _stream_from_db(self, chat_record, start_id: str):
+ """
+ 已完成 / 不存在于队列:从库里读。
+ 若带了 Last-Event-ID,则跳过已发送过的部分(按落库时的消息 id 对齐)。
+ """
+ try:
+ messages = chat_record.messages or []
+ resuming = start_id and start_id != "0"
+ passed = not resuming # 无续传点则全部下发
+
+ for msg in messages:
+ msg_id = str(msg.get("id", "")) if isinstance(msg, dict) else ""
+
+ if not passed:
+ # 尚未越过续传点:命中该 id 后,从下一条开始发
+ if msg_id and msg_id == start_id:
+ passed = True
+ continue
+
+ yield self._sse(msg_id, json.dumps(msg, ensure_ascii=False))
+
+ # 续传点在库里没匹配到(比如 id 体系不一致):退化为整段重放,别让客户端收到空流
+ if not passed:
+ for msg in messages:
+ msg_id = str(msg.get("id", "")) if isinstance(msg, dict) else ""
+ yield self._sse(msg_id, json.dumps(msg, ensure_ascii=False))
+ finally:
+ yield "data: [DONE]\n\n"
+
+
class OpenChatSerializers(serializers.Serializer):
workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID"))
application_id = serializers.UUIDField(required=True)
@@ -540,64 +1019,81 @@ class OpenChatSerializers(serializers.Serializer):
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- workspace_id = self.data.get('workspace_id')
- application_id = self.data.get('application_id')
+ workspace_id = self.data.get("workspace_id")
+ application_id = self.data.get("application_id")
query_set = QuerySet(Application).filter(id=application_id)
if workspace_id:
query_set = query_set.filter(workspace_id=workspace_id)
if not query_set.exists():
- raise AppApiException(500, gettext('Application does not exist'))
+ raise AppApiException(500, gettext("Application does not exist"))
- def open(self):
+ def open(self, chat_id=None):
self.is_valid(raise_exception=True)
- application_id = self.data.get('application_id')
+ application_id = self.data.get("application_id")
application = QuerySet(Application).get(id=application_id)
debug = self.data.get("debug")
if not debug:
- application_version = QuerySet(ApplicationVersion).filter(application_id=application_id).order_by(
- '-create_time')[0:1].first()
+ application_version = (
+ QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
+ )
if application_version is None:
- raise AppApiException(500,
- _("The application has not been published. Please use it after publishing."))
+ raise AppApiException(500, _("The application has not been published. Please use it after publishing."))
if application.type == ApplicationTypeChoices.SIMPLE:
- return self.open_simple(application)
+ return self.open_simple(application, chat_id)
else:
- return self.open_work_flow(application)
+ return self.open_work_flow(application, chat_id)
- def open_work_flow(self, application):
+ def open_work_flow(self, application, chat_id=None):
self.is_valid(raise_exception=True)
- application_id = self.data.get('application_id')
+ application_id = self.data.get("application_id")
chat_user_id = self.data.get("chat_user_id")
chat_user_type = self.data.get("chat_user_type")
ip_address = self.data.get("ip_address")
source = self.data.get("source")
debug = self.data.get("debug")
- chat_id = str(uuid.uuid7())
- ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [],
- [],
- application_id, debug).set_cache()
+ chat_id = chat_id or str(uuid.uuid7())
+ chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], [], application_id, debug)
+ chat_info.save_chat()
+ chat_info.set_cache()
return chat_id
- def open_simple(self, application):
- application_id = self.data.get('application_id')
+ def open_simple(self, application, chat_id=None):
+ application_id = self.data.get("application_id")
chat_user_id = self.data.get("chat_user_id")
chat_user_type = self.data.get("chat_user_type")
ip_address = self.data.get("ip_address")
source = self.data.get("source")
debug = self.data.get("debug")
- knowledge_id_list = [str(row.target_id) for row in
- QuerySet(ResourceMapping).filter(source_id=str(application_id),
- source_type='APPLICATION',
- target_type='KNOWLEDGE')]
-
- chat_id = str(uuid.uuid7())
- ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, knowledge_id_list,
- [str(document.id) for document in
- QuerySet(Document).filter(
- knowledge_id__in=knowledge_id_list,
- is_active=False)],
- application_id,
- debug=debug).set_cache()
+ if debug:
+ knowledge_id_list = [
+ str(row.target_id)
+ for row in QuerySet(ResourceMapping).filter(
+ source_id=str(application_id), source_type="APPLICATION", target_type="KNOWLEDGE"
+ )
+ ]
+ else:
+ application_version = (
+ QuerySet(ApplicationVersion).filter(application_id=application_id).order_by("-create_time")[0:1].first()
+ )
+ knowledge_id_list = application_version.knowledge_ids
+
+ chat_id = chat_id or str(uuid.uuid7())
+ chat_info = ChatInfo(
+ chat_id,
+ chat_user_id,
+ chat_user_type,
+ ip_address,
+ source,
+ knowledge_id_list,
+ [
+ str(document.id)
+ for document in QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)
+ ],
+ application_id,
+ debug=debug,
+ )
+ chat_info.save_chat()
+ chat_info.set_cache()
return chat_id
@@ -606,11 +1102,11 @@ class TextToSpeechSerializers(serializers.Serializer):
def text_to_speech(self, instance):
self.is_valid(raise_exception=True)
- application_id = self.data.get('application_id')
+ application_id = self.data.get("application_id")
application = QuerySet(Application).filter(id=application_id).first()
return ApplicationOperateSerializer(
- data={'application_id': application_id,
- 'user_id': application.user_id}).text_to_speech(instance, False)
+ data={"application_id": application_id, "user_id": application.user_id}
+ ).text_to_speech(instance, False)
class SpeechToTextSerializers(serializers.Serializer):
@@ -618,8 +1114,8 @@ class SpeechToTextSerializers(serializers.Serializer):
def speech_to_text(self, instance):
self.is_valid(raise_exception=True)
- application_id = self.data.get('application_id')
+ application_id = self.data.get("application_id")
application = QuerySet(Application).filter(id=application_id).first()
return ApplicationOperateSerializer(
- data={'application_id': application_id,
- 'user_id': application.user_id}).speech_to_text(instance, False)
+ data={"application_id": application_id, "user_id": application.user_id}
+ ).speech_to_text(instance, False)
diff --git a/apps/chat/serializers/chat_authentication.py b/apps/chat/serializers/chat_authentication.py
index b6c801b4a9c..e3a45bf6381 100644
--- a/apps/chat/serializers/chat_authentication.py
+++ b/apps/chat/serializers/chat_authentication.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: ChatAuthentication.py
- @date:2025/6/6 13:48
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: ChatAuthentication.py
+@date:2025/6/6 13:48
+@desc:
"""
+
import uuid_utils.compat as uuid
from django.core import signing
from django.core.cache import cache
@@ -13,80 +14,123 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
-from application.models import ApplicationAccessToken, ChatUserType, Application, ApplicationVersion
-from application.serializers.application import ApplicationSerializerModel
-from common.auth.common import ChatUserToken, ChatAuthentication
+from application.models import ApplicationAccessToken, Application, ApplicationVersion
+from common.auth.common import FileToken, ChatToken
+from common.auth.constants.operate_constants import Operate
from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version
from common.database_model_manage.database_model_manage import DatabaseModelManage
-from common.exception.app_exception import NotFound404, AppUnauthorizedFailed
+from common.exception.app_exception import NotFound404, AppUnauthorizedFailed, AppApiException
from common.utils.rsa_util import get_key_pair_by_sql
class AnonymousAuthenticationSerializer(serializers.Serializer):
+ """v3 匿名认证:application_id 为可选 query 参数。
+ 传入时颁发应用级令牌,未传入时颁发全局令牌。"""
+
+ application_id = serializers.UUIDField(required=False, label=_("application_id"))
+
+ def auth(self, request):
+ token = request.META.get("HTTP_AUTHORIZATION")
+ token_details = {}
+ try:
+ # 校验token
+ if token is not None:
+ token_details = signing.loads(token[7:])
+ except Exception:
+ pass
+ chat_user_id = token_details.get("id") or str(uuid.uuid7())
+ _type = AuthenticationType.CHAT_USER
+
+ application_id = self.validated_data.get("application_id")
+ if application_id:
+ application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
+ if application_access_token is None or not application_access_token.is_active:
+ raise AppApiException(500, _("Invalid application_id"))
+ application_id = str(application_id)
+ return (
+ ChatToken(chat_user_id, _type, str(Operate.ANNOTATION_AUTH), application_id=application_id).to_token(),
+ FileToken(chat_user_id, _type, application_id=application_id).to_token(),
+ )
+ return (
+ ChatToken(chat_user_id, _type, str(Operate.ANNOTATION_AUTH)).to_token(),
+ FileToken(chat_user_id, _type).to_token(),
+ )
+
+
+class AnonymousAuthenticationV2Serializer(serializers.Serializer):
+ """v2 匿名认证:application_id 不在 path,从 access_token 解出并写进 token,
+ 供 ChatUserToken handler 收窄到该应用。"""
+
access_token = serializers.CharField(required=True, label=_("access_token"))
def auth(self, request, with_valid=True):
- token = request.META.get('HTTP_AUTHORIZATION')
+ token = request.META.get("HTTP_AUTHORIZATION")
token_details = {}
try:
# 校验token
if token is not None:
token_details = signing.loads(token[7:])
- except Exception as e:
+ except Exception:
pass
if with_valid:
self.is_valid(raise_exception=True)
access_token = self.data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
- if application_access_token is not None and application_access_token.is_active:
- chat_user_id = token_details.get('chat_user_id') or str(uuid.uuid7())
- _type = AuthenticationType.CHAT_ANONYMOUS_USER
- return ChatUserToken(application_access_token.application_id, None, access_token, _type,
- ChatUserType.ANONYMOUS_USER,
- chat_user_id, ChatAuthentication(None)).to_token()
- else:
+ if application_access_token is None or not application_access_token.is_active:
raise NotFound404(404, _("Invalid access_token"))
+ chat_user_id = token_details.get("user_id") or token_details.get("id") or str(uuid.uuid7())
+ _type = AuthenticationType.CHAT_USER
+ application_id = str(application_access_token.application_id)
+ return ChatToken(
+ chat_user_id, _type, str(Operate.ANNOTATION_AUTH), application_id=application_id
+ ).to_token(), FileToken(chat_user_id, _type, application_id=application_id).to_token()
class AuthProfileSerializer(serializers.Serializer):
+ """v3: 直接通过 application_id 获取认证 profile"""
+
+ application_id = serializers.UUIDField(required=True, label=_("application_id"))
+
+ def profile(self):
+ self.is_valid(raise_exception=True)
+ application_id = self.validated_data.get("application_id")
+ application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
+ if application_access_token is None:
+ raise NotFound404(404, _("Invalid application_id"))
+ if not application_access_token.is_active:
+ raise NotFound404(404, _("Invalid application_id"))
+ login_value = application_access_token.authentication_value.get("login_value", [])
+ chat_platform = DatabaseModelManage.get_model("chat_platform")
+ if chat_platform is not None:
+ types = QuerySet(chat_platform).filter(is_active=True, is_valid=True).values_list("auth_type", flat=True)
+ login_value = list(set(login_value) & set(types))
+ if "LOCAL" in application_access_token.authentication_value.get("login_value", []):
+ login_value.insert(0, "LOCAL")
+ return {
+ "application_name": application_access_token.application.name,
+ "authentication": application_access_token.authentication,
+ "authentication_type": application_access_token.authentication_value.get("type", "password"),
+ "max_attempts": application_access_token.authentication_value.get("max_attempts", 1),
+ "login_value": login_value,
+ "rsaKey": get_key_pair_by_sql().get("key"),
+ }
+
+
+class AuthProfileV2Serializer(serializers.Serializer):
+ """v2: 通过 access_token 查表得到 application_id,委托给 AuthProfileSerializer"""
+
access_token = serializers.CharField(required=True, label=_("access_token"))
def profile(self):
self.is_valid(raise_exception=True)
- access_token = self.data.get("access_token")
+ access_token = self.validated_data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
if application_access_token is None:
raise NotFound404(404, _("Invalid access_token"))
if not application_access_token.is_active:
raise NotFound404(404, _("Invalid access_token"))
- application_id = application_access_token.application_id
- profile = {
- 'authentication': False
- }
- application_setting_model = DatabaseModelManage.get_model('application_setting')
- chat_platform = DatabaseModelManage.get_model('chat_platform')
- if application_setting_model and chat_platform:
- application_setting = QuerySet(application_setting_model).filter(application_id=application_id).first()
- types = QuerySet(chat_platform).filter(is_active=True, is_valid=True).values_list('auth_type', flat=True)
- login_value = application_access_token.authentication_value.get('login_value', [])
- max_attempts = application_access_token.authentication_value.get('max_attempts', 1)
- final_login_value = list(set(login_value) & set(types))
- if 'LOCAL' in login_value:
- final_login_value.insert(0, 'LOCAL')
- if application_setting is not None:
- profile = {
- 'icon': application_setting.application.icon,
- 'application_name': application_setting.application.name,
- 'bg_icon': application_setting.chat_background,
- 'authentication': application_access_token.authentication,
- 'authentication_type': application_access_token.authentication_value.get(
- 'type', 'password'),
- 'max_attempts': max_attempts,
- 'login_value': final_login_value,
- 'rsaKey' : get_key_pair_by_sql().get('key')
- }
- return profile
+ return AuthProfileSerializer(data={"application_id": application_access_token.application_id}).profile()
class ApplicationProfileSerializer(serializers.Serializer):
@@ -95,18 +139,30 @@ class ApplicationProfileSerializer(serializers.Serializer):
@staticmethod
def reset_application(application, application_version):
update_field_dict = {
- 'application_name': 'name', 'desc': 'desc', 'prologue': 'prologue', 'dialogue_number': 'dialogue_number',
- 'user_id': 'user_id', 'model_id': 'model_id', 'knowledge_setting': 'knowledge_setting',
- 'model_setting': 'model_setting', 'model_params_setting': 'model_params_setting',
- 'tts_model_params_setting': 'tts_model_params_setting',
- 'problem_optimization': 'problem_optimization', 'work_flow': 'work_flow',
- 'problem_optimization_prompt': 'problem_optimization_prompt', 'tts_model_id': 'tts_model_id',
- 'stt_model_id': 'stt_model_id', 'tts_model_enable': 'tts_model_enable',
- 'stt_model_enable': 'stt_model_enable', 'tts_type': 'tts_type',
- 'tts_autoplay': 'tts_autoplay', 'stt_autosend': 'stt_autosend', 'file_upload_enable': 'file_upload_enable',
- 'file_upload_setting': 'file_upload_setting'
+ "application_name": "name",
+ "desc": "desc",
+ "prologue": "prologue",
+ "dialogue_number": "dialogue_number",
+ "user_id": "user_id",
+ "model_id": "model_id",
+ "knowledge_setting": "knowledge_setting",
+ "model_setting": "model_setting",
+ "model_params_setting": "model_params_setting",
+ "tts_model_params_setting": "tts_model_params_setting",
+ "problem_optimization": "problem_optimization",
+ "work_flow": "work_flow",
+ "problem_optimization_prompt": "problem_optimization_prompt",
+ "tts_model_id": "tts_model_id",
+ "stt_model_id": "stt_model_id",
+ "tts_model_enable": "tts_model_enable",
+ "stt_model_enable": "stt_model_enable",
+ "tts_type": "tts_type",
+ "tts_autoplay": "tts_autoplay",
+ "stt_autosend": "stt_autosend",
+ "file_upload_enable": "file_upload_enable",
+ "file_upload_setting": "file_upload_setting",
}
- for (version_field, app_field) in update_field_dict.items():
+ for version_field, app_field in update_field_dict.items():
_v = getattr(application_version, version_field)
setattr(application, app_field, _v)
@@ -118,60 +174,74 @@ def profile(self, with_valid=True):
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application.id).first()
if application_access_token is None:
raise AppUnauthorizedFailed(500, _("Illegal User"))
- application_setting_model = DatabaseModelManage.get_model('application_setting')
- application_version = QuerySet(ApplicationVersion).filter(application_id=application.id).order_by(
- '-create_time').first()
+ application_setting_model = DatabaseModelManage.get_model("application_setting")
+ application_version = (
+ QuerySet(ApplicationVersion).filter(application_id=application.id).order_by("-create_time").first()
+ )
if application_version is not None:
self.reset_application(application, application_version)
- license_is_valid = cache.get(Cache_Version.SYSTEM.get_key(key='license_is_valid'),
- version=Cache_Version.SYSTEM.get_version())
+ license_is_valid = cache.get(
+ Cache_Version.SYSTEM.get_key(key="license_is_valid"), version=Cache_Version.SYSTEM.get_version()
+ )
application_setting_dict = {}
if application_setting_model is not None and license_is_valid:
- application_setting = QuerySet(application_setting_model).filter(
- application_id=application_access_token.application_id).first()
+ application_setting = (
+ QuerySet(application_setting_model)
+ .filter(application_id=application_access_token.application_id)
+ .first()
+ )
if application_setting is not None:
- custom_theme = getattr(application_setting, 'custom_theme', {})
- float_location = getattr(application_setting, 'float_location', {})
+ custom_theme = getattr(application_setting, "custom_theme", {})
+ float_location = getattr(application_setting, "float_location", {})
if not custom_theme:
- application_setting.custom_theme = {
- 'theme_color': '',
- 'header_font_color': ''
- }
+ application_setting.custom_theme = {"theme_color": "", "header_font_color": ""}
if not float_location:
application_setting.float_location = {
- 'x': {'type': '', 'value': ''},
- 'y': {'type': '', 'value': ''}
+ "x": {"type": "", "value": ""},
+ "y": {"type": "", "value": ""},
}
- application_setting_dict = {'show_source': application_access_token.show_source,
- 'show_history': application_setting.show_history,
- 'draggable': application_setting.draggable,
- 'show_guide': application_setting.show_guide,
- 'avatar': application_setting.avatar,
- 'show_avatar': application_setting.show_avatar,
- 'float_icon': application_setting.float_icon,
- 'disclaimer': application_setting.disclaimer,
- 'disclaimer_value': application_setting.disclaimer_value,
- 'custom_theme': application_setting.custom_theme,
- 'user_avatar': application_setting.user_avatar,
- 'show_user_avatar': application_setting.show_user_avatar,
- 'show_share': application_setting.show_share,
- 'float_location': application_setting.float_location,
- 'chat_background': application_setting.chat_background}
- base_node = [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
- node.get('id') == 'base-node']
- return {**ApplicationSerializerModel(application).data,
- 'stt_model_id': application.stt_model_id,
- 'tts_model_id': application.tts_model_id,
- 'stt_model_enable': application.stt_model_enable,
- 'tts_model_enable': application.tts_model_enable,
- 'tts_type': application.tts_type,
- 'tts_autoplay': application.tts_autoplay,
- 'stt_autosend': application.stt_autosend,
- 'file_upload_enable': application.file_upload_enable,
- 'file_upload_setting': application.file_upload_setting,
- 'work_flow': {'nodes': base_node} if base_node else None,
- 'show_source': application_access_token.show_source,
- 'show_exec': application_access_token.show_exec,
- 'show_share': True,
- 'language': application_access_token.language,
- **application_setting_dict}
+ application_setting_dict = {
+ "show_source": application_access_token.show_source,
+ "show_history": application_setting.show_history,
+ "draggable": application_setting.draggable,
+ "show_guide": application_setting.show_guide,
+ "avatar": application_setting.avatar,
+ "show_avatar": application_setting.show_avatar,
+ "float_icon": application_setting.float_icon,
+ "disclaimer": application_setting.disclaimer,
+ "disclaimer_value": application_setting.disclaimer_value,
+ "custom_theme": application_setting.custom_theme,
+ "user_avatar": application_setting.user_avatar,
+ "show_user_avatar": application_setting.show_user_avatar,
+ "show_share": application_setting.show_share,
+ "float_location": application_setting.float_location,
+ "chat_background": application_setting.chat_background,
+ }
+ base_node = [
+ node for node in ((application.work_flow or {}).get("nodes", []) or []) if node.get("id") == "base-node"
+ ]
+ return {
+ "id": application.id,
+ "name": application.name,
+ "desc": application.desc,
+ "prologue": application.prologue,
+ "icon": application.icon,
+ "type": application.type,
+ "dialogue_number": application.dialogue_number,
+ "problem_optimization": application.problem_optimization,
+ "stt_model_id": application.stt_model_id,
+ "tts_model_id": application.tts_model_id,
+ "stt_model_enable": application.stt_model_enable,
+ "tts_model_enable": application.tts_model_enable,
+ "tts_type": application.tts_type,
+ "tts_autoplay": application.tts_autoplay,
+ "stt_autosend": application.stt_autosend,
+ "file_upload_enable": application.file_upload_enable,
+ "file_upload_setting": application.file_upload_setting,
+ "work_flow": {"nodes": base_node} if base_node else None,
+ "show_source": application_access_token.show_source,
+ "show_exec": application_access_token.show_exec,
+ "show_share": True,
+ "language": application_access_token.language,
+ **application_setting_dict,
+ }
diff --git a/apps/chat/serializers/chat_record.py b/apps/chat/serializers/chat_record.py
index ee6d446dd57..de0db6fcf77 100644
--- a/apps/chat/serializers/chat_record.py
+++ b/apps/chat/serializers/chat_record.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: chat_record.py
- @date:2025/6/23 11:16
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: chat_record.py
+@date:2025/6/23 11:16
+@desc:
"""
+
from typing import Dict
from django.db import transaction
@@ -15,18 +16,20 @@
from application.models import VoteChoices, ChatRecord, Chat, ApplicationAccessToken, VoteReasonChoices
from application.serializers.application_chat import ChatCountSerializer
-from application.serializers.application_chat_record import ChatRecordSerializerModel, \
- ApplicationChatRecordQuerySerializers
+from application.serializers.application_chat_record import (
+ ChatRecordSerializerModel,
+ ApplicationChatRecordQuerySerializers,
+)
from common.db.search import page_search
from common.exception.app_exception import AppApiException
from common.utils.lock import RedisLock
class VoteRequest(serializers.Serializer):
- vote_status = serializers.ChoiceField(choices=VoteChoices.choices,
- label=_("Bidding Status"))
- vote_reason = serializers.ChoiceField(choices=VoteReasonChoices.choices, label=_("Vote Reason"), required=False,
- allow_null=True)
+ vote_status = serializers.ChoiceField(choices=VoteChoices.choices, label=_("Bidding Status"))
+ vote_reason = serializers.ChoiceField(
+ choices=VoteReasonChoices.choices, label=_("Vote Reason"), required=False, allow_null=True
+ )
vote_other_content = serializers.CharField(required=False, allow_blank=True, label=_("Vote other content"))
@@ -34,18 +37,14 @@ class VoteRequest(serializers.Serializer):
class HistoryChatModel(serializers.ModelSerializer):
class Meta:
model = Chat
- fields = ['id',
- 'application_id',
- 'abstract',
- 'create_time',
- 'update_time']
+ fields = ["id", "application_id", "abstract", "create_time", "update_time"]
class VoteSerializer(serializers.Serializer):
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
- chat_record_id = serializers.UUIDField(required=True,
- label=_("Conversation record id"))
+ chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id"))
@transaction.atomic
def vote(self, instance: Dict, with_valid=True):
@@ -53,13 +52,16 @@ def vote(self, instance: Dict, with_valid=True):
self.is_valid(raise_exception=True)
VoteRequest(data=instance).is_valid(raise_exception=True)
rlock = RedisLock()
- if not rlock.try_lock(self.data.get('chat_record_id')):
- raise AppApiException(500,
- gettext(
- "Voting on the current session minutes, please do not send repeated requests"))
+ if not rlock.try_lock(self.data.get("chat_record_id")):
+ raise AppApiException(
+ 500, gettext("Voting on the current session minutes, please do not send repeated requests")
+ )
try:
- chat_record_details_model = QuerySet(ChatRecord).get(id=self.data.get('chat_record_id'),
- chat_id=self.data.get('chat_id'))
+ chat_record_details_model = QuerySet(ChatRecord).get(
+ id=self.data.get("chat_record_id"),
+ chat_id=self.data.get("chat_id"),
+ chat__application_id=self.data.get("application_id"),
+ )
if chat_record_details_model is None:
raise AppApiException(500, gettext("Non-existent conversation chat_record_id"))
vote_status = instance.get("vote_status")
@@ -68,7 +70,7 @@ def vote(self, instance: Dict, with_valid=True):
if chat_record_details_model.vote_status == VoteChoices.UN_VOTE:
# 投票时获取字段
vote_reason = instance.get("vote_reason")
- vote_other_content = instance.get("vote_other_content") or ''
+ vote_other_content = instance.get("vote_other_content") or ""
if vote_status == VoteChoices.STAR:
# 点赞
@@ -88,25 +90,28 @@ def vote(self, instance: Dict, with_valid=True):
# 取消点赞
chat_record_details_model.vote_status = VoteChoices.UN_VOTE
chat_record_details_model.vote_reason = None
- chat_record_details_model.vote_other_content = ''
+ chat_record_details_model.vote_other_content = ""
chat_record_details_model.save()
else:
raise AppApiException(500, gettext("Already voted, please cancel first and then vote again"))
finally:
- rlock.un_lock(self.data.get('chat_record_id'))
- ChatCountSerializer(data={'chat_id': self.data.get('chat_id')}).update_chat()
+ rlock.un_lock(self.data.get("chat_record_id"))
+ ChatCountSerializer(data={"chat_id": self.data.get("chat_id")}).update_chat()
return True
class HistoricalConversationSerializer(serializers.Serializer):
- application_id = serializers.UUIDField(required=True, label=_('Application ID'))
- chat_user_id = serializers.UUIDField(required=True, label=_('Chat User ID'))
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
+ chat_user_id = serializers.UUIDField(required=True, label=_("Chat User ID"))
def get_queryset(self):
- chat_user_id = self.data.get('chat_user_id')
+ chat_user_id = self.data.get("chat_user_id")
application_id = self.data.get("application_id")
- return QuerySet(Chat).filter(application_id=application_id, chat_user_id=chat_user_id,
- is_deleted=False).order_by('-update_time', 'id')
+ return (
+ QuerySet(Chat)
+ .filter(application_id=application_id, chat_user_id=chat_user_id, is_deleted=False)
+ .order_by("-update_time", "id")
+ )
def list(self):
self.is_valid(raise_exception=True)
@@ -119,67 +124,90 @@ def page(self, current_page, page_size):
class EditAbstractSerializer(serializers.Serializer):
- abstract = serializers.CharField(required=True, label=_('Abstract'))
+ abstract = serializers.CharField(required=True, label=_("Abstract"))
class HistoricalConversationOperateSerializer(serializers.Serializer):
- application_id = serializers.UUIDField(required=True, label=_('Application ID'))
- chat_user_id = serializers.UUIDField(required=True, label=_('Chat User ID'))
- chat_id = serializers.UUIDField(required=True, label=_('Chat ID'))
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
+ chat_user_id = serializers.UUIDField(required=True, label=_("Chat User ID"))
+ chat_id = serializers.UUIDField(required=True, label=_("Chat ID"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- e = QuerySet(Chat).filter(id=self.data.get('chat_id'), application_id=self.data.get('application_id'),
- chat_user_id=self.data.get('chat_user_id')).exists()
+ e = (
+ QuerySet(Chat)
+ .filter(
+ id=self.data.get("chat_id"),
+ application_id=self.data.get("application_id"),
+ chat_user_id=self.data.get("chat_user_id"),
+ )
+ .exists()
+ )
if not e:
- raise AppApiException(500, _('Chat is not exist'))
+ raise AppApiException(500, _("Chat is not exist"))
def edit_abstract(self, instance, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
EditAbstractSerializer(data=instance).is_valid(raise_exception=True)
- QuerySet(Chat).filter(id=self.data.get('chat_id'), application_id=self.data.get('application_id'),
- chat_user_id=self.data.get('chat_user_id')).update(abstract=instance.get('abstract'))
+ chat = (
+ QuerySet(Chat)
+ .filter(
+ id=self.data.get("chat_id"),
+ application_id=self.data.get("application_id"),
+ chat_user_id=self.data.get("chat_user_id"),
+ )
+ .first()
+ )
+ if chat.is_deleted:
+ raise AppApiException(500, _("Chat has been deleted"))
+ chat.abstract = instance.get("abstract")
+ chat.save()
return True
def logic_delete(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- QuerySet(Chat).filter(id=self.data.get('chat_id'), application_id=self.data.get('application_id'),
- chat_user_id=self.data.get('chat_user_id')).update(is_deleted=True)
+ QuerySet(Chat).filter(
+ id=self.data.get("chat_id"),
+ application_id=self.data.get("application_id"),
+ chat_user_id=self.data.get("chat_user_id"),
+ ).update(is_deleted=True)
return True
class Clear(serializers.Serializer):
- application_id = serializers.UUIDField(required=True, label=_('Application ID'))
- chat_user_id = serializers.UUIDField(required=True, label=_('Chat User ID'))
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
+ chat_user_id = serializers.UUIDField(required=True, label=_("Chat User ID"))
def batch_logic_delete(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
- QuerySet(Chat).filter(application_id=self.data.get('application_id'),
- chat_user_id=self.data.get('chat_user_id')).update(is_deleted=True)
+ QuerySet(Chat).filter(
+ application_id=self.data.get("application_id"), chat_user_id=self.data.get("chat_user_id")
+ ).update(is_deleted=True)
return True
class HistoricalConversationRecordSerializer(serializers.Serializer):
- application_id = serializers.UUIDField(required=True, label=_('Application ID'))
- chat_id = serializers.UUIDField(required=True, label=_('Chat ID'))
- chat_user_id = serializers.UUIDField(required=True, label=_('Chat User ID'))
+ application_id = serializers.UUIDField(required=True, label=_("Application ID"))
+ chat_id = serializers.UUIDField(required=True, label=_("Chat ID"))
+ chat_user_id = serializers.UUIDField(required=True, label=_("Chat User ID"))
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
- chat_user_id = self.data.get('chat_user_id')
+ chat_user_id = self.data.get("chat_user_id")
application_id = self.data.get("application_id")
- chat_id = self.data.get('chat_id')
- chat_exist = QuerySet(Chat).filter(application_id=application_id, chat_user_id=chat_user_id,
- id=chat_id).exists()
+ chat_id = self.data.get("chat_id")
+ chat_exist = (
+ QuerySet(Chat).filter(application_id=application_id, chat_user_id=chat_user_id, id=chat_id).exists()
+ )
if not chat_exist:
- raise AppApiException(500, _('Non-existent chatID'))
+ raise AppApiException(500, _("Non-existent chatID"))
def get_queryset(self):
- chat_id = self.data.get('chat_id')
- return QuerySet(ChatRecord).filter(chat_id=chat_id).order_by('-create_time')
+ chat_id = self.data.get("chat_id")
+ return QuerySet(ChatRecord).filter(chat_id=chat_id).order_by("-create_time")
def list(self):
self.is_valid(raise_exception=True)
@@ -188,13 +216,14 @@ def list(self):
def page(self, current_page, page_size):
self.is_valid(raise_exception=True)
- application_access_token = QuerySet(ApplicationAccessToken).filter(
- application_id=self.data.get('application_id')).first()
+ application_access_token = (
+ QuerySet(ApplicationAccessToken).filter(application_id=self.data.get("application_id")).first()
+ )
show_source = False
show_exec = False
if application_access_token is not None:
show_exec = application_access_token.show_exec
show_source = application_access_token.show_source
return ApplicationChatRecordQuerySerializers(
- data={'application_id': self.data.get('application_id'), 'chat_id': self.data.get('chat_id')}).page(
- current_page, page_size, show_source=show_source, show_exec=show_exec)
+ data={"application_id": self.data.get("application_id"), "chat_id": self.data.get("chat_id")}
+ ).page(current_page, page_size, show_source=show_source, show_exec=show_exec)
diff --git a/apps/chat/serializers/chat_user_api_key_serializers.py b/apps/chat/serializers/chat_user_api_key_serializers.py
new file mode 100644
index 00000000000..ab0d7bce1f5
--- /dev/null
+++ b/apps/chat/serializers/chat_user_api_key_serializers.py
@@ -0,0 +1,56 @@
+# coding=utf-8
+
+import hashlib
+
+import uuid_utils.compat as uuid
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from common.db.search import page_search
+from system_manage.models import ChatUserApiKey
+
+
+class ChatUserApiKeyModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChatUserApiKey
+ fields = ['id', 'secret_key', 'is_active', 'create_time', 'user_id']
+
+
+class ChatUserApiKeySerializer(serializers.Serializer):
+ user_id = serializers.UUIDField(required=True, label=_('user id'))
+ order_by = serializers.CharField(required=False, label=_('order by'), allow_null=True, allow_blank=True)
+
+ def generate(self, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ api_key = ChatUserApiKey(
+ id=uuid.uuid7(),
+ secret_key=hashlib.md5(uuid.uuid7().bytes).hexdigest(),
+ user_id=self.data.get('user_id')
+ )
+ api_key.save()
+ return ChatUserApiKeyModelSerializer(api_key).data
+
+ def page(self, current_page: int, page_size: int, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ user_id = self.data.get('user_id')
+ query_set = QuerySet(ChatUserApiKey).filter(user_id=user_id)
+ order_by = '-create_time' if self.data.get('order_by') is None or self.data.get('order_by') == '' else self.data.get('order_by')
+ query_set = query_set.order_by(order_by)
+ return page_search(current_page, page_size,
+ query_set,
+ post_records_handler=lambda u: ChatUserApiKeyModelSerializer(u).data)
+
+ class Operate(serializers.Serializer):
+ id = serializers.UUIDField(required=True, label=_('api key id'))
+ user_id = serializers.UUIDField(required=True, label=_('user id'))
+
+ def destroy(self, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ QuerySet(ChatUserApiKey).filter(
+ id=self.data.get('id'), user_id=self.data.get('user_id')
+ ).delete()
+ return True
\ No newline at end of file
diff --git a/apps/chat/serializers/chat_user_serializer.py b/apps/chat/serializers/chat_user_serializer.py
new file mode 100644
index 00000000000..634029f3b7b
--- /dev/null
+++ b/apps/chat/serializers/chat_user_serializer.py
@@ -0,0 +1,88 @@
+import json
+
+from django.core.cache import cache
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.models import ApplicationAccessToken
+from common.constants.cache_version import Cache_Version
+from common.exception.app_exception import AppApiException
+from common.utils.common import password_encrypt
+from common.utils.common import password_verify, needs_password_upgrade
+from common.utils.rsa_util import decrypt
+from system_manage.models import ChatUser
+from users.serializers.login import LoginRequest
+
+system_version, system_get_key = Cache_Version.SYSTEM.value
+
+
+class ChatUserAccessTokenV3Serializer(serializers.Serializer):
+ @staticmethod
+ def get_auth_setting():
+ application_access_token = ApplicationAccessToken.objects.filter(is_active=True).first()
+
+ if not application_access_token:
+ raise AppApiException(1005, _("Invalid access token"))
+
+ return application_access_token.authentication_value
+
+ @staticmethod
+ def local_login(instance):
+ username = instance.get("username", "")
+ encryptedData = instance.get("encryptedData", "")
+ if encryptedData:
+ json_data = json.loads(decrypt(encryptedData))
+ instance.update(json_data)
+ try:
+ LoginRequest(data=instance).is_valid(raise_exception=True)
+ except Exception as e:
+ raise e
+ auth_setting = ChatUserAccessTokenV3Serializer.get_auth_setting()
+
+ max_attempts = auth_setting.get("max_attempts", 1)
+ password = instance.get("password")
+ captcha = instance.get("captcha", "")
+
+ # 判断是否需要验证码
+ need_captcha = True
+ if max_attempts == -1:
+ need_captcha = False
+ elif max_attempts > 0:
+ fail_count = cache.get(system_get_key(f"chat_{username}"), version=system_version) or 0
+ need_captcha = fail_count >= max_attempts
+
+ if need_captcha:
+ if not captcha:
+ raise AppApiException(1005, _("Captcha is required"))
+
+ captcha_cache = cache.get(
+ Cache_Version.CAPTCHA.get_key(captcha=f"chat_{username}"), version=Cache_Version.CAPTCHA.get_version()
+ )
+ if captcha_cache is None or captcha.lower() != captcha_cache:
+ raise AppApiException(1005, _("Captcha code error or expiration"))
+
+ user = ChatUser.objects.filter(username=username).first()
+
+ if not user or not password_verify(password, user.password):
+ record_login_fail(username)
+ raise AppApiException(500, _("The username or password is incorrect"))
+
+ if needs_password_upgrade(user.password):
+ user.password = password_encrypt(password)
+ user.save(update_fields=["password"])
+ if not user.is_active:
+ raise AppApiException(1005, _("The user has been disabled, please contact the administrator!"))
+ cache.delete(system_get_key(f"chat_{username}"), version=system_version)
+ return user
+
+
+def record_login_fail(username: str, expire: int = 600):
+ """记录登录失败次数"""
+ if not username:
+ return
+ fail_key = system_get_key(f"chat_{username}")
+ fail_count = cache.get(fail_key, version=system_version)
+ if fail_count is None:
+ cache.set(fail_key, 1, timeout=expire, version=system_version)
+ else:
+ cache.incr(fail_key, 1, version=system_version)
diff --git a/apps/chat/serializers/portal.py b/apps/chat/serializers/portal.py
new file mode 100644
index 00000000000..c58d3450e6e
--- /dev/null
+++ b/apps/chat/serializers/portal.py
@@ -0,0 +1,128 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:MaxKB
+@file: portal.py
+@date:2026/8/14
+@desc: 门户配置序列化器
+"""
+
+from django.db.models import Exists, OuterRef
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from application.models import Application, Chat
+from application.models.application_access_token import ApplicationAccessToken
+from common.db.search import page_search
+from system_manage.models.chat_user import (
+ ChatUser,
+ ResourceChatUserAuthorize,
+ ResourceChatUserGroupAuthorize,
+ ResourceType,
+ UserGroupRelation,
+)
+
+
+class PortalApplicationAuthMixin:
+ """门户应用授权过滤公共逻辑"""
+
+ @staticmethod
+ def get_authorized_application_queryset(user_id):
+ public_apps = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=False)
+ if not ChatUser.objects.filter(id=user_id).exists():
+ return Application.objects.filter(is_publish=True, is_portal=True).filter(Exists(public_apps))
+ authed_token_exists = ApplicationAccessToken.objects.filter(application_id=OuterRef("id"), authentication=True)
+ direct_auth = ResourceChatUserAuthorize.objects.filter(
+ resource_id=OuterRef("id"), resource_type=ResourceType.APPLICATION.value, is_auth=True, user_id=user_id
+ )
+ user_groups = UserGroupRelation.objects.filter(user_id=user_id).values_list("group_id", flat=True)
+ group_auth = ResourceChatUserGroupAuthorize.objects.filter(
+ resource_id=OuterRef("id"),
+ resource_type=ResourceType.APPLICATION.value,
+ is_auth=True,
+ user_group_id__in=user_groups,
+ )
+ return Application.objects.filter(is_publish=True, is_portal=True).filter(
+ Exists(public_apps) | (Exists(authed_token_exists) & (Exists(direct_auth) | Exists(group_auth)))
+ )
+
+
+class ApplicationResponseSerializer(serializers.Serializer):
+ id = serializers.CharField(required=True)
+ name = serializers.CharField(required=True)
+ desc = serializers.CharField(required=True)
+ icon = serializers.CharField(required=True)
+ type = serializers.CharField(required=True)
+ dialogue_number = serializers.IntegerField(required=True)
+ prologue = serializers.CharField(required=True)
+ is_publish = serializers.BooleanField(required=True)
+ is_portal = serializers.BooleanField(required=True)
+
+
+class PortalApplicationSerializer(serializers.Serializer):
+ class Query(PortalApplicationAuthMixin, serializers.Serializer):
+ name = serializers.CharField(
+ required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
+ )
+
+ def get_query_set(self):
+ queryset = Application.objects.filter(is_publish=True, is_portal=True)
+ name = self.data.get("name")
+ if name:
+ queryset = queryset.filter(name__icontains=name)
+ return queryset.order_by("-create_time")
+
+ def page(self, current_page, page_size, user_id, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ queryset = self.get_query_set()
+ queryset = queryset.filter(id__in=self.get_authorized_application_queryset(user_id).values("id"))
+ return page_search(
+ current_page,
+ page_size,
+ queryset,
+ post_records_handler=lambda app: ApplicationResponseSerializer(app).data,
+ )
+
+
+class PortalHistoricalConversationResponseSerializer(serializers.Serializer):
+ id = serializers.CharField(required=True)
+ abstract = serializers.CharField(required=True)
+ create_time = serializers.CharField(required=True)
+ update_time = serializers.CharField(required=True)
+ application = serializers.SerializerMethodField()
+
+ def get_application(self, chat):
+ return {
+ "id": str(chat.application_id),
+ "name": chat.application.name,
+ "icon": chat.application.icon,
+ }
+
+
+class PortalHistoricalConversationSerializer(serializers.Serializer):
+ class Query(PortalApplicationAuthMixin, serializers.Serializer):
+ name = serializers.CharField(
+ required=False, allow_blank=True, label=_("Application Name"), help_text=_("Application name")
+ )
+
+ def get_query_set(self, user_id):
+ queryset = Chat.objects.filter(
+ chat_user_id=user_id,
+ is_deleted=False,
+ application_id__in=self.get_authorized_application_queryset(user_id).values("id"),
+ )
+ name = self.data.get("name")
+ if name:
+ queryset = queryset.filter(application__name__icontains=name)
+ return queryset.select_related("application").order_by("-update_time", "id")
+
+ def page(self, current_page, page_size, user_id, with_valid=True):
+ if with_valid:
+ self.is_valid(raise_exception=True)
+ return page_search(
+ current_page,
+ page_size,
+ self.get_query_set(user_id),
+ post_records_handler=lambda chat: PortalHistoricalConversationResponseSerializer(chat).data,
+ )
diff --git a/apps/chat/urls.py b/apps/chat/urls.py
index 5fb3dc23fa0..9576208252c 100644
--- a/apps/chat/urls.py
+++ b/apps/chat/urls.py
@@ -1,32 +1,74 @@
-from django.urls import path
+from django.urls import path, include
from application.views import ChatRecordDetailView, ChatRecordLinkView
-from chat.views.mcp import mcp_view
-from . import views
+from chat.views import v2 as v2_views, v3 as v3_views
-app_name = 'chat'
+app_name = "chat"
# @formatter:off
# fmt: off
-urlpatterns = [
- path('embed', views.ChatEmbedView.as_view()),
- path('mcp', mcp_view),
- path('auth/anonymous', views.AnonymousAuthentication.as_view()),
- path('profile', views.AuthProfile.as_view()),
- path('application/profile', views.ApplicationProfile.as_view(), name='profile'),
- path('chat_message/', views.ChatView.as_view(), name='chat'),
- path('open', views.OpenView.as_view(), name='open'),
- path('text_to_speech', views.TextToSpeech.as_view()),
- path('speech_to_text', views.SpeechToText.as_view()),
- path('captcha', views.CaptchaView.as_view(), name='captcha'),
- path('/chat/completions', views.OpenAIView.as_view(), name='application/chat_completions'),
- path('vote/chat//chat_record/', views.VoteView.as_view(), name='vote'),
- path('historical_conversation', views.HistoricalConversationView.as_view(), name='historical_conversation'),
- path('historical_conversation//record/',views.ChatRecordView.as_view(),name='conversation_details'),
- path('historical_conversation//', views.HistoricalConversationView.PageView.as_view(), name='historical_conversation'),
- path('historical_conversation/clear',views.HistoricalConversationView.BatchDelete.as_view(), name='historical_conversation_clear'),
- path('historical_conversation/',views.HistoricalConversationView.Operate.as_view(), name='historical_conversation_operate'),
- path('historical_conversation_record/', views.HistoricalConversationRecordView.as_view(), name='historical_conversation_record'),
- path('historical_conversation_record///', views.HistoricalConversationRecordView.PageView.as_view(), name='historical_conversation_record'),
+
+v3=[
+ # ---- application 作用域:application_id 从 path 获取 ----
+ path('application//', include([
+ path("profile",v3_views.ApplicationProfile.as_view(), name='v3_profile'),
+ path('open', v3_views.OpenView.as_view(), name='v3_open'),
+ path('text_to_speech',v3_views.TextToSpeech.as_view(),name='v3_text_to_speech'),
+ path('speech_to_text',v3_views.SpeechToText.as_view(),name='v3_speech_to_text'),
+ path('chat/completions',v3_views.OpenAIView.as_view(), name='v3_chat_completions'),
+ path('chat/clear',v3_views.HistoricalConversationView.BatchDelete.as_view(), name='v3_historical_conversation_clear'),
+ path('chat',v3_views.HistoricalConversationView.as_view(), name='v3_historical_conversation'),
+ path('chat//',v3_views.HistoricalConversationView.PageView.as_view(),name='v3_historical_conversation_page'),
+ path('chat//chat_message',v3_views.ChatView.as_view(), name='v3_chat'),
+ path('chat//chat_record',v3_views.HistoricalConversationRecordView.as_view(), name='v3_historical_conversation_record'),
+ path('chat//chat_record//', v3_views.HistoricalConversationRecordView.PageView.as_view(), name='v3_historical_conversation_record_page'),
+ path('chat//chat_record/',v3_views.ChatRecordView.as_view(),name='v3_conversation_details'),
+ path('chat//chat_record//vote',v3_views.VoteView.as_view(), name='v3_vote'),
+ path('chat//share_chat',ChatRecordLinkView.as_view(),name='v3_share_chat'),
+ path('chat/',v3_views.HistoricalConversationView.Operate.as_view(), name='v3_historical_conversation_operate'),
+])),
+ # ---- 全局(非 application 作用域)----
+ path('embed', v3_views.ChatEmbedView.as_view()),
+ path('mcp', v3_views.mcp_view),
+ path('auth/anonymous', v3_views.AnonymousAuthentication.as_view()),
+ path('auth/login', v3_views.LocalLoginView.as_view()),
+ path('auth/logout', v3_views.Logout.as_view(), name='v3_logout'),
+ path('profile', v3_views.AuthProfile.as_view()),
+ path('captcha', v3_views.CaptchaView.as_view(), name='v3_captcha'),
+ path('share/', ChatRecordDetailView.as_view()),
+ path('chat_message//cancel', v3_views.CancelWorkflowView.as_view(), name='v3_cancel_workflow'),
+ path('chat_user/profile', v3_views.ChatUserProfileView.as_view(), name='v3_chat_user_profile'),
+ path('chat_user/current/reset_password', v3_views.ResetCurrentUserPasswordView.as_view(), name='v3_reset_password_current'),
+ path('api_key', v3_views.ChatUserApiKeyView.as_view()),
+ path('api_key//', v3_views.ChatUserApiKeyView.Page.as_view()),
+ path('api_key/', v3_views.ChatUserApiKeyView.Operate.as_view()),
+ path('portal/application//', v3_views.PortalApplicationView.as_view(), name='v3_portal_application'),
+ path('portal/chat//', v3_views.PortalHistoricalConversationView.as_view(), name='v3_portal_historical_conversation'),
+]
+v2=[
+ path('embed', v2_views.ChatEmbedView.as_view()),
+ path('mcp', v2_views.mcp_view),
+ path('auth/anonymous', v2_views.AnonymousAuthentication.as_view(), name='anonymous'),
+ path('profile', v2_views.AuthProfile.as_view()),
+ path('application/profile', v2_views.ApplicationProfile.as_view(), name='profile'),
+ path('chat_message/', v2_views.ChatView.as_view(), name='chat'),
+ path('open', v2_views.OpenView.as_view(), name='open'),
+ path('text_to_speech', v2_views.TextToSpeech.as_view()),
+ path('speech_to_text', v2_views.SpeechToText.as_view()),
+ path('captcha', v2_views.CaptchaView.as_view(), name='captcha'),
+ path('/chat/completions', v2_views.OpenAIView.as_view(), name='application/chat_completions'),
+ path('vote/chat//chat_record/', v2_views.VoteView.as_view(), name='vote'),
+ path('historical_conversation', v2_views.HistoricalConversationView.as_view(), name='historical_conversation'),
+ path('historical_conversation//record/',v2_views.ChatRecordView.as_view(),name='conversation_details'),
+ path('historical_conversation//', v2_views.HistoricalConversationView.PageView.as_view(), name='historical_conversation'),
+ path('historical_conversation/clear',v2_views.HistoricalConversationView.BatchDelete.as_view(), name='historical_conversation_clear'),
+ path('historical_conversation/',v2_views.HistoricalConversationView.Operate.as_view(), name='historical_conversation_operate'),
+ path('historical_conversation_record/', v2_views.HistoricalConversationRecordView.as_view(), name='historical_conversation_record'),
+ path('historical_conversation_record///', v2_views.HistoricalConversationRecordView.PageView.as_view(), name='historical_conversation_record'),
path('share/', ChatRecordDetailView.as_view()),
path('/chat//share_chat', ChatRecordLinkView.as_view()),
+
+]
+urlpatterns = [
+ *v2,
+ path('v3/',include(v3))
]
diff --git a/apps/chat/views/__init__.py b/apps/chat/views/__init__.py
index fa38335c952..da273c43822 100644
--- a/apps/chat/views/__init__.py
+++ b/apps/chat/views/__init__.py
@@ -6,6 +6,5 @@
@date:2025/5/29 16:08
@desc:
"""
-from .chat_embed import *
-from .chat import *
-from .chat_record import *
+from . import v2
+from . import v3
diff --git a/apps/chat/views/chat.py b/apps/chat/views/chat.py
deleted file mode 100644
index a18a5ad95dc..00000000000
--- a/apps/chat/views/chat.py
+++ /dev/null
@@ -1,274 +0,0 @@
-# coding=utf-8
-"""
- @project: MaxKB
- @Author:虎虎
- @file: chat.py
- @date:2025/6/6 11:18
- @desc:
-"""
-import requests
-from django.http import HttpResponse, StreamingHttpResponse
-from django.utils.translation import gettext_lazy as _
-from drf_spectacular.utils import extend_schema
-from rest_framework.parsers import MultiPartParser
-from rest_framework.request import Request
-from rest_framework.views import APIView
-
-from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
-from application.models import ChatUserType, ChatSourceChoices
-from chat.api.chat_api import ChatAPI
-from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI, OpenAIAPI
-from chat.serializers.chat import OpenChatSerializers, ChatSerializers, SpeechToTextSerializers, \
- TextToSpeechSerializers, OpenAIChatSerializer
-from chat.serializers.chat_authentication import AnonymousAuthenticationSerializer, ApplicationProfileSerializer, \
- AuthProfileSerializer
-from common.auth import ChatTokenAuth
-from common.constants.permission_constants import ChatAuth
-from common.exception.app_exception import AppAuthenticationFailed
-from common.log.log import _get_ip_address
-from common.result import result
-from knowledge.models import FileSourceType
-from oss.serializers.file import FileSerializer
-from users.api import CaptchaAPI
-from users.serializers.login import CaptchaSerializer
-
-
-def stream_image(response):
- """生成器函数,用于流式传输图片数据"""
- for chunk in response.iter_content(chunk_size=4096):
- if chunk: # 过滤掉保持连接的空块
- yield chunk
-
-
-class ResourceProxy(APIView):
- def get(self, request: Request):
- image_url = request.query_params.get("url")
- if not image_url:
- return result.error("Missing 'url' parameter")
- try:
-
- # 发送GET请求,流式获取图片内容
- response = requests.get(
- image_url,
- stream=True, # 启用流式响应
- allow_redirects=True,
- timeout=10
- )
- content_type = response.headers.get('Content-Type', '').split(';')[0]
- # 创建Django流式响应
- django_response = StreamingHttpResponse(
- stream_image(response), # 使用生成器
- content_type=content_type
- )
-
- return django_response
- except Exception as e:
- return result.error(f"Image request failed: {str(e)}")
-
-
-class OpenAIView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['POST'],
- description=_('OpenAI Interface Dialogue'),
- summary=_('OpenAI Interface Dialogue'),
- operation_id=_('OpenAI Interface Dialogue'), # type: ignore
- request=OpenAIAPI.get_request(),
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def post(self, request: Request, application_id: str):
- ip_address = _get_ip_address(request)
- if application_id != str(request.auth.application_id):
- raise AppAuthenticationFailed(500, _('Secret key is invalid'))
- return OpenAIChatSerializer(
- data={'application_id': application_id, 'chat_user_id': request.auth.chat_user_id,
- 'chat_user_type': request.auth.chat_user_type,
- 'ip_address': ip_address,
- 'source': {"type": ChatSourceChoices.API_CALL.value}}).chat(request.data)
-
-
-class AnonymousAuthentication(APIView):
- def options(self, request, *args, **kwargs):
- return HttpResponse(
- headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
- "Access-Control-Allow-Methods": "POST",
- "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
-
- @extend_schema(
- methods=['POST'],
- description=_('Application Anonymous Certification'),
- summary=_('Application Anonymous Certification'),
- operation_id=_('Application Anonymous Certification'), # type: ignore
- request=ChatAuthenticationAPI.get_request(),
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def post(self, request: Request):
- return result.success(
- AnonymousAuthenticationSerializer(data={'access_token': request.data.get("access_token")}).auth(
- request),
- headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
- "Access-Control-Allow-Methods": "POST",
- "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}
- )
-
-
-class ApplicationProfile(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get application related information"),
- summary=_("Get application related information"),
- operation_id=_("Get application related information"), # type: ignore
- request=None,
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request):
- if isinstance(request.auth, ChatAuth):
- return result.success(ApplicationProfileSerializer(
- data={'application_id': request.auth.application_id}).profile())
- raise AppAuthenticationFailed(401, "身份异常")
-
-
-class AuthProfile(APIView):
- @extend_schema(
- methods=['GET'],
- description=_("Get application authentication information"),
- summary=_("Get application authentication information"),
- operation_id=_("Get application authentication information"), # type: ignore
- parameters=ChatAuthenticationProfileAPI.get_parameters(),
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request):
- return result.success(
- AuthProfileSerializer(data={'access_token': request.query_params.get("access_token")}).profile())
-
-
-class ChatView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['POST'],
- description=_("dialogue"),
- summary=_("dialogue"),
- operation_id=_("dialogue"), # type: ignore
- request=ChatAPI.get_request(),
- parameters=ChatAPI.get_parameters(),
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def post(self, request: Request, chat_id: str):
- ip_address = _get_ip_address(request)
- return ChatSerializers(data={'chat_id': chat_id,
- 'chat_user_id': request.auth.chat_user_id,
- 'chat_user_type': request.auth.chat_user_type,
- 'application_id': request.auth.application_id,
- 'debug': False,
- 'ip_address': ip_address,
- 'source': {
- 'type': ChatSourceChoices.API_CALL.value if request.auth.chat_user_type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value}
- }
- ).chat(request.data)
-
-
-class OpenView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get the session id according to the application id"),
- summary=_("Get the session id according to the application id"),
- operation_id=_("Get the session id according to the application id"), # type: ignore
- parameters=ChatOpenAPI.get_parameters(),
- responses=None,
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request):
- ip_address = _get_ip_address(request)
- return result.success(OpenChatSerializers(
- data={'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id, 'chat_user_type': request.auth.chat_user_type,
- 'ip_address': ip_address,
- 'source': {
- 'type': ChatSourceChoices.API_CALL.value if request.auth.chat_user_type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value},
- 'debug': False}).open())
-
-
-class CaptchaView(APIView):
- @extend_schema(methods=['GET'],
- summary=_("Get Chat captcha"),
- description=_("Get Chat captcha"),
- operation_id=_("Get Chat captcha"), # type: ignore
- tags=[_("Chat")], # type: ignore
- responses=CaptchaAPI.get_response())
- def get(self, request: Request):
- username = request.query_params.get('username', None)
- accessToken = request.query_params.get('accessToken', None)
- return result.success(CaptchaSerializer().chat_generate(username, 'chat', accessToken))
-
-
-class SpeechToText(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['POST'],
- description=_("speech to text"),
- summary=_("speech to text"),
- operation_id=_("speech to text"), # type: ignore
- request=SpeechToTextAPI.get_request(),
- responses=SpeechToTextAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def post(self, request: Request):
- return result.success(
- SpeechToTextSerializers(
- data={'application_id': request.auth.application_id})
- .speech_to_text({'file': request.FILES.get('file')}))
-
-
-class TextToSpeech(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['POST'],
- description=_("text to speech"),
- summary=_("text to speech"),
- operation_id=_("text to speech"), # type: ignore
- request=TextToSpeechAPI.get_request(),
- responses=TextToSpeechAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def post(self, request: Request):
- byte_data = TextToSpeechSerializers(
- data={'application_id': request.auth.application_id}).text_to_speech(request.data)
- return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
- 'Content-Disposition': 'attachment; filename="abc.mp3"'})
-
-
-class UploadFile(APIView):
- authentication_classes = [ChatTokenAuth]
- parser_classes = [MultiPartParser]
-
- @extend_schema(
- methods=['POST'],
- description=_("Upload files"),
- summary=_("Upload files"),
- operation_id=_("Upload files"), # type: ignore
- request=TextToSpeechAPI.get_request(),
- responses=TextToSpeechAPI.get_response(),
- tags=[_('Application')] # type: ignore
- )
- def post(self, request: Request, chat_id: str):
- files = request.FILES.getlist('file')
- file_ids = []
- meta = {}
- for file in files:
- file_url = FileSerializer(
- data={'file': file, 'meta': meta, 'source_id': chat_id, 'source_type': FileSourceType.CHAT, }).upload()
- file_ids.append({'name': file.name, 'url': file_url, 'file_id': file_url.split('/')[-1]})
- return result.success(file_ids)
diff --git a/apps/chat/views/chat_record.py b/apps/chat/views/chat_record.py
deleted file mode 100644
index c50d95b6437..00000000000
--- a/apps/chat/views/chat_record.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# coding=utf-8
-"""
- @project: MaxKB
- @Author:虎虎
- @file: chat_record.py
- @date:2025/6/23 10:42
- @desc:
-"""
-from django.utils.translation import gettext_lazy as _
-from drf_spectacular.utils import extend_schema
-from rest_framework.request import Request
-from rest_framework.views import APIView
-
-from application.serializers.application_chat_record import ChatRecordOperateSerializer
-from chat.api.chat_api import HistoricalConversationAPI, PageHistoricalConversationAPI, \
- PageHistoricalConversationRecordAPI, HistoricalConversationRecordAPI, HistoricalConversationOperateAPI
-from chat.api.vote_api import VoteAPI
-from chat.serializers.chat_record import VoteSerializer, HistoricalConversationSerializer, \
- HistoricalConversationRecordSerializer, HistoricalConversationOperateSerializer
-from common import result
-from common.auth import ChatTokenAuth
-
-
-class VoteView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['PUT'],
- description=_("Like, Dislike"),
- summary=_("Like, Dislike"),
- operation_id=_("Like, Dislike"), # type: ignore
- parameters=VoteAPI.get_parameters(),
- request=VoteAPI.get_request(),
- responses=VoteAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def put(self, request: Request, chat_id: str, chat_record_id: str):
- return result.success(VoteSerializer(
- data={'chat_id': chat_id,
- 'chat_record_id': chat_record_id
- }).vote(request.data))
-
-
-class HistoricalConversationView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get historical conversation"),
- summary=_("Get historical conversation"),
- operation_id=_("Get historical conversation"), # type: ignore
- parameters=HistoricalConversationAPI.get_parameters(),
- responses=HistoricalConversationAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request):
- return result.success(HistoricalConversationSerializer(
- data={
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).list())
-
- class Operate(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['PUT'],
- description=_("Modify conversation about"),
- summary=_("Modify conversation about"),
- operation_id=_("Modify conversation about"), # type: ignore
- parameters=HistoricalConversationOperateAPI.get_parameters(),
- request=HistoricalConversationOperateAPI.get_request(),
- responses=HistoricalConversationOperateAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def put(self, request: Request, chat_id: str):
- return result.success(HistoricalConversationOperateSerializer(
- data={
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- 'chat_id': chat_id,
- }).edit_abstract(request.data)
- )
-
- @extend_schema(
- methods=['DELETE'],
- description=_("Delete history conversation"),
- summary=_("Delete history conversation"),
- operation_id=_("Delete history conversation"), # type: ignore
- parameters=HistoricalConversationOperateAPI.get_parameters(),
- responses=HistoricalConversationOperateAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def delete(self, request: Request, chat_id: str):
- return result.success(HistoricalConversationOperateSerializer(
- data={
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- 'chat_id': chat_id,
- }).logic_delete())
-
- class BatchDelete(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['DELETE'],
- description=_("Batch delete history conversation"),
- summary=_("Batch delete history conversation"),
- operation_id=_("Batch delete history conversation"), # type: ignore
- parameters=HistoricalConversationOperateAPI.get_parameters(),
- responses=HistoricalConversationOperateAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def delete(self, request: Request):
- return result.success(HistoricalConversationOperateSerializer.Clear(data={
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).batch_logic_delete())
-
- class PageView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get historical conversation by page"),
- summary=_("Get historical conversation by page"),
- operation_id=_("Get historical conversation by page"), # type: ignore
- parameters=PageHistoricalConversationAPI.get_parameters(),
- responses=PageHistoricalConversationAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request, current_page: int, page_size: int):
- return result.success(HistoricalConversationSerializer(
- data={
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).page(current_page, page_size))
-
-
-class HistoricalConversationRecordView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get historical conversation records"),
- summary=_("Get historical conversation records"),
- operation_id=_("Get historical conversation records"), # type: ignore
- parameters=HistoricalConversationRecordAPI.get_parameters(),
- responses=HistoricalConversationRecordAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request, chat_id: str):
- return result.success(HistoricalConversationRecordSerializer(
- data={
- 'chat_id': chat_id,
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).list())
-
- class PageView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get historical conversation records by page "),
- summary=_("Get historical conversation records by page"),
- operation_id=_("Get historical conversation records by page"), # type: ignore
- parameters=PageHistoricalConversationRecordAPI.get_parameters(),
- responses=PageHistoricalConversationRecordAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request, chat_id: str, current_page: int, page_size: int):
- return result.success(HistoricalConversationRecordSerializer(
- data={
- 'chat_id': chat_id,
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).page(current_page, page_size))
-
-
-class ChatRecordView(APIView):
- authentication_classes = [ChatTokenAuth]
-
- @extend_schema(
- methods=['GET'],
- description=_("Get conversation details"),
- summary=_("Get conversation details"),
- operation_id=_("Get conversation details"), # type: ignore
- parameters=PageHistoricalConversationRecordAPI.get_parameters(),
- responses=PageHistoricalConversationRecordAPI.get_response(),
- tags=[_('Chat')] # type: ignore
- )
- def get(self, request: Request, chat_id: str, chat_record_id: str):
- return result.success(ChatRecordOperateSerializer(
- data={
- 'chat_id': chat_id,
- 'chat_record_id': chat_record_id,
- 'application_id': request.auth.application_id,
- 'chat_user_id': request.auth.chat_user_id,
- }).one(False))
diff --git a/apps/chat/views/v2/__init__.py b/apps/chat/views/v2/__init__.py
new file mode 100644
index 00000000000..4cedec9c1d3
--- /dev/null
+++ b/apps/chat/views/v2/__init__.py
@@ -0,0 +1,12 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: __init__.py.py
+ @date:2025/5/29 16:08
+ @desc:
+"""
+from .chat_embed import *
+from .chat import *
+from .chat_record import *
+from .mcp import mcp_view
diff --git a/apps/chat/views/v2/chat.py b/apps/chat/views/v2/chat.py
new file mode 100644
index 00000000000..8f4816d33cf
--- /dev/null
+++ b/apps/chat/views/v2/chat.py
@@ -0,0 +1,445 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: chat.py
+ @date:2025/6/6 11:18
+ @desc:
+"""
+import json
+
+import requests
+from django.core.cache import cache
+from django.http import HttpResponse, StreamingHttpResponse
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import OpenApiParameter
+from rest_framework.parsers import MultiPartParser
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
+from application.models import ChatUserType, ChatSourceChoices
+from chat.api.chat_api import ChatAPI
+from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPIV2, ChatOpenAPI, OpenAIAPI
+from chat.serializers.chat import OpenChatSerializers, ChatSerializers, SpeechToTextSerializers, \
+ TextToSpeechSerializers, OpenAIChatSerializer
+from chat.serializers.chat_authentication import AnonymousAuthenticationV2Serializer, ApplicationProfileSerializer, \
+ AuthProfileV2Serializer
+from common.auth import ChatTokenAuth
+from common.auth.authentication import has_permissions
+from common.auth.common import FileToken
+from common.auth.constants.chat_permission_constants import ChatPermissionConstants
+from common.constants.authentication_type import AuthenticationType
+from common.constants.cache_version import Cache_Version
+from common.auth.common import ChatAuthentication
+from common.exception.app_exception import AppAuthenticationFailed, AppApiException
+from common.log.log import _get_ip_address, log
+from common.result import result
+from common.utils.rsa_util import decrypt
+from knowledge.models import FileSourceType
+from maxkb.const import CONFIG
+from models_provider.api.model import DefaultModelResponse
+from oss.serializers.file import FileSerializer
+from system_manage.serializers.chat_user import RePasswordSerializer, ChatUserProfileSerializer
+from system_manage.serializers.chat_user_serializer import ChatUserAccessTokenSerializer
+from users.api import CaptchaAPI, LoginAPI
+from users.api.user import ResetPasswordAPI, UserProfileAPI
+from users.serializers.login import CaptchaSerializer
+from users.views import get_re_password_details
+
+
+def stream_image(response):
+ """生成器函数,用于流式传输图片数据"""
+ for chunk in response.iter_content(chunk_size=4096):
+ if chunk: # 过滤掉保持连接的空块
+ yield chunk
+
+
+class ResourceProxy(APIView):
+ def get(self, request: Request):
+ image_url = request.query_params.get("url")
+ if not image_url:
+ return result.error("Missing 'url' parameter")
+ try:
+
+ # 发送GET请求,流式获取图片内容
+ response = requests.get(
+ image_url,
+ stream=True, # 启用流式响应
+ allow_redirects=True,
+ timeout=10
+ )
+ content_type = response.headers.get('Content-Type', '').split(';')[0]
+ # 创建Django流式响应
+ django_response = StreamingHttpResponse(
+ stream_image(response), # 使用生成器
+ content_type=content_type
+ )
+
+ return django_response
+ except Exception as e:
+ return result.error(f"Image request failed: {str(e)}")
+
+
+class OpenAIView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_('OpenAI Interface Dialogue'),
+ summary=_('OpenAI Interface Dialogue'),
+ operation_id=_('OpenAI Interface Dialogue'), # type: ignore
+ request=OpenAIAPI.get_request(),
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request, application_id: str):
+ ip_address = _get_ip_address(request)
+ if application_id != str(request.user.kwargs.get('application_id')):
+ raise AppAuthenticationFailed(500, _('Secret key is invalid'))
+ return OpenAIChatSerializer(
+ data={'application_id': application_id, 'chat_user_id': request.user.id,
+ 'chat_user_type': request.user.type,
+ 'ip_address': ip_address,
+ 'source': {"type": ChatSourceChoices.API_CALL.value}}).chat(request.data)
+
+
+class AnonymousAuthentication(APIView):
+ def options(self, request, *args, **kwargs):
+ return HttpResponse(
+ headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}, )
+
+ @extend_schema(
+ methods=['POST'],
+ description=_('Application Anonymous Certification'),
+ summary=_('Application Anonymous Certification'),
+ operation_id=_('Application Anonymous Certification'), # type: ignore
+ request=AnonymousAuthenticationV2Serializer,
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request):
+ token, f_token = AnonymousAuthenticationV2Serializer(data=request.data).auth(
+ request)
+ response = result.success(
+ token,
+ headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token"}
+ )
+ is_https = request.scheme == "https"
+
+ response.set_cookie(
+ key="mk_file_auth",
+ value=f_token,
+ max_age=7 * 24 * 3600,
+ path=f'{CONFIG.get_chat_path()}/{request.data.get("access_token")}',
+ secure=is_https,
+ httponly=True,
+ samesite="None" if is_https else "Lax",
+ )
+ return response
+
+
+class ApplicationProfile(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['GET'],
+ description=_("Get application related information"),
+ summary=_("Get application related information"),
+ operation_id=_("Get application related information"), # type: ignore
+ request=None,
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def get(self, request: Request):
+ return result.success(ApplicationProfileSerializer(
+ data={'application_id': request.user.kwargs.get('application_id')}).profile())
+
+
+class AuthProfile(APIView):
+ @extend_schema(
+ methods=['GET'],
+ description=_("Get application authentication information"),
+ summary=_("Get application authentication information"),
+ operation_id=_("Get application authentication information"), # type: ignore
+ parameters=ChatAuthenticationProfileAPIV2.get_parameters(),
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def get(self, request: Request):
+ return result.success(
+ AuthProfileV2Serializer(data={'access_token': request.query_params.get("access_token")}).profile())
+
+
+class ChatView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_("dialogue"),
+ summary=_("dialogue"),
+ operation_id=_("dialogue"), # type: ignore
+ request=ChatAPI.get_request(),
+ parameters=ChatAPI.get_parameters(),
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request, chat_id: str):
+ ip_address = _get_ip_address(request)
+ return ChatSerializers(data={'chat_id': chat_id,
+ 'chat_user_id': request.user.id,
+ 'chat_user_type': request.user.type,
+ 'application_id': request.user.kwargs.get('application_id'),
+ 'debug': False,
+ 'ip_address': ip_address,
+ 'source': {
+ 'type': ChatSourceChoices.API_CALL.value if request.user.type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value}
+ }
+ ).chat(request.data)
+
+
+class OpenView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['GET'],
+ description=_("Get the session id according to the application id"),
+ summary=_("Get the session id according to the application id"),
+ operation_id=_("Get the session id according to the application id"), # type: ignore
+ parameters=ChatOpenAPI.get_parameters(),
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request):
+ ip_address = _get_ip_address(request)
+ return result.success(OpenChatSerializers(
+ data={'application_id': request.user.kwargs.get('application_id'),
+ 'chat_user_id': request.user.id, 'chat_user_type': request.user.type,
+ 'ip_address': ip_address,
+ 'source': {
+ 'type': ChatSourceChoices.API_CALL.value if request.user.type == ChatUserType.APPLICATION_API_KEY.value else ChatSourceChoices.ONLINE.value},
+ 'debug': False}).open())
+
+
+class CancelWorkflowView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_("Cancel running workflow"),
+ summary=_("Cancel running workflow"),
+ operation_id=_("Cancel running workflow"), # type: ignore
+ parameters=[
+ OpenApiParameter(name='chat_id', type=OpenApiTypes.UUID, location=OpenApiParameter.PATH,
+ description=_('Chat ID')),
+ ],
+ responses=None,
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request, chat_id: str):
+ from application.workflow.workflow_run_registry import WorkflowRunRegistry, CancelResult
+ result_enum = WorkflowRunRegistry.cancel_by_chat_id(chat_id)
+ if result_enum == CancelResult.CANCELLED:
+ return result.success({'status': 'cancelled', 'chat_id': chat_id})
+ elif result_enum == CancelResult.NOT_FOUND:
+ return result.success({'status': 'not_found', 'chat_id': chat_id})
+ else:
+ return result.fail(500, _('Failed to cancel workflow'))
+
+
+class CaptchaView(APIView):
+ @extend_schema(methods=['GET'],
+ summary=_("Get Chat captcha"),
+ description=_("Get Chat captcha"),
+ operation_id=_("Get Chat captcha"), # type: ignore
+ tags=[_("Chat")], # type: ignore
+ responses=CaptchaAPI.get_response())
+ def get(self, request: Request):
+ username = request.query_params.get('username', None)
+ accessToken = request.query_params.get('accessToken', None)
+ return result.success(CaptchaSerializer().chat_generate(username, 'chat', accessToken))
+
+
+class SpeechToText(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_("speech to text"),
+ summary=_("speech to text"),
+ operation_id=_("speech to text"), # type: ignore
+ request=SpeechToTextAPI.get_request(),
+ responses=SpeechToTextAPI.get_response(),
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request):
+ return result.success(
+ SpeechToTextSerializers(
+ data={'application_id': request.user.kwargs.get('application_id')})
+ .speech_to_text({'file': request.FILES.get('file')}))
+
+
+class TextToSpeech(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_("text to speech"),
+ summary=_("text to speech"),
+ operation_id=_("text to speech"), # type: ignore
+ request=TextToSpeechAPI.get_request(),
+ responses=TextToSpeechAPI.get_response(),
+ tags=[_('Chat')] # type: ignore
+ )
+ def post(self, request: Request):
+ byte_data = TextToSpeechSerializers(
+ data={'application_id': request.user.kwargs.get('application_id')}).text_to_speech(request.data)
+ return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
+ 'Content-Disposition': 'attachment; filename="abc.mp3"'})
+
+
+class UploadFile(APIView):
+ authentication_classes = [ChatTokenAuth]
+ parser_classes = [MultiPartParser]
+
+ @extend_schema(
+ methods=['POST'],
+ description=_("Upload files"),
+ summary=_("Upload files"),
+ operation_id=_("Upload files"), # type: ignore
+ request=TextToSpeechAPI.get_request(),
+ responses=TextToSpeechAPI.get_response(),
+ tags=[_('Application')] # type: ignore
+ )
+ def post(self, request: Request, chat_id: str):
+ files = request.FILES.getlist('file')
+ file_ids = []
+ meta = {}
+ for file in files:
+ file_url = FileSerializer(
+ data={'file': file, 'meta': meta, 'source_id': chat_id, 'source_type': FileSourceType.CHAT, }).upload(
+ request.user.id)
+ file_ids.append({'name': file.name, 'url': file_url, 'file_id': file_url.split('/')[-1]})
+ return result.success(file_ids)
+
+
+class ResetCurrentUserPasswordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ summary=_("Modify current user password"),
+ description=_("Modify current user password"),
+ operation_id=_("Modify current user password"), # type: ignore
+ tags=[_("Chat User")], # type: ignore
+ request=ResetPasswordAPI.get_request(),
+ responses=DefaultModelResponse.get_response(),
+ )
+ @log(
+ menu="Chat User",
+ operate="Modify current user password",
+ get_operation_object=lambda r, k: {"name": r.user.username},
+ get_details=get_re_password_details,
+ )
+ def post(self, request: Request):
+ request_data = request.data
+ encrypted_data = request_data.get("encryptedData", "")
+ if encrypted_data:
+ try:
+ decrypted_raw = decrypt(encrypted_data)
+ # decrypt 可能返回非 JSON 字符串,防护解析异常
+ decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {}
+ if isinstance(decrypted_data, dict):
+ request_data = decrypted_data
+ except Exception as e:
+ raise AppApiException(500, _("Invalid encrypted data"))
+ serializer_obj = RePasswordSerializer(data=request_data)
+ if serializer_obj.reset_password(request.user.id):
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ auth = request.META.get("HTTP_AUTHORIZATION")
+ cache.delete(get_key(token=auth), version=version)
+ return result.success(True)
+ return result.error(_("Failed to change password"))
+
+
+class ChatUserProfileView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ summary=_("Get current user information"),
+ description=_("Get current user information"),
+ operation_id=_("Get current user information"), # type: ignore
+ tags=[_("Chat User")], # type: ignore
+ responses=UserProfileAPI.get_response(),
+ )
+ def get(self, request: Request):
+ return result.success(ChatUserProfileSerializer().profile(request.user))
+
+
+class BaseAuthView(APIView):
+ @staticmethod
+ def create_token_and_cache(access_token, user, request):
+ token = ChatUserAccessTokenSerializer.create_token_and_cache(access_token, user, request)
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ cache.set(get_key(token), user, timeout=60 * 60 * 2, version=version)
+ return token, FileToken(str(user.id), AuthenticationType.CHAT_USER.value).to_token()
+
+ @classmethod
+ def generate(self, request, f_token: str, response: HttpResponse, path: str = '/chat'):
+ secure = request.is_secure()
+ response.set_cookie(
+ "mk_file_auth",
+ value=f_token,
+ max_age=7 * 24 * 3600,
+ path=path,
+ domain=None,
+ secure=secure,
+ httponly=True,
+ samesite="Lax",
+ )
+ return response
+
+
+class LocalLoginView(BaseAuthView):
+ @extend_schema(
+ methods=["POST"],
+ description=_("Log in"),
+ summary=_("Log in"),
+ operation_id=_("Log in"), # type: ignore
+ tags=[_("Chat User/login")], # type: ignore
+ request=LoginAPI.get_request(),
+ responses=LoginAPI.get_response(),
+ )
+ def post(self, request: Request, access_token: str = None):
+ user = ChatUserAccessTokenSerializer.local_login(request.data, access_token)
+ user.source = "LOCAL"
+ token, f_token = self.create_token_and_cache(access_token, user, request)
+ response = result.success({'token': token})
+ return self.generate(request, f_token, response, path=f'/chat/{access_token}/')
+
+
+class Logout(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ summary=_("Sign out"),
+ description=_("Sign out"),
+ operation_id=_("Sign out"), # type: ignore
+ tags=[_("Chat User")], # type: ignore
+ responses=DefaultModelResponse.get_response(),
+ )
+ @log(menu="Chat User/logout", operate="Sign out", get_operation_object=lambda r, k: {"name": r.user.username})
+ def post(self, request: Request):
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ auth = request.META.get("HTTP_AUTHORIZATION")
+ cache.delete(get_key(token=auth[7:]), version=version)
+ return result.success(True)
diff --git a/apps/chat/views/chat_embed.py b/apps/chat/views/v2/chat_embed.py
similarity index 100%
rename from apps/chat/views/chat_embed.py
rename to apps/chat/views/v2/chat_embed.py
diff --git a/apps/chat/views/v2/chat_record.py b/apps/chat/views/v2/chat_record.py
new file mode 100644
index 00000000000..38456f977ef
--- /dev/null
+++ b/apps/chat/views/v2/chat_record.py
@@ -0,0 +1,239 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎
+@file: chat_record.py
+@date:2025/6/23 10:42
+@desc:
+"""
+
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from application.serializers.application_chat_record import ChatRecordOperateSerializer
+from chat.api.chat_api import (
+ HistoricalConversationAPI,
+ PageHistoricalConversationAPI,
+ PageHistoricalConversationRecordAPI,
+ HistoricalConversationRecordAPI,
+ HistoricalConversationOperateAPI,
+)
+from chat.api.vote_api import VoteAPI
+from chat.serializers.chat_record import (
+ VoteSerializer,
+ HistoricalConversationSerializer,
+ HistoricalConversationRecordSerializer,
+ HistoricalConversationOperateSerializer,
+)
+from common import result
+from common.auth import ChatTokenAuth
+
+
+class VoteView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["PUT"],
+ description=_("Like, Dislike"),
+ summary=_("Like, Dislike"),
+ operation_id=_("Like, Dislike"), # type: ignore
+ parameters=VoteAPI.get_parameters(),
+ request=VoteAPI.get_request(),
+ responses=VoteAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def put(self, request: Request, chat_id: str, chat_record_id: str):
+ return result.success(
+ VoteSerializer(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ }
+ ).vote(request.data)
+ )
+
+
+class HistoricalConversationView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation"),
+ summary=_("Get historical conversation"),
+ operation_id=_("Get historical conversation"), # type: ignore
+ parameters=HistoricalConversationAPI.get_parameters(),
+ responses=HistoricalConversationAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def get(self, request: Request):
+ return result.success(
+ HistoricalConversationSerializer(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).list()
+ )
+
+ class Operate(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["PUT"],
+ description=_("Modify conversation about"),
+ summary=_("Modify conversation about"),
+ operation_id=_("Modify conversation about"), # type: ignore
+ parameters=HistoricalConversationOperateAPI.get_parameters(),
+ request=HistoricalConversationOperateAPI.get_request(),
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def put(self, request: Request, chat_id: str):
+ return result.success(
+ HistoricalConversationOperateSerializer(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ "chat_id": chat_id,
+ }
+ ).edit_abstract(request.data)
+ )
+
+ @extend_schema(
+ methods=["DELETE"],
+ description=_("Delete history conversation"),
+ summary=_("Delete history conversation"),
+ operation_id=_("Delete history conversation"), # type: ignore
+ parameters=HistoricalConversationOperateAPI.get_parameters(),
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def delete(self, request: Request, chat_id: str):
+ return result.success(
+ HistoricalConversationOperateSerializer(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ "chat_id": chat_id,
+ }
+ ).logic_delete()
+ )
+
+ class BatchDelete(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["DELETE"],
+ description=_("Batch delete history conversation"),
+ summary=_("Batch delete history conversation"),
+ operation_id=_("Batch delete history conversation"), # type: ignore
+ parameters=[],
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def delete(self, request: Request):
+ return result.success(
+ HistoricalConversationOperateSerializer.Clear(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).batch_logic_delete()
+ )
+
+ class PageView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation by page"),
+ summary=_("Get historical conversation by page"),
+ operation_id=_("Get historical conversation by page"), # type: ignore
+ parameters=PageHistoricalConversationAPI.get_parameters(),
+ responses=PageHistoricalConversationAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def get(self, request: Request, current_page: int, page_size: int):
+ return result.success(
+ HistoricalConversationSerializer(
+ data={
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).page(current_page, page_size)
+ )
+
+
+class HistoricalConversationRecordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation records"),
+ summary=_("Get historical conversation records"),
+ operation_id=_("Get historical conversation records"), # type: ignore
+ parameters=HistoricalConversationRecordAPI.get_parameters(),
+ responses=HistoricalConversationRecordAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def get(self, request: Request, chat_id: str):
+ return result.success(
+ HistoricalConversationRecordSerializer(
+ data={
+ "chat_id": chat_id,
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).list()
+ )
+
+ class PageView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation records by page "),
+ summary=_("Get historical conversation records by page"),
+ operation_id=_("Get historical conversation records by page"), # type: ignore
+ parameters=PageHistoricalConversationRecordAPI.get_parameters(),
+ responses=PageHistoricalConversationRecordAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def get(self, request: Request, chat_id: str, current_page: int, page_size: int):
+ return result.success(
+ HistoricalConversationRecordSerializer(
+ data={
+ "chat_id": chat_id,
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).page(current_page, page_size)
+ )
+
+
+class ChatRecordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get conversation details"),
+ summary=_("Get conversation details"),
+ operation_id=_("Get conversation details"), # type: ignore
+ parameters=PageHistoricalConversationRecordAPI.get_parameters(),
+ responses=PageHistoricalConversationRecordAPI.get_response(),
+ tags=[_("Chat")], # type: ignore
+ )
+ def get(self, request: Request, chat_id: str, chat_record_id: str):
+ return result.success(
+ ChatRecordOperateSerializer(
+ data={
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ "application_id": request.user.kwargs.get("application_id"),
+ "chat_user_id": request.user.id,
+ }
+ ).one(False)
+ )
diff --git a/apps/chat/views/v2/mcp.py b/apps/chat/views/v2/mcp.py
new file mode 100644
index 00000000000..f4733a8c050
--- /dev/null
+++ b/apps/chat/views/v2/mcp.py
@@ -0,0 +1,53 @@
+import json
+
+from django.http import HttpResponse, JsonResponse
+from django.views.decorators.csrf import csrf_exempt
+
+from chat.mcp.tools import MCPToolHandler
+
+
+@csrf_exempt
+def mcp_view(request):
+ request_id = None
+ try:
+ data = json.loads(request.body)
+ method = data.get("method")
+ params = data.get("params", {})
+ request_id = data.get("id")
+
+ if request_id is None:
+ return HttpResponse(status=204)
+
+ auth_header = request.headers.get("Authorization", "").replace("Bearer ", "")
+ handler = MCPToolHandler(
+ auth_header,
+ request.headers.get("X-MaxKB-Chat-Files", ""),
+ request.headers.get("X-MaxKB-Form-Data", ""),
+ )
+
+ # 路由方法
+ if method == "initialize":
+ result = handler.initialize()
+
+ elif method == "tools/list":
+ result = handler.list_tools()
+
+ elif method == "tools/call":
+ result = handler.call_tool(params)
+
+ else:
+ return JsonResponse(
+ {
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
+ }
+ )
+
+ # 成功响应
+ return JsonResponse({"jsonrpc": "2.0", "id": request_id, "result": result})
+
+ except Exception as e:
+ return JsonResponse(
+ {"jsonrpc": "2.0", "id": request_id, "error": {"code": -32603, "message": f"Internal error: {str(e)}"}}
+ )
diff --git a/apps/chat/views/v3/__init__.py b/apps/chat/views/v3/__init__.py
new file mode 100644
index 00000000000..7220a4c91e4
--- /dev/null
+++ b/apps/chat/views/v3/__init__.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎
+@file: __init__.py
+@date:2025/6/6 11:18
+@desc:
+"""
+
+from .chat_embed import *
+from .chat import *
+from .chat_record import *
+from .chat_user_api_key import *
+from .portal import *
+from .mcp import mcp_view
diff --git a/apps/chat/views/v3/chat.py b/apps/chat/views/v3/chat.py
new file mode 100644
index 00000000000..8fd53a8baf1
--- /dev/null
+++ b/apps/chat/views/v3/chat.py
@@ -0,0 +1,506 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎
+@file: chat.py
+@date:2025/6/6 11:18
+@desc:
+"""
+
+import json
+
+import requests
+from django.core.cache import cache
+from django.http import HttpResponse, StreamingHttpResponse
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import OpenApiParameter
+from rest_framework.parsers import MultiPartParser
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
+from application.models import ChatUserType, ChatSourceChoices, ApplicationAccessToken
+from chat.api.chat_api import ChatAPI
+from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI, OpenAIAPI
+from chat.serializers.chat import (
+ OpenChatSerializers,
+ ChatSerializers,
+ SpeechToTextSerializers,
+ TextToSpeechSerializers,
+ OpenAIChatSerializer,
+)
+from chat.serializers.chat_authentication import (
+ AnonymousAuthenticationSerializer,
+ ApplicationProfileSerializer,
+ AuthProfileSerializer,
+)
+from common.auth import ChatTokenAuth
+from common.auth.authentication import has_permissions
+from common.auth.common import FileToken, ChatToken
+from common.auth.constants.chat_permission_constants import ChatPermissionConstants
+from common.auth.constants.operate_constants import Operate
+from common.constants.authentication_type import AuthenticationType
+from common.constants.cache_version import Cache_Version
+from common.exception.app_exception import AppApiException
+from common.log.log import _get_ip_address, log
+from common.result import result
+from common.utils.rsa_util import decrypt
+from knowledge.models import FileSourceType
+from maxkb.const import CONFIG
+from models_provider.api.model import DefaultModelResponse
+from oss.serializers.file import FileSerializer
+from system_manage.serializers.chat_user import RePasswordSerializer, ChatUserProfileSerializer
+from chat.serializers.chat_user_serializer import ChatUserAccessTokenV3Serializer
+from users.api import CaptchaAPI, LoginAPI
+from users.api.user import ResetPasswordAPI, UserProfileAPI
+from users.serializers.login import CaptchaSerializer
+from users.views import get_re_password_details
+
+
+def stream_image(response):
+ """生成器函数,用于流式传输图片数据"""
+ for chunk in response.iter_content(chunk_size=4096):
+ if chunk: # 过滤掉保持连接的空块
+ yield chunk
+
+
+class ResourceProxy(APIView):
+ def get(self, request: Request):
+ image_url = request.query_params.get("url")
+ if not image_url:
+ return result.error("Missing 'url' parameter")
+ try:
+ # 发送GET请求,流式获取图片内容
+ response = requests.get(
+ image_url,
+ stream=True, # 启用流式响应
+ allow_redirects=True,
+ timeout=10,
+ )
+ content_type = response.headers.get("Content-Type", "").split(";")[0]
+ # 创建Django流式响应
+ django_response = StreamingHttpResponse(
+ stream_image(response), # 使用生成器
+ content_type=content_type,
+ )
+
+ return django_response
+ except Exception as e:
+ return result.error(f"Image request failed: {str(e)}")
+
+
+class OpenAIView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("OpenAI Interface Dialogue"),
+ summary=_("OpenAI Interface Dialogue"),
+ operation_id=_("V3 OpenAI Interface Dialogue"), # type: ignore
+ request=OpenAIAPI.get_request(),
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ def post(self, request: Request, application_id: str):
+ ip_address = _get_ip_address(request)
+ return OpenAIChatSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "ip_address": ip_address,
+ "source": {"type": ChatSourceChoices.API_CALL.value},
+ }
+ ).chat(request.data)
+
+
+class AnonymousAuthentication(APIView):
+ def options(self, request, *args, **kwargs):
+ return HttpResponse(
+ headers={
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token",
+ },
+ )
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Application Anonymous Certification"),
+ summary=_("Application Anonymous Certification"),
+ operation_id=_("V3 Application Anonymous Certification"), # type: ignore
+ request=ChatAuthenticationAPI.get_request(),
+ parameters=ChatAuthenticationAPI.get_parameters(),
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ def post(self, request: Request):
+ serializer = AnonymousAuthenticationSerializer(data=request.query_params)
+ serializer.is_valid(raise_exception=True)
+ token, f_token = serializer.auth(request)
+ response = result.success(
+ token,
+ headers={
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "POST",
+ "Access-Control-Allow-Headers": "Origin,Content-Type,Cookie,Accept,Token",
+ },
+ )
+ is_https = request.scheme == "https"
+
+ application_id = serializer.validated_data.get("application_id")
+ cookie_path = f"{CONFIG.get_chat_path()}/{application_id}" if application_id else CONFIG.get_chat_path()
+ response.set_cookie(
+ key="mk_file_auth",
+ value=f_token,
+ max_age=7 * 24 * 3600,
+ path=cookie_path,
+ secure=is_https,
+ httponly=True,
+ samesite="None" if is_https else "Lax",
+ )
+ return response
+
+
+class ApplicationProfile(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get application related information"),
+ summary=_("Get application related information"),
+ operation_id=_("V3 Get application related information"), # type: ignore
+ request=None,
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str):
+ return result.success(ApplicationProfileSerializer(data={"application_id": application_id}).profile())
+
+
+class AuthProfile(APIView):
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get application authentication information"),
+ summary=_("Get application authentication information"),
+ operation_id=_("V3 Get application authentication information"), # type: ignore
+ parameters=ChatAuthenticationProfileAPI.get_parameters(),
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ def get(self, request: Request):
+ return result.success(
+ AuthProfileSerializer(data={"application_id": request.query_params.get("application_id")}).profile()
+ )
+
+
+class ChatView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("dialogue"),
+ summary=_("dialogue"),
+ operation_id=_("V3 dialogue"), # type: ignore
+ request=ChatAPI.get_request(),
+ parameters=ChatAPI.get_parameters(),
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def post(self, request: Request, application_id: str, chat_id: str):
+ ip_address = _get_ip_address(request)
+ return ChatSerializers(
+ data={
+ "chat_id": chat_id,
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "application_id": application_id,
+ "debug": False,
+ "ip_address": ip_address,
+ "source": {
+ "type": ChatSourceChoices.API_CALL.value
+ if request.user.type == ChatUserType.APPLICATION_API_KEY.value
+ else ChatSourceChoices.ONLINE.value
+ },
+ }
+ ).chat(request.data)
+
+
+class OpenView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get the session id according to the application id"),
+ summary=_("Get the session id according to the application id"),
+ operation_id=_("V3 Get the session id according to the application id"), # type: ignore
+ parameters=ChatOpenAPI.get_parameters(),
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str):
+ ip_address = _get_ip_address(request)
+ return result.success(
+ OpenChatSerializers(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ "chat_user_type": request.user.type,
+ "ip_address": ip_address,
+ "source": {
+ "type": ChatSourceChoices.API_CALL.value
+ if request.user.type == ChatUserType.APPLICATION_API_KEY.value
+ else ChatSourceChoices.ONLINE.value
+ },
+ "debug": False,
+ }
+ ).open()
+ )
+
+
+class CancelWorkflowView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Cancel running workflow"),
+ summary=_("Cancel running workflow"),
+ operation_id=_("V3 Cancel running workflow"), # type: ignore
+ parameters=[
+ OpenApiParameter(
+ name="chat_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, description=_("Chat ID")
+ ),
+ ],
+ responses=None,
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ def post(self, request: Request, chat_id: str):
+ from application.workflow.workflow_run_registry import WorkflowRunRegistry, CancelResult
+
+ result_enum = WorkflowRunRegistry.cancel_by_chat_id(chat_id)
+ if result_enum == CancelResult.CANCELLED:
+ return result.success({"status": "cancelled", "chat_id": chat_id})
+ elif result_enum == CancelResult.NOT_FOUND:
+ return result.success({"status": "not_found", "chat_id": chat_id})
+ else:
+ return result.fail(500, _("Failed to cancel workflow"))
+
+
+class CaptchaView(APIView):
+ @extend_schema(
+ methods=["GET"],
+ summary=_("Get Chat captcha"),
+ description=_("Get Chat captcha"),
+ operation_id=_("V3 Get Chat captcha"), # type: ignore
+ tags=[_("V3 Chat")], # type: ignore
+ responses=CaptchaAPI.get_response(),
+ )
+ def get(self, request: Request):
+ username = request.query_params.get("username", None)
+ application_id = request.query_params.get("application_id", None)
+ return result.success(CaptchaSerializer().chat_generate(username, "chat", application_id))
+
+
+class SpeechToText(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("speech to text"),
+ summary=_("speech to text"),
+ operation_id=_("V3 speech to text"), # type: ignore
+ request=SpeechToTextAPI.get_request(),
+ responses=SpeechToTextAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def post(self, request: Request, application_id: str):
+ return result.success(
+ SpeechToTextSerializers(data={"application_id": application_id}).speech_to_text(
+ {"file": request.FILES.get("file")}
+ )
+ )
+
+
+class TextToSpeech(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("text to speech"),
+ summary=_("text to speech"),
+ operation_id=_("V3 text to speech"), # type: ignore
+ request=TextToSpeechAPI.get_request(),
+ responses=TextToSpeechAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def post(self, request: Request, application_id: str):
+ byte_data = TextToSpeechSerializers(data={"application_id": application_id}).text_to_speech(request.data)
+ return HttpResponse(
+ byte_data,
+ status=200,
+ headers={"Content-Type": "audio/mp3", "Content-Disposition": 'attachment; filename="abc.mp3"'},
+ )
+
+
+class UploadFile(APIView):
+ authentication_classes = [ChatTokenAuth]
+ parser_classes = [MultiPartParser]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Upload files"),
+ summary=_("Upload files"),
+ operation_id=_("V3 Upload files"), # type: ignore
+ request=TextToSpeechAPI.get_request(),
+ responses=TextToSpeechAPI.get_response(),
+ tags=[_("V3 Application")], # type: ignore
+ )
+ def post(self, request: Request, chat_id: str):
+ files = request.FILES.getlist("file")
+ file_ids = []
+ meta = {}
+ for file in files:
+ file_url = FileSerializer(
+ data={
+ "file": file,
+ "meta": meta,
+ "source_id": chat_id,
+ "source_type": FileSourceType.CHAT,
+ }
+ ).upload(request.user.id)
+ file_ids.append({"name": file.name, "url": file_url, "file_id": file_url.split("/")[-1]})
+ return result.success(file_ids)
+
+
+class ResetCurrentUserPasswordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ summary=_("Modify current user password"),
+ description=_("Modify current user password"),
+ operation_id=_("V3 Modify current user password"), # type: ignore
+ tags=[_("V3 Chat User")], # type: ignore
+ request=ResetPasswordAPI.get_request(),
+ responses=DefaultModelResponse.get_response(),
+ )
+ @log(
+ menu="Chat User",
+ operate="Modify current user password",
+ get_operation_object=lambda r, k: {"name": r.user.username},
+ get_details=get_re_password_details,
+ )
+ def post(self, request: Request):
+ request_data = request.data
+ encrypted_data = request_data.get("encryptedData", "")
+ if encrypted_data:
+ try:
+ decrypted_raw = decrypt(encrypted_data)
+ # decrypt 可能返回非 JSON 字符串,防护解析异常
+ decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {}
+ if isinstance(decrypted_data, dict):
+ request_data = decrypted_data
+ except Exception:
+ raise AppApiException(500, _("Invalid encrypted data"))
+ serializer_obj = RePasswordSerializer(data=request_data)
+ if serializer_obj.reset_password(request.user.id):
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ auth = request.META.get("HTTP_AUTHORIZATION")
+ cache.delete(get_key(token=auth), version=version)
+ return result.success(True)
+ return result.error(_("Failed to change password"))
+
+
+class ChatUserProfileView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ summary=_("Get current user information"),
+ description=_("Get current user information"),
+ operation_id=_("V3 Get current user information"), # type: ignore
+ tags=[_("V3 Chat User")], # type: ignore
+ responses=UserProfileAPI.get_response(),
+ )
+ def get(self, request: Request):
+ return result.success(ChatUserProfileSerializer().profile(request.user.profile))
+
+
+class BaseAuthView(APIView):
+ @staticmethod
+ def create_token_and_cache(user, access_token, operate):
+ application_id = None
+ if access_token:
+ application_id = (
+ ApplicationAccessToken.objects.filter(access_token=access_token, is_active=True)
+ .values_list("application_id", flat=True)
+ .first()
+ )
+ token = ChatToken(
+ str(user.id), AuthenticationType.CHAT_USER, str(operate), application_id=application_id
+ ).to_token()
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ cache.set(get_key(token), user, timeout=60 * 60 * 2, version=version)
+ return token, FileToken(str(user.id), AuthenticationType.CHAT_USER.value).to_token()
+
+ @classmethod
+ def generate(self, request, f_token: str, response: HttpResponse, path: str = "/chat"):
+ secure = request.is_secure()
+ response.set_cookie(
+ "mk_file_auth",
+ value=f_token,
+ max_age=7 * 24 * 3600,
+ path=path,
+ domain=None,
+ secure=secure,
+ httponly=True,
+ samesite="Lax",
+ )
+ return response
+
+
+class LocalLoginView(BaseAuthView):
+ @extend_schema(
+ methods=["POST"],
+ description=_("Log in"),
+ summary=_("Log in"),
+ operation_id=_("V3 Log in"), # type: ignore
+ tags=[_("V3 Chat User/login")], # type: ignore
+ request=LoginAPI.get_request(),
+ responses=LoginAPI.get_response(),
+ )
+ def post(self, request: Request):
+ user = ChatUserAccessTokenV3Serializer.local_login(request.data)
+ user.source = "LOCAL"
+ access_token = request.query_params.get("accessToken")
+ token, f_token = self.create_token_and_cache(user, access_token, Operate.LOCAL)
+ response = result.success({"token": token})
+ return self.generate(request, f_token, response, path=f"/chat/{access_token + '/' if access_token else ''}")
+
+
+class Logout(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ summary=_("Sign out"),
+ description=_("Sign out"),
+ operation_id=_("V3 Sign out"), # type: ignore
+ tags=[_("V3 Chat User")], # type: ignore
+ responses=DefaultModelResponse.get_response(),
+ )
+ @log(menu="Chat User/logout", operate="Sign out", get_operation_object=lambda r, k: {"name": r.user.username})
+ def post(self, request: Request):
+ version, get_key = Cache_Version.CHAT_USER_TOKEN.value
+ auth = request.META.get("HTTP_AUTHORIZATION")
+ cache.delete(get_key(token=auth[7:]), version=version)
+ return result.success(True)
diff --git a/apps/chat/views/v3/chat_embed.py b/apps/chat/views/v3/chat_embed.py
new file mode 100644
index 00000000000..4e5310214b0
--- /dev/null
+++ b/apps/chat/views/v3/chat_embed.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: chat_embed.py
+ @date:2025/5/30 15:22
+ @desc:
+"""
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from chat.api.chat_embed_api import ChatEmbedAPI
+from chat.serializers.chat_embed_serializers import ChatEmbedSerializer
+
+
+class ChatEmbedView(APIView):
+
+ @extend_schema(
+ methods=['GET'],
+ description=_('Get embedded js'),
+ summary=_('Get embedded js'),
+ operation_id=_('V3 Get embedded js'), # type: ignore
+ parameters=ChatEmbedAPI.get_parameters(),
+ responses=ChatEmbedAPI.get_response(),
+ tags=[_('V3 Chat')] # type: ignore
+ )
+ def get(self, request: Request):
+ return ChatEmbedSerializer(
+ data={'protocol': request.query_params.get('protocol'), 'token': request.query_params.get('token'),
+ 'host': request.query_params.get('host'), }).get_embed(params=request.query_params)
diff --git a/apps/chat/views/v3/chat_record.py b/apps/chat/views/v3/chat_record.py
new file mode 100644
index 00000000000..38539992d66
--- /dev/null
+++ b/apps/chat/views/v3/chat_record.py
@@ -0,0 +1,246 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎
+@file: chat_record.py
+@date:2025/6/23 10:42
+@desc: v3 chat record views —— application_id 从 path 获取,用户身份从 request.user(Principal) 获取
+"""
+
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from application.serializers.application_chat_record import ChatRecordOperateSerializer
+from chat.api.chat_api import (
+ HistoricalConversationAPI,
+ PageHistoricalConversationAPI,
+ PageHistoricalConversationRecordAPI,
+ HistoricalConversationRecordAPI,
+ HistoricalConversationOperateAPI,
+)
+from chat.api.vote_api import VoteAPI
+from chat.serializers.chat_record import (
+ VoteSerializer,
+ HistoricalConversationSerializer,
+ HistoricalConversationRecordSerializer,
+ HistoricalConversationOperateSerializer,
+)
+from common import result
+from common.auth import ChatTokenAuth
+from common.auth.authentication import has_permissions
+from common.auth.constants.chat_permission_constants import ChatPermissionConstants
+
+
+class VoteView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["PUT"],
+ description=_("Like, Dislike"),
+ summary=_("Like, Dislike"),
+ operation_id=_("V3 Like, Dislike"), # type: ignore
+ parameters=VoteAPI.get_parameters(),
+ request=VoteAPI.get_request(),
+ responses=VoteAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def put(self, request: Request, application_id: str, chat_id: str, chat_record_id: str):
+ return result.success(
+ VoteSerializer(
+ data={"application_id": application_id, "chat_id": chat_id, "chat_record_id": chat_record_id}
+ ).vote(request.data)
+ )
+
+
+class HistoricalConversationView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation"),
+ summary=_("Get historical conversation"),
+ operation_id=_("V3 Get historical conversation"), # type: ignore
+ parameters=HistoricalConversationAPI.get_parameters(),
+ responses=HistoricalConversationAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str):
+ return result.success(
+ HistoricalConversationSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).list()
+ )
+
+ class Operate(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["PUT"],
+ description=_("Modify conversation about"),
+ summary=_("Modify conversation about"),
+ operation_id=_("V3 Modify conversation about"), # type: ignore
+ parameters=HistoricalConversationOperateAPI.get_parameters(),
+ request=HistoricalConversationOperateAPI.get_request(),
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def put(self, request: Request, application_id: str, chat_id: str):
+ return result.success(
+ HistoricalConversationOperateSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ "chat_id": chat_id,
+ }
+ ).edit_abstract(request.data)
+ )
+
+ @extend_schema(
+ methods=["DELETE"],
+ description=_("Delete history conversation"),
+ summary=_("Delete history conversation"),
+ operation_id=_("V3 Delete history conversation"), # type: ignore
+ parameters=HistoricalConversationOperateAPI.get_parameters(),
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def delete(self, request: Request, application_id: str, chat_id: str):
+ return result.success(
+ HistoricalConversationOperateSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ "chat_id": chat_id,
+ }
+ ).logic_delete()
+ )
+
+ class BatchDelete(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["DELETE"],
+ description=_("Batch delete history conversation"),
+ summary=_("Batch delete history conversation"),
+ operation_id=_("V3 Batch delete history conversation"), # type: ignore
+ parameters=[],
+ responses=HistoricalConversationOperateAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def delete(self, request: Request, application_id: str):
+ return result.success(
+ HistoricalConversationOperateSerializer.Clear(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).batch_logic_delete()
+ )
+
+ class PageView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation by page"),
+ summary=_("Get historical conversation by page"),
+ operation_id=_("V3 Get historical conversation by page"), # type: ignore
+ parameters=PageHistoricalConversationAPI.get_parameters(),
+ responses=PageHistoricalConversationAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str, current_page: int, page_size: int):
+ return result.success(
+ HistoricalConversationSerializer(
+ data={
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).page(current_page, page_size)
+ )
+
+
+class HistoricalConversationRecordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation records"),
+ summary=_("Get historical conversation records"),
+ operation_id=_("V3 Get historical conversation records"), # type: ignore
+ parameters=HistoricalConversationRecordAPI.get_parameters(),
+ responses=HistoricalConversationRecordAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str, chat_id: str):
+ return result.success(
+ HistoricalConversationRecordSerializer(
+ data={
+ "chat_id": chat_id,
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).list()
+ )
+
+ class PageView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get historical conversation records by page "),
+ summary=_("Get historical conversation records by page"),
+ operation_id=_("V3 Get historical conversation records by page"), # type: ignore
+ parameters=PageHistoricalConversationRecordAPI.get_parameters(),
+ responses=PageHistoricalConversationRecordAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str, chat_id: str, current_page: int, page_size: int):
+ return result.success(
+ HistoricalConversationRecordSerializer(
+ data={
+ "chat_id": chat_id,
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).page(current_page, page_size)
+ )
+
+
+class ChatRecordView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get conversation details"),
+ summary=_("Get conversation details"),
+ operation_id=_("V3 Get conversation details"), # type: ignore
+ parameters=PageHistoricalConversationRecordAPI.get_parameters(),
+ responses=PageHistoricalConversationRecordAPI.get_response(),
+ tags=[_("V3 Chat")], # type: ignore
+ )
+ @has_permissions(ChatPermissionConstants.get_aggregate_permissions())
+ def get(self, request: Request, application_id: str, chat_id: str, chat_record_id: str):
+ return result.success(
+ ChatRecordOperateSerializer(
+ data={
+ "chat_id": chat_id,
+ "chat_record_id": chat_record_id,
+ "application_id": application_id,
+ "chat_user_id": request.user.id,
+ }
+ ).one(False)
+ )
diff --git a/apps/chat/views/v3/chat_user_api_key.py b/apps/chat/views/v3/chat_user_api_key.py
new file mode 100644
index 00000000000..a8430e0fe28
--- /dev/null
+++ b/apps/chat/views/v3/chat_user_api_key.py
@@ -0,0 +1,69 @@
+from common.auth import ChatTokenAuth
+from common.log.log import log
+from common.result import result
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import OpenApiParameter
+
+from chat.serializers.chat_user_api_key_serializers import ChatUserApiKeySerializer
+
+
+class ChatUserApiKeyView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["POST"],
+ description=_("Create ChatUserAPIKey"),
+ summary=_("Create ChatUserAPIKey"),
+ operation_id="V3 Create ChatUserAPIKey",
+ responses=None,
+ tags=[_("V3 Chat User API Key")],
+ )
+ @log(menu="Chat User API Key", operate="Add chat user API key")
+ def post(self, request: Request):
+ return result.success(ChatUserApiKeySerializer(data={"user_id": request.user.id}).generate())
+
+ class Page(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get ChatUserAPIKey List"),
+ summary=_("Get ChatUserAPIKey List"),
+ operation_id="V3 Get ChatUserAPIKey List",
+ parameters=[
+ OpenApiParameter(name='order_by', type=OpenApiTypes.STR, location=OpenApiParameter.QUERY,
+ description=_('order by'), required=False),
+ ],
+ responses=None,
+ tags=[_("V3 Chat User API Key")],
+ )
+ def get(self, request: Request, current_page, page_size):
+ return result.success(
+ ChatUserApiKeySerializer(
+ data={"user_id": request.user.id, "order_by": request.query_params.get("order_by")}
+ ).page(current_page, page_size)
+ )
+
+ class Operate(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["DELETE"],
+ description=_("Delete ChatUserAPIKey"),
+ summary=_("Delete ChatUserAPIKey"),
+ operation_id="V3 Delete ChatUserAPIKey",
+ responses=None,
+ parameters=None,
+ tags=[_("V3 Chat User API Key")],
+ )
+ @log(menu="Chat User API Key", operate="Delete chat user API key")
+ def delete(self, request: Request, api_key_id: str):
+ return result.success(
+ ChatUserApiKeySerializer.Operate(
+ data={"id": api_key_id, "user_id": request.user.id}
+ ).destroy()
+ )
diff --git a/apps/chat/views/mcp.py b/apps/chat/views/v3/mcp.py
similarity index 100%
rename from apps/chat/views/mcp.py
rename to apps/chat/views/v3/mcp.py
diff --git a/apps/chat/views/v3/portal.py b/apps/chat/views/v3/portal.py
new file mode 100644
index 00000000000..3125ddc7b87
--- /dev/null
+++ b/apps/chat/views/v3/portal.py
@@ -0,0 +1,63 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:MaxKB
+@file: portal.py
+@date:2026/8/14
+@desc: 门户视图
+"""
+
+from django.utils.translation import gettext_lazy as _
+from drf_spectacular.utils import extend_schema
+from rest_framework.request import Request
+from rest_framework.views import APIView
+
+from common import result
+from common.auth import ChatTokenAuth
+from common.utils.common import query_params_to_single_dict
+
+from chat.api.portal_api import PortalAPI
+from chat.serializers.portal import (
+ PortalApplicationSerializer,
+ PortalHistoricalConversationSerializer,
+)
+
+
+class PortalApplicationView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get published application list by page"),
+ summary=_("Get published application list by page"),
+ operation_id=_("Get published application list by page"),
+ parameters=PortalAPI.Application.get_parameters(),
+ responses=PortalAPI.Application.get_response(),
+ tags=[_("V3 Chat")],
+ )
+ def get(self, request: Request, current_page: int, page_size: int):
+ return result.success(
+ PortalApplicationSerializer.Query(data={**query_params_to_single_dict(request.query_params)}).page(
+ current_page, page_size, str(request.user.id)
+ )
+ )
+
+
+class PortalHistoricalConversationView(APIView):
+ authentication_classes = [ChatTokenAuth]
+
+ @extend_schema(
+ methods=["GET"],
+ description=_("Get portal historical conversation by page"),
+ summary=_("Get portal historical conversation by page"),
+ operation_id=_("Get portal historical conversation by page"),
+ parameters=PortalAPI.Conversation.get_parameters(),
+ responses=PortalAPI.Conversation.get_response(),
+ tags=[_("V3 Chat")],
+ )
+ def get(self, request: Request, current_page: int, page_size: int):
+ return result.success(
+ PortalHistoricalConversationSerializer.Query(
+ data={**query_params_to_single_dict(request.query_params)}
+ ).page(current_page, page_size, str(request.user.id))
+ )
diff --git a/apps/common/auth/authenticate.py b/apps/common/auth/authenticate.py
index 71ee51bf1f6..9bf27c420aa 100644
--- a/apps/common/auth/authenticate.py
+++ b/apps/common/auth/authenticate.py
@@ -50,9 +50,30 @@ def new_instance_by_class_path(class_path: str):
return HandlerClass()
-handles = [new_instance_by_class_path(class_path) for class_path in settings.AUTH_HANDLES]
-chat_handles = [new_instance_by_class_path(class_path) for class_path in settings.CHAT_AUTH_HANDLES]
-all_handles = handles + chat_handles
+handles = None
+chat_handles = None
+all_handles = None
+
+
+def get_handles():
+ global handles
+ if handles is None:
+ handles = [new_instance_by_class_path(class_path) for class_path in settings.AUTH_HANDLES]
+ return handles
+
+
+def get_chat_handles():
+ global chat_handles
+ if chat_handles is None:
+ chat_handles = [new_instance_by_class_path(class_path) for class_path in settings.CHAT_AUTH_HANDLES]
+ return chat_handles
+
+
+def get_all_handles():
+ global all_handles
+ if all_handles is None:
+ all_handles = get_handles() + get_chat_handles()
+ return all_handles
class TokenDetails:
@@ -85,7 +106,7 @@ def authenticate(self, request):
try:
token = auth[7:]
token_details = TokenDetails(token)
- for handle in handles:
+ for handle in get_handles():
if handle.support(request, token, token_details.get_token_details):
return handle.handle(request, token, token_details.get_token_details)
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user'))
@@ -111,7 +132,7 @@ def authenticate(self, request):
try:
token = auth[7:]
token_details = TokenDetails(token)
- for handle in chat_handles:
+ for handle in get_chat_handles():
if handle.support(request, token, token_details.get_token_details):
return handle.handle(request, token, token_details.get_token_details)
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user'))
@@ -137,7 +158,7 @@ def authenticate(self, request):
try:
token = auth[7:]
token_details = TokenDetails(token)
- for handle in all_handles:
+ for handle in get_all_handles():
if handle.support(request, token, token_details.get_token_details):
return handle.handle(request, token, token_details.get_token_details)
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user'))
diff --git a/apps/common/auth/authentication.py b/apps/common/auth/authentication.py
index 8ee80f324f5..c3b1c8b5d74 100644
--- a/apps/common/auth/authentication.py
+++ b/apps/common/auth/authentication.py
@@ -1,142 +1,63 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: authentication.py
- @date:2025/4/15 20:12
- @desc:
+@project: MaxKB
+@file: authentication.py
+@desc: 适配层,复用 AggregatePermission
"""
-from typing import List
from django.utils.translation import gettext_lazy as _
-from rest_framework.request import Request
-
-from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants, \
- Permission, Role
-from common.exception.app_exception import AppUnauthorizedFailed
-
-
-def exist_permissions_by_permission_constants(user_permission: List[PermissionConstants],
- permission_list: List[PermissionConstants]):
- """
- 用户是否拥有 permission_list的权限
- :param user_permission: 用户权限
- :param permission_list: 需要的权限
- :return: 是否拥有
- """
- return any(list(map(lambda up: permission_list.__contains__(up), user_permission)))
-
-
-def exist_role_by_role_constants(user_role: List[RoleConstants],
- role_list: List[RoleConstants]):
- """
- 用户是否拥有这个角色
- :param user_role: 用户角色
- :param role_list: 需要拥有的角色
- :return: 是否拥有
- """
- return any([True for role in role_list if user_role.__contains__(role.value.__str__())])
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.aggregate_permission import AggregatePermission
+from common.auth.struct.permission import Role
-def exist_permissions_by_view_permission(user_role: List[RoleConstants],
- user_permission: List[PermissionConstants | object],
- permission: ViewPermission, request, **kwargs):
- """
- 用户是否存在这些权限
- :param request:
- :param user_role: 用户角色
- :param user_permission: 用户权限
- :param permission: 所属权限
- :return: 是否存在 True False
- """
-
- role_list = [user_r(request, kwargs) if callable(user_r) else user_r for user_r in
- permission.roleList]
- role_ok = any(list(map(lambda up: role_list.__contains__(up),
- user_role)))
- permission_list = [user_p(request, kwargs) if callable(user_p) else user_p for user_p in
- permission.permissionList
- ]
- permission_ok = any(list(map(lambda up: permission_list.__contains__(up),
- user_permission)))
- return role_ok | permission_ok if permission.compare == CompareConstants.OR else role_ok & permission_ok
-
-
-def exist_permissions(user_role: List[RoleConstants], user_permission: List[PermissionConstants], permission, request,
- **kwargs):
- if isinstance(permission, ViewPermission):
- return exist_permissions_by_view_permission(user_role, user_permission, permission, request, **kwargs)
- if isinstance(permission, RoleConstants):
- return exist_role_by_role_constants(user_role, [permission])
- if isinstance(permission, PermissionConstants):
- return exist_permissions_by_permission_constants(user_permission, [permission])
- if isinstance(permission, Permission):
- return user_permission.__contains__(permission)
- if isinstance(permission, Role):
- return user_role.__contains__(permission.__str__())
- return False
+from common.exception.app_exception import AppUnauthorizedFailed
-def exist(user_role: List[RoleConstants], user_permission: List[PermissionConstants], permission, request, **kwargs):
- if callable(permission):
- p = permission(request, kwargs)
- return exist_permissions(user_role, user_permission, p, request, **kwargs)
- return exist_permissions(user_role, user_permission, permission, request, **kwargs)
+def _build(items, request, kwargs, compare) -> AggregatePermission:
+ roles, permissions, aggregates = [], [], []
+ for it in items:
+ if callable(it) and not isinstance(it, AggregatePermission):
+ it = it(request, kwargs)
+ if isinstance(it, AggregatePermission):
+ aggregates.append(it)
+ elif isinstance(it, (RoleConstants, Role)):
+ roles.append(it)
+ else:
+ permissions.append(it)
+ return AggregatePermission(roles=roles, permissions=permissions, aggregatePermissions=aggregates, compare=compare)
def get_is_permissions(request, **kwargs):
def is_permissions(*permission, compare=CompareConstants.OR):
- exit_list = list(
- map(lambda p: exist(request.auth.role_list, request.auth.permission_list, p, request, **kwargs),
- permission))
- return any(exit_list) if compare == CompareConstants.OR else all(exit_list)
+ return _build(permission, request, kwargs, compare).hasPermission(request, **kwargs)
return is_permissions
-def check_batch_permissions(request: Request, id_list: List[str], id_key: str, permissions: tuple,
- compare=CompareConstants.OR, **kwargs) -> List[str]:
+def check_batch_permissions(request, id_list, id_key, permissions, compare=CompareConstants.OR, **kwargs):
if not id_list:
return []
-
- # workspace manager 直接放行
- # 预检
- kwargs[id_key] = '__workspace_level_pre_check__'
- pre_check = list(
- map(lambda p: exist(request.auth.role_list, request.auth.permission_list, p, request, **kwargs),
- permissions)
- )
- if any(pre_check) if compare == CompareConstants.OR else all(pre_check):
+ kwargs[id_key] = "__workspace_level_pre_check__"
+ if _build(permissions, request, kwargs, compare).hasPermission(request, **kwargs):
return list(id_list)
- # 逐个资源校验
result_list = []
for resource_id in id_list:
kwargs[id_key] = resource_id
- exit_list = list(
- map(lambda p: exist(request.auth.role_list, request.auth.permission_list, p, request, **kwargs),
- permissions)
- )
- if any(exit_list) if compare == CompareConstants.OR else all(exit_list):
+ if _build(permissions, request, kwargs, compare).hasPermission(request, **kwargs):
result_list.append(resource_id)
return result_list
+
def has_permissions(*permission, compare=CompareConstants.OR):
- """
- 权限 role or permission
- :param compare: 比较符号
- :param permission: 如果是角色 role:roleId
- :return: 权限装饰器函数,用于判断用户是否有权限访问当前接口
- """
+ """接口权限装饰器"""
def inner(func):
def run(view, request, **kwargs):
- exit_list = list(
- map(lambda p: exist(request.auth.role_list, request.auth.permission_list, p, request, **kwargs),
- permission))
- # 判断是否有权限
- if any(exit_list) if compare == CompareConstants.OR else all(exit_list):
+ if _build(permission, request, kwargs, compare).hasPermission(request, **kwargs):
return func(view, request, **kwargs)
- raise AppUnauthorizedFailed(403, _('No permission to access'))
+ raise AppUnauthorizedFailed(403, _("No permission to access"))
return run
diff --git a/apps/common/auth/common.py b/apps/common/auth/common.py
index ad8e0e50a48..7ee23988e57 100644
--- a/apps/common/auth/common.py
+++ b/apps/common/auth/common.py
@@ -1,17 +1,20 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: common.py
- @date:2025/6/6 19:55
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: common.py
+@date:2025/6/6 19:55
+@desc:
"""
+
import hashlib
import json
import threading
from django.core import signing, cache
+from application.models import ChatUserType
+from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version
from common.utils.rsa_util import encrypt, decrypt
@@ -54,9 +57,39 @@ def new_instance(authentication: str):
return ChatAuthentication(**auth)
+class FileToken:
+ def __init__(self, user_id, _type, application_id: str = None):
+ self.user_id = user_id
+ self.type = _type
+ self.application_id = application_id
+
+ def to_dict(self):
+ return (
+ {"user_id": self.user_id, "type": str(self.type), "application_id": self.application_id}
+ if self.application_id
+ else {"user_id": self.user_id, "type": str(self.type)}
+ )
+
+ def to_token(self):
+ return signing.dumps(self.to_dict())
+
+ @staticmethod
+ def new_instance(token):
+ token_dict = signing.loads(token)
+ return FileToken(token_dict.get("user_id"), token_dict.get("type"), token_dict.get("application_id"))
+
+
class ChatUserToken:
- def __init__(self, application_id, user_id, access_token, _type, chat_user_type, chat_user_id,
- authentication: ChatAuthentication):
+ def __init__(
+ self,
+ application_id,
+ user_id,
+ access_token,
+ _type,
+ chat_user_type,
+ chat_user_id,
+ authentication: ChatAuthentication,
+ ):
self.application_id = application_id
self.user_id = user_id
self.access_token = access_token
@@ -67,13 +100,13 @@ def __init__(self, application_id, user_id, access_token, _type, chat_user_type,
def to_dict(self):
return {
- 'application_id': str(self.application_id),
- 'user_id': str(self.user_id),
- 'access_token': self.access_token,
- 'type': str(self.type.value),
- 'chat_user_type': str(self.chat_user_type),
- 'chat_user_id': str(self.chat_user_id),
- 'authentication': self.authentication.to_string()
+ "application_id": str(self.application_id),
+ "user_id": str(self.user_id),
+ "access_token": self.access_token,
+ "type": str(self.type.value),
+ "chat_user_type": str(self.chat_user_type),
+ "chat_user_id": str(self.chat_user_id),
+ "authentication": self.authentication.to_string(),
}
def to_token(self):
@@ -81,7 +114,31 @@ def to_token(self):
@staticmethod
def new_instance(token_dict):
- return ChatUserToken(token_dict.get('application_id'), token_dict.get('user_id'),
- token_dict.get('access_token'), token_dict.get('type'), token_dict.get('chat_user_type'),
- token_dict.get('chat_user_id'),
- ChatAuthentication.new_instance(token_dict.get('authentication')))
+ return ChatUserToken(
+ token_dict.get("application_id"),
+ token_dict.get("user_id"),
+ token_dict.get("access_token"),
+ token_dict.get("type"),
+ token_dict.get("chat_user_type"),
+ token_dict.get("chat_user_id"),
+ ChatAuthentication.new_instance(token_dict.get("authentication")),
+ )
+
+
+class ChatToken:
+ def __init__(self, user_id, _type: AuthenticationType, login_type: str, **kwargs):
+ self.user_id = user_id
+ self.type = _type
+ self.login_type = login_type
+ self.kwargs = kwargs
+
+ def to_dict(self):
+ return {
+ "user_id": str(self.user_id),
+ "type": str(self.type.value),
+ "login_type": str(self.login_type),
+ "kwargs": self.kwargs,
+ }
+
+ def to_token(self):
+ return signing.dumps(self.to_dict())
diff --git a/apps/common/auth/constants/category_constants.py b/apps/common/auth/constants/category_constants.py
new file mode 100644
index 00000000000..734a2cefb20
--- /dev/null
+++ b/apps/common/auth/constants/category_constants.py
@@ -0,0 +1,36 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: category_constants.py
+ @date:2026/8/3 17:31
+ @desc: 一级目录分类常量(最顶层分类)
+"""
+from enum import Enum
+from django.utils.translation import gettext_lazy as _
+
+
+class Category(Enum):
+ """
+ 一级目录(最顶层分类),用于在 parent_group 之上再加一层分类
+ """
+ # 身份与权限
+ IAM = ("IAM", _("IAM"))
+ # 资源管理
+ RESOURCE = ("RESOURCE", _("Resource"))
+ # 共享资源
+ SHARED = ("SHARED", _("Shared"))
+ # 对话客户端
+ CHAT_CLIENT = ("CHAT_CLIENT", _("Chat Client"))
+ # 操作日志
+ OPERATION_LOG = ("OPERATION_LOG", _("Operation Log"))
+ # 系统设置
+ SYSTEM_SETTING = ("SYSTEM_SETTING", _("System Setting"))
+ # 工作空间
+ WORKSPACE = ("WORKSPACE", _("Workspace"))
+ # 其他
+ OTHER = ("OTHER", _("Other"))
+
+ def __init__(self, value, label):
+ self._value_ = value
+ self.label = label
diff --git a/apps/common/auth/constants/chat_permission_constants.py b/apps/common/auth/constants/chat_permission_constants.py
new file mode 100644
index 00000000000..4a1d3e16ab9
--- /dev/null
+++ b/apps/common/auth/constants/chat_permission_constants.py
@@ -0,0 +1,50 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: chat_permission_constants.py
+ @date:2026/8/6 16:38
+ @desc:
+"""
+from enum import Enum
+
+from common.auth.constants.group_constants import Group
+from common.auth.constants.operate_constants import Operate
+from common.auth.struct.aggregate_permission import AggregatePermission
+from common.auth.struct.permission import Permission
+
+
+class ChatPermissionConstants(Enum):
+ CHAT_USER_ANONYMOUS = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.ANNOTATION_AUTH, 0)
+ CHAT_USER_PASSWORD = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.PASSWORD, 1)
+ CHAT_USER_LOCAL = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LOCAL, 2)
+ CHAT_USER_CAS = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.CAS, 3)
+ CHAT_USER_DINGTALK = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.DINGTALK, 4)
+ CHAT_USER_WECOM = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.WECOM, 5)
+ CHAT_USER_LARK = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LARK, 6)
+ CHAT_USER_OIDC = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.OIDC, 7)
+ CHAT_USER_LDAP = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LDAP, 8)
+ CHAT_USER_OAUTH2 = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.OAUTH2, 9)
+
+ def get_permission(self):
+ return self._build_workspace_permission('application_id')
+
+ def _build_workspace_permission(self, resource_id_key=None):
+ def permission_factory(_, **kwargs):
+ return Permission(group=self.value.group,
+ sub_group=self.value.sub_group,
+ operate=self.value.operate,
+ bit_index=self.value.bit_index,
+ workspace_id=kwargs.get('workspace_id'),
+ resource_id=kwargs.get(resource_id_key) if resource_id_key else None)
+
+ return permission_factory
+
+ @staticmethod
+ def get_aggregate_permissions():
+ return AggregatePermission(
+ permissions=[_permission.get_permission() for _permission in ChatPermissionConstants])
+
+
+# 权限字符串与权限对象的Map
+CHAT_PERMISSION_STR_MAP = {_permission.value.__str__(): _permission for _permission in ChatPermissionConstants}
diff --git a/apps/common/auth/constants/compare_constants.py b/apps/common/auth/constants/compare_constants.py
new file mode 100644
index 00000000000..7c6a34dd0c7
--- /dev/null
+++ b/apps/common/auth/constants/compare_constants.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: compare_constants.py
+ @date:2026/8/5 10:42
+ @desc:
+"""
+from enum import Enum
+
+
+class CompareConstants(Enum):
+ # 或者
+ OR = "OR"
+ # 并且
+ AND = "AND"
diff --git a/apps/common/auth/constants/group_constants.py b/apps/common/auth/constants/group_constants.py
new file mode 100644
index 00000000000..beaeb226d9a
--- /dev/null
+++ b/apps/common/auth/constants/group_constants.py
@@ -0,0 +1,126 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: group_constants.py
+@date:2026/8/3 17:31
+@desc: 权限分组常量,用于菜单和权限分类
+"""
+
+from enum import Enum
+
+from django.utils.translation import gettext_lazy as _
+
+
+class Group(Enum):
+ """
+ 权限组 一个组一般对应前端一个菜单
+ 使用方式:
+ - 无子分组: group=Group.TOOL, sub_group=Group.TOOL
+ - 有子分组: group=Group.TOOL, sub_group=Group.FOLDER
+ """
+
+ # 用户管理
+ USER = ("USER_MANAGEMENT", _("User Management"))
+
+ # 资源主分组
+ APPLICATION = ("APPLICATION", _("Application"))
+ KNOWLEDGE = ("KNOWLEDGE", _("Knowledge"))
+ MODEL = ("MODEL", _("Model"))
+ TOOL = ("TOOL", _("Tool"))
+ TRIGGER = ("TRIGGER", _("Trigger"))
+
+ # 子分组 - 文件夹
+ FOLDER = ("FOLDER", _("Folder"))
+
+ # 子分组 - 知识库
+ DOCUMENT = ("DOCUMENT", _("Document"))
+ WORKFLOW = ("WORKFLOW", _("Workflow"))
+ TAG = ("TAG", _("Tag"))
+ PROBLEM = ("PROBLEM", _("Problem"))
+ TERMBASE = ("TERMBASE", _("Termbase"))
+ HIT_TEST = ("HIT_TEST", _("Hit-Test"))
+
+ # 子分组 - 应用
+ OVERVIEW = ("OVERVIEW", _("Overview"))
+ ACCESS = ("ACCESS", _("Application Access"))
+ CHAT_LOG = ("CHAT_LOG", _("Conversation log"))
+ CHAT_USER = ("CHAT_USER", _("Dialogue users"))
+
+ # 子分组 - 对话用户(知识库)
+ KNOWLEDGE_CHAT_USER = ("KNOWLEDGE_CHAT_USER", _("Dialogue users"))
+
+ # 系统功能分组
+ ROLE = ("ROLE", _("Role Management"))
+ WORKSPACE = ("WORKSPACE", _("Workspace"))
+ USER_GROUP = ("USER_GROUP", _("User Group"))
+ EMAIL_SETTING = ("EMAIL_SETTING", _("Email Setting"))
+ LOGIN_AUTH = ("LOGIN_AUTH", _("Login Auth"))
+ APPEARANCE_SETTINGS = ("APPEARANCE_SETTINGS", _("Appearance Settings"))
+ DISPLAY_SETTINGS = ("DISPLAY_SETTINGS", _("Display Settings"))
+
+ # 对话相关
+ CHAT_USER_GROUP = ("CHAT_USER_GROUP", _("Chat User Group"))
+ CHAT_USER_AUTH = ("CHAT_USER_AUTH", _("Chat User Auth"))
+ PORTAL = ("PORTAL", _("portal"))
+ # 其他
+ OTHER = ("OTHER", _("Other"))
+ HOMEPAGE = ("HOMEPAGE", _("Home page"))
+ OPERATION_LOG = ("OPERATION_LOG", _("Operation Log"))
+
+ # 资源授权分组
+ RESOURCE_PERMISSION = ("RESOURCE_PERMISSION", _("Resource Permission"))
+ APPLICATION_RESOURCE_PERMISSION = (
+ "APPLICATION_RESOURCE_PERMISSION",
+ _("Application"),
+ )
+ KNOWLEDGE_RESOURCE_PERMISSION = ("KNOWLEDGE_RESOURCE_PERMISSION", _("Knowledge"))
+ TOOL_RESOURCE_PERMISSION = ("TOOL_RESOURCE_PERMISSION", _("Tool"))
+ MODEL_RESOURCE_PERMISSION = ("MODEL_RESOURCE_PERMISSION", _("Model"))
+
+ # 工作空间分组
+ WORKSPACE_ROLE = ("WORKSPACE_ROLE", _("Role Management"))
+ WORKSPACE_WORKSPACE = ("WORKSPACE_WORKSPACE", _("Workspace"))
+ WORKSPACE_USER_GROUP = ("WORKSPACE_USER_GROUP", _("User Group"))
+ WORKSPACE_RESOURCE_PERMISSION = ("WORKSPACE_RESOURCE_PERMISSION", _("Resource Permission"))
+ WORKSPACE_CHAT_USER = ("WORKSPACE_CHAT_USER", _("Chat User"))
+ WORKSPACE_CHAT_USER_GROUP = ("WORKSPACE_CHAT_USER_GROUP", _("Chat User Group"))
+
+ # 用户级分组
+ USER_HOMEPAGE = ("USER_HOMEPAGE", _("Home page"))
+ USER_APPLICATION = ("USER_APPLICATION", _("Application"))
+ USER_KNOWLEDGE = ("USER_KNOWLEDGE", _("Knowledge"))
+ USER_MODEL = ("USER_MODEL", _("Model"))
+ USER_TOOL = ("USER_TOOL", _("Tool"))
+ USER_OTHER = ("USER_OTHER", _("Other"))
+
+ # 共享资源分组
+ SYSTEM_KNOWLEDGE = ("SYSTEM_KNOWLEDGE", _("Knowledge"))
+ SYSTEM_MODEL = ("SYSTEM_MODEL", _("Model"))
+ SYSTEM_TOOL = ("SYSTEM_TOOL", _("Tool"))
+
+ # 资源管理分组
+ SYSTEM_RES_APPLICATION = ("SYSTEM_RESOURCE_APPLICATION", _("Application"))
+ SYSTEM_RES_KNOWLEDGE = ("SYSTEM_RESOURCE_KNOWLEDGE", _("Knowledge"))
+ SYSTEM_RES_TOOL = ("SYSTEM_RESOURCE_TOOL", _("Tool"))
+ SYSTEM_RES_MODEL = ("SYSTEM_RESOURCE_MODEL", _("Model"))
+
+ # 系统资源子分组
+ SYSTEM_DOCUMENT = ("SYSTEM_DOCUMENT", _("Document"))
+ SYSTEM_WORKFLOW = ("SYSTEM_WORKFLOW", _("Workflow"))
+ SYSTEM_TAG = ("SYSTEM_TAG", _("Tag"))
+ SYSTEM_PROBLEM = ("SYSTEM_PROBLEM", _("Problem"))
+ SYSTEM_TERMBASE = ("SYSTEM_TERMBASE", _("Termbase"))
+ SYSTEM_HIT_TEST = ("SYSTEM_HIT_TEST", _("Hit-Test"))
+ SYSTEM_CHAT_USER = ("SYSTEM_CHAT_USER", _("Dialogue users"))
+ SYSTEM_OVERVIEW = ("SYSTEM_OVERVIEW", _("Overview"))
+ SYSTEM_ACCESS = ("SYSTEM_ACCESS", _("Application Access"))
+ SYSTEM_CHAT_LOG = ("SYSTEM_CHAT_LOG", _("Conversation log"))
+ CHAT = ("CHAT", _("Chat"))
+
+ def __init__(self, value, label):
+ self._value_ = value
+ self.label = label
+
+ def __str__(self):
+ return self.value
diff --git a/apps/common/auth/constants/operate_constants.py b/apps/common/auth/constants/operate_constants.py
new file mode 100644
index 00000000000..835c5e86935
--- /dev/null
+++ b/apps/common/auth/constants/operate_constants.py
@@ -0,0 +1,96 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: operate_constants.py
+@date:2026/8/3 17:32
+@desc: 操作权限常量
+"""
+
+from enum import Enum
+from django.utils.translation import gettext_lazy as _
+
+
+class Operate(Enum):
+ """
+ 一个权限组的操作权限
+ """
+
+ SELF = ("", "")
+ READ = ("READ", _("Read"))
+ EDIT = ("READ+EDIT", _("Edit"))
+ CREATE = ("READ+CREATE", _("Create"))
+ DELETE = ("READ+DELETE", _("Delete"))
+ """
+ 使用权限
+ """
+ USE = ("USE", _("Use"))
+ IMPORT = ("READ+IMPORT", _("Import"))
+ EXPORT = ("READ+EXPORT", _("Export"))
+ PUBLISH = ("READ+PUBLISH", _("Publish"))
+ SYNC = ("READ+SYNC", _("Sync"))
+ GENERATE = ("READ+GENERATE", _("Generate"))
+ ADD_MEMBER = ("READ+ADD_MEMBER", _("Add Member"))
+ REMOVE_MEMBER = ("READ+REMOVE_MEMBER", _("Remove Member"))
+ VECTOR = ("READ+VECTOR", _("Vector"))
+ MIGRATE = ("READ+MIGRATE", _("Migrate"))
+ RELATE = ("READ+RELATE", _("Relate"))
+ USER_GROUP = ("READ+USER_GROUP", _("User Group"))
+ ANNOTATION = ("READ+ANNOTATION", _("Annotation"))
+ CLEAR_POLICY = ("READ+CLEAR_POLICY", _("Clear Policy"))
+ EMBED = ("READ+EMBED", _("Embed third party"))
+ ACCESS = ("READ+ACCESS", _("Access restrictions"))
+ DISPLAY = ("READ+DISPLAY", _("Display Settings"))
+ API_KEY = ("READ+API_KEY", _("API KEY"))
+ PUBLIC_ACCESS = ("READ+PUBLIC_ACCESS", _("Public access link"))
+ Q_WEIXIN = ("READ+Q_WEIXIN", _("Enterprise WeiXin"))
+ FEISHU = ("READ+FEISHU", _("Feishu"))
+ DD = ("READ+DD", _("Dingding"))
+ WEIXIN_PUBLIC_ACCOUNT = ("READ+WEIXIN_PUBLIC_ACCOUNT", _("Weixin Public Account"))
+ SLACK = ("READ+SLACK", _("Slack"))
+ ADD_KNOWLEDGE = ("READ+ADD_KNOWLEDGE", _("Add to Knowledge Base"))
+ TO_CHAT = ("READ+TO_CHAT", _("To Chat"))
+ SETTING = ("READ+SETTING", _("Setting"))
+ DOWNLOAD = ("READ+DOWNLOAD", _("Download Original Document"))
+ COPY = ("READ+COPY", _("Copy"))
+ AUTH = ("READ+AUTH", _("resource authorization"))
+ TAG = ("READ+TAG", _("Tag Setting"))
+ REPLACE = ("READ+REPLACE", _("Replace Original Document"))
+ UPDATE = ("READ+UPDATE", _("Update License"))
+ RELATE_VIEW = ("READ+RELATE_VIEW", _("View related resources"))
+ RECORD = ("READ+RECORD", _("Read execute record"))
+ TRIGGER_READ = ("READ+TRIGGER_READ", _("Read Trigger"))
+ TRIGGER_EDIT = ("READ+TRIGGER_EDIT", _("Edit Trigger"))
+ TRIGGER_CREATE = ("READ+TRIGGER_CREATE", _("Create Trigger"))
+ TRIGGER_DELETE = ("READ+TRIGGER_DELETE", _("Delete Trigger"))
+ BATCH_DELETE = ("READ+BATCH_DELETE", _("Batch delete"))
+ BATCH_MOVE = ("READ+BATCH_MOVE", _("Batch move"))
+ TOKEN = ("READ+TOKEN", _("Token Index"))
+ TO_WORKSPACE = ("READ+TO_WORKSPACE", _("Authorize to Workspace"))
+ SET_ROLE = ("READ+SET_ROLE", _("Set Role"))
+ QUOTA_SETTING = ("READ+QUOTA_SETTING", _("Quota Setting"))
+
+ ABOUT = ("READ", _("About"))
+ LICENSE = ("READ+UPDATE", _("Update License"))
+ SWITCH_LANGUAGE = ("READ+EDIT", _("Switch Language"))
+ CHANGE_PASSWORD = ("READ+CREATE", _("Change Password"))
+ SYSTEM_API_KEY = ("READ+DELETE", _("System API Key"))
+ PORTAL = ("READ+PORTAL", _("Portal"))
+
+ ANNOTATION_AUTH = ('ANNOTATION', _("Annotation"))
+ PASSWORD = ("PASSWORD", _("Password verification"))
+ LOCAL = ("LOCAL", _("Account login"))
+ CAS = ("CAS", _("CAS"))
+ DINGTALK = ("DINGTALK", _("dingtalk"))
+ WECOM = ("WECOM", _("WeCom"))
+ LARK = ("LARK", _("lark"))
+ OIDC = ("OIDC", _("OIDC"))
+ LDAP = ("LDAP", _("LDAP"))
+ OAUTH2 = ("OAUTH2", _("OAUTH2"))
+
+ def __init__(self, value, label):
+ self._value_ = value
+ self.label = label
+
+ def __str__(self):
+ return self.value
diff --git a/apps/common/auth/constants/permission_constants.py b/apps/common/auth/constants/permission_constants.py
new file mode 100644
index 00000000000..9565a111ae5
--- /dev/null
+++ b/apps/common/auth/constants/permission_constants.py
@@ -0,0 +1,3688 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: permission_constants.py
+@date:2026/8/3 17:28
+@desc: 权限枚举常量(新格式)
+"""
+
+from enum import Enum
+from typing import List, Dict
+
+from common.auth.constants.category_constants import Category
+from common.auth.constants.group_constants import Group
+from common.auth.constants.operate_constants import Operate
+from common.auth.constants.permission_scope_constants import PermissionScopeConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.permission import Permission, PermissionMeta
+
+
+class ResourcePermissionGroup:
+ """资源权限组"""
+
+ def __init__(self, resource: Group, permission: str):
+ self.resource = resource
+ self.permission = permission
+
+ def __eq__(self, other):
+ return str(self.permission) == str(other.permission) and str(self.resource) == str(other.resource)
+
+ def __str__(self):
+ return f"{self.resource}_{self.permission}"
+
+ def __hash__(self):
+ return hash((self.resource, self.permission))
+
+
+class ResourcePermissionConst:
+ """资源权限常量"""
+
+ # 知识库
+ KNOWLEDGE_VIEW = ResourcePermissionGroup(Group.KNOWLEDGE, "VIEW")
+ KNOWLEDGE_MANAGE = ResourcePermissionGroup(Group.KNOWLEDGE, "MANAGE")
+ KNOWLEDGE_FOLDER_VIEW = ResourcePermissionGroup(Group.KNOWLEDGE, "FOLDER_VIEW")
+ KNOWLEDGE_FOLDER_MANAGE = ResourcePermissionGroup(Group.KNOWLEDGE, "FOLDER_MANAGE")
+
+ # 应用
+ APPLICATION_VIEW = ResourcePermissionGroup(Group.APPLICATION, "VIEW")
+ APPLICATION_MANAGE = ResourcePermissionGroup(Group.APPLICATION, "MANAGE")
+ APPLICATION_FOLDER_VIEW = ResourcePermissionGroup(Group.APPLICATION, "FOLDER_VIEW")
+ APPLICATION_FOLDER_MANAGE = ResourcePermissionGroup(Group.APPLICATION, "FOLDER_MANAGE")
+
+ # 工具
+ TOOL_VIEW = ResourcePermissionGroup(Group.TOOL, "VIEW")
+ TOOL_MANAGE = ResourcePermissionGroup(Group.TOOL, "MANAGE")
+ TOOL_FOLDER_VIEW = ResourcePermissionGroup(Group.TOOL, "FOLDER_VIEW")
+ TOOL_FOLDER_MANAGE = ResourcePermissionGroup(Group.TOOL, "FOLDER_MANAGE")
+
+ # 模型
+ MODEL_VIEW = ResourcePermissionGroup(Group.MODEL, "VIEW")
+ MODEL_MANAGE = ResourcePermissionGroup(Group.MODEL, "MANAGE")
+
+
+from maxkb import settings
+
+is_ee: bool = settings.edition == "EE"
+
+
+class PermissionConstants(Enum):
+ """
+ 权限枚举
+ """
+
+ # ==================== 首页 ====================
+ HOMEPAGE_READ = (
+ Permission(group=Group.HOMEPAGE, sub_group=Group.HOMEPAGE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.USER],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ HOMEPAGE_EXPORT = (
+ Permission(group=Group.HOMEPAGE, sub_group=Group.HOMEPAGE, operate=Operate.EXPORT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.USER],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ SYSTEM_HOMEPAGE_READ = (
+ Permission(group=Group.HOMEPAGE, sub_group=Group.HOMEPAGE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM),
+ )
+
+ SYSTEM_HOMEPAGE_EXPORT = (
+ Permission(group=Group.HOMEPAGE, sub_group=Group.HOMEPAGE, operate=Operate.EXPORT, bit_index=1),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM),
+ )
+
+ # ==================== 资源主分组(无子分组) ====================
+ KNOWLEDGE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.SELF, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ APPLICATION = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.SELF, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ MODEL = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.SELF, bit_index=0),
+ PermissionMeta(role_list=[RoleConstants.ADMIN, RoleConstants.USER], category=Category.RESOURCE),
+ )
+
+ TOOL = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.SELF, bit_index=0),
+ PermissionMeta(role_list=[RoleConstants.ADMIN, RoleConstants.USER], category=Category.RESOURCE),
+ )
+
+ # ==================== 用户管理 ====================
+ USER_READ = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_CREATE = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ USER_EDIT = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ USER_DELETE = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ USER_SET_ROLE = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.SET_ROLE, bit_index=4),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ USER_IMPORT = (
+ Permission(group=Group.USER, sub_group=Group.USER, operate=Operate.IMPORT, bit_index=5),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ # ==================== 系统用户组 ====================
+ SYSTEM_USER_GROUP_READ = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_USER_GROUP_CREATE = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_USER_GROUP_EDIT = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_USER_GROUP_DELETE = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_USER_GROUP_ADD_MEMBER = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.ADD_MEMBER, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_USER_GROUP_REMOVE_MEMBER = (
+ Permission(group=Group.USER_GROUP, sub_group=Group.USER_GROUP, operate=Operate.REMOVE_MEMBER, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 模型 ====================
+ MODEL_READ = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.READ, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_VIEW],
+ ),
+ )
+
+ MODEL_CREATE = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.CREATE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_MANAGE],
+ ),
+ )
+
+ MODEL_EDIT = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.EDIT, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_MANAGE],
+ ),
+ )
+
+ MODEL_DELETE = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.DELETE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_MANAGE],
+ ),
+ )
+
+ MODEL_RESOURCE_AUTHORIZATION = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.AUTH, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_MANAGE],
+ ),
+ )
+
+ MODEL_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.MODEL, sub_group=Group.MODEL, operate=Operate.RELATE_VIEW, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.MODEL_MANAGE],
+ ),
+ )
+
+ # ==================== 触发器 ====================
+ TRIGGER_READ = (
+ Permission(group=Group.TRIGGER, sub_group=Group.TRIGGER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TRIGGER_CREATE = (
+ Permission(group=Group.TRIGGER, sub_group=Group.TRIGGER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ TRIGGER_EDIT = (
+ Permission(group=Group.TRIGGER, sub_group=Group.TRIGGER, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TRIGGER_DELETE = (
+ Permission(group=Group.TRIGGER, sub_group=Group.TRIGGER, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TRIGGER_RECORD = (
+ Permission(group=Group.TRIGGER, sub_group=Group.TRIGGER, operate=Operate.RECORD, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE],
+ category=Category.WORKSPACE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 工具 ====================
+ TOOL_READ = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.READ, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_CREATE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.CREATE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ TOOL_BATCH_MOVE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.BATCH_MOVE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_BATCH_DELETE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.BATCH_DELETE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_EDIT = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.EDIT, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_DELETE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.DELETE, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_IMPORT = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.IMPORT, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_EXPORT = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.EXPORT, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_RESOURCE_AUTHORIZATION = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.AUTH, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.RELATE_VIEW, bit_index=10),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_PUBLISH = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.PUBLISH, bit_index=11),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_EXECUTE_RECORD = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.RECORD, bit_index=12),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # 工具触发器
+ TOOL_TRIGGER_READ = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.TRIGGER_READ, bit_index=13),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_TRIGGER_CREATE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.TRIGGER_CREATE, bit_index=14),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_TRIGGER_EDIT = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.TRIGGER_EDIT, bit_index=15),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_TRIGGER_DELETE = (
+ Permission(group=Group.TOOL, sub_group=Group.TOOL, operate=Operate.TRIGGER_DELETE, bit_index=16),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 工具文件夹 ====================
+ TOOL_FOLDER_READ = (
+ Permission(group=Group.TOOL, sub_group=Group.FOLDER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_FOLDER_CREATE = (
+ Permission(group=Group.TOOL, sub_group=Group.FOLDER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ TOOL_FOLDER_EDIT = (
+ Permission(group=Group.TOOL, sub_group=Group.FOLDER, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_FOLDER_DELETE = (
+ Permission(group=Group.TOOL, sub_group=Group.FOLDER, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ TOOL_FOLDER_AUTH = (
+ Permission(group=Group.TOOL, sub_group=Group.FOLDER, operate=Operate.AUTH, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.TOOL_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库 ====================
+ KNOWLEDGE_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.READ, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ ),
+ )
+
+ KNOWLEDGE_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.CREATE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ ),
+ )
+
+ KNOWLEDGE_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.EDIT, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.DELETE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_SYNC = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.SYNC, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_EXPORT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.EXPORT, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_VECTOR = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.VECTOR, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_GENERATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.GENERATE, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_BATCH_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.BATCH_DELETE, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_BATCH_MOVE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.BATCH_MOVE, bit_index=10),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_RESOURCE_AUTHORIZATION = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.AUTH, bit_index=11),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.KNOWLEDGE, operate=Operate.RELATE_VIEW, bit_index=12),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ # ==================== 知识库文件夹 ====================
+ KNOWLEDGE_FOLDER_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.FOLDER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ ),
+ )
+
+ KNOWLEDGE_FOLDER_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.FOLDER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_FOLDER_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.FOLDER, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_FOLDER_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.FOLDER, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_FOLDER_AUTH = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.FOLDER, operate=Operate.AUTH, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ # ==================== 知识库工作流 ====================
+ KNOWLEDGE_WORKFLOW_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.WORKFLOW, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ ),
+ )
+
+ KNOWLEDGE_WORKFLOW_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.WORKFLOW, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_WORKFLOW_EXPORT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.WORKFLOW, operate=Operate.EXPORT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ KNOWLEDGE_WORKFLOW_PUBLISH = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.WORKFLOW, operate=Operate.PUBLISH, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ ),
+ )
+
+ # ==================== 知识库文档 ====================
+ KNOWLEDGE_DOCUMENT_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_SYNC = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.SYNC, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_EXPORT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.EXPORT, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.DOWNLOAD, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_GENERATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.GENERATE, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_VECTOR = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.VECTOR, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_MIGRATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.MIGRATE, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_TAG = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.TAG, bit_index=10),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_REPLACE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.REPLACE, bit_index=11),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_DOCUMENT_TOKEN = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.DOCUMENT, operate=Operate.TOKEN, bit_index=12),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库命中测试 ====================
+ KNOWLEDGE_HIT_TEST = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.HIT_TEST, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库问题 ====================
+ KNOWLEDGE_PROBLEM_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.PROBLEM, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_PROBLEM_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.PROBLEM, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ KNOWLEDGE_PROBLEM_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.PROBLEM, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_PROBLEM_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.PROBLEM, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_PROBLEM_RELATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.PROBLEM, operate=Operate.RELATE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库术语库 ====================
+ KNOWLEDGE_TERMBASE_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TERMBASE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_TERMBASE_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TERMBASE, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ KNOWLEDGE_TERMBASE_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TERMBASE, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_TERMBASE_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TERMBASE, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库标签 ====================
+ KNOWLEDGE_TAG_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TAG, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_TAG_CREATE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TAG, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ KNOWLEDGE_TAG_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TAG, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_TAG_DELETE = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.TAG, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 知识库对话用户 ====================
+ KNOWLEDGE_CHAT_USER_READ = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.CHAT_USER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ KNOWLEDGE_CHAT_USER_EDIT = (
+ Permission(group=Group.KNOWLEDGE, sub_group=Group.CHAT_USER, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANAGE],
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 资源授权 ====================
+ APPLICATION_RESOURCE_PERMISSION_READ = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.APPLICATION_RESOURCE_PERMISSION,
+ operate=Operate.READ,
+ bit_index=0,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ APPLICATION_RESOURCE_PERMISSION_EDIT = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.APPLICATION_RESOURCE_PERMISSION,
+ operate=Operate.EDIT,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ KNOWLEDGE_RESOURCE_PERMISSION_READ = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.KNOWLEDGE_RESOURCE_PERMISSION,
+ operate=Operate.READ,
+ bit_index=0,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ KNOWLEDGE_RESOURCE_PERMISSION_EDIT = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.KNOWLEDGE_RESOURCE_PERMISSION,
+ operate=Operate.EDIT,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ TOOL_RESOURCE_PERMISSION_READ = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION, sub_group=Group.TOOL_RESOURCE_PERMISSION, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ TOOL_RESOURCE_PERMISSION_EDIT = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.TOOL_RESOURCE_PERMISSION,
+ operate=Operate.EDIT,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ MODEL_RESOURCE_PERMISSION_READ = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.MODEL_RESOURCE_PERMISSION,
+ operate=Operate.READ,
+ bit_index=0,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ MODEL_RESOURCE_PERMISSION_EDIT = (
+ Permission(
+ group=Group.RESOURCE_PERMISSION,
+ sub_group=Group.MODEL_RESOURCE_PERMISSION,
+ operate=Operate.EDIT,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 邮件设置 ====================
+ EMAIL_SETTING_READ = (
+ Permission(group=Group.EMAIL_SETTING, sub_group=Group.EMAIL_SETTING, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ EMAIL_SETTING_EDIT = (
+ Permission(group=Group.EMAIL_SETTING, sub_group=Group.EMAIL_SETTING, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ # ==================== 角色管理 ====================
+ ROLE_READ = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ ROLE_CREATE = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ ROLE_EDIT = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ ROLE_DELETE = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], category=Category.IAM, scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ ROLE_ADD_MEMBER = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.ADD_MEMBER, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ ROLE_REMOVE_MEMBER = (
+ Permission(group=Group.ROLE, sub_group=Group.ROLE, operate=Operate.REMOVE_MEMBER, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 工作空间管理 ====================
+ WORKSPACE_READ = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ WORKSPACE_CREATE = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.IAM, is_ee=is_ee, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ WORKSPACE_EDIT = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.IAM, is_ee=is_ee, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ WORKSPACE_DELETE = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.IAM, is_ee=is_ee, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ WORKSPACE_ADD_MEMBER = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.ADD_MEMBER, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ WORKSPACE_REMOVE_MEMBER = (
+ Permission(group=Group.WORKSPACE, sub_group=Group.WORKSPACE, operate=Operate.REMOVE_MEMBER, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.IAM,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 登录认证 ====================
+ LOGIN_AUTH_READ = (
+ Permission(group=Group.LOGIN_AUTH, sub_group=Group.LOGIN_AUTH, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ LOGIN_AUTH_EDIT = (
+ Permission(group=Group.LOGIN_AUTH, sub_group=Group.LOGIN_AUTH, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ # ==================== 应用 ====================
+ APPLICATION_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.READ, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_CREATE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.CREATE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_COPY = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.COPY, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_EDIT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.EDIT, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_DELETE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.DELETE, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_IMPORT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.IMPORT, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_EXPORT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.EXPORT, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_PUBLISH = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.PUBLISH, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_BATCH_DELETE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.BATCH_DELETE, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_BATCH_MOVE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.BATCH_MOVE, bit_index=10),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_RESOURCE_AUTHORIZATION = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.AUTH, bit_index=11),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.RELATE_VIEW, bit_index=12),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # 应用触发器
+ APPLICATION_TRIGGER_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.TRIGGER_READ, bit_index=13),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ APPLICATION_TRIGGER_CREATE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.TRIGGER_CREATE, bit_index=14),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ ),
+ )
+
+ APPLICATION_TRIGGER_EDIT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.TRIGGER_EDIT, bit_index=15),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ APPLICATION_TRIGGER_DELETE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.APPLICATION, operate=Operate.TRIGGER_DELETE, bit_index=16),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ ),
+ )
+
+ # ==================== 应用文件夹 ====================
+ APPLICATION_FOLDER_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_FOLDER_CREATE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_FOLDER_EDIT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_FOLDER_DELETE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_FOLDER_AUTH = (
+ Permission(group=Group.APPLICATION, sub_group=Group.FOLDER, operate=Operate.AUTH, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # ==================== 应用概览 ====================
+ APPLICATION_OVERVIEW_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_OVERVIEW_EMBED = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.EMBED, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_OVERVIEW_ACCESS = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.ACCESS, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_OVERVIEW_DISPLAY = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.DISPLAY, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_OVERVIEW_API_KEY = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.API_KEY, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_OVERVIEW_PUBLIC = (
+ Permission(group=Group.APPLICATION, sub_group=Group.OVERVIEW, operate=Operate.PUBLIC_ACCESS, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # ==================== 应用接入 ====================
+ APPLICATION_ACCESS_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.ACCESS, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_ACCESS_EDIT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.ACCESS, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # ==================== 应用对话用户 ====================
+ APPLICATION_CHAT_USER_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_USER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_CHAT_USER_EDIT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_USER, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # ==================== 应用对话日志 ====================
+ APPLICATION_CHAT_LOG_READ = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_LOG, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
+ ),
+ )
+
+ APPLICATION_CHAT_LOG_ANNOTATION = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_LOG, operate=Operate.ANNOTATION, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_CHAT_LOG_EXPORT = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_LOG, operate=Operate.EXPORT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_CHAT_LOG_CLEAR_POLICY = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_LOG, operate=Operate.CLEAR_POLICY, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ APPLICATION_CHAT_LOG_ADD_KNOWLEDGE = (
+ Permission(group=Group.APPLICATION, sub_group=Group.CHAT_LOG, operate=Operate.ADD_KNOWLEDGE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.WORKSPACE_MANAGE, RoleConstants.USER],
+ category=Category.RESOURCE,
+ scope=[PermissionScopeConstants.WORKSPACE_RESOURCE],
+ resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANAGE],
+ ),
+ )
+
+ # ==================== 其他 ====================
+ ABOUT_READ = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.ABOUT, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ LICENSE_UPDATE = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.LICENSE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SWITCH_LANGUAGE = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.SWITCH_LANGUAGE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHANGE_PASSWORD = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.CHANGE_PASSWORD, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SYSTEM_API_KEY_EDIT = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.SYSTEM_API_KEY, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ PORTAL = (
+ Permission(group=Group.OTHER, sub_group=Group.OTHER, operate=Operate.PORTAL, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.USER, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.SYSTEM_SETTING,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 外观设置 ====================
+ APPEARANCE_SETTINGS_READ = (
+ Permission(
+ group=Group.APPEARANCE_SETTINGS, sub_group=Group.APPEARANCE_SETTINGS, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ APPEARANCE_SETTINGS_EDIT = (
+ Permission(
+ group=Group.APPEARANCE_SETTINGS, sub_group=Group.APPEARANCE_SETTINGS, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.SYSTEM_SETTING, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ # ==================== 对话用户 ====================
+ CHAT_USER_READ = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_CREATE = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_SYNC = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.SYNC, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_EDIT = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.EDIT, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_DELETE = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.DELETE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_GROUP = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.USER_GROUP, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_QUOTA_SETTING = (
+ Permission(group=Group.CHAT_USER, sub_group=Group.CHAT_USER, operate=Operate.QUOTA_SETTING, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 对话用户组 ====================
+ USER_GROUP_READ = (
+ Permission(group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_GROUP_CREATE = (
+ Permission(group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_GROUP_EDIT = (
+ Permission(group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_GROUP_DELETE = (
+ Permission(group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_GROUP_ADD_MEMBER = (
+ Permission(
+ group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.ADD_MEMBER, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ USER_GROUP_REMOVE_MEMBER = (
+ Permission(
+ group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.REMOVE_MEMBER, bit_index=5
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
+ category=Category.CHAT_CLIENT,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ CHAT_USER_AUTH_READ = (
+ Permission(group=Group.CHAT_USER_GROUP, sub_group=Group.CHAT_USER_GROUP, operate=Operate.READ, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.CHAT_CLIENT, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ # ==================== 对话用户认证 ====================
+ CHAT_USER_AUTH_EDIT = (
+ Permission(group=Group.CHAT_USER_AUTH, sub_group=Group.CHAT_USER_AUTH, operate=Operate.EDIT, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.CHAT_CLIENT, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ PORTAL_READ = (
+ Permission(group=Group.PORTAL, sub_group=Group.PORTAL, operate=Operate.READ, bit_index=0),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ PORTAL_EDIT = (
+ Permission(group=Group.PORTAL, sub_group=Group.PORTAL, operate=Operate.EDIT, bit_index=0),
+ PermissionMeta(role_list=[RoleConstants.ADMIN], scope=[PermissionScopeConstants.SYSTEM]),
+ )
+
+ # ==================== 共享工具 ====================
+ SHARED_TOOL_READ = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_CREATE = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_EDIT = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_DELETE = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_IMPORT = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.IMPORT, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_EXPORT = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.EXPORT, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_PUBLISH = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.PUBLISH, bit_index=6),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.RELATE_VIEW, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_EXECUTE_RECORD = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.RECORD, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_TOOL_TO_WORKSPACE = (
+ Permission(group=Group.SYSTEM_TOOL, sub_group=Group.SYSTEM_TOOL, operate=Operate.TO_WORKSPACE, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 共享知识库 ====================
+ SHARED_KNOWLEDGE_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_CREATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_SYNC = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.SYNC, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_VECTOR = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.VECTOR, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_EXPORT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.EXPORT, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_GENERATE = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.GENERATE, bit_index=6
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DELETE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.DELETE, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_RELATE_RESOURCE_VIEW = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.RELATE_VIEW, bit_index=8
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TO_WORKSPACE = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_KNOWLEDGE, operate=Operate.TO_WORKSPACE, bit_index=9
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库工作流
+ SHARED_KNOWLEDGE_WORKFLOW_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_WORKFLOW_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_WORKFLOW_EXPORT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.EXPORT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_WORKFLOW_PUBLISH = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.PUBLISH, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库文档
+ SHARED_KNOWLEDGE_DOCUMENT_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_CREATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_DELETE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_SYNC = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.SYNC, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_EXPORT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.EXPORT, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.DOWNLOAD, bit_index=6
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_GENERATE = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.GENERATE, bit_index=7
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_VECTOR = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.VECTOR, bit_index=8),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_MIGRATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.MIGRATE, bit_index=9),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_TAG = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.TAG, bit_index=10),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_REPLACE = (
+ Permission(
+ group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.REPLACE, bit_index=11
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_DOCUMENT_TOKEN = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.TOKEN, bit_index=12),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库标签
+ SHARED_KNOWLEDGE_TAG_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TAG_CREATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TAG_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TAG_DELETE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库问题
+ SHARED_KNOWLEDGE_PROBLEM_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_PROBLEM_CREATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_PROBLEM_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_PROBLEM_DELETE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_PROBLEM_RELATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.RELATE, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库术语库
+ SHARED_KNOWLEDGE_TERMBASE_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TERMBASE_CREATE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TERMBASE_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TERMBASE_DELETE = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_TERMBASE_EXPORT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.EXPORT, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库命中测试
+ SHARED_KNOWLEDGE_HIT_TEST = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_HIT_TEST, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 共享知识库对话用户
+ SHARED_KNOWLEDGE_CHAT_USER_READ = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_KNOWLEDGE_CHAT_USER_EDIT = (
+ Permission(group=Group.SYSTEM_KNOWLEDGE, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 共享模型 ====================
+ SHARED_MODEL_READ = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_MODEL_CREATE = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_MODEL_EDIT = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_MODEL_DELETE = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_MODEL_RELATE_RESOURCE_VIEW = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.RELATE_VIEW, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ SHARED_MODEL_TO_WORKSPACE = (
+ Permission(group=Group.SYSTEM_MODEL, sub_group=Group.SYSTEM_MODEL, operate=Operate.TO_WORKSPACE, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.SHARED,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 资源管理 - 应用 ====================
+ RESOURCE_APPLICATION_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.READ,
+ bit_index=0,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.EDIT,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.DELETE,
+ bit_index=2,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.EXPORT,
+ bit_index=3,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_COPY = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.COPY,
+ bit_index=4,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_AUTH = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.AUTH,
+ bit_index=5,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_PUBLISH = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.PUBLISH,
+ bit_index=6,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_TRIGGER_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.TRIGGER_READ,
+ bit_index=7,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_TRIGGER_CREATE = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.TRIGGER_CREATE,
+ bit_index=8,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_TRIGGER_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.TRIGGER_EDIT,
+ bit_index=9,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_TRIGGER_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.TRIGGER_DELETE,
+ bit_index=10,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_RELATE_RESOURCE_VIEW = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_RES_APPLICATION,
+ operate=Operate.RELATE_VIEW,
+ bit_index=11,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 应用概览
+ RESOURCE_APPLICATION_OVERVIEW_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_OVERVIEW, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_OVERVIEW_EMBED = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_OVERVIEW, operate=Operate.EMBED, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_OVERVIEW_ACCESS = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_OVERVIEW, operate=Operate.ACCESS, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_OVERVIEW_DISPLAY = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_OVERVIEW, operate=Operate.DISPLAY, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_OVERVIEW_API_KEY = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_OVERVIEW, operate=Operate.API_KEY, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_OVERVIEW_PUBLIC = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_OVERVIEW,
+ operate=Operate.PUBLIC_ACCESS,
+ bit_index=5,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 应用接入
+ RESOURCE_APPLICATION_ACCESS_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_ACCESS, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_ACCESS_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_ACCESS, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 应用对话用户
+ RESOURCE_APPLICATION_CHAT_USER_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_CHAT_USER_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 应用对话日志
+ RESOURCE_APPLICATION_CHAT_LOG_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_CHAT_LOG, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_CHAT_LOG_ADD_KNOWLEDGE = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_CHAT_LOG,
+ operate=Operate.ADD_KNOWLEDGE,
+ bit_index=1,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_CHAT_LOG_ANNOTATION = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_CHAT_LOG, operate=Operate.ANNOTATION, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_CHAT_LOG_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION, sub_group=Group.SYSTEM_CHAT_LOG, operate=Operate.EXPORT, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_APPLICATION_CHAT_LOG_CLEAR_POLICY = (
+ Permission(
+ group=Group.SYSTEM_RES_APPLICATION,
+ sub_group=Group.SYSTEM_CHAT_LOG,
+ operate=Operate.CLEAR_POLICY,
+ bit_index=4,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 资源管理 - 知识库 ====================
+ RESOURCE_KNOWLEDGE_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.DELETE, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_SYNC = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.SYNC, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.EXPORT, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_PUBLISH = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.PUBLISH, bit_index=5
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_VECTOR = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.VECTOR, bit_index=6
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_GENERATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE,
+ sub_group=Group.SYSTEM_RES_KNOWLEDGE,
+ operate=Operate.GENERATE,
+ bit_index=7,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_AUTH = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.AUTH, bit_index=8
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_RELATE_RESOURCE_VIEW = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE,
+ sub_group=Group.SYSTEM_RES_KNOWLEDGE,
+ operate=Operate.RELATE_VIEW,
+ bit_index=9,
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库工作流
+ RESOURCE_KNOWLEDGE_WORKFLOW_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_WORKFLOW_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_WORKFLOW_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.EXPORT, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_WORKFLOW_PUBLISH = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_WORKFLOW, operate=Operate.PUBLISH, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库文档
+ RESOURCE_KNOWLEDGE_DOCUMENT_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_CREATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.CREATE, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.EDIT, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.DELETE, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_SYNC = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.SYNC, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.EXPORT, bit_index=5
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.DOWNLOAD, bit_index=6
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_GENERATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.GENERATE, bit_index=7
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_VECTOR = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.VECTOR, bit_index=8
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_MIGRATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.MIGRATE, bit_index=9
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_TAG = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.TAG, bit_index=10
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_REPLACE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.REPLACE, bit_index=11
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_DOCUMENT_TOKEN = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_DOCUMENT, operate=Operate.TOKEN, bit_index=12
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库命中测试
+ RESOURCE_KNOWLEDGE_HIT_TEST = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_HIT_TEST, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库问题
+ RESOURCE_KNOWLEDGE_PROBLEM_READ = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_PROBLEM_CREATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.CREATE, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_PROBLEM_EDIT = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_PROBLEM_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.DELETE, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_PROBLEM_RELATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_PROBLEM, operate=Operate.RELATE, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库术语库
+ RESOURCE_KNOWLEDGE_TERMBASE_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TERMBASE_CREATE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.CREATE, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TERMBASE_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.EDIT, bit_index=2
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TERMBASE_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.DELETE, bit_index=3
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TERMBASE_EXPORT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TERMBASE, operate=Operate.EXPORT, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库标签
+ RESOURCE_KNOWLEDGE_TAG_READ = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TAG_CREATE = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.CREATE, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TAG_EDIT = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.EDIT, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_TAG_DELETE = (
+ Permission(group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_TAG, operate=Operate.DELETE, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # 资源管理 - 知识库对话用户
+ RESOURCE_KNOWLEDGE_CHAT_USER_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.READ, bit_index=0
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_KNOWLEDGE_CHAT_USER_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_KNOWLEDGE, sub_group=Group.SYSTEM_CHAT_USER, operate=Operate.EDIT, bit_index=1
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 资源管理 - 工具 ====================
+ RESOURCE_TOOL_READ = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_EDIT = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_DELETE = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.DELETE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_EXPORT = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.EXPORT, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_PUBLISH = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.PUBLISH, bit_index=4),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_AUTH = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.AUTH, bit_index=5),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_RELATE_RESOURCE_VIEW = (
+ Permission(
+ group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.RELATE_VIEW, bit_index=6
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_EXECUTE_RECORD = (
+ Permission(group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.RECORD, bit_index=7),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_TRIGGER_READ = (
+ Permission(
+ group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_READ, bit_index=8
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_TRIGGER_CREATE = (
+ Permission(
+ group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_CREATE, bit_index=9
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_TRIGGER_EDIT = (
+ Permission(
+ group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_EDIT, bit_index=10
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_TOOL_TRIGGER_DELETE = (
+ Permission(
+ group=Group.SYSTEM_RES_TOOL, sub_group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_DELETE, bit_index=11
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 资源管理 - 模型 ====================
+ RESOURCE_MODEL_READ = (
+ Permission(group=Group.SYSTEM_RES_MODEL, sub_group=Group.SYSTEM_RES_MODEL, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_MODEL_EDIT = (
+ Permission(group=Group.SYSTEM_RES_MODEL, sub_group=Group.SYSTEM_RES_MODEL, operate=Operate.EDIT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_MODEL_DELETE = (
+ Permission(group=Group.SYSTEM_RES_MODEL, sub_group=Group.SYSTEM_RES_MODEL, operate=Operate.DELETE, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_MODEL_AUTH = (
+ Permission(group=Group.SYSTEM_RES_MODEL, sub_group=Group.SYSTEM_RES_MODEL, operate=Operate.AUTH, bit_index=3),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ RESOURCE_MODEL_RELATE_RESOURCE_VIEW = (
+ Permission(
+ group=Group.SYSTEM_RES_MODEL, sub_group=Group.SYSTEM_RES_MODEL, operate=Operate.RELATE_VIEW, bit_index=4
+ ),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN],
+ category=Category.RESOURCE,
+ is_ee=is_ee,
+ scope=[PermissionScopeConstants.SYSTEM],
+ ),
+ )
+
+ # ==================== 操作日志 ====================
+ OPERATION_LOG_READ = (
+ Permission(group=Group.OPERATION_LOG, sub_group=Group.OPERATION_LOG, operate=Operate.READ, bit_index=0),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.OPERATION_LOG, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ OPERATION_LOG_EXPORT = (
+ Permission(group=Group.OPERATION_LOG, sub_group=Group.OPERATION_LOG, operate=Operate.EXPORT, bit_index=1),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.OPERATION_LOG, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ OPERATION_LOG_CLEAR_POLICY = (
+ Permission(group=Group.OPERATION_LOG, sub_group=Group.OPERATION_LOG, operate=Operate.CLEAR_POLICY, bit_index=2),
+ PermissionMeta(
+ role_list=[RoleConstants.ADMIN], category=Category.OPERATION_LOG, scope=[PermissionScopeConstants.SYSTEM]
+ ),
+ )
+
+ def __init__(self, value, meta):
+ self._value_ = value
+ self.meta = meta
+
+ def _build_workspace_permission(self, resource_id_key=None):
+ def permission_factory(_, kwargs):
+ return Permission(
+ group=self.value.group,
+ sub_group=self.value.sub_group,
+ operate=self.value.operate,
+ bit_index=self.value.bit_index,
+ workspace_id=kwargs.get("workspace_id"),
+ resource_id=kwargs.get(resource_id_key) if resource_id_key else None,
+ )
+
+ return permission_factory
+
+ def get_workspace_application_permission(self):
+ return self._build_workspace_permission(resource_id_key="application_id")
+
+ def get_workspace_knowledge_permission(self):
+ return self._build_workspace_permission(resource_id_key="knowledge_id")
+
+ def get_workspace_model_permission(self):
+ return self._build_workspace_permission(resource_id_key="model_id")
+
+ def get_workspace_tool_permission(self):
+ return self._build_workspace_permission(resource_id_key="tool_id")
+
+ def get_workspace_permission(self):
+ return self._build_workspace_permission()
+
+ def get_workspace_permission_workspace_manage_role(self):
+ """
+ 工作空间管理员的特权权限
+ @return: 工作空间管理员特权权限
+ """
+
+ def permission_factory(_, kwargs):
+ return Permission(
+ group=self.value.group,
+ sub_group=self.value.sub_group,
+ operate=self.value.operate,
+ bit_index=self.value.bit_index,
+ workspace_id=kwargs.get("workspace_id"),
+ flag=RoleConstants.WORKSPACE_MANAGE.value,
+ )
+
+ return permission_factory
+
+
+def group_by_all_resource_permissions() -> Dict[str, List[Permission]]:
+ grouped = {}
+
+ for _permission in PermissionConstants:
+ meta = _permission.meta
+ if meta.resource_permission_group_list:
+ for group in meta.resource_permission_group_list:
+ _array = grouped.get(str(group)) or []
+ _array.append(_permission)
+ grouped[str(group)] = _array
+ return dict(grouped)
+
+
+def group_permissions_by_scope() -> Dict[str, List[Permission]]:
+ grouped = {}
+
+ for _permission in PermissionConstants:
+ permission = _permission.value
+ meta = _permission.meta
+ if meta.scope:
+ for scope_item in meta.scope:
+ _array = grouped.get(scope_item) or []
+ _array.append(_permission)
+ grouped[scope_item] = _array
+ return dict(grouped)
+
+
+# 权限字符串与权限对象的Map
+PERMISSION_STR_MAP = {_permission.value.__str__(): _permission for _permission in PermissionConstants}
+
+# 资源组Map
+RESOURCE_PERMISSION_MAP = group_by_all_resource_permissions()
+
+# 权限 SCOPE Map
+SCOPE_PERMISSION_MAP = group_permissions_by_scope()
diff --git a/apps/common/auth/constants/permission_scope_constants.py b/apps/common/auth/constants/permission_scope_constants.py
new file mode 100644
index 00000000000..122057f7825
--- /dev/null
+++ b/apps/common/auth/constants/permission_scope_constants.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: permission_scope_constants.py
+ @date:2026/8/4 11:50
+ @desc:
+"""
+from enum import Enum
+
+
+class PermissionScopeConstants(Enum):
+ SYSTEM = 'SYSTEM'
+ WORKSPACE = 'WORKSPACE'
+ WORKSPACE_RESOURCE = 'WORKSPACE_RESOURCE'
diff --git a/apps/common/auth/constants/resource_auth_type_constants.py b/apps/common/auth/constants/resource_auth_type_constants.py
new file mode 100644
index 00000000000..c8079fa0eca
--- /dev/null
+++ b/apps/common/auth/constants/resource_auth_type_constants.py
@@ -0,0 +1,21 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: resource_auth_type_constants.py
+ @date:2026/8/4 15:31
+ @desc:
+"""
+
+from django.db import models
+
+
+class ResourceAuthType(models.TextChoices):
+ """
+ 资源授权类型
+ """
+ "当授权类型是Role时候"
+ ROLE = "ROLE"
+
+ """资源权限组"""
+ RESOURCE_PERMISSION_GROUP = "RESOURCE_PERMISSION_GROUP"
diff --git a/apps/common/auth/constants/role_constants.py b/apps/common/auth/constants/role_constants.py
new file mode 100644
index 00000000000..94b250b9522
--- /dev/null
+++ b/apps/common/auth/constants/role_constants.py
@@ -0,0 +1,34 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: role_constants.py
+ @date:2026/8/4 9:50
+ @desc:
+"""
+from enum import Enum
+
+from common.auth.constants.role_group import RoleGroup
+from common.auth.struct.permission import Role, RoleMeta
+
+
+class RoleConstants(Enum):
+ ADMIN = (Role("ADMIN"), RoleMeta('系统管理员', RoleGroup.SYSTEM_USER))
+ WORKSPACE_MANAGE = (Role("WORKSPACE_MANAGE"), RoleMeta('工作空间管理员', RoleGroup.SYSTEM_USER))
+ USER = (Role("USER"), RoleMeta('普通用户', RoleGroup.SYSTEM_USER))
+ EXTENDS_ADMIN = (Role("EXTENDS_ADMIN"), RoleMeta('继承系统管理员', RoleGroup.SYSTEM_USER))
+ EXTENDS_WORKSPACE_MANAGE = (Role("EXTENDS_WORKSPACE_MANAGE"), RoleMeta('继承工作空间管理员', RoleGroup.SYSTEM_USER))
+ EXTENDS_USER = (Role("EXTENDS_USER"), RoleMeta('继承普通用户', RoleGroup.SYSTEM_USER))
+
+ CHAT_ANONYMOUS_USER = (Role("CHAT_ANONYMOUS_USER"), RoleMeta('对话匿名用户', RoleGroup.CHAT_USER))
+ CHAT_USER = (Role("CHAT_USER"), RoleMeta('对话用户', RoleGroup.CHAT_USER))
+
+ def __init__(self, value, meta):
+ self._value_ = value
+ self.meta = meta
+
+ def __str__(self):
+ return self.value.__str__()
+
+ def get_workspace_role(self):
+ return lambda r, kwargs: Role(self.value.name, kwargs.get('workspace_id'))
diff --git a/apps/common/auth/constants/role_group.py b/apps/common/auth/constants/role_group.py
new file mode 100644
index 00000000000..95abccd2ef1
--- /dev/null
+++ b/apps/common/auth/constants/role_group.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: role_group.py
+ @date:2026/8/4 9:57
+ @desc:
+"""
+from enum import Enum
+
+
+class RoleGroup(Enum):
+ # 系统用户
+ SYSTEM_USER = "SYSTEM_USER"
+ # 对话用户
+ CHAT_USER = "CHAT_USER"
diff --git a/apps/common/auth/handle/impl/application_key.py b/apps/common/auth/handle/impl/application_key.py
index 259f21b872d..48bc60d1681 100644
--- a/apps/common/auth/handle/impl/application_key.py
+++ b/apps/common/auth/handle/impl/application_key.py
@@ -12,7 +12,7 @@
from application.models import ApplicationApiKey, ChatUserType, ApplicationAccessToken
from common.auth.handle.auth_base_handle import AuthBaseHandle
-from common.constants.permission_constants import Permission, Group, Operate, RoleConstants, ChatAuth
+from common.auth.struct.auth import Principal, Auth
from common.exception.app_exception import AppAuthenticationFailed
@@ -32,14 +32,8 @@ def handle(self, request, token: str, get_token_details):
if application_access_token.authentication_value.get('type',
'password') != 'password':
raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
- return None, ChatAuth(
- current_role_list=[RoleConstants.CHAT_ANONYMOUS_USER],
- permission_list=[
- Permission(group=Group.APPLICATION,
- operate=Operate.READ)],
- application_id=application_api_key.application_id,
- chat_user_id=str(application_api_key.id),
- chat_user_type=ChatUserType.APPLICATION_API_KEY.value)
+ return Principal(str(application_api_key.id), ChatUserType.APPLICATION_API_KEY,
+ application_id=str(application_api_key.application_id)), Auth(set(), {})
def support(self, request, token: str, get_token_details):
return str(token).startswith("application-") or str(token).startswith('agent-')
diff --git a/apps/common/auth/handle/impl/chat_anonymous_user_token.py b/apps/common/auth/handle/impl/chat_anonymous_user_token.py
deleted file mode 100644
index 7d8cc56e533..00000000000
--- a/apps/common/auth/handle/impl/chat_anonymous_user_token.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# coding=utf-8
-"""
- @project: MaxKB
- @Author:虎虎
- @file: chat_anonymous_user_token.py
- @date:2025/6/6 15:08
- @desc:
-"""
-from django.db.models import QuerySet
-from django.utils.translation import gettext_lazy as _
-
-from application.models import ApplicationAccessToken
-from common.auth.common import ChatUserToken
-from common.auth.handle.auth_base_handle import AuthBaseHandle
-from common.constants.authentication_type import AuthenticationType
-from common.constants.permission_constants import RoleConstants, Permission, Group, Operate, ChatAuth
-from common.database_model_manage.database_model_manage import DatabaseModelManage
-from common.exception.app_exception import AppAuthenticationFailed
-from maxkb.settings import edition
-
-
-class ChatAnonymousUserToken(AuthBaseHandle):
- def support(self, request, token: str, get_token_details):
- token_details = get_token_details()
- if token_details is None:
- return False
- return (
- 'application_id' in token_details and
- 'access_token' in token_details and
- token_details.get('type') == AuthenticationType.CHAT_ANONYMOUS_USER.value)
-
- def handle(self, request, token: str, get_token_details):
- auth_details = get_token_details()
- chat_user_token = ChatUserToken.new_instance(auth_details)
- application_id = chat_user_token.application_id
- access_token = chat_user_token.access_token
- application_access_token = QuerySet(ApplicationAccessToken).filter(
- application_id=application_id).first()
- if application_access_token is None:
- raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
- if not application_access_token.is_active:
- raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
- if not application_access_token.access_token == access_token:
- raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
- if application_access_token.authentication and ['PE', 'EE'].__contains__(edition):
- if chat_user_token.authentication.auth_type != application_access_token.authentication_value.get('type',
- ''):
- raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
- return None, ChatAuth(
- current_role_list=[RoleConstants.CHAT_ANONYMOUS_USER],
- permission_list=[
- Permission(group=Group.APPLICATION,
- operate=Operate.USE)],
- application_id=application_access_token.application_id,
- chat_user_id=chat_user_token.chat_user_id,
- chat_user_type=chat_user_token.chat_user_type)
diff --git a/apps/common/auth/handle/impl/chat_user_token.py b/apps/common/auth/handle/impl/chat_user_token.py
new file mode 100644
index 00000000000..e9c5c8c0eea
--- /dev/null
+++ b/apps/common/auth/handle/impl/chat_user_token.py
@@ -0,0 +1,104 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎
+ @file: chat_anonymous_user_token.py
+ @date:2025/6/6 15:08
+ @desc:
+"""
+from functools import reduce
+
+from django.db.models import QuerySet, Q
+from django.utils.translation import gettext_lazy as _
+
+from application.models import ApplicationAccessToken, ChatUserType
+from common.auth.constants.chat_permission_constants import ChatPermissionConstants, CHAT_PERMISSION_STR_MAP
+from common.auth.constants.group_constants import Group
+from common.auth.constants.operate_constants import Operate
+from common.auth.handle.auth_base_handle import AuthBaseHandle
+from common.auth.struct.auth import Principal, Auth
+from common.constants.authentication_type import AuthenticationType
+from common.exception.app_exception import AppUnauthorizedFailed
+from system_manage.models import ResourceChatUserGroupAuthorize, ResourceType, ResourceChatUserAuthorize, \
+ UserGroupRelation, ChatUser
+
+login_type_list = [Operate.LOCAL.value, Operate.CAS.value, Operate.DINGTALK.value, Operate.WECOM.value,
+ Operate.LARK.value, Operate.OIDC.value, Operate.LDAP.value,
+ Operate.OAUTH2.value]
+
+
+class ChatUserToken(AuthBaseHandle):
+ def support(self, request, token: str, get_token_details):
+ token_details = get_token_details()
+ if token_details is None:
+ return False
+ return token_details.get('type') == AuthenticationType.CHAT_USER.value
+
+ def handle(self, request, token: str, get_token_details):
+ auth_details = get_token_details()
+ application_access_token_list = QuerySet(ApplicationAccessToken).filter(
+ is_active=True
+ )
+ _type = ChatUserType.ANONYMOUS_USER
+ login_type = auth_details.get('login_type')
+ user_id = auth_details.get('user_id')
+ application_id = (auth_details.get('kwargs') or {}).get('application_id')
+ if login_type.upper() == str(Operate.ANNOTATION_AUTH):
+ application_access_token_list = application_access_token_list.filter(authentication=False)
+ elif login_type.upper() == str(Operate.PASSWORD):
+ application_access_token_list = (application_access_token_list
+ .filter(authentication=True, authentication_value__type='password'))
+
+ elif login_type_list.__contains__(login_type.upper()):
+ _type = ChatUserType.CHAT_USER
+ user_group_ids = QuerySet(UserGroupRelation).filter(
+ user_id=user_id,
+ ).values_list('group_id', flat=True)
+
+ group_qs = QuerySet(ResourceChatUserGroupAuthorize).filter(
+ resource_type=ResourceType.APPLICATION,
+ is_auth=True,
+ user_group_id__in=user_group_ids,
+ ).values_list('resource_id', flat=True)
+
+ user_qs = QuerySet(ResourceChatUserAuthorize).filter(
+ resource_type=ResourceType.APPLICATION,
+ is_auth=True,
+ user_id=user_id,
+ ).values_list('resource_id', flat=True)
+ application_access_token_list = application_access_token_list.filter(
+ Q(authentication_value__type='login'),
+ Q(authentication_value__login_value__contains=login_type),
+ Q(application_id__in=group_qs) | Q(application_id__in=user_qs),
+ )
+ if application_id:
+ application_access_token_list = application_access_token_list.filter(application_id=application_id)
+ permissions = {}
+ for application_access_token in application_access_token_list:
+ permission_list = []
+ if application_access_token.authentication:
+ authentication_value = application_access_token.authentication_value
+ if authentication_value.get('type') == 'password':
+ permission_list.append(ChatPermissionConstants.CHAT_USER_PASSWORD.value)
+ elif authentication_value.get('type') == 'login':
+ login_value = authentication_value.get('login_value') or []
+ for _value in login_value:
+ permission_str = f'{Group.CHAT_USER}_{_value.upper()}'
+ permission = CHAT_PERMISSION_STR_MAP.get(permission_str)
+ if permission:
+ permission_list.append(permission.value)
+
+ else:
+ permission_list.append(ChatPermissionConstants.CHAT_USER_ANONYMOUS.value)
+ k = f"{Group.CHAT_USER}:r:{application_access_token.application_id}"
+ permissions[k] = reduce(lambda x, y: x | y, [p.bit() for p in permission_list], 0)
+ chat_user = QuerySet(ChatUser).filter(id=user_id).first()
+ if application_id:
+ # 指定了 application_id(v2 流程)时,直接校验该应用是否有权限,无权限直接抛错,
+ # 避免返回一个空权限的 Principal 造成静默失败。
+ if not permissions.get(f"{Group.CHAT_USER}:r:{application_id}"):
+ raise AppUnauthorizedFailed(403, _('No permission to access'))
+ return Principal(auth_details.get('user_id'), _type, application_id=application_id,
+ profile=chat_user), Auth(set(),
+ permissions)
+ return Principal(auth_details.get('user_id'), _type, profile=chat_user), Auth(set(), permissions)
diff --git a/apps/common/auth/handle/impl/user_token.py b/apps/common/auth/handle/impl/user_token.py
index 18dd9d074df..935d961bc39 100644
--- a/apps/common/auth/handle/impl/user_token.py
+++ b/apps/common/auth/handle/impl/user_token.py
@@ -1,233 +1,191 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: authenticate.py
- @date:2024/3/14 03:02
- @desc: 用户认证
+@project: MaxKB
+@Author:虎虎
+@file: authenticate.py
+@date:2024/3/14 03:02
+@desc: 用户认证
"""
+
from functools import reduce
-from typing import List
from django.core.cache import cache
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
+from common.auth.constants.permission_constants import RESOURCE_PERMISSION_MAP, PERMISSION_STR_MAP
+from common.auth.constants.permission_scope_constants import PermissionScopeConstants
+from common.constants.resource_permission_constants import ResourceAuthType
+from common.auth.constants.role_constants import RoleConstants
from common.auth.handle.auth_base_handle import AuthBaseHandle
-from common.constants.authentication_type import AuthenticationType
+from common.auth.struct.auth import Auth, Principal
+from common.constants.authentication_type import AuthenticationType, UserType
from common.constants.cache_version import Cache_Version
-from common.constants.permission_constants import Auth, PermissionConstants, ResourcePermissionGroup, \
- get_permission_list_by_resource_group, ResourceAuthType, \
- ResourcePermissionRole, get_default_role_permission_mapping_list, get_default_workspace_user_role_mapping_list, \
- RoleConstants, ResourcePermission, Resource, WorkspaceGroup
+
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import AppAuthenticationFailed
-from common.utils.common import group_by
+from common.utils.common import group_by, flat_map
from maxkb.const import CONFIG
+from system_manage.models.workspace_user_group_permission import WorkspaceUserGroupResourcePermission
from system_manage.models.workspace_user_permission import WorkspaceUserResourcePermission
from users.models import User
-permission_constants_dict = {p.value.__str__(): p for p in PermissionConstants}
-
-def get_permission(permission_id):
- """
- 获取权限字符串
- @param permission_id: 权限id
- @return: 权限字符串
- """
- if isinstance(permission_id, PermissionConstants):
- permission_id = permission_id.value
- return f"{permission_id}"
-
-
-def get_workspace_permission(permission_id, workspace_id, role=None):
- """
- 获取工作空间权限字符串
- @param permission_id: 权限id
- @param workspace_id: 工作空间id
- @param role: 角色
- @return:
- """
- if isinstance(permission_id, PermissionConstants):
- permission_id = permission_id.value
- if role and role.type == RoleConstants.WORKSPACE_MANAGE.value.__str__():
- return [f"{permission_id}:/WORKSPACE/{workspace_id}:ROLE/{role.type}",
- f"{permission_id}:/WORKSPACE/{workspace_id}"]
- return [f"{permission_id}:/WORKSPACE/{workspace_id}"]
-
-
-def get_role_permission(role, workspace_id):
- """
- 获取工作空间角色
- @param role: 角色
- @param workspace_id: 工作空间id
- @return:
- """
- if isinstance(role, RoleConstants):
- role = role.value
- return f"{role}:/WORKSPACE/{workspace_id}"
-
-
-def get_workspace_permission_list(role_permission_mapping_dict, workspace_user_role_mapping_list, role_model_dict):
- """
- 获取工作空间下所有的权限
- @param role_permission_mapping_dict: 角色权限关联字典
- @param workspace_user_role_mapping_list: 工作空间用户角色关联列表
- @param role_model_dict: 角色字典
- @return: 工作空间下的权限
- """
- workspace_permission_list = [
- [get_workspace_permission(role_permission_mapping.permission_id, w_u_r.workspace_id,
- role_model_dict.get(w_u_r.role_id, None)) for role_permission_mapping
- in
- role_permission_mapping_dict.get(w_u_r.role_id, [])] for w_u_r in workspace_user_role_mapping_list]
- return reduce(lambda x, y: [*x, *y], reduce(lambda x, y: [*x, *y], workspace_permission_list, []), [])
-
-
-def get_workspace_resource_permission_list(
- workspace_user_resource_permission_list: List[WorkspaceUserResourcePermission],
- role_permission_mapping_dict,
- workspace_user_role_mapping_dict):
- """
-
- @param workspace_user_resource_permission_list: 工作空间用户资源权限列表
- @param role_permission_mapping_dict: 角色权限关联字典 key为role_id
- @param workspace_user_role_mapping_dict: 工作空间用户角色映射字典 key为role_id
- @return: 工作空间资源权限列表
- """
- resource_permission_list = [
- get_workspace_resource_permission_list_by_workspace_user_permission(workspace_user_resource_permission,
- role_permission_mapping_dict,
- workspace_user_role_mapping_dict) for
- workspace_user_resource_permission in workspace_user_resource_permission_list]
- # 将二维数组扁平为一维
- return reduce(lambda x, y: [*x, *y], resource_permission_list, [])
-
-
-def get_workspace_resource_permission_list_by_workspace_user_permission(
- workspace_user_resource_permission: WorkspaceUserResourcePermission,
- role_permission_mapping_dict,
- workspace_user_role_mapping_dict):
- """
-
- @param workspace_user_resource_permission: 工作空间用户资源权限对象
- @param role_permission_mapping_dict: 角色权限关联字典 key为role_id
- @param workspace_user_role_mapping_dict: 工作空间用户角色关联字典 key为role_id
- @return: 工作空间用户资源的权限列表
- """
- # 判断用户在当前工作空间是否为内置USER
- workspace_role_ids = [
- wur.role_id
- for wur in
- workspace_user_role_mapping_dict.get(workspace_user_resource_permission.workspace_id,[])
- ]
- is_builtin_user = RoleConstants.USER.value.__str__() in workspace_role_ids
-
- role_permission_mapping_list = [role_permission_mapping_dict.get(workspace_user_role_mapping.role_id, []) for
- workspace_user_role_mapping in
- workspace_user_role_mapping_dict.get(
- workspace_user_resource_permission.workspace_id)]
- role_permission_mapping_list = reduce(lambda x, y: [*x, *y], role_permission_mapping_list, [])
- # 如果是根据角色
- if (workspace_user_resource_permission.auth_type == ResourceAuthType.ROLE
- and workspace_user_resource_permission.permission_list.__contains__(
- ResourcePermissionRole.ROLE)):
- per_op_permissions = [
- f"{role_permission_mapping.permission_id}:/WORKSPACE/{workspace_user_resource_permission.workspace_id}/{workspace_user_resource_permission.auth_target_type}/{workspace_user_resource_permission.target}"
- for role_permission_mapping in role_permission_mapping_list if (permission_constants_dict.get(role_permission_mapping.permission_id).value.parent_group or []).__contains__(
- WorkspaceGroup(workspace_user_resource_permission.auth_target_type))]
- if is_builtin_user:
- per_op_permissions.append(
- f"{workspace_user_resource_permission.auth_target_type}:/WORKSPACE/{workspace_user_resource_permission.workspace_id}/{workspace_user_resource_permission.auth_target_type}/{workspace_user_resource_permission.target}"
- )
- return per_op_permissions
- elif workspace_user_resource_permission.auth_type == ResourceAuthType.RESOURCE_PERMISSION_GROUP:
- resource_permission_list = [
- [
- f"{permission}:/WORKSPACE/{workspace_user_resource_permission.workspace_id}/{workspace_user_resource_permission.auth_target_type}/{workspace_user_resource_permission.target}"
- for permission in get_permission_list_by_resource_group(
- ResourcePermissionGroup(Resource(workspace_user_resource_permission.auth_target_type),
- ResourcePermission(resource_permission)))]
- for resource_permission in workspace_user_resource_permission.permission_list if
- ResourcePermission.values.__contains__(resource_permission)]
- # 将二维数组扁平为一维
- return reduce(lambda x, y: [*x, *y], resource_permission_list, [])
- return []
-
-def get_permission_list(user,
- workspace_user_role_mapping_model,
- workspace_model,
- role_model,
- role_permission_mapping_model):
+def get_permissions(
+ user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model
+):
user_id = user.id
version = Cache_Version.PERMISSION_LIST.get_version()
key = Cache_Version.PERMISSION_LIST.get_key(user_id=user_id)
# 获取权限列表
- is_query_model = workspace_user_role_mapping_model is not None and workspace_model is not None and role_model is not None and role_permission_mapping_model is not None
- permission_list = cache.get(key, version=version)
- if permission_list is None:
+ is_query_model = (
+ workspace_user_role_mapping_model is not None
+ and workspace_model is not None
+ and role_model is not None
+ and role_permission_mapping_model is not None
+ )
+ permission_map = cache.get(key, version=version)
+ if permission_map is None:
+ permission_map = {}
if is_query_model:
# 获取工作空间 用户 角色映射数据
workspace_user_role_mapping_list = QuerySet(workspace_user_role_mapping_model).filter(user_id=user_id)
- workspace_user_role_mapping_dict = group_by(workspace_user_role_mapping_list,
- lambda item: item.workspace_id)
- role_id_list = list(set([workspace_user_role_mapping.role_id for workspace_user_role_mapping in
- workspace_user_role_mapping_list]))
+
+ role_id_list = list(
+ set(
+ [
+ workspace_user_role_mapping.role_id
+ for workspace_user_role_mapping in workspace_user_role_mapping_list
+ ]
+ )
+ )
# 获取角色权限映射数据
- role_permission_mapping_list = QuerySet(role_permission_mapping_model).filter(
- role_id__in=role_id_list)
+ role_permission_mapping_list = QuerySet(role_permission_mapping_model).filter(role_id__in=role_id_list)
role_model_list = QuerySet(role_model).filter(id__in=role_id_list)
role_model_dict = {role_model.id: role_model for role_model in role_model_list}
- role_permission_mapping_dict = group_by(
- role_permission_mapping_list, lambda item: item.role_id)
+ role_permission_mapping_dict = group_by(role_permission_mapping_list, lambda item: str(item.role_id))
workspace_user_permission_list = QuerySet(WorkspaceUserResourcePermission).filter(
- workspace_id__in=[workspace_user_role.workspace_id for workspace_user_role in
- workspace_user_role_mapping_list if
- (role_model_dict.get(workspace_user_role.role_id).type == 'USER' if
- role_model_dict.get(workspace_user_role.role_id) else False)],
- user_id=user_id)
-
- # 资源权限
- workspace_resource_permission_list = get_workspace_resource_permission_list(workspace_user_permission_list,
- role_permission_mapping_dict,
- workspace_user_role_mapping_dict)
+ workspace_id__in=[
+ workspace_user_role.workspace_id
+ for workspace_user_role in workspace_user_role_mapping_list
+ if (
+ role_model_dict.get(workspace_user_role.role_id).type == "USER"
+ if role_model_dict.get(workspace_user_role.role_id)
+ else False
+ )
+ ],
+ user_id=user_id,
+ )
- workspace_permission_list = get_workspace_permission_list(role_permission_mapping_dict,
- workspace_user_role_mapping_list, role_model_dict)
- # 系统权限
- system_permission_list = [role_permission_mapping.permission_id for role_permission_mapping in
- role_permission_mapping_list]
- # 合并权限
- permission_list = system_permission_list + workspace_permission_list + workspace_resource_permission_list
- permission_list = list(set(permission_list))
- cache.set(key, permission_list, version=version)
+ workspace_user_group_resource_permission_list = (
+ QuerySet(WorkspaceUserGroupResourcePermission)
+ .filter(user_group__user_relations__user_id=user_id)
+ .select_related("user_group")
+ .distinct()
+ )
+ "----------------------处理资源权限--------------------------------------------------"
+ for _ in list(workspace_user_permission_list) + list(workspace_user_group_resource_permission_list):
+ if _.auth_type == ResourceAuthType.RESOURCE_PERMISSION_GROUP:
+ all_permissions = flat_map(
+ [
+ RESOURCE_PERMISSION_MAP.get(f"{_.auth_target_type}_{_resource_permission}")
+ for _resource_permission in _.permission_list
+ if _resource_permission in ["VIEW", "MANAGE"]
+ ]
+ )
+ for group, permissions in group_by(
+ all_permissions, lambda _permission: _permission.value.group
+ ).items():
+ k = f"{group}:{_.workspace_id}:{_.target}"
+ bits = reduce(lambda x, y: x | y, [_permission.value.bit() for _permission in permissions], 0)
+ permission_map[k] = permission_map.get(k, 0) | bits
+ elif _.auth_type == ResourceAuthType.ROLE:
+ role_ids = [m.role_id for m in workspace_user_role_mapping_list if m.workspace_id == _.workspace_id]
+
+ permissions = []
+ for role_id in role_ids:
+ for m in role_permission_mapping_dict.get(str(role_id)) or []:
+ p = PERMISSION_STR_MAP.get(m.permission_id)
+ if PermissionScopeConstants.WORKSPACE in p.meta.scope:
+ permissions.append(p)
+
+ for group, ps in group_by(permissions, lambda p: p.meta.group).items():
+ k = f"{group}:w:{_.workspace_id}:r:{_.target}"
+ bits = reduce(lambda x, y: x | y, [p.value.bit() for p in ps], 0)
+ permission_map[k] = permission_map.get(k, 0) | bits
+
+ "----------------------处理工作空间权限--------------------------------------------------"
+ for _ in workspace_user_role_mapping_list:
+ _role_permission_mapping_list = role_permission_mapping_dict.get(str(_.role_id)) or []
+ permissions = [
+ PERMISSION_STR_MAP.get(_role_permission_mapping.permission_id)
+ for _role_permission_mapping in _role_permission_mapping_list
+ ]
+ # 过滤工作空间权限
+ permissions = [
+ _permission
+ for _permission in permissions
+ if PermissionScopeConstants.WORKSPACE in _permission.meta.scope
+ ]
+ for group, ps in group_by(permissions, lambda p: p.meta.group).items():
+ k = f"{group}:w:{_.workspace_id}"
+ bits = reduce(lambda x, y: x | y, [p.value.bit() for p in ps], 0)
+ permission_map[k] = permission_map.get(k, 0) | bits
+ "----------------------处理系统权限--------------------------------------------------"
+ system_permissions = [
+ PERMISSION_STR_MAP.get(_role_permission_mapping.permission_id)
+ for _role_permission_mapping in role_permission_mapping_list
+ ]
+ system_permissions = [
+ _permission
+ for _permission in system_permissions
+ if PermissionScopeConstants.SYSTEM in _permission.meta.scope
+ ]
+ for group, permissions in group_by(system_permissions, lambda _permission: _permission.meta.group).items():
+ permission_map[f"{group}"] = reduce(
+ lambda x, y: x | y, [_permission.value.bit() for _permission in permissions], 0
+ )
+ cache.set(key, permission_map, version=version)
else:
- workspace_id_list = ['default']
- workspace_user_resource_permission_list = QuerySet(WorkspaceUserResourcePermission).filter(
- workspace_id__in=workspace_id_list, user_id=user_id)
- role_permission_mapping_list = get_default_role_permission_mapping_list()
- role_permission_mapping_dict = group_by(role_permission_mapping_list, lambda item: item.role_id)
- workspace_user_role_mapping_list = get_default_workspace_user_role_mapping_list([user.role])
- workspace_user_role_mapping_dict = group_by(workspace_user_role_mapping_list,
- lambda item: item.workspace_id)
- # 资源权限
- workspace_resource_permission_list = get_workspace_resource_permission_list(
- workspace_user_resource_permission_list,
- role_permission_mapping_dict,
- workspace_user_role_mapping_dict)
- # 合并权限
- permission_list = workspace_resource_permission_list
- permission_list = list(set(permission_list))
- cache.set(key, permission_list, version=version)
- return permission_list
-
+ workspace_id_list = ["default"]
+ workspace_user_permission_list = QuerySet(WorkspaceUserResourcePermission).filter(
+ workspace_id__in=workspace_id_list, user_id=user_id
+ )
+ workspace_user_group_resource_permission_list = (
+ QuerySet(WorkspaceUserGroupResourcePermission)
+ .filter(user_group__user_relations__user_id=user_id)
+ .select_related("user_group")
+ .distinct()
+ )
-system_role_list = [RoleConstants.ADMIN.value.name, RoleConstants.WORKSPACE_MANAGE.value.name,
- RoleConstants.USER.value.name]
+ for _ in list(workspace_user_permission_list) + list(workspace_user_group_resource_permission_list):
+ if _.auth_type == ResourceAuthType.RESOURCE_PERMISSION_GROUP:
+ all_permissions = flat_map(
+ [
+ RESOURCE_PERMISSION_MAP.get(f"{_.auth_target_type}_{_resource_permission}")
+ for _resource_permission in _.permission_list
+ if _resource_permission in ["VIEW", "MANAGE"]
+ ]
+ )
+ for group, permissions in group_by(
+ all_permissions, lambda _permission: _permission.value.group
+ ).items():
+ permission_map[f"{group}:w:{_.workspace_id}:r:{_.target}"] = reduce(
+ lambda x, y: x | y, [_permission.value.bit() for _permission in permissions], 0
+ )
+ cache.set(key, permission_map, version=version)
+
+ return permission_map
+
+
+system_role_list = [
+ RoleConstants.ADMIN.value.name,
+ RoleConstants.WORKSPACE_MANAGE.value.name,
+ RoleConstants.USER.value.name,
+]
system_role = RoleConstants.ADMIN.value.name
@@ -237,22 +195,18 @@ def reset_workspace_role(role_id, workspace_id, role_dict):
if system_role == role_id:
return [role_id]
else:
- return [f"{role_id}:/WORKSPACE/{workspace_id}", role_id]
+ return [f"{role_id}:w:{workspace_id}", role_id]
else:
r = role_dict.get(role_id)
if r is None:
- return ''
+ return []
role_type = role_dict.get(role_id).type
if system_role == role_type:
return [RoleConstants.EXTENDS_ADMIN.value.name]
- return [f"EXTENDS_{role_type}:/WORKSPACE/{workspace_id}", f"EXTENDS_{role_type}"]
+ return [f"EXTENDS_{role_type}:w:{workspace_id}"]
-def get_role_list(user,
- workspace_user_role_mapping_model,
- workspace_model,
- role_model,
- role_permission_mapping_model):
+def get_role_list(user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model):
"""
获取当前用户的角色列表
"""
@@ -260,7 +214,12 @@ def get_role_list(user,
key = Cache_Version.ROLE_LIST.get_key(user_id=user.id)
role_list = cache.get(key, version=version)
# 获取权限列表
- is_query_model = workspace_user_role_mapping_model is not None and workspace_model is not None and role_model is not None and role_permission_mapping_model is not None
+ is_query_model = (
+ workspace_user_role_mapping_model is not None
+ and workspace_model is not None
+ and role_model is not None
+ and role_permission_mapping_model is not None
+ )
if role_list is None:
if is_query_model:
# 获取工作空间 用户 角色映射数据
@@ -268,18 +227,25 @@ def get_role_list(user,
role_list = QuerySet(role_model).filter(id__in=[wurm.role_id for wurm in workspace_user_role_mapping_list])
role_dict = {r.id: r for r in role_list}
role_list = list(
- set(reduce(lambda x, y: [*x, *y], [reset_workspace_role(workspace_user_role_mapping.role_id,
- workspace_user_role_mapping.workspace_id,
- role_dict)
- for
- workspace_user_role_mapping in
- workspace_user_role_mapping_list], [])))
+ set(
+ reduce(
+ lambda x, y: [*x, *y],
+ [
+ reset_workspace_role(
+ workspace_user_role_mapping.role_id, workspace_user_role_mapping.workspace_id, role_dict
+ )
+ for workspace_user_role_mapping in workspace_user_role_mapping_list
+ ],
+ [],
+ )
+ )
+ )
cache.set(key, role_list, version=version)
else:
if user.role == RoleConstants.ADMIN.value.__str__():
- role_list = [user.role, get_role_permission(RoleConstants.WORKSPACE_MANAGE, 'default')]
+ role_list = [user.role, f"{RoleConstants.WORKSPACE_MANAGE}:w:default"]
else:
- role_list = [user.role, get_role_permission(RoleConstants.USER, 'default')]
+ role_list = [user.role, f"{RoleConstants.USER}:w:default"]
cache.set(key, role_list, version=version)
return role_list
@@ -290,11 +256,13 @@ def get_auth(user):
role_model = DatabaseModelManage.get_model("role_model")
role_permission_mapping_model = DatabaseModelManage.get_model("role_permission_mapping_model")
- permission_list = get_permission_list(user, workspace_user_role_mapping_model, workspace_model,
- role_model, role_permission_mapping_model)
- role_list = get_role_list(user, workspace_user_role_mapping_model, workspace_model,
- role_model, role_permission_mapping_model)
- return Auth(role_list, permission_list)
+ permissions = get_permissions(
+ user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model
+ )
+ role_list = get_role_list(
+ user, workspace_user_role_mapping_model, workspace_model, role_model, role_permission_mapping_model
+ )
+ return Auth(set(role_list), permissions)
class UserToken(AuthBaseHandle):
@@ -302,18 +270,18 @@ def support(self, request, token: str, get_token_details):
auth_details = get_token_details()
if auth_details is None:
return False
- return 'id' in auth_details and auth_details.get('type') == AuthenticationType.SYSTEM_USER.value
+ return "id" in auth_details and auth_details.get("type") == AuthenticationType.SYSTEM_USER.value
def handle(self, request, token: str, get_token_details):
version, get_key = Cache_Version.TOKEN.value
cache_token = cache.get(get_key(token), version=version)
if cache_token is None:
- raise AppAuthenticationFailed(1002, _('Login expired'))
+ raise AppAuthenticationFailed(1002, _("Login expired"))
auth_details = get_token_details()
timeout = CONFIG.get_session_timeout()
cache.touch(token, timeout=timeout, version=version)
- user = QuerySet(User).get(id=auth_details['id'])
+ user = QuerySet(User).get(id=auth_details["id"])
if not user.is_active or user.password != cache_token.password:
- raise AppAuthenticationFailed(1002, _('Authentication information is incorrect'))
+ raise AppAuthenticationFailed(1002, _("Authentication information is incorrect"))
auth = get_auth(user)
- return user, auth
+ return Principal(user.id, UserType.SYSTEM_USER, user), auth
diff --git a/apps/common/auth/struct/aggregate_permission.py b/apps/common/auth/struct/aggregate_permission.py
new file mode 100644
index 00000000000..cb1c7e92098
--- /dev/null
+++ b/apps/common/auth/struct/aggregate_permission.py
@@ -0,0 +1,85 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: aggregate_permission.py
+@date:2026/8/5 10:11
+@desc:
+"""
+
+from typing import Protocol, List, Union
+
+from rest_framework.request import Request
+
+from common.auth.constants.compare_constants import CompareConstants
+from common.auth.constants.permission_constants import PermissionConstants
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.permission import Role, Permission
+
+
+class RoleFunc(Protocol):
+ def __call__(self, request: Request, kwargs) -> RoleConstants | Role: ...
+
+
+class PermissionFunc(Protocol):
+ def __call__(self, request: Request, kwargs) -> PermissionConstants | Permission: ...
+
+
+class AggregatePermission:
+ def __init__(
+ self,
+ roles: List[Union[RoleConstants, RoleFunc]] = None,
+ permissions: List[Union[PermissionConstants, PermissionFunc]] = None,
+ aggregatePermissions: List["AggregatePermission"] = None,
+ compare: CompareConstants = CompareConstants.OR,
+ ):
+ # 不能用可变默认值;原 stub 的 `= list` 其实是把类型对象赋进去了
+ self.roles = roles if roles is not None else []
+ self.permissions = permissions if permissions is not None else []
+ self.aggregatePermissions = aggregatePermissions if aggregatePermissions is not None else []
+ self.compare = compare
+
+ def hasPermission(self, request: Request, **kwargs) -> bool:
+ user_roles = request.auth.roles
+ user_permissions = request.auth.permissions
+
+ # 无任何约束 => 放行(对应 Java 里五个集合全空的判断)
+ if not (self.roles or self.permissions or self.aggregatePermissions):
+ return True
+
+ is_and = self.compare == CompareConstants.AND
+
+ # 惰性产出每一项的命中结果,保证 OR/AND 的短路语义
+ # (return 后生成器不再前进,后面的 permission/role 不会被求值)
+ def results():
+ for role in self.roles:
+ resolved = role(request, kwargs) if callable(role) else role
+ yield self._match_role(resolved, user_roles)
+ for permission in self.permissions:
+ resolved = permission(request, kwargs) if callable(permission) else permission
+ yield self._match_permission(resolved, user_permissions)
+ for aggregate in self.aggregatePermissions:
+ yield aggregate.hasPermission(request, **kwargs)
+
+ for has in results():
+ if has and not is_and: # OR:命中一个即通过
+ return True
+ if not has and is_and: # AND:缺一个即失败
+ return False
+
+ # AND 全部通过 => True;OR 一个都没命中 => False
+ return is_and
+
+ @staticmethod
+ def _match_permission(permission: Union[PermissionConstants, Permission], user_permissions: dict) -> bool:
+ p = permission.value if isinstance(permission, PermissionConstants) else permission
+ key = p.get_resource_permission_key(p.resource_id) if p.resource_id else str(p)
+ return key in user_permissions and (user_permissions[key] & p.bit()) > 0
+
+ @staticmethod
+ def _match_role(role: Union[RoleConstants, Role], user_roles) -> bool:
+ r = role.value if isinstance(role, RoleConstants) else role
+ return str(r) in user_roles
+
+
+ViewPermission = AggregatePermission
diff --git a/apps/common/auth/struct/auth.py b/apps/common/auth/struct/auth.py
new file mode 100644
index 00000000000..2c87af82a19
--- /dev/null
+++ b/apps/common/auth/struct/auth.py
@@ -0,0 +1,41 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: auth.py
+ @date:2026/8/5 9:45
+ @desc:
+"""
+from typing import Dict
+
+from application.models import ChatUserType
+from common.auth.constants.role_constants import RoleConstants
+from common.auth.struct.permission import Role
+from common.constants.authentication_type import UserType
+
+
+class Auth:
+ """
+ 用于存储当前用户的角色和权限
+ """
+
+ def __init__(self,
+ roles: set[RoleConstants | Role | str],
+ permissions: Dict[str, int],
+ **kwargs):
+ # 权限列表
+ self.permissions = permissions
+ # 角色列表
+ self.roles = roles
+ self.kwargs = kwargs
+
+
+class Principal:
+ def __init__(self, _id,
+ _type: ChatUserType | UserType,
+ profile=None,
+ **kwargs):
+ self.id = _id
+ self.type = _type
+ self.profile = profile
+ self.kwargs = kwargs
diff --git a/apps/common/auth/struct/permission.py b/apps/common/auth/struct/permission.py
new file mode 100644
index 00000000000..22ae43f2b04
--- /dev/null
+++ b/apps/common/auth/struct/permission.py
@@ -0,0 +1,72 @@
+# coding=utf-8
+"""
+@project: MaxKB
+@Author:虎虎虎
+@file: permission.py
+@date:2026/8/3 17:29
+@desc:
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional
+
+from common.auth.constants.category_constants import Category
+from common.auth.constants.group_constants import Group
+from common.auth.constants.operate_constants import Operate
+from common.auth.constants.permission_scope_constants import PermissionScopeConstants
+from common.auth.constants.role_group import RoleGroup
+
+
+@dataclass(frozen=True)
+class Permission:
+ """
+ 权限信息
+ """
+
+ group: Group | str
+ sub_group: Group | str
+ operate: Operate
+ bit_index: int
+ workspace_id: Optional[str] = None
+ resource_id: Optional[str] = None
+ flag: str = None
+
+ def bit(self):
+ return 1 << self.bit_index
+
+ def get_resource_permission_key(self, resource_id):
+ workspace = f":w:{self.workspace_id}" if self.workspace_id else ""
+ resource = f":r:{self.resource_id}" if self.resource_id else ""
+ return f"{self.group}{workspace}{resource}"
+
+ def __str__(self):
+ sub = f"_{self.sub_group}" if self.sub_group != self.group else ""
+ flag = f"_{self.flag}" if self.flag else ""
+ return f"{self.group}{sub}_{self.operate}{flag}"
+
+
+@dataclass
+class PermissionMeta:
+ role_list: list = field(default_factory=list)
+ category: Optional[Category] = None
+ resource_permission_group_list: Optional[list] = None
+ scope: list[PermissionScopeConstants] = field(default_factory=list)
+ is_ee: bool = False
+
+
+@dataclass(frozen=True)
+class Role:
+ name: str
+ workspace_id: str = None
+
+ def __str__(self):
+ return f"{self.name}{(':w:' + self.workspace_id) if self.workspace_id else ''}"
+
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+
+@dataclass(frozen=True)
+class RoleMeta:
+ desc: str
+ group: RoleGroup
diff --git a/apps/common/constants/authentication_type.py b/apps/common/constants/authentication_type.py
index 1880fe4d3cd..fe0696363ad 100644
--- a/apps/common/constants/authentication_type.py
+++ b/apps/common/constants/authentication_type.py
@@ -8,13 +8,15 @@
"""
from enum import Enum
+from django.db import models
+
class AuthenticationType(Enum):
# 系统用户
SYSTEM_USER = "SYSTEM_USER"
# 对话用户
CHAT_USER = "CHAT_USER"
- # 对话匿名用户
- CHAT_ANONYMOUS_USER = "CHAT_ANONYMOUS_USER"
- # APIKEY
- API_KEY = "API_KEY"
+
+
+class UserType(models.TextChoices):
+ SYSTEM_USER = "SYSTEM_USER", '系统用户'
diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py
index 0aed4715e2e..5642b7929ae 100644
--- a/apps/common/constants/cache_version.py
+++ b/apps/common/constants/cache_version.py
@@ -41,6 +41,8 @@ class Cache_Version(Enum):
TOOL_WORKFLOW_EXECUTE = "TOOL_WORKFLOW_EXECUTE", lambda key: key
+ DEBUG_WORKFLOW_CONTEXT = "DEBUG_WORKFLOW_CONTEXT", lambda chat_record_id: chat_record_id
+
def get_version(self):
return self.value[0]
diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py
deleted file mode 100644
index b1d40debdbc..00000000000
--- a/apps/common/constants/permission_constants.py
+++ /dev/null
@@ -1,2134 +0,0 @@
-"""
- @project: qabot
- @Author:虎虎
- @file: permission_constants.py
- @date:2023/9/13 18:23
- @desc: 权限,角色 常量
-"""
-from enum import Enum
-from functools import reduce
-from typing import List
-
-from django.db import models
-from django.utils.translation import gettext_lazy as _
-
-from maxkb import settings
-
-
-class Group(Enum):
- """
- 权限组 一个组一般对应前端一个菜单
- """
-
- USER = "USER_MANAGEMENT"
- # 应用
- APPLICATION = "APPLICATION"
- # 应用概览
- APPLICATION_OVERVIEW = "APPLICATION_OVERVIEW"
- # 应用接入
- APPLICATION_ACCESS = "APPLICATION_ACCESS"
- # 应用 对话用户
- APPLICATION_CHAT_USER = "APPLICATION_CHAT_USER"
- # 知识库 对话用户
- KNOWLEDGE_CHAT_USER = "KNOWLEDGE_CHAT_USER"
- # 应用对话日志
- APPLICATION_CHAT_LOG = "APPLICATION_CHAT_LOG"
-
- KNOWLEDGE = "KNOWLEDGE"
- SYSTEM_KNOWLEDGE = "SYSTEM_KNOWLEDGE"
- SYSTEM_RES_KNOWLEDGE = "SYSTEM_RESOURCE_KNOWLEDGE"
- KNOWLEDGE_HIT_TEST = "KNOWLEDGE_HIT_TEST"
- KNOWLEDGE_DOCUMENT = "KNOWLEDGE_DOCUMENT"
- KNOWLEDGE_WORKFLOW = "KNOWLEDGE_WORKFLOW"
- KNOWLEDGE_TAG = "KNOWLEDGE_TAG"
- SYSTEM_KNOWLEDGE_DOCUMENT = "SYSTEM_KNOWLEDGE_DOCUMENT"
- SYSTEM_KNOWLEDGE_WORKFLOW = "SYSTEM_KNOWLEDGE_WORKFLOW"
- SYSTEM_RES_KNOWLEDGE_DOCUMENT = "SYSTEM_RESOURCE_KNOWLEDGE_DOCUMENT"
- SYSTEM_RES_KNOWLEDGE_WORKFLOW = "SYSTEM_RESOURCE_KNOWLEDGE_WORKFLOW"
- SYSTEM_RES_KNOWLEDGE_TAG = "SYSTEM_RES_KNOWLEDGE_TAG"
- SYSTEM_KNOWLEDGE_TAG = "SYSTEM_KNOWLEDGE_TAG"
-
- KNOWLEDGE_PROBLEM = "KNOWLEDGE_PROBLEM"
- KNOWLEDGE_TERMBASE = "KNOWLEDGE_TERMBASE"
- SYSTEM_KNOWLEDGE_PROBLEM = "SYSTEM_KNOWLEDGE_PROBLEM"
- SYSTEM_KNOWLEDGE_TERMBASE = "SYSTEM_KNOWLEDGE_TERMBASE"
- SYSTEM_RES_KNOWLEDGE_PROBLEM = "SYSTEM_RESOURCE_KNOWLEDGE_PROBLEM"
- SYSTEM_RES_KNOWLEDGE_TERMBASE = "SYSTEM_RESOURCE_KNOWLEDGE_TERMBASE"
-
- SYSTEM_KNOWLEDGE_HIT_TEST = "SYSTEM_KNOWLEDGE_HIT_TEST"
- SYSTEM_RES_KNOWLEDGE_HIT_TEST = "SYSTEM_RESOURCE_KNOWLEDGE_HIT_TEST"
- SYSTEM_KNOWLEDGE_CHAT_USER = "SYSTEM_KNOWLEDGE_CHAT_USER"
- SYSTEM_RES_KNOWLEDGE_CHAT_USER = "SYSTEM_RESOURCE_KNOWLEDGE_CHAT_USER"
-
- MODEL = "MODEL"
- SYSTEM_MODEL = "SYSTEM_MODEL"
- SYSTEM_RES_MODEL = "SYSTEM_RESOURCE_MODEL"
- SYSTEM_RES_APPLICATION = "SYSTEM_RESOURCE_APPLICATION"
- SYSTEM_RES_APPLICATION_OVERVIEW = "SYSTEM_RESOURCE_APPLICATION_OVERVIEW"
- SYSTEM_RES_APPLICATION_ACCESS = "SYSTEM_RESOURCE_APPLICATION_ACCESS"
- SYSTEM_RES_APPLICATION_CHAT_USER = "SYSTEM_RESOURCE_APPLICATION_CHAT_USER"
- SYSTEM_RES_APPLICATION_CHAT_LOG = "SYSTEM_RESOURCE_APPLICATION_CHAT_LOG"
-
- TOOL = "TOOL"
- SYSTEM_TOOL = "SYSTEM_TOOL"
- SYSTEM_RES_TOOL = "SYSTEM_RESOURCE_TOOL"
-
- TRIGGER = "TRIGGER"
-
- APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION = "APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION"
- KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION = "KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION"
- TOOL_WORKSPACE_USER_RESOURCE_PERMISSION = "TOOL_WORKSPACE_USER_RESOURCE_PERMISSION"
- MODEL_WORKSPACE_USER_RESOURCE_PERMISSION = "MODEL_WORKSPACE_USER_RESOURCE_PERMISSION"
-
- EMAIL_SETTING = "EMAIL_SETTING"
- ROLE = "ROLE"
- WORKSPACE_ROLE = "WORKSPACE_ROLE"
- WORKSPACE = "WORKSPACE"
- WORKSPACE_WORKSPACE = "WORKSPACE_WORKSPACE"
-
- DISPLAY_SETTINGS = "DISPLAY_SETTINGS"
- LOGIN_AUTH = "LOGIN_AUTH"
- SYSTEM_API_KEY = "SYSTEM_API_KEY"
- APPEARANCE_SETTINGS = "APPEARANCE_SETTINGS"
- CHAT_USER = "CHAT_USER"
- WORKSPACE_CHAT_USER = "WORKSPACE_CHAT_USER"
- USER_GROUP = "USER_GROUP"
- WORKSPACE_USER_GROUP = "WORKSPACE_USER_GROUP"
- CHAT_USER_AUTH = "CHAT_USER_AUTH"
- OTHER = "OTHER"
- OVERVIEW = "OVERVIEW"
- OPERATION_LOG = "OPERATION_LOG"
-
- APPLICATION_FOLDER = "APPLICATION_FOLDER"
- KNOWLEDGE_FOLDER = "KNOWLEDGE_FOLDER"
- TOOL_FOLDER = "TOOL_FOLDER"
-
-
-class SystemGroup(Enum):
- """
- 一级菜单
- """
- USER_MANAGEMENT = "USER_MANAGEMENT"
- ROLE = "ROLE"
- WORKSPACE = "WORKSPACE"
- # RESOURCE = "RESOURCE"
- RESOURCE_APPLICATION = "RESOURCE_APPLICATION"
- RESOURCE_KNOWLEDGE = "RESOURCE_KNOWLEDGE"
- RESOURCE_TOOL = "RESOURCE_TOOL"
- RESOURCE_MODEL = "RESOURCE_MODEL"
- RESOURCE_PERMISSION = "RESOURCE_PERMISSION"
- SHARED_KNOWLEDGE = "SHARED_KNOWLEDGE"
- SHARED_MODEL = "SHARED_MODEL"
- SHARED_TOOL = "SHARED_TOOL"
- CHAT_USER = "CHAT_USER"
- SYSTEM_SETTING = "SYSTEM_SETTING"
- OPERATION_LOG = "OPERATION_LOG"
- OTHER = "OTHER"
-
-
-class WorkspaceGroup(Enum):
- SYSTEM_MANAGEMENT = "SYSTEM_MANAGEMENT"
- APPLICATION = "APPLICATION"
- KNOWLEDGE = "KNOWLEDGE"
- MODEL = "MODEL"
- TOOL = "TOOL"
- TRIGGER = "TRIGGER"
- RESOURCE_PERMISSION = "RESOURCE_PERMISSION"
- OTHER = "OTHER"
-
-
-class UserGroup(Enum):
- APPLICATION = "APPLICATION"
- KNOWLEDGE = "KNOWLEDGE"
- MODEL = "MODEL"
- TOOL = "TOOL"
- OTHER = "OTHER"
-
-
-class Operate(Enum):
- """
- 一个权限组的操作权限
- """
- SELF = ""
- READ = 'READ'
- EDIT = "READ+EDIT"
- CREATE = "READ+CREATE"
- DELETE = "READ+DELETE"
- """
- 使用权限
- """
- USE = "USE"
- IMPORT = "READ+IMPORT"
- EXPORT = "READ+EXPORT" # 导入导出
- PUBLISH = "READ+PUBLISH" # 发布
- SYNC = "READ+SYNC" # 同步
- GENERATE = "READ+GENERATE" # 生成
- ADD_MEMBER = "READ+ADD_MEMBER" # 添加成员
- REMOVE_MEMBER = "READ+REMOVE_MEMBER" # 添加成员
- VECTOR = "READ+VECTOR" # 向量化
- MIGRATE = "READ+MIGRATE" # 迁移
- RELATE = "READ+RELATE" # 关联
- USER_GROUP = "READ+USER_GROUP" # 用户组
- ANNOTATION = "READ+ANNOTATION" # 标注
- CLEAR_POLICY = "READ+CLEAR_POLICY"
- EMBED = "READ+EMBED" # 嵌入
- ACCESS = "READ+ACCESS" # 访问限制
- DISPLAY = "READ+DISPLAY" # 显示设置
- API_KEY = "READ+API_KEY" # API_KEY
- PUBLIC_ACCESS = "READ+PUBLIC_ACCESS" # 公共访问链接
- Q_WEIXIN = "READ+Q_WEIXIN" # 企业微信
- FEISHU = "READ+FEISHU" # 飞书
- DD = "READ+DD" # 钉钉
- WEIXIN_PUBLIC_ACCOUNT = "READ+WEIXIN_PUBLIC_ACCOUNT" # 微信公众号
- SLACK = "READ+SLACK" # SLACK
- ADD_KNOWLEDGE = "READ+ADD_KNOWLEDGE" # 添加到知识库
- TO_CHAT = "READ+TO_CHAT" # 去对话
- SETTING = "READ+SETTING" # 管理
- DOWNLOAD = "READ+DOWNLOAD" # 下载
- COPY = "READ+COPY"
- AUTH = "READ+AUTH" # 资源授权
- TAG = "READ+TAG" # 标签设置
- REPLACE = "READ+REPLACE" # 标签设置
- UPDATE = "READ+UPDATE" # 更新license
- RELATE_VIEW = "READ+RELATE_VIEW"
- RECORD = "READ+RECORD"
- TRIGGER_READ = "READ+TRIGGER_READ"
- TRIGGER_EDIT = "READ+TRIGGER_EDIT"
- TRIGGER_CREATE = "READ+TRIGGER_CREATE"
- TRIGGER_DELETE = "READ+TRIGGER_DELETE"
- BATCH_DELETE = "READ+BATCH_DELETE"
- BATCH_MOVE = "READ+BATCH_MOVE"
-
-
-class RoleGroup(Enum):
- # 系统用户
- SYSTEM_USER = "SYSTEM_USER"
- # 对话用户
- CHAT_USER = "CHAT_USER"
-
-
-class ResourcePermissionRole(models.TextChoices):
- """
- 资源权限根据角色
- """
- ROLE = "ROLE"
-
- def __eq__(self, other):
- return str(self) == str(other)
-
-
-class ResourcePermission(models.TextChoices):
- """
- 资源权限组
- """
- # 查看
- VIEW = "VIEW"
- # 管理
- MANAGE = "MANAGE"
-
- def __eq__(self, other):
- return str(self) == str(other)
-
-
-class Resource(models.TextChoices):
- KNOWLEDGE = Group.KNOWLEDGE.value
- KNOWLEDGE_FOLDER = Group.KNOWLEDGE_FOLDER.value
- APPLICATION = Group.APPLICATION.value
- APPLICATION_FOLDER = Group.APPLICATION_FOLDER.value
- TOOL = Group.TOOL.value
- TOOL_FOLDER = Group.TOOL_FOLDER.value
- MODEL = Group.MODEL.value
-
- def __eq__(self, other):
- return str(self) == str(other)
-
-
-class ResourcePermissionGroup:
- def __init__(self, resource: Resource, permission: ResourcePermission):
- self.permission = permission
- self.resource = resource
-
- def __eq__(self, other):
- return str(self.permission) == str(other.permission) and str(self.resource) == str(other.resource)
-
-
-class ResourcePermissionConst:
- KNOWLEDGE_MANGE = ResourcePermissionGroup(Resource.KNOWLEDGE, ResourcePermission.MANAGE)
- KNOWLEDGE_FOLDER_MANGE = ResourcePermissionGroup(Resource.KNOWLEDGE_FOLDER, ResourcePermission.MANAGE)
- KNOWLEDGE_FOLDER_VIEW = ResourcePermissionGroup(Resource.KNOWLEDGE_FOLDER, ResourcePermission.VIEW)
- KNOWLEDGE_VIEW = ResourcePermissionGroup(Resource.KNOWLEDGE, ResourcePermission.VIEW)
- APPLICATION_MANGE = ResourcePermissionGroup(Resource.APPLICATION, ResourcePermission.MANAGE)
- APPLICATION_FOLDER_MANGE = ResourcePermissionGroup(Resource.APPLICATION_FOLDER, ResourcePermission.MANAGE)
- APPLICATION_FOLDER_VIEW = ResourcePermissionGroup(Resource.APPLICATION_FOLDER, ResourcePermission.VIEW)
- APPLICATION_VIEW = ResourcePermissionGroup(Resource.APPLICATION, ResourcePermission.VIEW)
- TOOL_MANGE = ResourcePermissionGroup(Resource.TOOL, ResourcePermission.MANAGE)
- TOOL_FOLDER_MANGE = ResourcePermissionGroup(Resource.TOOL_FOLDER, ResourcePermission.MANAGE)
- TOOL_FOLDER_VIEW = ResourcePermissionGroup(Resource.TOOL_FOLDER, ResourcePermission.VIEW)
- TOOL_VIEW = ResourcePermissionGroup(Resource.TOOL, ResourcePermission.VIEW)
- MODEL_MANGE = ResourcePermissionGroup(Resource.MODEL, ResourcePermission.MANAGE)
- MODEL_VIEW = ResourcePermissionGroup(Resource.MODEL, ResourcePermission.VIEW)
-
-
-class ResourceAuthType(models.TextChoices):
- """
- 资源授权类型
- """
- "当授权类型是Role时候"
- ROLE = "ROLE"
-
- """资源权限组"""
- RESOURCE_PERMISSION_GROUP = "RESOURCE_PERMISSION_GROUP"
-
-
-class Role:
- def __init__(self, name: str, decs: str, group: RoleGroup, resource_path=None):
- self.name = name
- self.decs = decs
- self.group = group
- self.resource_path = resource_path
-
- def __str__(self):
- return self.name + (
- (":" + self.resource_path) if self.resource_path is not None else '')
-
- def __eq__(self, other):
- return str(self) == str(other)
-
- def get_workspace_role(self):
- return lambda r, kwargs: Role(self.name, self.decs, self.group,
- resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}")
-
-
-class RoleConstants(Enum):
- ADMIN = Role("ADMIN", '超级管理员', RoleGroup.SYSTEM_USER)
- WORKSPACE_MANAGE = Role("WORKSPACE_MANAGE", '工作空间管理员', RoleGroup.SYSTEM_USER)
- USER = Role("USER", '普通用户', RoleGroup.SYSTEM_USER)
- CHAT_ANONYMOUS_USER = Role("CHAT_ANONYMOUS_USER", "对话匿名用户", RoleGroup.CHAT_USER)
- CHAT_USER = Role("CHAT_USER", "对话用户", RoleGroup.CHAT_USER)
-
- EXTENDS_ADMIN = Role("EXTENDS_ADMIN", '继承超级管理员', RoleGroup.SYSTEM_USER)
- EXTENDS_WORKSPACE_MANAGE = Role("EXTENDS_WORKSPACE_MANAGE", "继承工作空间管理员", RoleGroup.CHAT_USER)
- EXTENDS_USER = Role("EXTENDS_USER", "继承普通用户", RoleGroup.CHAT_USER)
-
- def get_workspace_role(self):
- return lambda r, kwargs: Role(name=self.value.name,
- decs=self.value.decs,
- group=self.value.group,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}")
-
-
-Permission_Label = {
- SystemGroup.SYSTEM_SETTING.value: _("System Setting"),
- SystemGroup.USER_MANAGEMENT.value: _("User Management"),
- SystemGroup.ROLE.value: _("Role"),
- SystemGroup.WORKSPACE.value: _("Workspace"),
- SystemGroup.RESOURCE_APPLICATION.value: _("Resource Application"),
- SystemGroup.RESOURCE_KNOWLEDGE.value: _("Resource Knowledge"),
- SystemGroup.RESOURCE_TOOL.value: _("Resource Tool"),
- SystemGroup.RESOURCE_MODEL.value: _("Resource Model"),
- SystemGroup.RESOURCE_PERMISSION.value: _("Resource Permission"),
- SystemGroup.SHARED_KNOWLEDGE.value: _("Shared Knowledge"),
- SystemGroup.SHARED_MODEL.value: _("Shared Model"),
- SystemGroup.SHARED_TOOL.value: _("Shared Tool"),
- SystemGroup.OPERATION_LOG.value: _("Operation Log"),
- SystemGroup.OTHER.value: _("Other"),
- WorkspaceGroup.SYSTEM_MANAGEMENT.value: _("System Management"),
- WorkspaceGroup.APPLICATION.value: _("Application"),
- WorkspaceGroup.KNOWLEDGE.value: _("Knowledge"),
- WorkspaceGroup.MODEL.value: _("Model"),
- WorkspaceGroup.TOOL.value: _("Tool"),
- WorkspaceGroup.TRIGGER.value: _("Trigger"),
- WorkspaceGroup.OTHER.value: _("Other"),
- Operate.READ.value: _("Read"),
- Operate.EDIT.value: _("Edit"),
- Operate.COPY.value: _('Copy'),
- Operate.PUBLISH.value: _("Publish"),
- Operate.CREATE.value: _("Create"),
- Operate.DELETE.value: _("Delete"),
- Group.EMAIL_SETTING.value: _("Email Setting"),
- Group.APPLICATION.value: _("Application"),
- Group.KNOWLEDGE.value: _("Knowledge"),
- Group.KNOWLEDGE_DOCUMENT.value: _("Document"),
- Group.KNOWLEDGE_TERMBASE.value: _("Termbase"),
- Group.KNOWLEDGE_WORKFLOW.value: _("Workflow"),
- Group.KNOWLEDGE_TAG.value: _("Tag"),
- Group.KNOWLEDGE_PROBLEM.value: _("Problem"),
- Group.KNOWLEDGE_HIT_TEST.value: _("Hit-Test"),
- Operate.IMPORT.value: _("Import"),
- Operate.EXPORT.value: _("Export"),
- Operate.SYNC.value: _("Sync"),
- Operate.GENERATE.value: _("Generate"),
- Operate.ADD_MEMBER.value: _("Add Member"),
- Operate.REMOVE_MEMBER.value: _("Remove Member"),
- Operate.VECTOR.value: _("Vector"),
- Operate.MIGRATE.value: _("Migrate"),
- Operate.RELATE.value: _("Relate"),
- Operate.ANNOTATION.value: _("Annotation"),
- Operate.CLEAR_POLICY.value: _("Clear Policy"),
- Operate.DOWNLOAD.value: _('Download Original Document'),
- Operate.EMBED.value: _('Embed third party'),
- Operate.ACCESS.value: _('Access restrictions'),
- Operate.DISPLAY.value: _('Display Settings'),
- Operate.API_KEY.value: _('API KEY'),
- Operate.PUBLIC_ACCESS.value: _('Public access link'),
- Operate.Q_WEIXIN.value: _('Enterprise WeiXin'),
- Operate.FEISHU.value: _('Feishu'),
- Operate.DD.value: _('Dingding'),
- Operate.WEIXIN_PUBLIC_ACCOUNT.value: _('Weixin Public Account'),
- Operate.ADD_KNOWLEDGE.value: _('Add to Knowledge Base'),
- Operate.AUTH.value: _('resource authorization'),
- Operate.TAG.value: _('Tag Setting'),
- Operate.REPLACE.value: _('Replace Original Document'),
- Operate.RELATE_VIEW.value: _('View related resources'),
- Operate.TRIGGER_READ.value: _('Read Trigger'),
- Operate.TRIGGER_CREATE.value: _('Create Trigger'),
- Operate.TRIGGER_EDIT.value: _('Edit Trigger'),
- Operate.TRIGGER_DELETE.value: _('Delete Trigger'),
- Operate.RECORD.value: _('Read execute record'),
- Operate.BATCH_DELETE.value: _('Batch delete'),
- Operate.BATCH_MOVE.value: _('Batch move'),
-
- Group.APPLICATION_OVERVIEW.value: _('Overview'),
- Group.APPLICATION_ACCESS.value: _('Application Access'),
- Group.APPLICATION_CHAT_USER.value: _('Dialogue users'),
- Group.APPLICATION_CHAT_LOG.value: _('Conversation log'),
- Group.KNOWLEDGE_CHAT_USER.value: _('Dialogue users'),
-
- Group.LOGIN_AUTH.value: _("Login Auth"),
- Group.DISPLAY_SETTINGS.value: _("Display Settings"),
- Group.SYSTEM_API_KEY.value: _("System API Key"),
- Group.APPEARANCE_SETTINGS.value: _("Appearance Settings"),
- Group.CHAT_USER.value: _("Chat User"),
- Group.USER_GROUP.value: _("User Group"),
- Group.CHAT_USER_AUTH.value: _("Chat User Auth"),
- Group.OVERVIEW.value: _("Overview"),
- Group.SYSTEM_TOOL.value: _("Tool"),
- Group.SYSTEM_MODEL.value: _("Model"),
- Group.SYSTEM_KNOWLEDGE.value: _("Knowledge"),
- Group.SYSTEM_KNOWLEDGE_DOCUMENT.value: _("Document"),
- Group.SYSTEM_KNOWLEDGE_TERMBASE.value: _("Termbase"),
- Group.SYSTEM_KNOWLEDGE_WORKFLOW.value: _("Workflow"),
- Group.SYSTEM_KNOWLEDGE_TAG.value: _("Tag"),
- Group.SYSTEM_KNOWLEDGE_PROBLEM.value: _("Problem"),
- Group.SYSTEM_KNOWLEDGE_HIT_TEST.value: _("Hit-Test"),
- Group.SYSTEM_KNOWLEDGE_CHAT_USER.value: _("Dialogue users"),
- Group.SYSTEM_RES_TOOL.value: _("Tool"),
- Group.SYSTEM_RES_MODEL.value: _("Model"),
- Group.SYSTEM_RES_KNOWLEDGE.value: _("Knowledge"),
- Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT.value: _("Document"),
- Group.SYSTEM_RES_KNOWLEDGE_TERMBASE.value: _("Termbase"),
- Group.SYSTEM_RES_KNOWLEDGE_WORKFLOW.value: _("Workflow"),
- Group.SYSTEM_RES_KNOWLEDGE_TAG.value: _("Tag"),
- Group.SYSTEM_RES_KNOWLEDGE_PROBLEM.value: _("Problem"),
- Group.SYSTEM_RES_KNOWLEDGE_HIT_TEST.value: _("Hit-Test"),
- Group.SYSTEM_RES_KNOWLEDGE_CHAT_USER.value: _("Dialogue users"),
- Group.WORKSPACE_USER_GROUP.value: _("User Group"),
- Group.WORKSPACE_CHAT_USER.value: _("Chat User"),
- Group.WORKSPACE_WORKSPACE.value: _("Workspace"),
- Group.WORKSPACE_ROLE.value: _("Role"),
- Group.APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION.value: _("Application"),
- Group.KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION.value: _("Knowledge"),
- Group.MODEL_WORKSPACE_USER_RESOURCE_PERMISSION.value: _("Model"),
- Group.TOOL_WORKSPACE_USER_RESOURCE_PERMISSION.value: _("Tool"),
- Group.SYSTEM_RES_APPLICATION.value: _("Application"),
- Group.SYSTEM_RES_APPLICATION_OVERVIEW.value: _("Overview"),
- Group.SYSTEM_RES_APPLICATION_ACCESS.value: _("Application Access"),
- Group.SYSTEM_RES_APPLICATION_CHAT_USER.value: _("Dialogue users"),
- Group.SYSTEM_RES_APPLICATION_CHAT_LOG.value: _("Conversation log"),
- Group.APPLICATION_FOLDER.value: _("Folder"),
- Group.KNOWLEDGE_FOLDER.value: _("Folder"),
- Group.TOOL_FOLDER.value: _("Folder"),
- # SystemGroup.RESOURCE.value: _("Resource"),
-}
-
-
-class Permission:
- """
- 权限信息
- """
-
- def __init__(self, group: Group, operate: Operate, resource_path=None, role_list=None,
- resource_permission_group_list=None, parent_group=None, label=None, is_ee=True):
- if role_list is None:
- role_list = []
- if resource_permission_group_list is None:
- resource_permission_group_list = []
- self.group = group
- self.operate = operate
- self.resource_path = resource_path
- # 用于获取角色与权限的关系,只适用于没有权限管理的
- self.role_list = role_list
- # 用于资源权限权限分组
- self.resource_permission_group_list = resource_permission_group_list
- self.parent_group = parent_group # 新增字段:父级组
- self.label = label
- self.is_ee = is_ee # 是否是企业版权限
-
- @staticmethod
- def new_instance(permission_str: str):
- permission_split = permission_str.split(":")
- group = Group[permission_split[0]]
- operate = Operate[permission_split[1]]
- if len(permission_split) > 2:
- dynamic_tag = ":".join(permission_split[2:])
- return Permission(group, operate, dynamic_tag)
- return Permission(group, operate)
-
- def __str__(self):
-
- return self.group.value + (
- (":" + self.operate.value) if self.operate.value else '') + (
- (":" + self.resource_path) if self.resource_path is not None else '')
-
- def __eq__(self, other):
- return str(self) == str(other)
-
-
-class PermissionConstants(Enum):
- """
- 权限枚举
- """
- KNOWLEDGE = Permission(
- group=Group.KNOWLEDGE, operate=Operate.SELF, role_list=[RoleConstants.ADMIN, RoleConstants.USER]
- )
- APPLICATION = Permission(
- group=Group.APPLICATION, operate=Operate.SELF, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- )
- MODEL = Permission(
- group=Group.MODEL, operate=Operate.SELF, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- )
- TOOL = Permission(
- group=Group.TOOL, operate=Operate.SELF, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- )
- USER_READ = Permission(
- group=Group.USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.USER_MANAGEMENT]
- )
-
- USER_CREATE = Permission(
- group=Group.USER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.USER_MANAGEMENT]
- )
-
- USER_EDIT = Permission(
- group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.USER_MANAGEMENT]
- )
-
- USER_DELETE = Permission(
- group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.USER_MANAGEMENT]
- )
-
- MODEL_READ = Permission(
- group=Group.MODEL, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_VIEW]
- )
-
- MODEL_CREATE = Permission(
- group=Group.MODEL, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE]
- )
-
- MODEL_EDIT = Permission(
- group=Group.MODEL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE]
- )
- MODEL_DELETE = Permission(
- group=Group.MODEL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE]
- )
- MODEL_RESOURCE_AUTHORIZATION = Permission(
- group=Group.MODEL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE]
- )
- MODEL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.MODEL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL],
- resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE]
- )
- # trigger
- TRIGGER_READ = Permission(
- group=Group.TRIGGER, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.TRIGGER],
- )
- TRIGGER_CREATE = Permission(
- group=Group.TRIGGER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.TRIGGER],
- )
- TRIGGER_EDIT = Permission(
- group=Group.TRIGGER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.TRIGGER],
- )
- TRIGGER_DELETE = Permission(
- group=Group.TRIGGER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.TRIGGER],
- )
- TRIGGER_RECORD = Permission(
- group=Group.TRIGGER, operate=Operate.RECORD, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.TRIGGER],
- )
- TOOL_READ = Permission(
- group=Group.TOOL, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW]
- )
-
- TOOL_CREATE = Permission(
- group=Group.TOOL, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_BATCH_MOVE = Permission(
- group=Group.TOOL, operate=Operate.BATCH_MOVE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_BATCH_DELETE = Permission(
- group=Group.TOOL, operate=Operate.BATCH_DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_EDIT = Permission(
- group=Group.TOOL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
-
- TOOL_DELETE = Permission(
- group=Group.TOOL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_IMPORT = Permission(
- group=Group.TOOL, operate=Operate.IMPORT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_EXPORT = Permission(
- group=Group.TOOL, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_RESOURCE_AUTHORIZATION = Permission(
- group=Group.TOOL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.TOOL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_PUBLISH = Permission(
- group=Group.TOOL, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_EXECUTE_RECORD = Permission(
- group=Group.TOOL, operate=Operate.RECORD, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- # source point trigger
- TOOL_TRIGGER_READ = Permission(
- group=Group.TOOL, operate=Operate.TRIGGER_READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_TRIGGER_CREATE = Permission(
- group=Group.TOOL, operate=Operate.TRIGGER_CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW]
- )
- TOOL_TRIGGER_EDIT = Permission(
- group=Group.TOOL, operate=Operate.TRIGGER_EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW]
- )
- TOOL_TRIGGER_DELETE = Permission(
- group=Group.TOOL, operate=Operate.TRIGGER_DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW]
- )
- TOOL_FOLDER_READ = Permission(
- group=Group.TOOL_FOLDER, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_VIEW]
- )
- TOOL_FOLDER_CREATE = Permission(
- group=Group.TOOL_FOLDER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_FOLDER_EDIT = Permission(
- group=Group.TOOL_FOLDER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_FOLDER_DELETE = Permission(
- group=Group.TOOL_FOLDER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- TOOL_FOLDER_AUTH = Permission(
- group=Group.TOOL_FOLDER, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL],
- resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE]
- )
- KNOWLEDGE_READ = Permission(
- group=Group.KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_CREATE = Permission(
- group=Group.KNOWLEDGE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_EDIT = Permission(
- group=Group.KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DELETE = Permission(
- group=Group.KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_SYNC = Permission(
- group=Group.KNOWLEDGE, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_EXPORT = Permission(
- group=Group.KNOWLEDGE, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_VECTOR = Permission(
- group=Group.KNOWLEDGE, operate=Operate.VECTOR, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_GENERATE = Permission(
- group=Group.KNOWLEDGE, operate=Operate.GENERATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_BATCH_DELETE = Permission(group=Group.KNOWLEDGE, operate=Operate.BATCH_DELETE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE],
- )
- KNOWLEDGE_BATCH_MOVE = Permission(group=Group.KNOWLEDGE, operate=Operate.BATCH_MOVE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE],
- )
- KNOWLEDGE_RESOURCE_AUTHORIZATION = Permission(
- group=Group.KNOWLEDGE, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_RELATE_RESOURCE_VIEW = Permission(
- group=Group.KNOWLEDGE, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE]
- )
- KNOWLEDGE_FOLDER_READ = Permission(
- group=Group.KNOWLEDGE_FOLDER, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_FOLDER_CREATE = Permission(
- group=Group.KNOWLEDGE_FOLDER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_FOLDER_EDIT = Permission(
- group=Group.KNOWLEDGE_FOLDER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_FOLDER_DELETE = Permission(
- group=Group.KNOWLEDGE_FOLDER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_FOLDER_AUTH = Permission(
- group=Group.KNOWLEDGE_FOLDER, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_WORKFLOW_READ = Permission(
- group=Group.KNOWLEDGE_WORKFLOW, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_WORKFLOW_EDIT = Permission(
- group=Group.KNOWLEDGE_WORKFLOW, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_WORKFLOW_EXPORT = Permission(
- group=Group.KNOWLEDGE_WORKFLOW, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_WORKFLOW_PUBLISH = Permission(
- group=Group.KNOWLEDGE_WORKFLOW, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_READ = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_CREATE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_EDIT = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_DELETE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_SYNC = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_EXPORT = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.EXPORT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.DOWNLOAD,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_GENERATE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.GENERATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_VECTOR = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.VECTOR,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_MIGRATE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.MIGRATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_TAG = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.TAG,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_DOCUMENT_REPLACE = Permission(
- group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.REPLACE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_HIT_TEST = Permission(
- group=Group.KNOWLEDGE_HIT_TEST, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_PROBLEM_READ = Permission(
- group=Group.KNOWLEDGE_PROBLEM, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_PROBLEM_CREATE = Permission(
- group=Group.KNOWLEDGE_PROBLEM, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_PROBLEM_EDIT = Permission(
- group=Group.KNOWLEDGE_PROBLEM, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_PROBLEM_DELETE = Permission(
- group=Group.KNOWLEDGE_PROBLEM, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_PROBLEM_RELATE = Permission(
- group=Group.KNOWLEDGE_PROBLEM, operate=Operate.RELATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TERMBASE_READ = Permission(
- group=Group.KNOWLEDGE_TERMBASE, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TERMBASE_CREATE = Permission(
- group=Group.KNOWLEDGE_TERMBASE, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TERMBASE_EDIT = Permission(
- group=Group.KNOWLEDGE_TERMBASE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TERMBASE_DELETE = Permission(
- group=Group.KNOWLEDGE_TERMBASE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TAG_READ = Permission(
- group=Group.KNOWLEDGE_TAG, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TAG_CREATE = Permission(
- group=Group.KNOWLEDGE_TAG, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TAG_EDIT = Permission(
- group=Group.KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- KNOWLEDGE_TAG_DELETE = Permission(
- group=Group.KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE]
- )
- APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION_READ = Permission(
- group=Group.APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION_EDIT = Permission(
- group=Group.APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION_READ = Permission(
- group=Group.KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION_EDIT = Permission(
- group=Group.KNOWLEDGE_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- TOOL_WORKSPACE_USER_RESOURCE_PERMISSION_READ = Permission(
- group=Group.TOOL_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- TOOL_WORKSPACE_USER_RESOURCE_PERMISSION_EDIT = Permission(
- group=Group.TOOL_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
-
- )
- MODEL_WORKSPACE_USER_RESOURCE_PERMISSION_READ = Permission(
- group=Group.MODEL_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
- MODEL_WORKSPACE_USER_RESOURCE_PERMISSION_EDIT = Permission(
- group=Group.MODEL_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE],
- parent_group=[SystemGroup.RESOURCE_PERMISSION, WorkspaceGroup.RESOURCE_PERMISSION]
- )
-
- EMAIL_SETTING_READ = Permission(
- group=Group.EMAIL_SETTING, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
- EMAIL_SETTING_EDIT = Permission(
- group=Group.EMAIL_SETTING, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
-
- ROLE_READ = Permission(
- group=Group.ROLE, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.ROLE]
- )
- ROLE_CREATE = Permission(
- group=Group.ROLE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.ROLE]
- )
- ROLE_EDIT = Permission(
- group=Group.ROLE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.ROLE]
- )
- ROLE_DELETE = Permission(
- group=Group.ROLE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.ROLE]
- )
- ROLE_ADD_MEMBER = Permission(
- group=Group.ROLE, operate=Operate.ADD_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.ROLE]
- )
- ROLE_REMOVE_MEMBER = Permission(
- group=Group.ROLE, operate=Operate.REMOVE_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.ROLE]
- )
- WORKSPACE_ROLE_READ = Permission(
- group=Group.WORKSPACE_ROLE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_ROLE_ADD_MEMBER = Permission(
- group=Group.WORKSPACE_ROLE, operate=Operate.ADD_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_ROLE_REMOVE_MEMBER = Permission(
- group=Group.WORKSPACE_ROLE, operate=Operate.REMOVE_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
-
- WORKSPACE_READ = Permission(
- group=Group.WORKSPACE, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_CREATE = Permission(
- group=Group.WORKSPACE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_EDIT = Permission(
- group=Group.WORKSPACE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_DELETE = Permission(
- group=Group.WORKSPACE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_ADD_MEMBER = Permission(
- group=Group.WORKSPACE, operate=Operate.ADD_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_REMOVE_MEMBER = Permission(
- group=Group.WORKSPACE, operate=Operate.REMOVE_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.WORKSPACE], is_ee=settings.edition == "EE"
- )
- WORKSPACE_WORKSPACE_READ = Permission(
- group=Group.WORKSPACE_WORKSPACE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT], is_ee=settings.edition == "EE"
- )
- WORKSPACE_WORKSPACE_ADD_MEMBER = Permission(
- group=Group.WORKSPACE_WORKSPACE, operate=Operate.ADD_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT], is_ee=settings.edition == "EE"
- )
- WORKSPACE_WORKSPACE_REMOVE_MEMBER = Permission(
- group=Group.WORKSPACE_WORKSPACE, operate=Operate.REMOVE_MEMBER, role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT], is_ee=settings.edition == "EE"
- )
- LOGIN_AUTH_READ = Permission(
- group=Group.LOGIN_AUTH, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
- LOGIN_AUTH_EDIT = Permission(
- group=Group.LOGIN_AUTH, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
- APPLICATION_READ = Permission(group=Group.APPLICATION, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
- )
- APPLICATION_CREATE = Permission(group=Group.APPLICATION, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_COPY = Permission(group=Group.APPLICATION, operate=Operate.COPY,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_EDIT = Permission(group=Group.APPLICATION, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_DELETE = Permission(group=Group.APPLICATION, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_IMPORT = Permission(group=Group.APPLICATION, operate=Operate.IMPORT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_EXPORT = Permission(group=Group.APPLICATION, operate=Operate.EXPORT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- )
- APPLICATION_PUBLISH = Permission(group=Group.APPLICATION, operate=Operate.PUBLISH,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- )
- APPLICATION_BATCH_DELETE = Permission(group=Group.APPLICATION, operate=Operate.BATCH_DELETE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- )
- APPLICATION_BATCH_MOVE = Permission(group=Group.APPLICATION, operate=Operate.BATCH_MOVE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- )
- APPLICATION_RESOURCE_AUTHORIZATION = Permission(group=Group.APPLICATION, operate=Operate.AUTH,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_RELATE_RESOURCE_VIEW = Permission(
- group=Group.APPLICATION, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_TRIGGER_READ = Permission(
- group=Group.APPLICATION, operate=Operate.TRIGGER_READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_TRIGGER_CREATE = Permission(
- group=Group.APPLICATION, operate=Operate.TRIGGER_CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_TRIGGER_EDIT = Permission(
- group=Group.APPLICATION, operate=Operate.TRIGGER_EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_TRIGGER_DELETE = Permission(
- group=Group.APPLICATION, operate=Operate.TRIGGER_DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_FOLDER_READ = Permission(group=Group.APPLICATION_FOLDER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW]
- )
- APPLICATION_FOLDER_CREATE = Permission(group=Group.APPLICATION_FOLDER, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_FOLDER_EDIT = Permission(group=Group.APPLICATION_FOLDER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_FOLDER_DELETE = Permission(group=Group.APPLICATION_FOLDER, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_FOLDER_AUTH = Permission(group=Group.APPLICATION_FOLDER, operate=Operate.AUTH,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE]
- )
- APPLICATION_OVERVIEW_READ = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
- )
-
- APPLICATION_OVERVIEW_EMBED = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.EMBED,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
-
- )
-
- APPLICATION_OVERVIEW_ACCESS = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.ACCESS,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
-
- )
- APPLICATION_OVERVIEW_DISPLAY = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.DISPLAY,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
-
- )
- APPLICATION_OVERVIEW_API_KEY = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.API_KEY,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
-
- )
- APPLICATION_OVERVIEW_PUBLIC = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.PUBLIC_ACCESS,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
-
- )
- # 应用接入
- APPLICATION_ACCESS_READ = Permission(group=Group.APPLICATION_ACCESS, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
-
- )
- APPLICATION_ACCESS_EDIT = Permission(group=Group.APPLICATION_ACCESS, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE])
-
- APPLICATION_CHAT_USER_READ = Permission(group=Group.APPLICATION_CHAT_USER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
- )
- APPLICATION_CHAT_USER_EDIT = Permission(group=Group.APPLICATION_CHAT_USER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
- KNOWLEDGE_CHAT_USER_READ = Permission(group=Group.KNOWLEDGE_CHAT_USER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW],
- )
-
- KNOWLEDGE_CHAT_USER_EDIT = Permission(group=Group.KNOWLEDGE_CHAT_USER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE],
- resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE],
- )
-
- APPLICATION_CHAT_LOG_READ = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_VIEW],
- )
-
- APPLICATION_CHAT_LOG_ANNOTATION = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.ANNOTATION,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
- )
-
- APPLICATION_CHAT_LOG_EXPORT = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.EXPORT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE],
- )
-
- APPLICATION_CHAT_LOG_CLEAR_POLICY = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.CLEAR_POLICY,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
- )
- APPLICATION_CHAT_LOG_ADD_KNOWLEDGE = Permission(group=Group.APPLICATION_CHAT_LOG, operate=Operate.ADD_KNOWLEDGE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION],
- resource_permission_group_list=[
- ResourcePermissionConst.APPLICATION_MANGE],
- )
-
- ABOUT_READ = Permission(group=Group.OTHER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.OTHER, WorkspaceGroup.OTHER, UserGroup.OTHER],
- label=_('About')
- )
- ABOUT_UPDATE = Permission(group=Group.OTHER, operate=Operate.UPDATE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.OTHER],
- label=_('Update License')
- )
- SWITCH_LANGUAGE = Permission(group=Group.OTHER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.OTHER, WorkspaceGroup.OTHER, UserGroup.OTHER],
- label=_('Switch Language')
- )
- CHANGE_PASSWORD = Permission(group=Group.OTHER, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.OTHER, WorkspaceGroup.OTHER, UserGroup.OTHER],
- label=_('Change Password')
- )
-
- SYSTEM_API_KEY_EDIT = Permission(group=Group.OTHER, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN, RoleConstants.USER],
- parent_group=[SystemGroup.OTHER, WorkspaceGroup.OTHER, UserGroup.OTHER],
- label=_('System API Key')
- )
-
- APPEARANCE_SETTINGS_READ = Permission(group=Group.APPEARANCE_SETTINGS, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
- APPEARANCE_SETTINGS_EDIT = Permission(group=Group.APPEARANCE_SETTINGS, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SYSTEM_SETTING]
- )
- CHAT_USER_READ = Permission(group=Group.CHAT_USER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER],
- )
- CHAT_USER_CREATE = Permission(group=Group.CHAT_USER, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_SYNC = Permission(group=Group.CHAT_USER, operate=Operate.SYNC,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_EDIT = Permission(group=Group.CHAT_USER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_DELETE = Permission(group=Group.CHAT_USER, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_GROUP = Permission(group=Group.CHAT_USER, operate=Operate.USER_GROUP,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER],
- label=_('Set up user groups')
- )
- USER_GROUP_READ = Permission(group=Group.USER_GROUP, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- USER_GROUP_CREATE = Permission(group=Group.USER_GROUP, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- USER_GROUP_EDIT = Permission(group=Group.USER_GROUP, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- USER_GROUP_DELETE = Permission(group=Group.USER_GROUP, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- USER_GROUP_ADD_MEMBER = Permission(group=Group.USER_GROUP, operate=Operate.ADD_MEMBER,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- USER_GROUP_REMOVE_MEMBER = Permission(group=Group.USER_GROUP, operate=Operate.REMOVE_MEMBER,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_AUTH_READ = Permission(group=Group.CHAT_USER_AUTH, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- CHAT_USER_AUTH_EDIT = Permission(group=Group.CHAT_USER_AUTH, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.CHAT_USER]
- )
- WORKSPACE_CHAT_USER_READ = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_CHAT_USER_CREATE = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_CHAT_USER_EDIT = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_CHAT_USER_DELETE = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_CHAT_USER_GROUP = Permission(group=Group.WORKSPACE_CHAT_USER, operate=Operate.USER_GROUP,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT],
- label=_('Set up user groups')
- )
- WORKSPACE_USER_GROUP_READ = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.READ,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_USER_GROUP_CREATE = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.CREATE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_USER_GROUP_EDIT = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.EDIT,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_USER_GROUP_DELETE = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.DELETE,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_USER_GROUP_ADD_MEMBER = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.ADD_MEMBER,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
- WORKSPACE_USER_GROUP_REMOVE_MEMBER = Permission(group=Group.WORKSPACE_USER_GROUP, operate=Operate.REMOVE_MEMBER,
- role_list=[RoleConstants.ADMIN],
- parent_group=[WorkspaceGroup.SYSTEM_MANAGEMENT]
- )
-
- SHARED_TOOL_READ = Permission(group=Group.SYSTEM_TOOL, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
-
- SHARED_TOOL_CREATE = Permission(group=Group.SYSTEM_TOOL, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
-
- SHARED_TOOL_EDIT = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
-
- SHARED_TOOL_DELETE = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_TOOL_IMPORT = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.IMPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_TOOL_EXPORT = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_TOOL_PUBLISH = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_TOOL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_TOOL_EXECUTE_RECORD = Permission(
- group=Group.SYSTEM_TOOL, operate=Operate.RECORD, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_TOOL], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_CREATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_SYNC = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_VECTOR = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.VECTOR, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_EXPORT = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_GENERATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.GENERATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DELETE = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_KNOWLEDGE, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_WORKFLOW_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_WORKFLOW, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_WORKFLOW_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_WORKFLOW, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_WORKFLOW_EXPORT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_WORKFLOW, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_WORKFLOW_PUBLISH = Permission(
- group=Group.SYSTEM_KNOWLEDGE_WORKFLOW, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_CREATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_DELETE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_SYNC = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_EXPORT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.DOWNLOAD, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_GENERATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.GENERATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_VECTOR = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.VECTOR, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_MIGRATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.MIGRATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_TAG = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.TAG, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_DOCUMENT_REPLACE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.REPLACE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TAG_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TAG_CREATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TAG_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TAG_DELETE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_PROBLEM_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_PROBLEM_CREATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_PROBLEM_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_PROBLEM_DELETE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_PROBLEM_RELATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.RELATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TERMBASE_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TERMBASE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TERMBASE_CREATE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TERMBASE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TERMBASE_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TERMBASE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TERMBASE_DELETE = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TERMBASE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_TERMBASE_EXPORT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_TERMBASE, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_HIT_TEST = Permission(
- group=Group.SYSTEM_KNOWLEDGE_HIT_TEST, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_CHAT_USER_READ = Permission(
- group=Group.SYSTEM_KNOWLEDGE_CHAT_USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_KNOWLEDGE_CHAT_USER_EDIT = Permission(
- group=Group.SYSTEM_KNOWLEDGE_CHAT_USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- SHARED_MODEL_READ = Permission(
- group=Group.SYSTEM_MODEL, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_MODEL], is_ee=settings.edition == "EE"
- )
- SHARED_MODEL_CREATE = Permission(
- group=Group.SYSTEM_MODEL, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_MODEL], is_ee=settings.edition == "EE"
- )
-
- SHARED_MODEL_EDIT = Permission(
- group=Group.SYSTEM_MODEL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_MODEL], is_ee=settings.edition == "EE"
- )
- SHARED_MODEL_DELETE = Permission(
- group=Group.SYSTEM_MODEL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_MODEL], is_ee=settings.edition == "EE"
- )
- SHARED_MODEL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_MODEL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.SHARED_MODEL], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_EDIT = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_DELETE = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_EXPORT = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_COPY = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.COPY,
- role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_AUTH = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_PUBLISH = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_TRIGGER_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.TRIGGER_READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_TRIGGER_CREATE = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.TRIGGER_CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_TRIGGER_EDIT = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.TRIGGER_EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_TRIGGER_DELETE = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.TRIGGER_DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_RES_APPLICATION, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_EMBED = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.EMBED, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_ACCESS = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.ACCESS, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_DISPLAY = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.DISPLAY, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_API_KEY = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.API_KEY, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_OVERVIEW_PUBLIC = Permission(
- group=Group.SYSTEM_RES_APPLICATION_OVERVIEW, operate=Operate.PUBLIC_ACCESS, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- # 应用接入
- RESOURCE_APPLICATION_ACCESS_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION_ACCESS, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_ACCESS_EDIT = Permission(
- group=Group.SYSTEM_RES_APPLICATION_ACCESS, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_USER_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_USER_EDIT = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_LOG_READ = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_LOG, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_LOG_ADD_KNOWLEDGE = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_LOG, operate=Operate.ADD_KNOWLEDGE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_LOG_ANNOTATION = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_LOG, operate=Operate.ANNOTATION, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_LOG_EXPORT = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_LOG, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- RESOURCE_APPLICATION_CHAT_LOG_CLEAR_POLICY = Permission(
- group=Group.SYSTEM_RES_APPLICATION_CHAT_LOG, operate=Operate.CLEAR_POLICY, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_APPLICATION], is_ee=settings.edition == "EE"
- )
- # 知识库
- RESOURCE_KNOWLEDGE_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DELETE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_SYNC = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_EXPORT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PUBLISH = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_VECTOR = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.VECTOR, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_GENERATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.GENERATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_AUTH = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- # 文档
- RESOURCE_KNOWLEDGE_WORKFLOW_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_WORKFLOW, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_WORKFLOW_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_WORKFLOW, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_WORKFLOW_EXPORT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_WORKFLOW, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_WORKFLOW_PUBLISH = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_WORKFLOW, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_CREATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_DELETE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_SYNC = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.SYNC, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_EXPORT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_DOWNLOAD_SOURCE_FILE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.DOWNLOAD, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_GENERATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.GENERATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_VECTOR = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.VECTOR, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_MIGRATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.MIGRATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_TAG = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.TAG, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_DOCUMENT_REPLACE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_DOCUMENT, operate=Operate.REPLACE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_HIT_TEST = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_HIT_TEST, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PROBLEM_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PROBLEM_CREATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PROBLEM_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PROBLEM_DELETE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_PROBLEM_RELATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.RELATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TERMBASE_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TERMBASE, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TERMBASE_CREATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TERMBASE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TERMBASE_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TERMBASE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TERMBASE_DELETE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TERMBASE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TERMBASE_EXPORT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TERMBASE, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TAG_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TAG_CREATE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TAG_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_TAG_DELETE = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_CHAT_USER_READ = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_CHAT_USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_KNOWLEDGE_CHAT_USER_EDIT = Permission(
- group=Group.SYSTEM_RES_KNOWLEDGE_CHAT_USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_READ = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_EDIT = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_DELETE = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_EXPORT = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_PUBLISH = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.PUBLISH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_AUTH = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_EXECUTE_RECORD = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.RECORD, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_TRIGGER_READ = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_TRIGGER_CREATE = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_CREATE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_TRIGGER_EDIT = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_TOOL_TRIGGER_DELETE = Permission(
- group=Group.SYSTEM_RES_TOOL, operate=Operate.TRIGGER_DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_TOOL], is_ee=settings.edition == "EE"
- )
- RESOURCE_MODEL_READ = Permission(
- group=Group.SYSTEM_RES_MODEL, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_MODEL], is_ee=settings.edition == "EE"
- )
- RESOURCE_MODEL_EDIT = Permission(
- group=Group.SYSTEM_RES_MODEL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_MODEL], is_ee=settings.edition == "EE"
- )
- RESOURCE_MODEL_DELETE = Permission(
- group=Group.SYSTEM_RES_MODEL, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_MODEL], is_ee=settings.edition == "EE"
- )
- RESOURCE_MODEL_AUTH = Permission(
- group=Group.SYSTEM_RES_MODEL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_MODEL], is_ee=settings.edition == "EE"
- )
- RESOURCE_MODEL_RELATE_RESOURCE_VIEW = Permission(
- group=Group.SYSTEM_RES_MODEL, operate=Operate.RELATE_VIEW, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.RESOURCE_MODEL], is_ee=settings.edition == "EE"
- )
- OPERATION_LOG_READ = Permission(
- group=Group.OPERATION_LOG, operate=Operate.READ, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.OPERATION_LOG]
- )
- OPERATION_LOG_EXPORT = Permission(
- group=Group.OPERATION_LOG, operate=Operate.EXPORT, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.OPERATION_LOG]
- )
- OPERATION_LOG_CLEAR_POLICY = Permission(
- group=Group.OPERATION_LOG, operate=Operate.CLEAR_POLICY, role_list=[RoleConstants.ADMIN],
- parent_group=[SystemGroup.OPERATION_LOG]
- )
-
- def get_workspace_application_permission(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}/APPLICATION/{kwargs.get('application_id')}")
-
- def get_workspace_knowledge_permission(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}/KNOWLEDGE/{kwargs.get('knowledge_id')}")
-
- def get_workspace_model_permission(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}/MODEL/{kwargs.get('model_id')}")
-
- def get_workspace_tool_permission(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}/TOOL/{kwargs.get('tool_id')}")
-
- def get_workspace_permission(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}")
-
- def get_workspace_permission_workspace_manage_role(self):
- return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
- resource_path=
- f"/WORKSPACE/{kwargs.get('workspace_id')}:ROLE/{RoleConstants.WORKSPACE_MANAGE.value.__str__()}")
-
- def __eq__(self, other):
- if isinstance(other, PermissionConstants):
- return other == self
- else:
- return self.value == other
-
-
-def get_default_permission_list_by_role(role: RoleConstants):
- """
- 根据角色 获取角色对应的权限
- :param role: 角色
- :return: 权限
- """
- return list(map(lambda k: PermissionConstants[k],
- list(filter(lambda k: PermissionConstants[k].value.role_list.__contains__(role),
- PermissionConstants.__members__))))
-
-
-class RolePermissionMapping:
- def __init__(self, role_id, permission_id):
- self.role_id = role_id
- self.permission_id = permission_id
-
-
-class WorkspaceUserRoleMapping:
- def __init__(self, workspace_id, role_id, user_id):
- self.workspace_id = workspace_id
- self.role_id = role_id
- self.user_id = user_id
-
-
-def get_default_role_permission_mapping_list():
- role_permission_mapping_list = [
- [RolePermissionMapping(role.value.name, PermissionConstants[k].value.__str__()) for role in
- PermissionConstants[k].value.role_list] for k in PermissionConstants.__members__]
- return reduce(lambda x, y: [*x, *y], role_permission_mapping_list, [])
-
-
-def get_default_workspace_user_role_mapping_list(user_role_list: list):
- return [WorkspaceUserRoleMapping('default', role.value.name, 'default') for role in RoleConstants if
- user_role_list.__contains__(role.value.name)]
-
-
-def get_permission_list_by_resource_group(resource_group: ResourcePermissionGroup):
- """
- 根据资源组获取权限
- """
- return [PermissionConstants[k].value for k in PermissionConstants.__members__ if
- PermissionConstants[k].value.resource_permission_group_list.__contains__(resource_group)]
-
-
-class ChatAuth:
- def __init__(self,
- current_role_list: List[RoleConstants | Role],
- permission_list: List[PermissionConstants | Permission],
- chat_user_id,
- chat_user_type,
- application_id):
- # 权限列表
- self.permission_list = permission_list
- # 角色列表
- self.role_list = current_role_list
- self.chat_user_id = chat_user_id
- self.chat_user_type = chat_user_type
- self.application_id = application_id
-
-
-class Auth:
- """
- 用于存储当前用户的角色和权限
- """
-
- def __init__(self,
- current_role_list: List[RoleConstants | Role],
- permission_list: List[PermissionConstants | Permission],
- **keywords):
- # 权限列表
- self.permission_list = permission_list
- # 角色列表
- self.role_list = current_role_list
- self.keywords = keywords
-
-
-class CompareConstants(Enum):
- # 或者
- OR = "OR"
- # 并且
- AND = "AND"
-
-
-class ViewPermission:
- def __init__(self, roleList: List[RoleConstants], permissionList: List[PermissionConstants | object],
- compare=CompareConstants.OR):
- self.roleList = roleList
- self.permissionList = permissionList
- self.compare = compare
diff --git a/apps/common/constants/resource_permission_constants.py b/apps/common/constants/resource_permission_constants.py
new file mode 100644
index 00000000000..a692f72d5f4
--- /dev/null
+++ b/apps/common/constants/resource_permission_constants.py
@@ -0,0 +1,43 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:虎虎虎
+ @file: resource_permission_constants.py
+ @date:2026/8/4 15:38
+ @desc:
+"""
+from django.db import models
+
+
+class ResourcePermissionConstants(models.TextChoices):
+ """
+ 资源权限组
+ """
+ # 查看
+ VIEW = "VIEW"
+ # 管理
+ MANAGE = "MANAGE"
+ # 角色
+ ROLE = "ROLE"
+
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+
+class ResourceAuthType(models.TextChoices):
+ """
+ 资源授权类型
+ """
+ "当授权类型是Role时候"
+ ROLE = "ROLE"
+
+ """资源权限组"""
+ RESOURCE_PERMISSION_GROUP = "RESOURCE_PERMISSION_GROUP"
+
+
+class AuthTargetType(models.TextChoices):
+ """授权目标"""
+ KNOWLEDGE = 'KNOWLEDGE', '知识库'
+ APPLICATION = 'APPLICATION', '应用'
+ TOOL = 'TOOL', '工具'
+ MODEL = 'MODEL', '模型'
diff --git a/apps/common/event/listener_manage.py b/apps/common/event/listener_manage.py
index f9617bbb417..74fbad95a8c 100644
--- a/apps/common/event/listener_manage.py
+++ b/apps/common/event/listener_manage.py
@@ -31,8 +31,9 @@
Termbase,
)
from knowledge.serializers.common import create_knowledge_index
-from langchain_core.embeddings import Embeddings
+from knowledge.services.paragraph_assets import embed_paragraph_assets
from maxkb.conf import PROJECT_DIR
+from models_provider.base_model_provider import MaxKBBaseEmbeddingModel
from common.config.embedding_config import VectorStore
from common.db.search import get_dynamics_model, native_search, native_update
@@ -61,7 +62,7 @@ def __init__(self, source_url_list: List[str], selector: str, handler):
class UpdateProblemArgs:
- def __init__(self, problem_id: str, problem_content: str, embedding_model: Embeddings):
+ def __init__(self, problem_id: str, problem_content: str, embedding_model: MaxKBBaseEmbeddingModel):
self.problem_id = problem_id
self.problem_content = problem_content
self.embedding_model = embedding_model
@@ -79,7 +80,7 @@ def __init__(
paragraph_id_list: List[str],
target_document_id: str,
target_knowledge_id: str,
- target_embedding_model: Embeddings = None,
+ target_embedding_model: MaxKBBaseEmbeddingModel = None,
):
self.paragraph_id_list = paragraph_id_list
self.target_document_id = target_document_id
@@ -89,11 +90,11 @@ def __init__(
class ListenerManagement:
@staticmethod
- def embedding_by_problem(args, embedding_model: Embeddings):
+ def embedding_by_problem(args, embedding_model: MaxKBBaseEmbeddingModel):
VectorStore.get_embedding_vector().save(**args, embedding=embedding_model)
@staticmethod
- def embedding_by_paragraph_list(paragraph_id_list, embedding_model: Embeddings):
+ def embedding_by_paragraph_list(paragraph_id_list, embedding_model: MaxKBBaseEmbeddingModel):
try:
data_list = native_search(
{
@@ -117,7 +118,7 @@ def embedding_by_paragraph_list(paragraph_id_list, embedding_model: Embeddings):
)
@staticmethod
- def embedding_by_paragraph_data_list(data_list, paragraph_id_list, embedding_model: Embeddings):
+ def embedding_by_paragraph_data_list(data_list, paragraph_id_list, embedding_model: MaxKBBaseEmbeddingModel):
maxkb_logger.info(
_("Start--->Embedding paragraph: {paragraph_id_list}").format(paragraph_id_list=paragraph_id_list)
)
@@ -130,6 +131,7 @@ def is_save_function():
# 批量向量化
VectorStore.get_embedding_vector().batch_save(data_list, embedding_model, is_save_function)
+ embed_paragraph_assets(paragraph_id_list, embedding_model)
ListenerManagement.update_status(
QuerySet(Paragraph).filter(id__in=paragraph_id_list), TaskType.EMBEDDING, State.SUCCESS
)
@@ -148,7 +150,7 @@ def is_save_function():
)
@staticmethod
- def embedding_by_paragraph(paragraph_id, embedding_model: Embeddings):
+ def embedding_by_paragraph(paragraph_id, embedding_model: MaxKBBaseEmbeddingModel):
"""
向量化段落 根据段落id
@param paragraph_id: 段落id
@@ -180,6 +182,7 @@ def is_the_task_interrupted():
# 批量向量化
VectorStore.get_embedding_vector().batch_save(data_list, embedding_model, is_the_task_interrupted)
+ embed_paragraph_assets([paragraph_id], embedding_model)
# 更新到开始状态
ListenerManagement.update_status(
QuerySet(Paragraph).filter(id=paragraph_id), TaskType.EMBEDDING, State.SUCCESS
@@ -197,7 +200,7 @@ def is_the_task_interrupted():
maxkb_logger.info(_("End--->Embedding paragraph: {paragraph_id}").format(paragraph_id=paragraph_id))
@staticmethod
- def embedding_by_data_list(data_list: List, embedding_model: Embeddings):
+ def embedding_by_data_list(data_list: List, embedding_model: MaxKBBaseEmbeddingModel):
# 批量向量化
VectorStore.get_embedding_vector().batch_save(data_list, embedding_model, lambda: False)
@@ -224,13 +227,11 @@ def tokenize_by_paragraph(paragraph_id):
chunks = paragraph.chunks
# 提前查询一次用户词汇,避免循环内重复查询
user_words = list(
- QuerySet(Termbase)
- .filter(knowledge_id=paragraph.knowledge_id)
- .values_list("content", flat=True)
+ QuerySet(Termbase).filter(knowledge_id=paragraph.knowledge_id).values_list("content", flat=True)
)
data_list = list(QuerySet(Embedding).filter(paragraph_id=paragraph_id))
for data, chunk in zip(data_list, chunks):
- data.search_vector = SearchVector(Value(to_ts_vector(chunk, user_words=user_words)), config='simple')
+ data.search_vector = SearchVector(Value(to_ts_vector(chunk, user_words=user_words)), config="simple")
# 批量保存,减少数据库写入次数
QuerySet(Embedding).filter(paragraph_id=paragraph_id).bulk_update(data_list, ["search_vector"])
@@ -351,7 +352,7 @@ def update_status(query_set: QuerySet, taskType: TaskType, state: State):
lock.release()
@staticmethod
- def embedding_by_document(document_id, embedding_model: Embeddings, state_list=None):
+ def embedding_by_document(document_id, embedding_model: MaxKBBaseEmbeddingModel, state_list=None):
"""
向量化文档
@param state_list:
@@ -412,7 +413,7 @@ def is_the_task_interrupted():
rlock.un_lock("embedding:" + str(document_id))
@staticmethod
- def embedding_by_knowledge(knowledge_id, embedding_model: Embeddings):
+ def embedding_by_knowledge(knowledge_id, embedding_model: MaxKBBaseEmbeddingModel):
"""
向量化知识库
@param knowledge_id: 知识库id
@@ -509,10 +510,18 @@ def hit_test(
top_number: int,
similarity: float,
search_mode: SearchMode,
- embedding: Embeddings,
+ embedding: MaxKBBaseEmbeddingModel,
+ image_list: list[str] | None = None,
):
return VectorStore.get_embedding_vector().hit_test(
- query_text, knowledge_id, exclude_document_id_list, top_number, similarity, search_mode, embedding
+ query_text,
+ knowledge_id,
+ exclude_document_id_list,
+ top_number,
+ similarity,
+ search_mode,
+ embedding,
+ image_list,
)
@staticmethod
@@ -544,8 +553,8 @@ def is_the_task_interrupted():
.annotate(
reversed_status=Reverse("status"),
task_type_status=Coalesce(
- NullIf(Substr("reversed_status", TaskType.TOKENIZE.value, 1), Value('')),
- Value('n'),
+ NullIf(Substr("reversed_status", TaskType.TOKENIZE.value, 1), Value("")),
+ Value("n"),
),
)
.filter(task_type_status__in=state_list, document_id=document_id)
diff --git a/apps/common/handle/impl/common_handle.py b/apps/common/handle/impl/common_handle.py
index 16c647a9626..99cab6f63c6 100644
--- a/apps/common/handle/impl/common_handle.py
+++ b/apps/common/handle/impl/common_handle.py
@@ -1,14 +1,14 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎
- @file: tools.py
- @date:2024/9/11 16:41
- @desc:
+@project: MaxKB
+@Author:虎
+@file: tools.py
+@date:2024/9/11 16:41
+@desc:
"""
+
import io
import traceback
-from functools import reduce
from io import BytesIO
from xml.etree.ElementTree import fromstring
from zipfile import ZipFile
@@ -23,8 +23,42 @@
from knowledge.models import File
from PIL import ImageFile
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
-PILImage.MAX_IMAGE_PIXELS = None
+
+# 全局图片解码像素上限(不再禁用 Pillow 的解压炸弹保护)。
+# 超过该上限 Pillow 会告警,超过 2 倍会直接抛错,避免超大图片耗尽 worker 内存。
+PILImage.MAX_IMAGE_PIXELS = 50_000_000
+
+# 内嵌图片解码保护(防解压炸弹 / 超大尺寸图片导致共享 worker OOM)。
+MAX_EMBED_IMAGE_PIXELS = 16_000_000
+MAX_EMBED_IMAGE_AGGREGATE_PIXELS = 64_000_000
+
+# XLSX(zip) 压缩包防护,限制成员数 / 解压后总大小 / 解压膨胀比。
+MAX_EMBED_ARCHIVE_MEMBERS = 10_000
+MAX_EMBED_ARCHIVE_UNCOMPRESSED_BYTES = 1024 * 1024 * 1024
+MAX_EMBED_ARCHIVE_EXPANSION_RATIO = 50
+
+
+def validate_xlsx_archive(archive: ZipFile):
+ infolist = archive.infolist()
+ if len(infolist) > MAX_EMBED_ARCHIVE_MEMBERS:
+ raise ValueError(f"XLSX archive member count exceeds limit: {len(infolist)}")
+ total_uncompressed = sum(info.file_size for info in infolist)
+ total_compressed = sum(info.compress_size for info in infolist)
+ if total_uncompressed > MAX_EMBED_ARCHIVE_UNCOMPRESSED_BYTES:
+ raise ValueError("XLSX archive uncompressed size exceeds limit")
+ if total_compressed > 0 and total_uncompressed > total_compressed * MAX_EMBED_ARCHIVE_EXPANSION_RATIO:
+ raise ValueError("XLSX archive expansion ratio exceeds limit")
+
+
+def validate_xlsx_buffer(buffer):
+ archive = ZipFile(buffer)
+ try:
+ validate_xlsx_archive(archive)
+ finally:
+ archive.close()
+
def parse_element(element) -> {}:
data = {}
@@ -87,15 +121,16 @@ def handle_images(deps, archive: ZipFile) -> []:
def xlsx_embed_cells_images(buffer) -> {}:
archive = ZipFile(buffer)
+ validate_xlsx_archive(archive)
# 解析cellImage.xml文件
deps = get_dependents(archive, get_rels_path("xl/cellimages.xml"))
image_rel = handle_images(deps=deps, archive=archive)
# 工作表及其中图片ID
sheet_list = {}
for item in archive.namelist():
- if not item.startswith('xl/worksheets/sheet'):
+ if not item.startswith("xl/worksheets/sheet"):
continue
- key = item.split('/')[-1].split('.')[0].split('sheet')[-1]
+ key = item.split("/")[-1].split(".")[0].split("sheet")[-1]
sheet_list[key] = parse_element_sheet_xml(fromstring(archive.read(item)))
cell_images_xml = parse_element(fromstring(archive.read("xl/cellimages.xml")))
cell_images_rel = {}
@@ -104,18 +139,11 @@ def xlsx_embed_cells_images(buffer) -> {}:
for cnv, embed in cell_images_xml.items():
cell_images_xml[cnv] = cell_images_rel.get(embed)
result = {}
+ total_pixels = 0
for key, img in cell_images_xml.items():
- all_cells = [
- cell
- for _sheet_id, sheet in sheet_list.items()
- if sheet is not None
- for cell in sheet or []
- ]
-
- image_excel_id_list = [
- cell for cell in all_cells
- if isinstance(cell, str) and key in cell
- ]
+ all_cells = [cell for _sheet_id, sheet in sheet_list.items() if sheet is not None for cell in sheet or []]
+
+ image_excel_id_list = [cell for cell in all_cells if isinstance(cell, str) and key in cell]
# print(key, img)
if img is None:
continue
@@ -123,9 +151,24 @@ def xlsx_embed_cells_images(buffer) -> {}:
image_excel_id = image_excel_id_list[-1]
f = archive.open(img.target)
img_byte = io.BytesIO()
- im = PILImage.open(f).convert('RGB')
- im.save(img_byte, format='JPEG')
- image = File(id=uuid.uuid7(), file_name=img.path, meta={'debug': False, 'content': img_byte.getvalue()})
- result['=' + image_excel_id] = image
+ try:
+ with PILImage.open(f) as im:
+ width, height = im.size
+ pixels = width * height
+ if pixels > MAX_EMBED_IMAGE_PIXELS:
+ maxkb_logger.warning(
+ f"Skip oversized embedded image {img.path}: {width}x{height} pixels exceeds limit"
+ )
+ continue
+ total_pixels += pixels
+ if total_pixels > MAX_EMBED_IMAGE_AGGREGATE_PIXELS:
+ maxkb_logger.warning("Skip embedded images in archive: aggregate pixels exceed limit")
+ break
+ im.convert("RGB").save(img_byte, format="JPEG")
+ except Exception as e:
+ maxkb_logger.error(f"Error decoding image {img.target}: {e}, {traceback.format_exc()}")
+ continue
+ image = File(id=uuid.uuid7(), file_name=img.path, meta={"debug": False, "content": img_byte.getvalue()})
+ result["=" + image_excel_id] = image
archive.close()
return result
diff --git a/apps/common/handle/impl/qa/xlsx_parse_qa_handle.py b/apps/common/handle/impl/qa/xlsx_parse_qa_handle.py
index 71332b1b9e1..ff5987763f6 100644
--- a/apps/common/handle/impl/qa/xlsx_parse_qa_handle.py
+++ b/apps/common/handle/impl/qa/xlsx_parse_qa_handle.py
@@ -1,18 +1,19 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: xlsx_parse_qa_handle.py
- @date:2024/5/21 14:59
- @desc:
+@project: maxkb
+@Author:虎
+@file: xlsx_parse_qa_handle.py
+@date:2024/5/21 14:59
+@desc:
"""
+
import io
import traceback
import openpyxl
from common.handle.base_parse_qa_handle import BaseParseQAHandle, get_title_row_index_dict, get_row_value
-from common.handle.impl.common_handle import xlsx_embed_cells_images
+from common.handle.impl.common_handle import xlsx_embed_cells_images, validate_xlsx_buffer
from common.utils.logger import maxkb_logger
@@ -22,28 +23,26 @@ def handle_sheet(file_name, sheet, image_dict):
title_row_list = next(rows)
title_row_list = [row.value for row in title_row_list]
except Exception as e:
- return {'name': file_name, 'paragraphs': []}
+ return {"name": file_name, "paragraphs": []}
if len(title_row_list) == 0:
- return {'name': file_name, 'paragraphs': []}
+ return {"name": file_name, "paragraphs": []}
title_row_index_dict = get_title_row_index_dict(title_row_list)
paragraph_list = []
for row in rows:
- content = get_row_value(row, title_row_index_dict, 'content')
+ content = get_row_value(row, title_row_index_dict, "content")
if content is None or content.value is None:
continue
- problem = get_row_value(row, title_row_index_dict, 'problem_list')
- problem = str(problem.value) if problem is not None and problem.value is not None else ''
- problem_list = [{'content': p[0:255]} for p in problem.split('\n') if len(p.strip()) > 0]
- title = get_row_value(row, title_row_index_dict, 'title')
- title = str(title.value) if title is not None and title.value is not None else ''
+ problem = get_row_value(row, title_row_index_dict, "problem_list")
+ problem = str(problem.value) if problem is not None and problem.value is not None else ""
+ problem_list = [{"content": p[0:255]} for p in problem.split("\n") if len(p.strip()) > 0]
+ title = get_row_value(row, title_row_index_dict, "title")
+ title = str(title.value) if title is not None and title.value is not None else ""
content = str(content.value)
image = image_dict.get(content, None)
if image is not None:
- content = f''
- paragraph_list.append({'title': title[0:255],
- 'content': content[0:102400],
- 'problem_list': problem_list})
- return {'name': file_name, 'paragraphs': paragraph_list}
+ content = f""
+ paragraph_list.append({"title": title[0:255], "content": content[0:102400], "problem_list": problem_list})
+ return {"name": file_name, "paragraphs": paragraph_list}
class XlsxParseQAHandle(BaseParseQAHandle):
@@ -56,6 +55,7 @@ def support(self, file, get_buffer):
def handle(self, file, get_buffer, save_image):
buffer = get_buffer(file)
try:
+ validate_xlsx_buffer(io.BytesIO(buffer))
workbook = openpyxl.load_workbook(io.BytesIO(buffer))
try:
image_dict: dict = xlsx_embed_cells_images(io.BytesIO(buffer))
@@ -64,12 +64,16 @@ def handle(self, file, get_buffer, save_image):
image_dict = {}
worksheets = workbook.worksheets
worksheets_size = len(worksheets)
- return [row for row in
- [handle_sheet(file.name,
- sheet,
- image_dict) if worksheets_size == 1 and sheet.title == 'Sheet1' else handle_sheet(
- sheet.title, sheet, image_dict) for sheet
- in worksheets] if row is not None]
+ return [
+ row
+ for row in [
+ handle_sheet(file.name, sheet, image_dict)
+ if worksheets_size == 1 and sheet.title == "Sheet1"
+ else handle_sheet(sheet.title, sheet, image_dict)
+ for sheet in worksheets
+ ]
+ if row is not None
+ ]
except Exception as e:
maxkb_logger.error(f"Error processing XLSX file {file.name}: {e}, {traceback.format_exc()}")
- return [{'name': file.name, 'paragraphs': []}]
+ return [{"name": file.name, "paragraphs": []}]
diff --git a/apps/common/handle/impl/qa/zip_parse_qa_handle.py b/apps/common/handle/impl/qa/zip_parse_qa_handle.py
index cdf56ef1524..02b135c368e 100644
--- a/apps/common/handle/impl/qa/zip_parse_qa_handle.py
+++ b/apps/common/handle/impl/qa/zip_parse_qa_handle.py
@@ -20,7 +20,7 @@
from common.handle.impl.qa.csv_parse_qa_handle import CsvParseQAHandle
from common.handle.impl.qa.xls_parse_qa_handle import XlsParseQAHandle
from common.handle.impl.qa.xlsx_parse_qa_handle import XlsxParseQAHandle
-from common.utils.common import parse_md_image
+from common.utils.common import parse_md_image, parse_md_file_link
from knowledge.models import File
@@ -70,42 +70,39 @@ def is_valid_uuid(uuid_str: str):
def get_image_list(result_list: list, zip_files: List[str]):
- """
- 获取图片文件列表
- @param result_list:
- @param zip_files:
- @return:
- """
image_file_list = []
for result in result_list:
for p in result.get('paragraphs', []):
content: str = p.get('content', '')
- image_list = parse_md_image(content)
- for image in image_list:
- search = re.search("\(.*\)", image)
- if search:
- new_image_id = str(uuid.uuid7())
- source_image_path = search.group().replace('(', '').replace(')', '')
- image_path = urljoin(result.get('name'), '.' + source_image_path if source_image_path.startswith(
- '/') else source_image_path)
- if not zip_files.__contains__(image_path):
- continue
- if image_path.startswith('oss/file/') or image_path.startswith('oss/image/'):
- image_id = image_path.replace('oss/file/', '')
- if is_valid_uuid(image_id):
- image_file_list.append({'source_file': image_path,
- 'image_id': image_id})
- else:
- image_file_list.append({'source_file': image_path,
- 'image_id': new_image_id})
- content = content.replace(source_image_path, f'./oss/file/{new_image_id}')
- p['content'] = content
+ tokens = parse_md_image(content) + parse_md_file_link(content)
+ for token in tokens:
+ src_match = re.search(r'\bsrc=["\']([^"\']+)["\']', token)
+ paren_match = re.search(r'\(([^)]*)\)', token)
+ if src_match:
+ source_path = src_match.group(1).strip()
+ elif paren_match:
+ source_path = paren_match.group(1).strip().split(" ")[0]
+ else:
+ continue
+ new_image_id = str(uuid.uuid7())
+ image_path = urljoin(result.get('name'), '.' + source_path if source_path.startswith(
+ '/') else source_path)
+ if image_path not in zip_files:
+ continue
+ if image_path.startswith('oss/file/') or image_path.startswith('oss/image/'):
+ image_id = image_path.replace('oss/file/', '').replace('oss/image/', '')
+ if is_valid_uuid(image_id):
+ image_file_list.append({"source_file": image_path, "image_id": new_image_id})
+ content = content.replace(source_path, f"./oss/file/{new_image_id}")
+ p['content'] = content
else:
- image_file_list.append({'source_file': image_path,
- 'image_id': new_image_id})
- content = content.replace(source_image_path, f'./oss/file/{new_image_id}')
+ image_file_list.append({'source_file': image_path, 'image_id': new_image_id})
+ content = content.replace(source_path, f'./oss/file/{new_image_id}')
p['content'] = content
-
+ else:
+ image_file_list.append({'source_file': image_path, 'image_id': new_image_id})
+ content = content.replace(source_path, f'./oss/file/{new_image_id}')
+ p['content'] = content
return image_file_list
diff --git a/apps/common/handle/impl/table/xlsx_parse_table_handle.py b/apps/common/handle/impl/table/xlsx_parse_table_handle.py
index 2acf5aa1a95..fb76678b282 100644
--- a/apps/common/handle/impl/table/xlsx_parse_table_handle.py
+++ b/apps/common/handle/impl/table/xlsx_parse_table_handle.py
@@ -6,14 +6,15 @@
from openpyxl import load_workbook
from common.handle.base_parse_table_handle import BaseParseTableHandle
-from common.handle.impl.common_handle import xlsx_embed_cells_images
+from common.handle.impl.common_handle import xlsx_embed_cells_images, validate_xlsx_buffer
+from common.handle.impl.xlsx_utils import iter_sheet_content_rows
from common.utils.logger import maxkb_logger
class XlsxParseTableHandle(BaseParseTableHandle):
def support(self, file, get_buffer):
file_name: str = file.name.lower()
- if file_name.endswith('.xlsx'):
+ if file_name.endswith(".xlsx"):
return True
return False
@@ -22,14 +23,19 @@ def fill_merged_cells(self, sheet, image_dict):
# 获取第一行作为标题行
headers = []
- for idx, cell in enumerate(sheet[1]):
+ rows = iter_sheet_content_rows(sheet)
+ try:
+ title_row = next(rows)
+ except StopIteration:
+ return data
+ for idx, cell in enumerate(title_row):
if cell.value is None:
- headers.append(' ' * (idx + 1))
+ headers.append(" " * (idx + 1))
else:
headers.append(cell.value)
# 从第二行开始遍历每一行
- for row in sheet.iter_rows(min_row=2, values_only=False):
+ for row in rows:
row_data = {}
for col_idx, cell in enumerate(row):
cell_value = cell.value
@@ -41,10 +47,10 @@ def fill_merged_cells(self, sheet, image_dict):
cell_value = sheet[merged_range.min_row][merged_range.min_col - 1].value
break
if cell_value is None:
- cell_value = ''
+ cell_value = ""
image = image_dict.get(cell_value, None)
if image is not None:
- cell_value = f''
+ cell_value = f""
# 使用标题作为键,单元格的值作为值存入字典
row_data[headers[col_idx]] = cell_value
@@ -55,6 +61,7 @@ def fill_merged_cells(self, sheet, image_dict):
def handle(self, file, get_buffer, save_image):
buffer = get_buffer(file)
try:
+ validate_xlsx_buffer(io.BytesIO(buffer))
wb = load_workbook(io.BytesIO(buffer))
try:
image_dict: dict = xlsx_embed_cells_images(io.BytesIO(buffer))
@@ -70,13 +77,13 @@ def handle(self, file, get_buffer, save_image):
for row in data:
row_output = "; ".join([f"{key}: {value}" for key, value in row.items()])
# print(row_output)
- paragraphs.append({'title': '', 'content': row_output})
+ paragraphs.append({"title": "", "content": row_output})
- result.append({'name': sheetname, 'paragraphs': paragraphs})
+ result.append({"name": sheetname, "paragraphs": paragraphs})
except BaseException as e:
maxkb_logger.error(f"Error processing XLSX file {file.name}: {e}, {traceback.format_exc()}")
- return [{'name': file.name, 'paragraphs': []}]
+ return [{"name": file.name, "paragraphs": []}]
return result
def get_content(self, file, save_image):
@@ -88,9 +95,9 @@ def get_content(self, file, save_image):
if len(image_dict) > 0:
save_image(image_dict.values())
except Exception as e:
- maxkb_logger.error(f'Exception: {e}')
+ maxkb_logger.error(f"Exception: {e}")
image_dict = {}
- md_tables = ''
+ md_tables = ""
# 遍历所有工作表
for sheetname in workbook.sheetnames:
sheet = workbook[sheetname]
@@ -99,22 +106,25 @@ def get_content(self, file, save_image):
continue
# 添加 sheet 名称作为标题
- md_tables += f'## {sheetname}\n\n'
+ md_tables += f"## {sheetname}\n\n"
# 提取表头和内容
headers = [f"{key}" for key, value in rows[0].items()]
# 构建 Markdown 表格
- md_table = '| ' + ' | '.join(headers) + ' |\n'
- md_table += '| ' + ' | '.join(['---'] * len(headers)) + ' |\n'
+ md_table = "| " + " | ".join(headers) + " |\n"
+ md_table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
for row in rows:
- r = [f'{value}' for key, value in row.items()]
- md_table += '| ' + ' | '.join(
- [str(cell).replace('\n', '
') if cell is not None else '' for cell in r]) + ' |\n'
+ r = [f"{value}" for key, value in row.items()]
+ md_table += (
+ "| "
+ + " | ".join([str(cell).replace("\n", "
") if cell is not None else "" for cell in r])
+ + " |\n"
+ )
- md_tables += md_table + '\n\n'
+ md_tables += md_table + "\n\n"
return md_tables
except Exception as e:
- maxkb_logger.error(f'excel split handle error: {e}')
- return f'error: {e}'
+ maxkb_logger.error(f"excel split handle error: {e}")
+ return f"error: {e}"
diff --git a/apps/common/handle/impl/text/pdf_split_handle.py b/apps/common/handle/impl/text/pdf_split_handle.py
index dde048887a1..ee2203d1474 100644
--- a/apps/common/handle/impl/text/pdf_split_handle.py
+++ b/apps/common/handle/impl/text/pdf_split_handle.py
@@ -14,9 +14,9 @@
import traceback
from typing import List
+from django.utils.translation import gettext_lazy as _
from pypdf import PdfReader
from pypdf.generic import Destination
-from django.utils.translation import gettext_lazy as _
from common.handle.base_split_handle import BaseSplitHandle
from common.utils.logger import maxkb_logger
@@ -76,9 +76,7 @@ def handle(
return {"name": file.name, "content": result}
# 没目录但是有链接的pdf
- result = self.handle_links(
- pdf_document, pattern_list, with_filter, limit
- )
+ result = self.handle_links(pdf_document, pattern_list, with_filter, limit)
if result is not None and len(result) > 0:
return {"name": file.name, "content": result}
@@ -88,13 +86,9 @@ def handle(
if pattern_list is not None and len(pattern_list) > 0:
split_model = SplitModel(pattern_list, with_filter, limit)
else:
- split_model = SplitModel(
- default_pattern_list, with_filter=with_filter, limit=limit
- )
+ split_model = SplitModel(default_pattern_list, with_filter=with_filter, limit=limit)
except BaseException as e:
- maxkb_logger.error(
- f"File: {file.name}, error: {e}, {traceback.format_exc()}"
- )
+ maxkb_logger.error(f"File: {file.name}, error: {e}, {traceback.format_exc()}")
return {"name": file.name, "content": []}
finally:
# 处理完后可以删除临时文件
@@ -147,9 +141,7 @@ def handle_pdf_content(file, pdf_document):
content = content.replace("\0", "")
elapsed_time = time.time() - start_time
- maxkb_logger.debug(
- f"File: {file.name}, Page: {page_num + 1}, Time: {elapsed_time:.3f}s"
- )
+ maxkb_logger.debug(f"File: {file.name}, Page: {page_num + 1}, Time: {elapsed_time:.3f}s")
return content
@@ -228,7 +220,80 @@ def collect_toc(doc, outline, level, toc):
title = item.get("/Title")
if title is None:
title = str(item)
- toc.append((level, str(title).replace("\0", ""), page_number))
+ toc.append(
+ (
+ level,
+ str(title).replace("\0", ""),
+ page_number,
+ PdfSplitHandle.get_destination_top(item),
+ )
+ )
+
+ @staticmethod
+ def get_destination_top(destination):
+ top = getattr(destination, "top", None)
+ try:
+ return float(top)
+ except (TypeError, ValueError):
+ return None
+
+ @staticmethod
+ def extract_page_text_by_position(page, top=None, bottom=None):
+ if top is None and bottom is None:
+ return PdfSplitHandle.extract_page_text(page)
+
+ text_parts = []
+
+ def visitor_text(text, cm, tm, font_dict, font_size):
+ if not text:
+ return
+
+ # Text matrix coordinates can be relative to a page-level transform.
+ # Convert the text origin to PDF user-space coordinates before comparing
+ # it with the outline destination's /Top value.
+ x = tm[4] if len(tm) > 4 else 0
+ y = tm[5] if len(tm) > 5 else 0
+ if len(cm) > 5:
+ y = x * cm[1] + y * cm[3] + cm[5]
+
+ if top is not None and y > top:
+ return
+ if bottom is not None and y <= bottom:
+ return
+ text_parts.append(text)
+
+ try:
+ page.extract_text(visitor_text=visitor_text)
+ except BaseException:
+ return PdfSplitHandle.extract_page_text(page)
+ return "".join(text_parts).replace("\0", "")
+
+ @staticmethod
+ def remove_leading_title(text, *titles):
+ for title in titles:
+ title = title.strip()
+ if not title:
+ continue
+ pattern = r"^\s*" + r"\s*".join(re.escape(char) for char in title)
+ stripped_text, count = re.subn(pattern, "", text, count=1)
+ if count:
+ return stripped_text
+ return text
+
+ @staticmethod
+ def discard_ambiguous_destination_tops(toc):
+ position_counts = {}
+ for _level, _title, page_number, top in toc:
+ if top is not None:
+ position = (page_number, top)
+ position_counts[position] = position_counts.get(position, 0) + 1
+
+ ambiguous_tops = {top for (_page_number, top), count in position_counts.items() if count > 1}
+
+ return [
+ (level, title, page_number, None if top in ambiguous_tops else top)
+ for level, title, page_number, top in toc
+ ]
@staticmethod
def handle_toc(doc, limit):
@@ -236,19 +301,29 @@ def handle_toc(doc, limit):
toc = PdfSplitHandle.get_toc(doc)
if toc is None or len(toc) == 0:
return None
+ # Some PDF generators assign the same default position to every bookmark
+ # on a page. Such coordinates cannot define chapter boundaries, so preserve
+ # the title-based behavior for those entries.
+ toc = PdfSplitHandle.discard_ambiguous_destination_tops(toc)
# 创建存储章节内容的数组
chapters = []
# 遍历目录并按章节提取文本
for i, entry in enumerate(toc):
- level, title, start_page = entry
+ level, title, start_page, start_top = entry
chapter_title = title
# 确定结束页码,如果是最后一个章节则到文档末尾
if i + 1 < len(toc):
- end_page = toc[i + 1][2] - 1
+ _next_level, next_title, next_start_page, next_top = toc[i + 1]
+ # A positioned bookmark can start partway down a page. Include that
+ # page and keep only the text above the next bookmark for this chapter.
+ end_page = next_start_page if next_top is not None else next_start_page - 1
else:
end_page = len(doc.pages) - 1
+ next_title = None
+ next_start_page = None
+ next_top = None
end_page = max(start_page, end_page)
# 去掉标题中的符号
@@ -257,20 +332,23 @@ def handle_toc(doc, limit):
# 提取该章节的文本内容
chapter_text = ""
for page_num in range(start_page, end_page + 1):
- text = PdfSplitHandle.extract_page_text(doc.pages[page_num])
+ page_top = start_top if page_num == start_page else None
+ page_bottom = next_top if page_num == next_start_page else None
+ text = PdfSplitHandle.extract_page_text_by_position(doc.pages[page_num], page_top, page_bottom)
text = re.sub(r"(? -1:
- text = text[idx + len(title) :]
+ if page_num == start_page:
+ if start_top is not None:
+ text = PdfSplitHandle.remove_leading_title(text, chapter_title, title)
+ else:
+ idx = text.find(title)
+ if idx > -1:
+ text = text[idx + len(title) :]
- if i + 1 < len(toc):
- _level, next_title, next_start_page = toc[i + 1]
- next_title = PdfSplitHandle.handle_chapter_title(next_title)
- # print(f'next_title: {next_title}')
- idx = text.find(next_title)
+ if next_title is not None and next_top is None:
+ handled_next_title = PdfSplitHandle.handle_chapter_title(next_title)
+ idx = text.find(handled_next_title)
if idx > -1:
text = text[:idx]
@@ -284,12 +362,16 @@ def handle_toc(doc, limit):
if 0 < limit < len(chapter_text):
split_text = smart_split_paragraph(chapter_text, limit)
for text in split_text:
- chapters.append({"title": real_chapter_title, "content": text})
+ chapters.append(
+ {"title": real_chapter_title, "content": text.encode("utf-8", "ignore").decode("utf-8")}
+ )
else:
chapters.append(
{
"title": real_chapter_title,
- "content": chapter_text if chapter_text else real_chapter_title,
+ "content": (chapter_text if chapter_text else real_chapter_title)
+ .encode("utf-8", "ignore")
+ .decode("utf-8"),
}
)
# 保存章节内容和章节标题
@@ -334,13 +416,9 @@ def handle_links(doc, pattern_list, with_filter, limit):
next_link = links[num + 1] if num + 1 < len(links) else None
next_link_title = None
if next_link is not None:
- next_link_title = PdfSplitHandle.extract_link_title(
- page, next_link["from"]
- )
+ next_link_title = PdfSplitHandle.extract_link_title(page, next_link["from"])
if not next_link_title:
- next_link_title = PdfSplitHandle.extract_first_line(
- doc.pages[next_link["page"]]
- )
+ next_link_title = PdfSplitHandle.extract_first_line(doc.pages[next_link["page"]])
end_page = next_link["page"]
# 提取章节内容
@@ -383,24 +461,14 @@ def handle_links(doc, pattern_list, with_filter, limit):
else:
pre_toc[-1]["content"] += line
for i in range(len(pre_toc)):
- pre_toc[i]["content"] = re.sub(
- r"(? 0:
split_model = SplitModel(pattern_list, with_filter, limit)
else:
- split_model = SplitModel(
- default_pattern_list, with_filter=with_filter, limit=limit
- )
+ split_model = SplitModel(default_pattern_list, with_filter=with_filter, limit=limit)
# 插入目录前的部分
page_content = re.sub(r"(?= len(doc.pages):
continue
rect = annotation.get("/Rect")
- links.append(
- {"page": dest_page, "from": PdfSplitHandle.normalize_rect(rect)}
- )
+ links.append({"page": dest_page, "from": PdfSplitHandle.normalize_rect(rect)})
return links
@staticmethod
@@ -465,9 +529,7 @@ def get_destination_page_number(doc, destination):
return PdfSplitHandle.get_page_number_by_reference(doc, destination[0])
if hasattr(destination, "get") and destination.get("/D") is not None:
- return PdfSplitHandle.get_destination_page_number(
- doc, destination.get("/D")
- )
+ return PdfSplitHandle.get_destination_page_number(doc, destination.get("/D"))
return None
@@ -511,8 +573,7 @@ def visitor_text(text, cm, tm, font_dict, font_size):
text_top = y + (float(font_size) if font_size else 0)
in_horizontal_range = left - tolerance <= x <= right + tolerance
in_vertical_range = (
- bottom - tolerance <= y <= top + tolerance
- or bottom - tolerance <= text_top <= top + tolerance
+ bottom - tolerance <= y <= top + tolerance or bottom - tolerance <= text_top <= top + tolerance
)
if in_horizontal_range and in_vertical_range:
text_parts.append(text)
diff --git a/apps/common/handle/impl/text/xlsx_split_handle.py b/apps/common/handle/impl/text/xlsx_split_handle.py
index 13f9c41d17b..4da581c0e93 100644
--- a/apps/common/handle/impl/text/xlsx_split_handle.py
+++ b/apps/common/handle/impl/text/xlsx_split_handle.py
@@ -1,11 +1,12 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: xlsx_parse_qa_handle.py
- @date:2024/5/21 14:59
- @desc:
+@project: maxkb
+@Author:虎
+@file: xlsx_parse_qa_handle.py
+@date:2024/5/21 14:59
+@desc:
"""
+
import io
import traceback
from typing import List
@@ -14,39 +15,46 @@
from openpyxl import load_workbook
from common.handle.base_split_handle import BaseSplitHandle
-from common.handle.impl.common_handle import xlsx_embed_cells_images
+from common.handle.impl.common_handle import xlsx_embed_cells_images, validate_xlsx_buffer
+from common.handle.impl.xlsx_utils import iter_sheet_content_rows
from common.utils.logger import maxkb_logger
-splitter = '\n`-----------------------------------`\n'
+splitter = "\n`-----------------------------------`\n"
def post_cell(image_dict, cell_value):
image = image_dict.get(cell_value, None)
if image is not None:
- return f''
- return cell_value.replace('\n', '
').replace('|', '|')
+ return f""
+ return cell_value.replace("\n", "
").replace("|", "|")
def row_to_md(row, image_dict):
- return '| ' + ' | '.join(
- [post_cell(image_dict, str(cell.value if cell.value is not None else '')) if cell is not None else '' for cell
- in row]) + ' |\n'
+ return (
+ "| "
+ + " | ".join(
+ [
+ post_cell(image_dict, str(cell.value if cell.value is not None else "")) if cell is not None else ""
+ for cell in row
+ ]
+ )
+ + " |\n"
+ )
def handle_sheet(file_name, sheet, image_dict, limit: int):
- rows = sheet.rows
+ rows = iter_sheet_content_rows(sheet)
paragraphs = []
- result = {'name': file_name, 'content': paragraphs}
+ result = {"name": file_name, "content": paragraphs}
try:
title_row_list = next(rows)
title_md_content = row_to_md(title_row_list, image_dict)
- title_md_content += '| ' + ' | '.join(
- ['---' if cell is not None else '' for cell in title_row_list]) + ' |\n'
+ title_md_content += "| " + " | ".join(["---" if cell is not None else "" for cell in title_row_list]) + " |\n"
except Exception as e:
return result
if len(title_row_list) == 0:
return result
- result_item_content = ''
+ result_item_content = ""
for row in rows:
next_md_content = row_to_md(row, image_dict)
next_md_content_len = len(next_md_content)
@@ -58,10 +66,10 @@ def handle_sheet(file_name, sheet, image_dict, limit: int):
if result_item_content_len + next_md_content_len < limit:
result_item_content += next_md_content
else:
- paragraphs.append({'content': result_item_content, 'title': ''})
+ paragraphs.append({"content": result_item_content, "title": ""})
result_item_content = title_md_content + next_md_content
if len(result_item_content) > 0:
- paragraphs.append({'content': result_item_content, 'title': ''})
+ paragraphs.append({"content": result_item_content, "title": ""})
return result
@@ -71,14 +79,19 @@ def fill_merged_cells(self, sheet, image_dict):
# 获取第一行作为标题行
headers = []
- for idx, cell in enumerate(sheet[1]):
+ rows = iter_sheet_content_rows(sheet)
+ try:
+ title_row = next(rows)
+ except StopIteration:
+ return data
+ for idx, cell in enumerate(title_row):
if cell.value is None:
- headers.append(' ' * (idx + 1))
+ headers.append(" " * (idx + 1))
else:
headers.append(cell.value)
# 从第二行开始遍历每一行
- for row in sheet.iter_rows(min_row=2, values_only=False):
+ for row in rows:
row_data = {}
for col_idx, cell in enumerate(row):
cell_value = cell.value
@@ -92,7 +105,7 @@ def fill_merged_cells(self, sheet, image_dict):
image = image_dict.get(cell_value, None)
if image is not None:
- cell_value = f''
+ cell_value = f""
# 使用标题作为键,单元格的值作为值存入字典
row_data[headers[col_idx]] = cell_value
@@ -103,6 +116,7 @@ def fill_merged_cells(self, sheet, image_dict):
def handle(self, file, pattern_list: List, with_filter: bool, limit: int, get_buffer, save_image):
buffer = get_buffer(file)
try:
+ validate_xlsx_buffer(io.BytesIO(buffer))
if type(limit) is str:
limit = int(limit)
workbook = openpyxl.load_workbook(io.BytesIO(buffer))
@@ -113,16 +127,19 @@ def handle(self, file, pattern_list: List, with_filter: bool, limit: int, get_bu
image_dict = {}
worksheets = workbook.worksheets
worksheets_size = len(worksheets)
- return [row for row in
- [handle_sheet(file.name,
- sheet,
- image_dict,
- limit) if worksheets_size == 1 and sheet.title == 'Sheet1' else handle_sheet(
- sheet.title, sheet, image_dict, limit) for sheet
- in worksheets] if row is not None]
+ return [
+ row
+ for row in [
+ handle_sheet(file.name, sheet, image_dict, limit)
+ if worksheets_size == 1 and sheet.title == "Sheet1"
+ else handle_sheet(sheet.title, sheet, image_dict, limit)
+ for sheet in worksheets
+ ]
+ if row is not None
+ ]
except Exception as e:
maxkb_logger.error(f"Error processing XLSX file {file.name}: {e}, {traceback.format_exc()}")
- return [{'name': file.name, 'content': []}]
+ return [{"name": file.name, "content": []}]
def get_content(self, file, save_image):
try:
@@ -133,9 +150,9 @@ def get_content(self, file, save_image):
if len(image_dict) > 0:
save_image(image_dict.values())
except Exception as e:
- maxkb_logger.error(f'Exception: {e}')
+ maxkb_logger.error(f"Exception: {e}")
image_dict = {}
- md_tables = ''
+ md_tables = ""
# 遍历所有工作表
for sheetname in workbook.sheetnames:
sheet = workbook[sheetname]
@@ -144,41 +161,41 @@ def get_content(self, file, save_image):
continue
# 添加 sheet 名称作为标题
- md_tables += f'## {sheetname}\n\n'
+ md_tables += f"## {sheetname}\n\n"
# 提取表头和内容
headers = [f"{key}" for key, value in rows[0].items()]
# 构建 Markdown 表格
- md_table = '| ' + ' | '.join(headers) + ' |\n'
- md_table += '| ' + ' | '.join(['---'] * len(headers)) + ' |\n'
+ md_table = "| " + " | ".join(headers) + " |\n"
+ md_table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
for row in rows:
r = [self._escape_cell_content(value) for key, value in row.items()]
- md_table += '| ' + ' | '.join(r) + ' |\n'
+ md_table += "| " + " | ".join(r) + " |\n"
- md_tables += md_table + '\n\n'
+ md_tables += md_table + "\n\n"
return md_tables
except Exception as e:
- maxkb_logger.error(f'excel split handle error: {e}')
- return f'error: {e}'
+ maxkb_logger.error(f"excel split handle error: {e}")
+ return f"error: {e}"
def _escape_cell_content(self, cell_value):
"""转义单元格内容,避免破坏 Markdown 表格结构"""
if cell_value is None:
- return ''
+ return ""
cell_str = str(cell_value)
# 替换换行符为
- cell_str = cell_str.replace('\n', '
')
+ cell_str = cell_str.replace("\n", "
")
# 转义管道符 | 为 HTML 实体
- cell_str = cell_str.replace('|', '|')
+ cell_str = cell_str.replace("|", "|")
# 如果内容包含反引号,需要转义
- if '`' in cell_str:
- cell_str = cell_str.replace('`', '`')
+ if "`" in cell_str:
+ cell_str = cell_str.replace("`", "`")
return cell_str
diff --git a/apps/common/handle/impl/text/zip_split_handle.py b/apps/common/handle/impl/text/zip_split_handle.py
index 75e418665d9..14f1b7c310a 100644
--- a/apps/common/handle/impl/text/zip_split_handle.py
+++ b/apps/common/handle/impl/text/zip_split_handle.py
@@ -91,7 +91,8 @@ def _collect_file_refs(tokens: list, base_name: str, zip_files: List[str], conte
if file_path.startswith("oss/file/") or file_path.startswith("oss/image/"):
file_id = file_path.replace("oss/file/", "").replace("oss/image/", "")
if is_valid_uuid(file_id):
- file_list.append({"source_file": file_path, "image_id": file_id})
+ file_list.append({"source_file": file_path, "image_id": new_id})
+ content = update_content(content, source_path, f"./oss/file/{new_id}")
else:
file_list.append({"source_file": file_path, "image_id": new_id})
content = update_content(content, source_path, f"./oss/file/{new_id}")
diff --git a/apps/common/handle/impl/xlsx_utils.py b/apps/common/handle/impl/xlsx_utils.py
new file mode 100644
index 00000000000..3cd99defe96
--- /dev/null
+++ b/apps/common/handle/impl/xlsx_utils.py
@@ -0,0 +1,15 @@
+# coding=utf-8
+
+
+def get_sheet_content_max_column(sheet):
+ return max(
+ (cell.column for cell in sheet._cells.values() if cell.value is not None),
+ default=0,
+ )
+
+
+def iter_sheet_content_rows(sheet, min_row=1):
+ max_column = get_sheet_content_max_column(sheet)
+ if max_column == 0:
+ return iter(())
+ return sheet.iter_rows(min_row=min_row, max_col=max_column)
diff --git a/apps/common/init/init_doc.py b/apps/common/init/init_doc.py
index 156275e4ae6..09154e4fed1 100644
--- a/apps/common/init/init_doc.py
+++ b/apps/common/init/init_doc.py
@@ -1,51 +1,91 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: init_doc.py
- @date:2024/5/24 14:11
- @desc:
+@project: maxkb
+@Author:虎
+@file: init_doc.py
+@date:2024/5/24 14:11
+@desc:
"""
+
import hashlib
-from django.urls import path, URLPattern
+from django.urls import path, URLPattern, URLResolver
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
from maxkb.const import CONFIG
-chat_api_prefix = CONFIG.get_chat_path()[1:] + '/api/'
+chat_api_prefix = CONFIG.get_chat_path()[1:] + "/api/"
+
+
+def flatten_url_patterns(patterns, prefix=""):
+ """
+ 递归展开 urlpatterns,遇到 include() 产生的 URLResolver 时向下钻取,
+ 累加各层路由前缀,最终产出 (完整路由字符串, URLPattern) 元组。
+ """
+ for entry in patterns:
+ if isinstance(entry, URLResolver):
+ yield from flatten_url_patterns(entry.url_patterns, prefix + str(entry.pattern))
+ elif isinstance(entry, URLPattern):
+ yield prefix + str(entry.pattern), entry
def init_app_doc(system_urlpatterns):
system_urlpatterns += [
- path(f'{CONFIG.get_admin_path()[1:]}/api-doc/schema/', SpectacularAPIView.as_view(), name='schema'),
+ path(f"{CONFIG.get_admin_path()[1:]}/api-doc/schema/", SpectacularAPIView.as_view(), name="schema"),
# schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的
- path(f'{CONFIG.get_admin_path()[1:]}/api-doc/', SpectacularSwaggerView.as_view(url_name='schema'),
- name='swagger-ui'), # swagger-ui的路由
+ path(
+ f"{CONFIG.get_admin_path()[1:]}/api-doc/",
+ SpectacularSwaggerView.as_view(url_name="schema"),
+ name="swagger-ui",
+ ), # swagger-ui的路由
]
class ChatSpectacularSwaggerView(SpectacularSwaggerView):
@staticmethod
def _swagger_ui_resource(filename):
- return f'{CONFIG.get_chat_path()}/api-doc/swagger-ui-dist/{filename}'
+ return f"{CONFIG.get_chat_path()}/api-doc/swagger-ui-dist/{filename}"
@staticmethod
def _swagger_ui_favicon():
- return f'{CONFIG.get_chat_path()}/api-doc/swagger-ui-dist/favicon-32x32.png'
+ return f"{CONFIG.get_chat_path()}/api-doc/swagger-ui-dist/favicon-32x32.png"
+
+
+def build_curated_patterns(chat_urlpatterns, doc_names):
+ """按 name 集合从(递归展开后的)chat 路由里挑出 curated 端点,重建为带完整 path 的 URLPattern。"""
+ return [
+ URLPattern(
+ pattern=f"{chat_api_prefix}{full_path}", callback=url.callback, default_args=url.default_args, name=url.name
+ )
+ for full_path, url in flatten_url_patterns(chat_urlpatterns)
+ if doc_names.__contains__(getattr(url, "name", None))
+ ]
def init_chat_doc(system_urlpatterns, chat_urlpatterns):
+ chat_path = CONFIG.get_chat_path()[1:]
+ v3_patterns = build_curated_patterns(
+ chat_urlpatterns,
+ ["v3_chat", "v3_open", "v3_profile", "v3_portal_application", "v3_portal_historical_conversation"],
+ )
+ v2_patterns = build_curated_patterns(chat_urlpatterns, ["chat", "open", "profile", "anonymous"])
system_urlpatterns += [
- path(f'{CONFIG.get_chat_path()[1:]}/api-doc/schema/',
- SpectacularAPIView.as_view(patterns=[
- URLPattern(pattern=f'{chat_api_prefix}{str(url.pattern)}', callback=url.callback,
- default_args=url.default_args,
- name=url.name) for url in chat_urlpatterns if
- ['chat', 'open', 'profile'].__contains__(url.name)]),
- name='chat_schema'), # schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的
- path(f'{CONFIG.get_chat_path()[1:]}/api-doc/', ChatSpectacularSwaggerView.as_view(url_name='chat_schema'),
- name='swagger-ui'), # swagger-ui的路由
+ # v3 curated 文档(主路径)
+ path(
+ f"{chat_path}/api-doc/schema/", SpectacularAPIView.as_view(patterns=v3_patterns), name="chat_schema"
+ ), # schema的配置文件的路由,下面ui根据它生成
+ path(
+ f"{chat_path}/api-doc/", ChatSpectacularSwaggerView.as_view(url_name="chat_schema"), name="swagger-ui"
+ ), # swagger-ui的路由
+ # v2 curated 文档(保留)
+ path(
+ f"{chat_path}/api-doc/v2/schema/", SpectacularAPIView.as_view(patterns=v2_patterns), name="chat_schema_v2"
+ ),
+ path(
+ f"{chat_path}/api-doc/v2/",
+ ChatSpectacularSwaggerView.as_view(url_name="chat_schema_v2"),
+ name="swagger-ui-v2",
+ ),
]
@@ -58,23 +98,40 @@ def encrypt(text):
def get_call(application_urlpatterns, patterns, params, func):
def run():
- if params['valid']():
- func(*params['get_params'](application_urlpatterns, patterns))
+ if params["valid"]():
+ func(*params["get_params"](application_urlpatterns, patterns))
return run
-init_list = [(init_app_doc, {'valid': lambda: CONFIG.get('DOC_PASSWORD') is not None and encrypt(
- CONFIG.get('DOC_PASSWORD')) == 'd4fc097197b4b90a122b92cbd5bbe867',
- 'get_call': get_call,
- 'get_params': lambda application_urlpatterns, patterns: (application_urlpatterns,)}),
- (init_chat_doc, {'valid': lambda: CONFIG.get('DOC_PASSWORD') is not None and encrypt(
- CONFIG.get('DOC_PASSWORD')) == 'd4fc097197b4b90a122b92cbd5bbe867' or True, 'get_call': get_call,
- 'get_params': lambda application_urlpatterns, patterns: (
- application_urlpatterns, patterns)})]
+init_list = [
+ (
+ init_app_doc,
+ {
+ "valid": lambda: (
+ CONFIG.get("DOC_PASSWORD") is not None
+ and encrypt(CONFIG.get("DOC_PASSWORD")) == "d4fc097197b4b90a122b92cbd5bbe867"
+ ),
+ "get_call": get_call,
+ "get_params": lambda application_urlpatterns, patterns: (application_urlpatterns,),
+ },
+ ),
+ (
+ init_chat_doc,
+ {
+ "valid": lambda: (
+ CONFIG.get("DOC_PASSWORD") is not None
+ and encrypt(CONFIG.get("DOC_PASSWORD")) == "d4fc097197b4b90a122b92cbd5bbe867"
+ or True
+ ),
+ "get_call": get_call,
+ "get_params": lambda application_urlpatterns, patterns: (application_urlpatterns, patterns),
+ },
+ ),
+]
def init_doc(system_urlpatterns, chat_patterns):
for init, params in init_list:
- if params['valid']():
+ if params["valid"]():
get_call(system_urlpatterns, chat_patterns, params, init)()
diff --git a/apps/common/job/__init__.py b/apps/common/job/__init__.py
index 4984886b423..8b4aa3542c1 100644
--- a/apps/common/job/__init__.py
+++ b/apps/common/job/__init__.py
@@ -1,14 +1,16 @@
# coding=utf-8
"""
- @project: maxkb
- @Author:虎
- @file: __init__.py
- @date:2024/3/14 11:54
- @desc:
+@project: maxkb
+@Author:虎
+@file: __init__.py
+@date:2024/3/14 11:54
+@desc:
"""
+
from .clean_chat_job import *
from .clean_debug_file_job import *
from .client_access_num_job import *
+from knowledge.services.knowledge_sync_schedule import restore_knowledge_sync_jobs
def run():
@@ -16,3 +18,4 @@ def run():
clean_chat_job.run()
clean_debug_file_job.run()
client_access_num_job.run()
+ restore_knowledge_sync_jobs()
diff --git a/apps/common/job/clean_chat_job.py b/apps/common/job/clean_chat_job.py
index eab82630dac..5451d7fe4dd 100644
--- a/apps/common/job/clean_chat_job.py
+++ b/apps/common/job/clean_chat_job.py
@@ -3,10 +3,11 @@
import datetime
from django.db import transaction
-from django.db.models import Q, Max
+from django.db.models import CharField, Q, Max
+from django.db.models.functions import Cast
from django.utils import timezone
-from application.models import Application, Chat, ChatRecord
+from application.models import Application, Chat, ChatRecord, ApplicationChatUserStats
from common.job.scheduler import scheduler
from common.utils.lock import lock, RedisLock
from common.utils.logger import maxkb_logger
@@ -17,19 +18,17 @@ def clean_chat_log_job():
clean_chat_log_job_lock()
-@lock(lock_key='clean_chat_log_job_execute', timeout=30)
+@lock(lock_key="clean_chat_log_job_execute", timeout=30)
def clean_chat_log_job_lock():
from django.utils.translation import gettext_lazy as _
- maxkb_logger.info(_('start clean chat log'))
+
+ maxkb_logger.info(_("start clean chat log"))
now = timezone.now()
- applications = Application.objects.all().values('id', 'clean_time', 'file_clean_time')
- cutoff_dates = {
- app['id']: now - datetime.timedelta(days=app['clean_time'] or 180)
- for app in applications
- }
+ applications = Application.objects.all().values("id", "clean_time", "file_clean_time")
+ cutoff_dates = {app["id"]: now - datetime.timedelta(days=app["clean_time"] or 180) for app in applications}
file_cutoff_dates = {
- app['id']: now - datetime.timedelta(days=app['file_clean_time'] or app['clean_time'] or 180)
+ app["id"]: now - datetime.timedelta(days=app["file_clean_time"] or app["clean_time"] or 180)
for app in applications
}
file_conditions = Q()
@@ -42,31 +41,88 @@ def clean_chat_log_job_lock():
query_conditions |= Q(chat__application_id=app_id, create_time__lt=cutoff_date)
clean_method(query_conditions)
- maxkb_logger.info(_('end clean chat log'))
+ maxkb_logger.info(_("end clean chat log"))
+
+
+def delete_orphan_chats(orphan_chat_ids):
+ if not orphan_chat_ids:
+ return
+
+ orphan_chats = list(Chat.objects.filter(id__in=orphan_chat_ids))
+
+ # 按 (application_id, chat_user_id) 收集孤儿会话的用户,
+ # 仅当该用户在该应用下不再有其它会话时才删除其访问统计,避免误删其他应用或仍活跃用户的统计
+ app_user_ids = {}
+ for chat in orphan_chats:
+ if chat.chat_user_id:
+ chat_user_id = str(chat.chat_user_id)
+ app_user_ids.setdefault(chat.application_id, set()).add(chat_user_id)
+
+ if app_user_ids:
+ all_user_ids = set()
+ for user_ids in app_user_ids.values():
+ all_user_ids.update(user_ids)
+
+ remaining_keys = (
+ Chat.objects.filter(
+ application_id__in=app_user_ids.keys(),
+ chat_user_id__in=all_user_ids,
+ )
+ .exclude(id__in=orphan_chat_ids)
+ .values_list("application_id", "chat_user_id")
+ .distinct()
+ )
+
+ remaining_app_user_ids = {}
+ for app_id, user_id in remaining_keys:
+ remaining_app_user_ids.setdefault(app_id, set()).add(user_id)
+
+ for app_id, user_ids in app_user_ids.items():
+ user_ids_to_delete = user_ids - remaining_app_user_ids.get(app_id, set())
+ if user_ids_to_delete:
+ ApplicationChatUserStats.objects.annotate(
+ chat_user_id_str=Cast("chat_user_id", output_field=CharField(max_length=128))
+ ).filter(
+ application_id=app_id,
+ chat_user_id_str__in=user_ids_to_delete,
+ ).delete()
+
+ deleted_chat_count, _ = Chat.objects.filter(id__in=orphan_chat_ids).delete()
+ maxkb_logger.info(f"[clean_chat_log] delete orphan chats, count={deleted_chat_count}")
def clean_method(query_conditions, clean_log=True):
batch_size = 500
while True:
with transaction.atomic():
- chat_records = ChatRecord.objects.filter(query_conditions).select_related('chat').only('id', 'chat_id',
- 'create_time')[
- :batch_size]
+ chat_records = (
+ ChatRecord.objects.filter(query_conditions)
+ .select_related("chat")
+ .only("id", "chat_id", "create_time")[:batch_size]
+ )
if not chat_records:
break
chat_record_ids = [record.id for record in chat_records]
chat_ids = {record.chat_id for record in chat_records}
# 计算每个 chat_id 的最大 create_time
- max_create_times = ChatRecord.objects.filter(id__in=chat_record_ids).values('chat_id').annotate(
- max_create_time=Max('create_time'))
+ max_create_times = (
+ ChatRecord.objects.filter(id__in=chat_record_ids)
+ .values("chat_id")
+ .annotate(max_create_time=Max("create_time"))
+ )
# 收集需要删除的文件
files_to_delete = []
for record in chat_records:
max_create_time = next(
- (item['max_create_time'] for item in max_create_times if
- str(item['chat_id']) == str(record.chat_id)), None)
+ (
+ item["max_create_time"]
+ for item in max_create_times
+ if str(item["chat_id"]) == str(record.chat_id)
+ ),
+ None,
+ )
if max_create_time:
files_to_delete.extend(
File.objects.filter(source_id=str(record.chat_id), create_time__lt=max_create_time)
@@ -75,36 +131,51 @@ def clean_method(query_conditions, clean_log=True):
deleted_count = 0
if clean_log:
deleted_count = ChatRecord.objects.filter(id__in=chat_record_ids).delete()[0]
+ maxkb_logger.info(f"[clean_chat_log] delete chat_records, count={deleted_count}")
from django.db.models import Count
- updated_counts = ChatRecord.objects.filter(chat_id__in=chat_ids) \
- .values('chat_id') \
- .annotate(count=Count('id'))
- count_map = {item['chat_id']: item['count'] for item in updated_counts}
+ updated_counts = (
+ ChatRecord.objects.filter(chat_id__in=chat_ids).values("chat_id").annotate(count=Count("id"))
+ )
+
+ count_map = {item["chat_id"]: item["count"] for item in updated_counts}
for chat_id in chat_ids:
count = count_map.get(chat_id, 0) # 如果没有记录则为0
Chat.objects.filter(id=chat_id).update(chat_record_count=count)
- # 删除没有关联 ChatRecord 的 Chat
- Chat.objects.filter(chatrecord__isnull=True, id__in=chat_ids).delete()
+ # 删除已经没有关联 ChatRecord 的 Chat
+ orphan_chat_ids = [chat_id for chat_id in chat_ids if count_map.get(chat_id, 0) == 0]
+ delete_orphan_chats(orphan_chat_ids)
File.objects.filter(loid__in=[file.loid for file in files_to_delete]).delete()
if deleted_count < batch_size:
break
+ if clean_log:
+ orphan_chat_ids = list(Chat.objects.filter(chatrecord__isnull=True).values_list("id", flat=True))
+ maxkb_logger.info(f"[clean_chat_log] final orphan_chat_count={len(orphan_chat_ids)}")
+ delete_orphan_chats(orphan_chat_ids)
+
def run():
rlock = RedisLock()
- if rlock.try_lock('clean_chat_log_job', 30 * 30):
+ if rlock.try_lock("clean_chat_log_job", 30 * 30):
try:
- maxkb_logger.debug('get lock clean_chat_log_job')
+ maxkb_logger.debug("get lock clean_chat_log_job")
- existing_job = scheduler.get_job(job_id='clean_chat_log')
+ existing_job = scheduler.get_job(job_id="clean_chat_log")
if existing_job is not None:
existing_job.remove()
- scheduler.add_job(clean_chat_log_job, 'cron', hour='0', minute='5', id='clean_chat_log',
- misfire_grace_time=300, max_instances=1)
+ scheduler.add_job(
+ clean_chat_log_job,
+ "cron",
+ hour="0",
+ minute="5",
+ id="clean_chat_log",
+ misfire_grace_time=300,
+ max_instances=1,
+ )
finally:
- rlock.un_lock('clean_chat_log_job')
+ rlock.un_lock("clean_chat_log_job")
diff --git a/apps/common/log/log.py b/apps/common/log/log.py
index faca1cdf881..272acef689d 100644
--- a/apps/common/log/log.py
+++ b/apps/common/log/log.py
@@ -1,12 +1,11 @@
# coding=utf-8
"""
- @project: MaxKB
- @Author:虎虎
- @file: log.py
- @date:2025/6/4 14:13
- @desc:
+@project: MaxKB
+@Author:虎虎
+@file: log.py
+@date:2025/6/4 14:13
+@desc:
"""
-from qianfan.utils.utils import get_ip_address
from system_manage.models.log_management import Log
@@ -17,11 +16,11 @@ def _get_ip_address(request):
@param request:
@return:
"""
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
- ip = x_forwarded_for.split(',')[0]
+ ip = x_forwarded_for.split(",")[0]
else:
- ip = request.META.get('REMOTE_ADDR')
+ ip = request.META.get("REMOTE_ADDR")
return ip
@@ -33,9 +32,9 @@ def _get_user(request):
"""
user = request.user
if user is None:
- return {
-
- }
+ return {}
+ if hasattr(user, 'profile') and user.profile is not None:
+ user = user.profile
user_info = {
"id": str(user.id),
"email": user.email,
@@ -43,9 +42,8 @@ def _get_user(request):
"nick_name": user.nick_name,
"username": user.username,
}
- # 如果是 User 模型且有 role 属性
- if hasattr(user, 'role'):
- user_info['role'] = user.role
+ if hasattr(user, "role"):
+ user_info["role"] = user.role
return user_info
@@ -53,28 +51,31 @@ def _get_details(request):
path = request.path
body = request.data
- sensitive_fields = {'password', 're_password'}
+ sensitive_fields = {"password", "re_password"}
- body_copy = dict(body) if hasattr(body, 'items') else body
+ body_copy = dict(body) if hasattr(body, "items") else body
if isinstance(body_copy, dict):
for field in sensitive_fields:
body_copy.pop(field, None)
query = request.query_params
- return {
- 'path': path,
- 'body': body_copy,
- 'query': query
- }
+ return {"path": path, "body": body_copy, "query": query}
def _get_workspace_id(request, kwargs):
- return kwargs.get('workspace_id', 'None')
-
-
-def log(menu: str, operate, get_user=_get_user, get_ip_address=_get_ip_address, get_details=_get_details,
- get_operation_object=None, get_workspace_id=_get_workspace_id):
+ return kwargs.get("workspace_id", "None")
+
+
+def log(
+ menu: str,
+ operate,
+ get_user=_get_user,
+ get_ip_address=_get_ip_address,
+ get_details=_get_details,
+ get_operation_object=None,
+ get_workspace_id=_get_workspace_id,
+):
"""
记录审计日志
@param menu: 操作菜单 str
@@ -110,17 +111,33 @@ def run(view, request, **kwargs):
if callable(operate):
_operate = operate(request)
# 插入审计日志
- Log(menu=menu, operate=_operate, user=user, status=status, ip_address=ip, details=details,
- operation_object=operation_object, workspace_id=workspace_id).save()
+ Log(
+ menu=menu,
+ operate=_operate,
+ user=user,
+ status=status,
+ ip_address=ip,
+ details=details,
+ operation_object=operation_object,
+ workspace_id=workspace_id,
+ ).save()
return run
return inner
-def record_log(menu: str, operate: str, request, user: dict = None, status: int = 200,
- get_details=_get_details, get_operation_object=None, workspace_id: str = 'default',
- operation_object: dict = None):
+def record_log(
+ menu: str,
+ operate: str,
+ request,
+ user: dict = None,
+ status: int = 200,
+ get_details=_get_details,
+ get_operation_object=None,
+ workspace_id: str = "default",
+ operation_object: dict = None,
+):
"""
手动记录审计日志(适用于无法使用装饰器的场景,如第三方登录回调)
@@ -154,7 +171,7 @@ def record_log(menu: str, operate: str, request, user: dict = None, status: int
ip_address=ip,
details=details,
operation_object=operation_object or {},
- workspace_id=workspace_id
+ workspace_id=workspace_id,
).save()
except Exception as e:
# 日志记录失败不应影响主业务流程
diff --git a/apps/common/middleware/doc_headers_middleware.py b/apps/common/middleware/doc_headers_middleware.py
index 5f1884e7731..5afa66a523c 100644
--- a/apps/common/middleware/doc_headers_middleware.py
+++ b/apps/common/middleware/doc_headers_middleware.py
@@ -10,7 +10,7 @@
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
-from common.auth import TokenDetails, handles
+from common.auth import TokenDetails, get_handles
from maxkb.const import CONFIG
content = """
@@ -126,7 +126,7 @@ def process_response(self, request, response):
try:
token = auth[7:]
token_details = TokenDetails(token)
- for handle in handles:
+ for handle in get_handles():
if handle.support(request, token, token_details.get_token_details):
handle.handle(request, token, token_details.get_token_details)
return response
diff --git a/apps/common/sql/list_embedding_text.sql b/apps/common/sql/list_embedding_text.sql
index 8f4f14dfd6d..431b8857458 100644
--- a/apps/common/sql/list_embedding_text.sql
+++ b/apps/common/sql/list_embedding_text.sql
@@ -20,10 +20,19 @@ SELECT
paragraph."id" AS paragraph_id,
paragraph.knowledge_id AS knowledge_id,
1 AS source_type,
- concat_ws(E'\n',paragraph.title,paragraph."content") AS "text",
+ concat_ws(
+ E'\n',
+ paragraph.title,
+ paragraph."content",
+ (
+ SELECT string_agg(concat_ws(E'\n', asset.caption, asset.ocr_text, asset.description), E'\n' ORDER BY asset.position)
+ FROM paragraph_asset asset
+ WHERE asset.paragraph_id = paragraph."id" AND asset.sync_state = 'active'
+ )
+ ) AS "text",
paragraph.is_active AS is_active,
paragraph.chunks AS chunks
FROM
paragraph paragraph
- ${paragraph}
\ No newline at end of file
+ ${paragraph}
diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/credential/__init__.py b/apps/common/storage/__init__.py
similarity index 100%
rename from apps/models_provider/impl/tencent_cloud_model_provider/credential/__init__.py
rename to apps/common/storage/__init__.py
diff --git a/apps/common/storage/seaweedfs.py b/apps/common/storage/seaweedfs.py
new file mode 100644
index 00000000000..d3e57dbce57
--- /dev/null
+++ b/apps/common/storage/seaweedfs.py
@@ -0,0 +1,25 @@
+import boto3
+from botocore.client import Config
+from maxkb.const import CONFIG
+
+
+def is_seaweedfs_enabled() -> bool:
+ return bool(CONFIG.get("S3_ENDPOINT"))
+
+
+def get_bucket() -> str:
+ return CONFIG.get("S3_BUCKET") or "maxkb"
+
+
+def get_s3_client():
+ addr = CONFIG.get("S3_ENDPOINT") or ""
+ if addr and not addr.startswith(("http://", "https://")):
+ addr = f"http://{addr}"
+ return boto3.client(
+ "s3",
+ endpoint_url=addr,
+ aws_access_key_id=CONFIG.get("S3_ACCESS_KEY"),
+ aws_secret_access_key=CONFIG.get("S3_SECRET_KEY"),
+ config=Config(signature_version="s3v4"),
+ region_name="us-east-1",
+ )
diff --git a/apps/common/tests.py b/apps/common/tests.py
new file mode 100644
index 00000000000..113d6ec6279
--- /dev/null
+++ b/apps/common/tests.py
@@ -0,0 +1,16 @@
+from django.test import SimpleTestCase
+
+from common.utils.common import markdown_to_plain_text
+
+
+class MarkdownToPlainTextTestCase(SimpleTestCase):
+ def test_removes_embedded_markup_contents(self):
+ cases = {
+ 'before after': "before after",
+ 'before after': "before after",
+ 'before {"label":"private"} after': "before after",
+ }
+
+ for markup, expected in cases.items():
+ with self.subTest(markup=markup):
+ self.assertEqual(markdown_to_plain_text(markup), expected)
diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py
index 43370db2ef1..aa9a10616e6 100644
--- a/apps/common/utils/common.py
+++ b/apps/common/utils/common.py
@@ -159,8 +159,18 @@ def _remove_empty_lines(text):
def markdown_to_plain_text(md: str) -> str:
+ # 先移除特定媒体标签(优先级高于通用 Markdown 和 HTML 处理)
+ text = re.sub(
+ r"<(audio|video)(?:\s+[^>]*)?>.*?\1>",
+ "",
+ md,
+ flags=re.DOTALL | re.IGNORECASE,
+ )
+ text = re.sub(r"
]*>", "", text) # 匹配图片标签
+ # 去除表单渲染
+ text = re.sub(r".*?", "", text, flags=re.DOTALL)
# 移除图片 
- text = re.sub(r"!\[.*?\]\(.*?\)", "", md)
+ text = re.sub(r"!\[.*?\]\(.*?\)", "", text)
# 移除链接 [text](url)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
# 移除 Markdown 标题符号 (#, ##, ###)
@@ -179,15 +189,8 @@ def markdown_to_plain_text(md: str) -> str:
text = re.sub(r"\n{2,}", "\n", text)
# 使用正则表达式去除所有 HTML 标签
text = re.sub(r"<[^>]+>", "", text)
- # 先移除特定媒体标签(优先级高于通用HTML标签移除)
- text = re.sub(
- r"<(?:audio|video)(?:\s+[^>]*)?>.*?(?:(?:audio|video)>)?", "", text, flags=re.DOTALL | re.IGNORECASE
- )
- text = re.sub(r"
]*>", "", text) # 匹配图片标签
# 去除多余的空白字符(包括换行符、制表符等)
text = re.sub(r"\s+", " ", text)
- # 去除表单渲染
- text = re.sub(r".*?<\/form_rander>", "", text, flags=re.DOTALL)
# 去除首尾空格
text = text.strip()
return text
@@ -235,6 +238,33 @@ def bytes_to_uploaded_file(file_bytes, file_name="file.txt"):
return uploaded_file
+def guess_image_format(file_bytes: bytes, file_name: str = "") -> str:
+ content_type, _ = mimetypes.guess_type(file_name)
+ if content_type and content_type.startswith("image/"):
+ return content_type.split("/", 1)[1]
+
+ if file_bytes.startswith(b"\xff\xd8\xff"):
+ return "jpeg"
+ if file_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
+ return "png"
+ if file_bytes.startswith((b"GIF87a", b"GIF89a")):
+ return "gif"
+ if file_bytes.startswith(b"RIFF") and file_bytes[8:12] == b"WEBP":
+ return "webp"
+ if file_bytes.startswith(b"BM"):
+ return "bmp"
+ if file_bytes.startswith((b"II*\x00", b"MM\x00*")):
+ return "tiff"
+ if file_bytes.startswith(b"\x00\x00\x01\x00"):
+ return "x-icon"
+
+ stripped = file_bytes.lstrip()
+ if stripped.startswith(b"