소스 검색

Merge branch '1.1.3-master' of http://8.130.72.63:18081/shenzhen/tjdify into 1.1.3-master

‘suhuihui’ 3 달 전
부모
커밋
5fd96bf9a2

+ 3 - 1
api/controllers/console/app/app.py

@@ -65,6 +65,9 @@ class AppListApi(Resource):
             required=False,
         )
         parser.add_argument("name", type=str, location="args", required=False)
+        parser.add_argument("auth_type", type=str, location="args", required=False)
+        parser.add_argument("creator_dept", type=str, location="args", required=False)
+        parser.add_argument("creator", type=str, location="args", required=False)
         parser.add_argument("tag_ids", type=uuid_list, location="args", required=False)
         parser.add_argument("is_created_by_me", type=inputs.boolean, location="args", required=False)
 
@@ -75,7 +78,6 @@ class AppListApi(Resource):
         app_pagination = app_service.get_paginate_apps(current_user.id, current_user.current_tenant_id, args)
         if not app_pagination:
             return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
-
         return marshal(app_pagination, app_pagination_fields)
 
     @setup_required

+ 231 - 41
api/controllers/console/intention.py

@@ -1,10 +1,15 @@
+import json
 import logging
+import os
+import zipfile
 
-from flask import request
+from flask import request, send_file
 from flask_restful import Resource, marshal, marshal_with, reqparse
 from werkzeug.exceptions import Forbidden, NotFound
 
+import services
 from controllers.console import api
+from controllers.console.error import FileTooLargeError, UnsupportedFileTypeError
 from controllers.console.wraps import account_initialization_required, setup_required
 from fields.intention_fields import (
     intention_corpus_detail_fields,
@@ -13,20 +18,32 @@ from fields.intention_fields import (
     intention_keyword_detail_fields,
     intention_keyword_fields,
     intention_page_fields,
+    intention_train_file_binding_fields,
+    intention_train_file_fields,
+    intention_train_task_fields,
     intention_type_detail_fields,
     intention_type_page_fields,
 )
-from libs.login import login_required
+from libs.login import current_user, login_required
+from models import UploadFile
+from models.intention import IntentionTrainTask
+from services.errors.intention import IntentionTrainFileDuplicateError
+from services.file_service import FileService
 from services.intention_service import (
     IntentionCorpusService,
     IntentionCorpusSimilarityQuestionService,
     IntentionKeywordService,
     IntentionService,
+    IntentionTrainFileBindingService,
+    IntentionTrainFileService,
+    IntentionTrainTaskService,
     IntentionTypeService,
 )
+from services.upload_file_service import UploadFileService
 
 
 class IntentionListApi(Resource):
+
     @setup_required
     @login_required
     @account_initialization_required
@@ -37,7 +54,8 @@ class IntentionListApi(Resource):
         name_search = request.args.get("name_search", default=None, type=str)
         intentions, total = IntentionService.get_intentions(page, limit, type_id, name_search)
         data = marshal(intentions, intention_page_fields)
-        response = {"data": data, "has_more": len(intentions) == limit, "limit": limit, "total": total, "page": page}
+        response = {"data": data, "has_more": len(intentions) == limit, "limit": limit,
+                    "total": total, "page": page}
         return response, 200
 
     @setup_required
@@ -62,7 +80,6 @@ class IntentionListApi(Resource):
         response = marshal(intention, intention_detail_fields)
         return response, 200
 
-
 class IntentionApi(Resource):
     @setup_required
     @login_required
@@ -100,8 +117,8 @@ class IntentionApi(Resource):
         IntentionService.delete_intention(intention_id)
         return 200
 
-
 class IntentionTypeListApi(Resource):
+
     @setup_required
     @login_required
     @account_initialization_required
@@ -111,13 +128,8 @@ class IntentionTypeListApi(Resource):
         search = request.args.get("search", default=None, type=str)
         intention_types, total = IntentionTypeService.get_intention_types(page, limit, search)
         data = marshal(intention_types, intention_type_page_fields)
-        response = {
-            "data": data,
-            "has_more": len(intention_types) == limit,
-            "limit": limit,
-            "total": total,
-            "page": page,
-        }
+        response = {"data": data, "has_more": len(intention_types) == limit, "limit": limit,
+                    "total": total, "page": page}
         return response, 200
 
     @setup_required
@@ -136,7 +148,6 @@ class IntentionTypeListApi(Resource):
         response = marshal(intention_type, intention_type_detail_fields)
         return response, 200
 
-
 class IntentionTypeApi(Resource):
     @setup_required
     @login_required
@@ -167,7 +178,6 @@ class IntentionTypeApi(Resource):
         IntentionTypeService.delete_intention_type(intention_type_id)
         return 200
 
-
 class IntentionKeywordListApi(Resource):
     @setup_required
     @login_required
@@ -211,8 +221,8 @@ class IntentionKeywordListApi(Resource):
         IntentionKeywordService.delete_intention_keywords_by_intention_id(intention_id)
         return 200
 
-
 class IntentionKeywordApi(Resource):
+
     @setup_required
     @login_required
     @account_initialization_required
@@ -251,8 +261,8 @@ class IntentionKeywordApi(Resource):
         IntentionKeywordService.delete_intention_keyword(intention_keyword_id)
         return 200
 
-
 class IntentionKeywordBatchApi(Resource):
+
     @setup_required
     @login_required
     @account_initialization_required
@@ -286,7 +296,6 @@ class IntentionKeywordBatchApi(Resource):
         else:
             raise NotFound(f"method with name {method} not found")
 
-
 class IntentionCorpusListApi(Resource):
     @setup_required
     @login_required
@@ -297,16 +306,10 @@ class IntentionCorpusListApi(Resource):
         question_search = request.args.get("question_search", default=None, type=str)
         intention_id = request.args.get("intention_id", default=None, type=str)
         intention_corpus, total = IntentionCorpusService.get_page_intention_corpus(
-            page, limit, question_search, intention_id
-        )
+            page, limit, question_search, intention_id)
         data = marshal(intention_corpus, intention_corpus_detail_fields)
-        response = {
-            "data": data,
-            "has_more": len(intention_corpus) == limit,
-            "limit": limit,
-            "total": total,
-            "page": page,
-        }
+        response = {"data": data, "has_more": len(intention_corpus) == limit, "limit": limit,
+                    "total": total, "page": page}
         return response, 200
 
     @setup_required
@@ -336,7 +339,6 @@ class IntentionCorpusListApi(Resource):
         intention_corpus = IntentionCorpusService.save_intention_corpus(args)
         return marshal(intention_corpus, intention_corpus_detail_fields), 200
 
-
 class IntentionCorpusApi(Resource):
     @setup_required
     @login_required
@@ -391,7 +393,6 @@ class IntentionCorpusApi(Resource):
         IntentionCorpusService.delete_intention_corpus(intention_corpus)
         return 200
 
-
 class IntentionCorpusSimilarityQuestionApi(Resource):
     @setup_required
     @login_required
@@ -399,9 +400,8 @@ class IntentionCorpusSimilarityQuestionApi(Resource):
     def get(self, corpus_id):
         search = request.args.get("search", default=None, type=str)
         similarity_questions = (
-            IntentionCorpusSimilarityQuestionService.get_similarity_questions_by_corpus_id_like_question(
-                corpus_id, search
-            )
+            IntentionCorpusSimilarityQuestionService
+            .get_similarity_questions_by_corpus_id_like_question(corpus_id, search)
         )
         return marshal(similarity_questions, intention_corpus_similarity_question_fields), 200
 
@@ -424,8 +424,8 @@ class IntentionCorpusSimilarityQuestionApi(Resource):
             location="json",
         )
         args = parser.parse_args()
-        intention_corpus_similarity_question = IntentionCorpusSimilarityQuestionService.save_similarity_question(
-            corpus_id, args
+        intention_corpus_similarity_question = (
+            IntentionCorpusSimilarityQuestionService.save_similarity_question(corpus_id, args)
         )
         return marshal(intention_corpus_similarity_question, intention_corpus_similarity_question_fields), 200
 
@@ -436,7 +436,6 @@ class IntentionCorpusSimilarityQuestionApi(Resource):
         IntentionCorpusSimilarityQuestionService.delete_similarity_question_by_corpus_id(corpus_id)
         return 200
 
-
 class IntentionCorpusSimilarityQuestionUpdateAndDeleteApi(Resource):
     @setup_required
     @login_required
@@ -464,8 +463,8 @@ class IntentionCorpusSimilarityQuestionUpdateAndDeleteApi(Resource):
         )
         args = parser.parse_args()
 
-        similarity_question = IntentionCorpusSimilarityQuestionService.update_similarity_question(
-            similarity_question_id, args
+        similarity_question = (
+            IntentionCorpusSimilarityQuestionService.update_similarity_question(similarity_question_id, args)
         )
         return marshal(similarity_question, intention_corpus_similarity_question_fields), 200
 
@@ -476,7 +475,6 @@ class IntentionCorpusSimilarityQuestionUpdateAndDeleteApi(Resource):
         IntentionCorpusSimilarityQuestionService.delete_similarity_question_by_id(similarity_question_id)
         return 200
 
-
 class IntentionCorpusSimilarityQuestionBatchApi(Resource):
     @setup_required
     @login_required
@@ -511,6 +509,187 @@ class IntentionCorpusSimilarityQuestionBatchApi(Resource):
         else:
             raise NotFound(f"method with name {method} not found")
 
+class IntentionTrainTaskListApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self):
+        page = request.args.get("page", default=1, type=int)
+        limit = request.args.get("limit", default=20, type=int)
+        search = request.args.get("search", default=None, type=str)
+        intention_train_tasks, total = IntentionTrainTaskService.get_page_intention_train_tasks(
+            page, limit, search)
+        data = marshal(intention_train_tasks, intention_train_task_fields)
+        response = {"data": data, "has_more": len(intention_train_tasks) == limit, "limit": limit,
+                    "total": total, "page": page}
+        return response, 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="name is required.",
+            location="json",
+        )
+        parser.add_argument(
+            "status",
+            nullable=False,
+            required=True,
+            help="status is required.",
+            choices=IntentionTrainTask.STATUS_LIST,
+            location="json",
+        )
+        args = parser.parse_args()
+        train_task = IntentionTrainTaskService.save_train_task(args)
+        return marshal(train_task, intention_train_task_fields), 200
+
+class IntentionTrainTaskApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def patch(self, task_id):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "name",
+            nullable=False,
+            required=True,
+            help="name is required.",
+            location="json",
+        )
+        parser.add_argument(
+            "status",
+            nullable=False,
+            required=True,
+            help="status is required.",
+            choices=IntentionTrainTask.STATUS_LIST,
+            location="json",
+        )
+        args = parser.parse_args()
+
+        train_task = IntentionTrainTaskService.update_train_task(task_id, args)
+        return marshal(train_task, intention_train_task_fields), 200
+
+class IntentionTrainTaskDownloadApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self, task_id):
+        train_task = IntentionTrainTaskService.get_train_task(task_id)
+        if train_task.status != "COMPLETED":
+            raise Forbidden(f"Task with id {task_id} not completed")
+
+        dataset_info = train_task.dataset_info
+        dataset_source_info = json.loads(dataset_info.data_source_info)
+        dataset_file_id = dataset_source_info["upload_file_id"]
+        dataset_upload_file: UploadFile = UploadFileService.get_upload_file(dataset_file_id)
+
+        model_info = train_task.model_info
+        model_source_info = json.loads(model_info.data_source_info)
+        model_file_id = model_source_info["upload_file_id"]
+        model_upload_file: UploadFile = UploadFileService.get_upload_file(model_file_id)
+
+        def file2zip(zip_filename: str, upload_files: list[UploadFile]):
+            with zipfile.ZipFile(zip_filename, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
+                for upload_file in upload_files:
+                    filename = f"storage/{dataset_upload_file.key}"
+                    zip_file.write(filename, arcname=upload_file.name)
+
+        # 生成待下载的zip包
+        zip_filename = f"{train_task.name}.zip"
+        upload_files: list[UploadFile] = [dataset_upload_file, model_upload_file]
+        file2zip(zip_filename, upload_files)
+
+        # 下载zip包
+        response = send_file(zip_filename, as_attachment=True, download_name=zip_filename)
+        os.remove(zip_filename)
+        return response
+
+class IntentionTrainFileApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def get(self):
+        name = request.args.get("name", default=None, type=str)
+        version = request.args.get("version", default=None, type=str)
+        type = request.args.get("type", default=None, type=str)
+        train_files = IntentionTrainFileService.get_train_files(name, version, type)
+        return marshal(train_files, intention_train_file_fields), 200
+
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        name = request.form.get("name")
+        version = request.form.get("version")
+        type = request.form.get("type")
+        train_file = IntentionTrainFileService.get_train_file(name, version, type)
+        if train_file:
+            raise IntentionTrainFileDuplicateError(f"IntentionTrainFile with name-version-type "
+                                                   f"{name}-{version}-{type} already exists.")
+
+        data_source_type = request.form.get("data_source_type")
+
+        # get file from request
+        file = request.files["file"]
+        filename = file.filename
+        mimetype = file.mimetype
+        if not filename or not mimetype:
+            raise Forbidden("Invalid request.")
+
+        try:
+            upload_file = FileService.upload_file(
+                filename=filename,
+                content=file.read(),
+                mimetype=mimetype,
+                user=current_user,
+                source=None,
+            )
+
+            args = {
+                "name": name,
+                "version": version,
+                "type": type,
+                "data_source_type": data_source_type,
+                "data_source_info": {
+                    "upload_file_id": upload_file.id
+                }
+            }
+            intention_train_file = IntentionTrainFileService.save_train_file(args)
+            return marshal(intention_train_file, intention_train_file_fields), 200
+        except services.errors.file.FileTooLargeError as file_too_large_error:
+            raise FileTooLargeError(file_too_large_error.description)
+        except services.errors.file.UnsupportedFileTypeError:
+            raise UnsupportedFileTypeError()
+
+class IntentionTrainFileBindingApi(Resource):
+    @setup_required
+    @login_required
+    @account_initialization_required
+    def post(self):
+        parser = reqparse.RequestParser()
+        parser.add_argument(
+            "file_id",
+            nullable=False,
+            required=True,
+            help="file_id is required.",
+            location="json",
+        )
+        parser.add_argument(
+            "task_id",
+            nullable=False,
+            required=True,
+            help="task_id is required.",
+            location="json",
+        )
+        args = parser.parse_args()
+
+        train_file_binding = IntentionTrainFileBindingService.save_train_file_binding(args)
+        return marshal(train_file_binding, intention_train_file_binding_fields), 200
 
 api.add_resource(IntentionListApi, "/intentions")
 api.add_resource(IntentionApi, "/intentions/<uuid:intention_id>")
@@ -524,8 +703,19 @@ api.add_resource(IntentionCorpusListApi, "/intentions/corpus")
 api.add_resource(IntentionCorpusApi, "/intentions/corpus/<uuid:corpus_id>")
 api.add_resource(IntentionCorpusSimilarityQuestionApi, "/intentions/corpus/<uuid:corpus_id>/similarity_questions")
 
-api.add_resource(
-    IntentionCorpusSimilarityQuestionUpdateAndDeleteApi,
-    "/intentions/similarity_questions/<uuid:similarity_question_id>",
-)
+api.add_resource(IntentionCorpusSimilarityQuestionUpdateAndDeleteApi,
+                 "/intentions/similarity_questions/<uuid:similarity_question_id>")
 api.add_resource(IntentionCorpusSimilarityQuestionBatchApi, "/intentions/similarity_questions/batch")
+
+api.add_resource(IntentionTrainTaskListApi, "/intentions/train_tasks")
+api.add_resource(IntentionTrainTaskApi, "/intentions/train_tasks/<uuid:task_id>")
+api.add_resource(IntentionTrainTaskDownloadApi, "/intentions/train_tasks/download/<uuid:task_id>")
+
+api.add_resource(IntentionTrainFileApi, "/intentions/train_files")
+
+api.add_resource(IntentionTrainFileBindingApi, "/intentions/train_file_bindings")
+
+
+
+
+

+ 4 - 0
api/fields/app_fields.py

@@ -63,6 +63,8 @@ app_detail_fields = {
     "created_at": TimestampField,
     "updated_by": fields.String,
     "updated_at": TimestampField,
+    "edit_auth": fields.Integer,
+    "has_edit_permission": fields.Boolean,
 }
 
 prompt_config_fields = {
@@ -98,6 +100,8 @@ app_partial_fields = {
     "updated_by": fields.String,
     "updated_at": TimestampField,
     "tags": fields.List(fields.Nested(tag_fields)),
+    "dept_id": fields.String,
+    "edit_auth": fields.Integer,
 }
 
 

+ 29 - 1
api/fields/intention_fields.py

@@ -40,7 +40,7 @@ intention_keyword_fields = {
 intention_keyword_detail_fields = {
     "id": fields.String,
     "name": fields.String,
-    "intention": fields.Nested(intention_fields),
+    "intention": fields.Nested(intention_fields)
 }
 
 intention_corpus_fields = {
@@ -93,3 +93,31 @@ intention_detail_fields = {
     "updated_by": fields.String,
     "updated_at": TimestampField,
 }
+
+intention_train_file_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "version": fields.String,
+    "type": fields.String,
+    "data_source_type": fields.String,
+    "data_source_info": fields.String,
+    "created_by": fields.String,
+    "created_at": TimestampField,
+}
+
+intention_train_file_binding_fields = {
+    "id": fields.String,
+    "file_id": fields.String,
+    "task_id": fields.String,
+}
+
+intention_train_task_fields = {
+    "id": fields.String,
+    "name": fields.String,
+    "status": fields.String,
+    "dataset_info": fields.Nested(intention_train_file_fields, allow_null=True),
+    "model_info": fields.Nested(intention_train_file_fields, allow_null=True),
+    "created_by": fields.String,
+    "created_at": TimestampField,
+}
+

+ 130 - 21
api/models/intention.py

@@ -5,8 +5,10 @@ from .types import StringUUID
 
 
 class IntentionType(db.Model):
-    __tablename__ = "intention_types"
-    __table_args__ = (db.PrimaryKeyConstraint("id", name="intention_type_id_pkey"),)
+    __tablename__ = 'intention_types'
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_type_id_pkey'),
+    )
 
     id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
     name = db.Column(db.String(255), nullable=False)
@@ -17,12 +19,19 @@ class IntentionType(db.Model):
 
     @property
     def intention_count(self):
-        return db.session.query(func.count(Intention.id)).filter(Intention.type_id == self.id).scalar()
+        return (
+            db.session.query(func.count(Intention.id))
+            .filter(Intention.type_id==self.id)
+            .scalar()
+        )
 
     @property
     def intentions(self):
-        return db.session.query(Intention).filter(Intention.type_id == self.id).all()
-
+        return (
+            db.session.query(Intention)
+            .filter(Intention.type_id == self.id)
+            .all()
+        )
 
 class Intention(db.Model):
     __tablename__ = "intentions"
@@ -41,34 +50,57 @@ class Intention(db.Model):
 
     @property
     def type_name(self):
-        return db.session.query(IntentionType.name).filter(IntentionType.id == self.type_id).first().name
+        return (
+            db.session.query(IntentionType.name)
+            .filter(IntentionType.id==self.type_id)
+            .first().name
+        )
 
     @property
     def type(self):
-        return db.session.query(IntentionType).filter(IntentionType.id == self.type_id).first()
+        return (
+            db.session.query(IntentionType)
+            .filter(IntentionType.id==self.type_id)
+            .first()
+        )
 
     @property
     def corpus(self):
-        return db.session.query(IntentionCorpus).filter(IntentionCorpus.intention_id == self.id).all()
+        return (
+            db.session.query(IntentionCorpus)
+            .filter(IntentionCorpus.intention_id==self.id)
+            .all()
+        )
 
     @property
     def keywords(self):
-        return db.session.query(IntentionKeyword).filter(IntentionKeyword.intention_id == self.id).all()
+        return (
+            db.session.query(IntentionKeyword)
+            .filter(IntentionKeyword.intention_id==self.id)
+            .all()
+        )
 
     @property
     def corpus_count(self):
-        return db.session.query(func.count(IntentionCorpus.id)).filter(IntentionCorpus.intention_id == self.id).scalar()
+        return (
+            db.session.query(func.count(IntentionCorpus.id))
+            .filter(IntentionCorpus.intention_id == self.id)
+            .scalar()
+        )
 
     @property
     def keywords_count(self):
         return (
-            db.session.query(func.count(IntentionKeyword.id)).filter(IntentionKeyword.intention_id == self.id).scalar()
+            db.session.query(func.count(IntentionKeyword.id))
+            .filter(IntentionKeyword.intention_id == self.id)
+            .scalar()
         )
 
-
 class IntentionKeyword(db.Model):
     __tablename__ = "intention_keywords"
-    __table_args__ = (db.PrimaryKeyConstraint("id", name="intention_keyword_pkey"),)
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_keyword_pkey'),
+    )
 
     id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
     name = db.Column(db.String(255), nullable=False)
@@ -80,13 +112,14 @@ class IntentionKeyword(db.Model):
 
     @property
     def intention(self):
-        return db.session.query(Intention).filter(Intention.id == self.intention_id).first()
-
+        return (
+            db.session.query(Intention).filter(Intention.id==self.intention_id).first()
+        )
 
 class IntentionCorpus(db.Model):
     __tablename__ = "intention_corpus"
     __table_args__ = (
-        db.PrimaryKeyConstraint("id", name="intention_corpus_pkey"),
+        db.PrimaryKeyConstraint('id', name='intention_corpus_pkey'),
         db.Index("intention_corpus_idx", "intention_id"),
     )
 
@@ -101,21 +134,24 @@ class IntentionCorpus(db.Model):
 
     @property
     def intention(self):
-        return db.session.query(Intention).filter(Intention.id == self.intention_id).first()
+        return (
+            db.session.query(Intention)
+            .filter(Intention.id==self.intention_id)
+            .first()
+        )
 
     @property
     def similarity_questions(self):
         return (
             db.session.query(IntentionCorpusSimilarityQuestion)
-            .filter(IntentionCorpusSimilarityQuestion.corpus_id == self.id)
+            .filter(IntentionCorpusSimilarityQuestion.corpus_id==self.id)
             .all()
         )
 
-
 class IntentionCorpusSimilarityQuestion(db.Model):
     __tablename__ = "intention_corpus_similarity_questions"
     __table_args__ = (
-        db.PrimaryKeyConstraint("id", name="intention_corpus_similarity_question_pkey"),
+        db.PrimaryKeyConstraint('id', name='intention_corpus_similarity_question_pkey'),
         db.Index("intention_corpus_similarity_question_idx", "corpus_id"),
     )
 
@@ -130,4 +166,77 @@ class IntentionCorpusSimilarityQuestion(db.Model):
 
     @property
     def corpus(self):
-        return db.session.query(IntentionCorpus).filter(IntentionCorpus.id == self.corpus_id).first()
+        return (
+            db.session.query(IntentionCorpus)
+            .filter(IntentionCorpus.id==self.corpus_id)
+            .first()
+        )
+
+class IntentionTrainTask(db.Model):
+    __tablename__ = "intention_train_tasks"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_train_log_pkey'),
+    )
+
+    STATUS_LIST = ["CREATED", "TRAINING", "COMPLETED"]
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    name = db.Column(db.String(255), nullable=False)
+    status = db.Column(db.String(255), nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+
+    @property
+    def dataset_info(self):
+        dataset_info = (
+            db.session.query(IntentionTrainFile)
+            .join(IntentionTrainFileBinding, IntentionTrainFileBinding.file_id == IntentionTrainFile.id)
+            .filter(
+                IntentionTrainFile.type == "DATASET",
+                IntentionTrainFileBinding.task_id == self.id
+            )
+            .first()
+        )
+        return dataset_info
+
+    @property
+    def model_info(self):
+        model_info = (
+            db.session.query(IntentionTrainFile)
+            .join(IntentionTrainFileBinding, IntentionTrainFileBinding.file_id == IntentionTrainFile.id)
+            .filter(
+                IntentionTrainFile.type == "MODEL",
+                IntentionTrainFileBinding.task_id == self.id
+            )
+            .first()
+        )
+        return model_info
+
+class IntentionTrainFile(db.Model):
+    __tablename__ = "intention_train_files"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_train_file_pkey'),
+    )
+
+    TRAIN_FILE_TYPE_LIST = ["DATASET", "MODEL"]
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    name = db.Column(db.String(255), nullable=False)
+    version = db.Column(db.String(255), nullable=False)
+    type = db.Column(db.String(255), nullable=False)
+    data_source_type = db.Column(db.String(255), nullable=False)
+    data_source_info = db.Column(db.JSON, nullable=False)
+    created_by = db.Column(StringUUID, nullable=False)
+    created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
+
+class IntentionTrainFileBinding(db.Model):
+    __tablename__ = "intention_train_file_bindings"
+    __table_args__ = (
+        db.PrimaryKeyConstraint('id', name='intention_train_file_binding_pkey'),
+        db.Index("intention_train_file_binding_idx", "file_id"),
+        db.Index("intention_train_file_binding_task_idx", "task_id"),
+    )
+
+    id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
+    file_id = db.Column(StringUUID, nullable=False)
+    task_id = db.Column(StringUUID, nullable=False)

+ 61 - 1
api/services/app_service.py

@@ -5,6 +5,7 @@ from typing import Optional, cast
 
 from flask_login import current_user  # type: ignore
 from flask_sqlalchemy.pagination import Pagination
+from sqlalchemy import literal, select, union
 
 from configs import dify_config
 from constants.model_template import default_app_templates
@@ -25,6 +26,7 @@ from tasks.remove_app_and_related_data_task import remove_app_and_related_data_t
 
 
 class AppService:
+
     def get_paginate_apps(self, user_id: str, tenant_id: str, args: dict) -> Pagination | None:
         """
         Get app list with pagination
@@ -60,8 +62,66 @@ class AppService:
             else:
                 return None
 
+        # 初始化 main_query
+        main_query = db.select(App).where(*filters)
+        # 添加 auth_type 参数
+        auth_type = args.get("auth_type")
+        # 根据 auth_type 构建不同的查询
+        if auth_type is None:
+            query1 = db.select(App.id).where(
+                *filters,
+                App.created_by == user_id
+            )
+            query2 = db.select(App.id).join(
+                Account, App.dept_id == Account.dept_id
+            ).where(
+                *filters,
+                App.edit_auth == 2,
+                Account.id == user_id
+            )
+            query3 = db.select(App.id).join(
+                AppPermissionAll, App.id == AppPermissionAll.app_id
+            ).where(
+                *filters,
+                AppPermissionAll.has_read_permission == True,
+                AppPermissionAll.account_id == user_id
+            )
+            union_query = union(query1, query2, query3).subquery()
+            main_query = db.select(App).where(App.id.in_(select(union_query.c.id)))
+        elif auth_type == "1":
+            # 只查询部门编辑权限
+            main_query = db.select(App).where(
+                *filters,
+                App.created_by == user_id
+            )
+        elif auth_type == "2":
+            # 只查询部门编辑权限
+            main_query = db.select(App).join(
+                Account, App.dept_id == Account.dept_id
+            ).where(
+                *filters,
+                App.edit_auth == 2,
+                Account.id == user_id
+            )
+        elif auth_type == "3":
+            # 只查询授权编辑权限
+            main_query = db.select(App).join(
+                AppPermissionAll, App.id == AppPermissionAll.app_id
+            ).where(
+                *filters,
+                AppPermissionAll.has_read_permission == True,
+                AppPermissionAll.account_id == user_id
+            )
+        if args.get("creator"):
+            main_query = main_query.where(App.created_by == args.get("creator"))
+        if args.get("creator_dept"):
+            main_query = main_query.join(Account, App.created_by == Account.id)
+            main_query = main_query.where(Account.dept_id == args.get("creator_dept"))
+
+        print(str(main_query))
+
         app_models = db.paginate(
-            db.select(App).where(*filters).order_by(App.created_at.desc()),
+            main_query.order_by(App.created_at.desc()),
             page=args["page"],
             per_page=args["limit"],
             error_out=False,

+ 3 - 4
api/services/errors/intention.py

@@ -4,18 +4,17 @@ from services.errors.base import BaseServiceError
 class IntentionNameDuplicateError(BaseServiceError):
     pass
 
-
 class IntentionTypeNameDuplicateError(BaseServiceError):
     pass
 
-
 class IntentionKeywordNameDuplicateError(BaseServiceError):
     pass
 
-
 class IntentionCorpusQuestionDuplicateError(BaseServiceError):
     pass
 
-
 class IntentionCorpusSimilarityQuestionDuplicateError(BaseServiceError):
     pass
+
+class IntentionTrainFileDuplicateError(BaseServiceError):
+    pass

+ 179 - 46
api/services/intention_service.py

@@ -12,12 +12,16 @@ from models.intention import (
     IntentionCorpus,
     IntentionCorpusSimilarityQuestion,
     IntentionKeyword,
+    IntentionTrainFile,
+    IntentionTrainFileBinding,
+    IntentionTrainTask,
     IntentionType,
 )
 from services.errors.intention import (
     IntentionCorpusQuestionDuplicateError,
     IntentionKeywordNameDuplicateError,
     IntentionNameDuplicateError,
+    IntentionTrainFileDuplicateError,
     IntentionTypeNameDuplicateError,
 )
 
@@ -33,7 +37,7 @@ class IntentionTypeService:
 
     @staticmethod
     def save_intention_type(args: dict) -> IntentionType:
-        name = args["name"]
+        name = args['name']
         intention_type = IntentionTypeService.get_intention_type_by_name(name)
         if intention_type:
             raise IntentionTypeNameDuplicateError(f"IntentionType with name {name} already exists.")
@@ -53,11 +57,13 @@ class IntentionTypeService:
         if not intention_type:
             raise NotFound("IntentionType not found")
 
-        name = args["name"]
-        intention_type_new = IntentionType.query.filter(
-            IntentionType.id != intention_type.id,
-            IntentionType.name == name,
-        ).first()
+        name = args['name']
+        intention_type_new = (
+            IntentionType.query.filter(
+                IntentionType.id != intention_type.id,
+                IntentionType.name == name,
+            ).first()
+        )
         if intention_type_new:
             raise IntentionTypeNameDuplicateError(f"IntentionType with name {name} already exists.")
         intention_type.name = name
@@ -87,7 +93,6 @@ class IntentionTypeService:
         intention_type: Optional[IntentionType] = IntentionType.query.filter_by(name=name).first()
         return intention_type
 
-
 class IntentionService:
     @staticmethod
     def get_intentions(page, per_page, type_id=None, name_search=None):
@@ -132,7 +137,12 @@ class IntentionService:
             raise NotFound("Intention not found")
 
         name = args["name"]
-        intention_new = Intention.query.filter(Intention.id != intention.id, Intention.name == name).first()
+        intention_new = (
+            Intention.query.filter(
+                Intention.id != intention.id,
+                Intention.name == name
+            ).first()
+        )
         if intention_new:
             raise IntentionNameDuplicateError(f"Intention with name {name} already exists.")
 
@@ -157,15 +167,13 @@ class IntentionService:
 
         # 1.若存在关键词,则无法删除
         if intention.keywords_count > 0:
-            raise Forbidden(
-                f"You are not allowed to delete intention, because {intention.keywords_count} keywords were found."
-            )
+            raise Forbidden(f"You are not allowed to delete intention, "
+                            f"because {intention.keywords_count} keywords were found.")
 
         # 2.若关联训练预料,则无法删除
         if intention.corpus_count > 0:
-            raise Forbidden(
-                f"You are not allowed to delete intention, because {intention.corpus_count} corpus were found."
-            )
+            raise Forbidden(f"You are not allowed to delete intention, "
+                            f"because {intention.corpus_count} corpus were found.")
 
         db.session.delete(intention)
         db.session.commit()
@@ -181,7 +189,6 @@ class IntentionService:
         intention: Optional[Intention] = Intention.query.filter(Intention.name == name).first()
         return intention
 
-
 class IntentionKeywordService:
     @staticmethod
     def get_intention_keywords(intention_id: str, search=None):
@@ -215,9 +222,12 @@ class IntentionKeywordService:
             raise NotFound("IntentionKeyword not found")
 
         name = args["name"]
-        intention_keyword_new = IntentionKeyword.query.filter(
-            IntentionKeyword.id != intention_keyword.id, IntentionKeyword.name == name
-        ).first()
+        intention_keyword_new = (
+            IntentionKeyword.query.filter(
+                IntentionKeyword.id != intention_keyword.id,
+                IntentionKeyword.name == name)
+            .first()
+        )
         if intention_keyword_new:
             raise IntentionKeywordNameDuplicateError(f"IntentionKeyword with name {name} already exists.")
         intention_keyword.name = name
@@ -258,26 +268,27 @@ class IntentionKeywordService:
 
     @staticmethod
     def get_intention_keyword(intention_keyword_id: str) -> Optional[IntentionKeyword]:
-        intention_keyword: Optional[IntentionKeyword] = IntentionKeyword.query.filter_by(
-            id=intention_keyword_id
-        ).first()
+        intention_keyword: Optional[IntentionKeyword] = (
+            IntentionKeyword.query.filter_by(id=intention_keyword_id).first()
+        )
         return intention_keyword
 
     @staticmethod
     def get_intention_keyword_by_name(intention_id, name: str) -> Optional[IntentionKeyword]:
-        intention_keyword: Optional[IntentionKeyword] = IntentionKeyword.query.filter(
-            IntentionKeyword.intention_id == intention_id,
-            IntentionKeyword.name == name,
-        ).first()
+        intention_keyword: Optional[IntentionKeyword] = (
+            IntentionKeyword.query.filter(
+                IntentionKeyword.intention_id == intention_id,
+                IntentionKeyword.name == name,
+            ).first()
+        )
         return intention_keyword
 
-
 class IntentionCorpusService:
     @staticmethod
     def get_intention_corpus(corpus_id: str) -> Optional[IntentionCorpus]:
-        intention_corpus: Optional[IntentionCorpus] = IntentionCorpus.query.filter(
-            IntentionCorpus.id == corpus_id
-        ).first()
+        intention_corpus: Optional[IntentionCorpus] = (
+            IntentionCorpus.query.filter(IntentionCorpus.id == corpus_id).first()
+        )
         return intention_corpus
 
     @staticmethod
@@ -294,9 +305,9 @@ class IntentionCorpusService:
 
     @staticmethod
     def get_intention_corpus_by_question(question: str) -> Optional[IntentionCorpus]:
-        intention_corpus: Optional[IntentionCorpus] = IntentionCorpus.query.filter(
-            IntentionCorpus.question == question
-        ).first()
+        intention_corpus: Optional[IntentionCorpus] = (
+            IntentionCorpus.query.filter(IntentionCorpus.question == question).first()
+        )
         return intention_corpus
 
     @staticmethod
@@ -334,9 +345,12 @@ class IntentionCorpusService:
 
         if "question" in args:
             question = args["question"]
-            intention_corpus_new = IntentionCorpus.query.filter(
-                IntentionCorpus.id != corpus_id, IntentionCorpus.question == question
-            ).first()
+            intention_corpus_new = (
+                IntentionCorpus.query.filter(
+                    IntentionCorpus.id != corpus_id,
+                    IntentionCorpus.question == question
+                ).first()
+            )
             if intention_corpus_new:
                 raise IntentionCorpusQuestionDuplicateError(f"IntentionCorpus with question {question} already exists.")
             intention_corpus.question = question
@@ -373,7 +387,6 @@ class IntentionCorpusService:
         db.session.delete(intention_corpus)
         db.session.commit()
 
-
 class IntentionCorpusSimilarityQuestionService:
     @staticmethod
     def save_similarity_question(corpus_id: str, args: dict):
@@ -427,9 +440,9 @@ class IntentionCorpusSimilarityQuestionService:
 
     @staticmethod
     def get_similarity_question(similarity_question_id: str) -> Optional[IntentionCorpusSimilarityQuestion]:
-        similarity_question: Optional[IntentionCorpus] = IntentionCorpusSimilarityQuestion.query.filter_by(
-            id=similarity_question_id
-        ).first()
+        similarity_question: Optional[IntentionCorpus] = (
+            IntentionCorpusSimilarityQuestion.query.filter_by(id=similarity_question_id).first()
+        )
         return similarity_question
 
     @staticmethod
@@ -440,10 +453,12 @@ class IntentionCorpusSimilarityQuestionService:
         return similarity_question
 
     @staticmethod
-    def get_similarity_questions_by_corpus_id_like_question(corpus_id, search=None):
-        query = IntentionCorpusSimilarityQuestion.query.filter(
-            IntentionCorpusSimilarityQuestion.corpus_id == corpus_id
-        ).order_by(IntentionCorpusSimilarityQuestion.created_at.desc())
+    def get_similarity_questions_by_corpus_id_like_question(corpus_id, search = None):
+        query = (
+            IntentionCorpusSimilarityQuestion.query
+            .filter(IntentionCorpusSimilarityQuestion.corpus_id==corpus_id)
+            .order_by(IntentionCorpusSimilarityQuestion.created_at.desc())
+        )
         if search:
             query = query.filter(IntentionCorpusSimilarityQuestion.question.ilike(f"%{search}%"))
 
@@ -473,11 +488,14 @@ class IntentionCorpusSimilarityQuestionService:
 
     @staticmethod
     def delete_similarity_questions_by_ids(similarity_question_ids: list[str]):
-        similarity_questions = IntentionCorpusSimilarityQuestion.query.filter(
-            IntentionCorpusSimilarityQuestion.id.in_(similarity_question_ids)
-        ).all()
+        similarity_questions = (
+            IntentionCorpusSimilarityQuestion.query
+            .filter(IntentionCorpusSimilarityQuestion.id.in_(similarity_question_ids))
+            .all()
+        )
         IntentionCorpusSimilarityQuestionService.delete_similarity_questions(similarity_questions)
 
+
     @staticmethod
     def delete_similarity_questions(similarity_questions: list[IntentionCorpusSimilarityQuestion]):
         if not similarity_questions:
@@ -486,3 +504,118 @@ class IntentionCorpusSimilarityQuestionService:
         for similarity_question in similarity_questions:
             db.session.delete(similarity_question)
         db.session.commit()
+
+class IntentionTrainTaskService:
+
+    @staticmethod
+    def get_page_intention_train_tasks(page, per_page, search=None):
+        query = (
+            IntentionTrainTask.query.order_by(IntentionTrainTask.created_at.desc())
+        )
+        if search:
+            query = query.filter(IntentionTrainTask.name.ilike(f"%{search}%"))
+
+        intention_train_tasks = query.paginate(page=page, per_page=per_page, error_out=False)
+        return intention_train_tasks.items, intention_train_tasks.total
+
+    @staticmethod
+    def get_train_task(train_task_id: str) -> Optional[IntentionTrainTask]:
+        train_task: Optional[IntentionTrainTask] = (
+            IntentionTrainTask.query.filter_by(id=train_task_id).first()
+        )
+        return train_task
+
+    @staticmethod
+    def save_train_task(args: dict):
+        train_task = IntentionTrainTask(
+            id=str(uuid.uuid4()),
+            name=args["name"],
+            status=args["status"],
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+        db.session.add(train_task)
+        db.session.commit()
+        return train_task
+
+    @staticmethod
+    def update_train_task(task_id: str, args: dict):
+        train_task = IntentionTrainTaskService.get_train_task(task_id)
+        if not train_task:
+            raise NotFound(f"IntentionTrainTask with id {task_id} not found")
+
+        if "name" in args:
+            train_task.name = args["name"]
+        if "status" in args:
+            train_task.status = args["status"]
+
+        db.session.add(train_task)
+        db.session.commit()
+        return train_task
+
+
+class IntentionTrainFileService:
+    @staticmethod
+    def get_train_file(name: str, version: str, type: str) -> Optional[IntentionTrainFile]:
+        train_file = (
+            IntentionTrainFile.query
+            .filter_by(
+                name=name,
+                version=version,
+                type=type,
+            )
+            .first()
+        )
+        return train_file
+
+    @staticmethod
+    def get_train_files(name=None, version=None, type=None):
+        query = IntentionTrainFile.query.order_by(IntentionTrainFile.created_at.desc())
+        if name:
+            query = query.filter_by(name=name)
+        if version:
+            query = query.filter_by(version=version)
+        if type:
+            query = query.filter_by(type=type)
+        train_files = query.all()
+        return train_files
+
+    @staticmethod
+    def save_train_file(args: dict):
+        name = args["name"]
+        version = args["version"]
+        type = args["type"]
+        train_file = IntentionTrainFileService.get_train_file(name, version, type)
+        if train_file:
+            raise IntentionTrainFileDuplicateError(f"IntentionTrainFile with name-version-type "
+                                                   f"{name}-{version}-{type} already exists.")
+
+        intention_train_file = IntentionTrainFile(
+            id=str(uuid.uuid4()),
+            name=name,
+            version=version,
+            type=type,
+            data_source_type=args["data_source_type"],
+            data_source_info=args["data_source_info"],
+            created_by=current_user.id,
+            created_at=datetime.now(),
+        )
+
+        db.session.add(intention_train_file)
+        db.session.commit()
+        return intention_train_file
+
+class IntentionTrainFileBindingService:
+    @staticmethod
+    def save_train_file_binding(args: dict):
+        file_id = args["file_id"]
+        task_id = args["task_id"]
+        train_file_binding = IntentionTrainFileBinding(
+            id=str(uuid.uuid4()),
+            file_id=file_id,
+            task_id=task_id,
+        )
+        db.session.add(train_file_binding)
+        db.session.commit()
+        return train_file_binding
+

+ 13 - 0
api/services/upload_file_service.py

@@ -0,0 +1,13 @@
+from typing import Optional
+
+from models import UploadFile, db
+
+
+class UploadFileService:
+    @staticmethod
+    def get_upload_file(upload_file_id) -> Optional[UploadFile]:
+        upload_file: Optional[UploadFile] = (
+            db.session.query(UploadFile).filter_by(id=upload_file_id).first()
+        )
+        return upload_file
+

+ 12 - 4
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx

@@ -6,12 +6,10 @@ import useSWR from 'swr'
 import { useTranslation } from 'react-i18next'
 import { useBoolean } from 'ahooks'
 import {
-  Cog8ToothIcon,
   DocumentTextIcon,
   PaperClipIcon,
 } from '@heroicons/react/24/outline'
 import {
-  Cog8ToothIcon as Cog8ToothSolidIcon,
   // CommandLineIcon as CommandLineSolidIcon,
   DocumentTextIcon as DocumentTextSolidIcon,
 } from '@heroicons/react/24/solid'
@@ -31,6 +29,7 @@ import { getLocaleOnClient } from '@/i18n'
 import { useAppContext } from '@/context/app-context'
 import Tooltip from '@/app/components/base/tooltip'
 import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
+import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
 
 export type IAppDetailLayoutProps = {
   children: React.ReactNode
@@ -156,6 +155,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
     url: 'fetchDatasetDetail',
     datasetId,
   }, apiParams => fetchDatasetDetail(apiParams.datasetId))
+  const { isCreate, isEdit, isOperation } = GetDatasetAuth(datasetRes)
 
   const { data: relatedApps } = useSWR({
     action: 'fetchDatasetRelatedApps',
@@ -164,11 +164,19 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
 
   const navigation = useMemo(() => {
     const baseNavigation = [
-      { name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
       // { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
-      { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
+      // { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
     ]
 
+    if (isEdit) {
+      baseNavigation.unshift({
+        name: t('common.datasetMenus.hitTesting'),
+        href: `/datasets/${datasetId}/hitTesting`,
+        icon: TargetIcon,
+        selectedIcon: TargetSolidIcon,
+      })
+    }
+
     if (datasetRes?.provider !== 'external') {
       baseNavigation.unshift({
         name: t('common.datasetMenus.documents'),

+ 21 - 4
web/app/(commonLayout)/datasets/Container.tsx

@@ -78,10 +78,10 @@ const Container = () => {
     handleTagsUpdate()
   }
 
-  useEffect(() => {
-    if (currentWorkspace.role === 'normal')
-      return router.replace('/apps')
-  }, [currentWorkspace, router])
+  // useEffect(() => {
+  //   if (currentWorkspace.role === 'normal')
+  //     return router.replace('/apps')
+  // }, [currentWorkspace, router])
 
   const [type, setType] = useState<any>()
   const [searchType, setSearchType] = useState('')
@@ -207,3 +207,20 @@ const Container = () => {
 }
 
 export default Container
+
+export const GetDatasetAuth = (row: any) => {
+  const { currentWorkspace, userProfile } = useAppContext()
+  let isCreate = false
+  let isEdit = false
+  let isOperation = false
+  if (row) {
+    isCreate = currentWorkspace.role === 'owner' || row.created_by === userProfile.id
+    isEdit = isCreate || currentWorkspace.role === 'admin' || (row.edit_auth === 2 && currentWorkspace.role === 'leader' && row.dept_id === userProfile.dept_id)
+    isOperation = isEdit || row.has_edit_permission || (row.edit_auth === 2 && row.dept_id === userProfile.dept_id)
+  }
+  return {
+    isCreate,
+    isEdit,
+    isOperation,
+  }
+}

+ 3 - 7
web/app/(commonLayout)/datasets/DatasetCard.tsx

@@ -20,6 +20,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant'
 import TagSelector from '@/app/components/base/tag-management/selector'
 import CornerLabel from '@/app/components/base/corner-label'
 import { useAppContext } from '@/context/app-context'
+import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
 
 export type DatasetCardProps = {
   dataset: DataSet
@@ -34,6 +35,7 @@ const DatasetCard = ({
   const { notify } = useContext(ToastContext)
   const { push } = useRouter()
   const EXTERNAL_PROVIDER = 'external' as const
+  const { isCreate, isEdit, isOperation } = GetDatasetAuth(dataset)
 
   const { isCurrentWorkspaceDatasetOperator, currentWorkspace, userProfile } = useAppContext()
   const [tags, setTags] = useState<Tag[]>(dataset.tags)
@@ -194,13 +196,7 @@ const DatasetCard = ({
             </div>
           </div>
           {
-            (
-              currentWorkspace.role === 'owner' // 所有者
-              || currentWorkspace.role === 'admin' // 管理员
-              || dataset.has_edit_permission // 编辑授权
-              || dataset.created_by === userProfile.id // 创建
-              || (dataset.edit_auth === 2 && dataset.dept_id === userProfile.dept_id) // 部门领导
-            ) && (
+            isEdit && (
               <>
                 <div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
                 <div className='!hidden shrink-0 group-hover:!flex'>

+ 58 - 51
web/app/components/datasets/documents/index.tsx

@@ -32,6 +32,7 @@ import StatusWithAction from '../common/document-status-with-action/status-with-
 import { SimpleSelect } from '@/app/components/base/select'
 import DetailModal from './mould/index'
 import { useAppContext } from '@/context/app-context'
+import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
 
 const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => {
   return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
@@ -68,12 +69,13 @@ const EmptyElement: FC<{ canAdd: boolean; onClick: () => void; type?: 'upload' |
         {type === 'upload' ? <FolderPlusIcon /> : <NotionIcon />}
       </div>
       <span className={s.emptyTitle}>{t('datasetDocuments.list.empty.title')}<ThreeDotsIcon className='relative -left-1.5 -top-3 inline' /></span>
+
       <div className={s.emptyTip}>
         {t(`datasetDocuments.list.empty.${type}.tip`)}
       </div>
-      {type === 'upload' && canAdd && <Button onClick={onClick} className={s.addFileBtn}>
-        <PlusIcon className={s.plusIcon} />{t('datasetDocuments.list.addFile')}
-      </Button>}
+      {/* {type === 'upload' && canAdd && <Button onClick={onClick} className={s.addFileBtn}> */}
+      {/*  <PlusIcon className={s.plusIcon} />{t('datasetDocuments.list.addFile')} */}
+      {/* </Button>} */}
     </div>
   </div>
 }
@@ -121,6 +123,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
     },
     refetchInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0,
   })
+  const { isCreate, isEdit, isOperation } = GetDatasetAuth(dataset)
 
   const invalidDocumentList = useInvalidDocumentList(datasetId)
 
@@ -305,54 +308,58 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
                 placeholder="请选择审核状态"
               />
             </div>
-            <div className='flex !h-8 items-center justify-center gap-2'>
-              {
-                ['owner', 'admin'].includes(appContext.currentWorkspace.role) && (<Button variant='primary' onClick={() => {
-                  setTransfer({ mode: 'manage' })
-                  setMouldModalVisible(true)
-                }} className='shrink-0'>
-                  <BookOpenIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
-                  模板管理
-                </Button>)
-              }
-              <Button variant='primary' onClick={() => {
-                setTransfer({ mode: 'download' })
-                setMouldModalVisible(true)
-              }} className='shrink-0'>
-                <ArrowDownTrayIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
-                模板下载
-              </Button>
+            {
+              isOperation && (
+                <div className='flex !h-8 items-center justify-center gap-2'>
+                  {
+                    isEdit && (<Button variant='primary' onClick={() => {
+                      setTransfer({ mode: 'manage' })
+                      setMouldModalVisible(true)
+                    }} className='shrink-0'>
+                      <BookOpenIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
+                      模板管理
+                    </Button>)
+                  }
+                  <Button variant='primary' onClick={() => {
+                    setTransfer({ mode: 'download' })
+                    setMouldModalVisible(true)
+                  }} className='shrink-0'>
+                    <ArrowDownTrayIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
+                    模板下载
+                  </Button>
 
-              {!isFreePlan && <AutoDisabledDocument datasetId={datasetId} />}
-              <IndexFailed datasetId={datasetId} />
-              {!embeddingAvailable && <StatusWithAction type='warning' description={t('dataset.embeddingModelNotAvailable')} />}
-              {embeddingAvailable && (
-                <Button variant='secondary' className='shrink-0' onClick={showEditMetadataModal}>
-                  <RiDraftLine className='mr-1 size-4' />
-                  {t('dataset.metadata.metadata')}
-                </Button>
-              )}
-              {isShowEditMetadataModal && (
-                <DatasetMetadataDrawer
-                  userMetadata={datasetMetaData || []}
-                  onClose={hideEditMetadataModal}
-                  onAdd={handleAddMetaData}
-                  onRename={handleRename}
-                  onRemove={handleDeleteMetaData}
-                  builtInMetadata={builtInMetaData || []}
-                  isBuiltInEnabled={!!builtInEnabled}
-                  onIsBuiltInEnabledChange={setBuiltInEnabled}
-                />
-              )}
-              {embeddingAvailable && (
-                <Button variant='primary' onClick={routeToDocCreate} className='shrink-0'>
-                  <PlusIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
-                  {isDataSourceNotion && t('datasetDocuments.list.addPages')}
-                  {isDataSourceWeb && t('datasetDocuments.list.addUrl')}
-                  {(!dataset?.data_source_type || isDataSourceFile) && t('datasetDocuments.list.addFile')}
-                </Button>
-              )}
-            </div>
+                  {!isFreePlan && <AutoDisabledDocument datasetId={datasetId} />}
+                  <IndexFailed datasetId={datasetId} />
+                  {!embeddingAvailable && <StatusWithAction type='warning' description={t('dataset.embeddingModelNotAvailable')} />}
+                  {embeddingAvailable && (
+                    <Button variant='secondary' className='shrink-0' onClick={showEditMetadataModal}>
+                      <RiDraftLine className='mr-1 size-4' />
+                      {t('dataset.metadata.metadata')}
+                    </Button>
+                  )}
+                  {isShowEditMetadataModal && (
+                    <DatasetMetadataDrawer
+                      userMetadata={datasetMetaData || []}
+                      onClose={hideEditMetadataModal}
+                      onAdd={handleAddMetaData}
+                      onRename={handleRename}
+                      onRemove={handleDeleteMetaData}
+                      builtInMetadata={builtInMetaData || []}
+                      isBuiltInEnabled={!!builtInEnabled}
+                      onIsBuiltInEnabledChange={setBuiltInEnabled}
+                    />
+                  )}
+                  {embeddingAvailable && (
+                    <Button variant='primary' onClick={routeToDocCreate} className='shrink-0'>
+                      <PlusIcon className={cn('mr-2 h-4 w-4 stroke-current')} />
+                      {isDataSourceNotion && t('datasetDocuments.list.addPages')}
+                      {isDataSourceWeb && t('datasetDocuments.list.addUrl')}
+                      {(!dataset?.data_source_type || isDataSourceFile) && t('datasetDocuments.list.addFile')}
+                    </Button>
+                  )}
+                </div>
+              )
+            }
           </div>
           {isListLoading
             ? <Loading type='app' />
@@ -372,7 +379,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
                   onChange: setCurrPage,
                 }}
                 onManageMetadata={showEditMetadataModal}
-                optionsExamineStatus={optionsExamineStatus}
+                dataset={dataset}
               />
               : <EmptyElement canAdd={embeddingAvailable} onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} />
           }

+ 90 - 66
web/app/components/datasets/documents/list.tsx

@@ -56,6 +56,7 @@ import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils
 import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
 import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
 import { useAppContext } from '@/context/app-context'
+import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
 
 export const useIndexStatus = () => {
   const { t } = useTranslation()
@@ -470,7 +471,7 @@ type IDocumentListProps = {
   pagination: PaginationProps
   onUpdate: () => void
   onManageMetadata: () => void,
-  optionsExamineStatus: any[]
+  dataset: any
 }
 
 /**
@@ -485,8 +486,9 @@ const DocumentList: FC<IDocumentListProps> = ({
   pagination,
   onUpdate,
   onManageMetadata,
-  optionsExamineStatus,
+  dataset,
 }) => {
+  const { isCreate, isEdit, isOperation } = GetDatasetAuth(dataset)
   const { t } = useTranslation()
   const { formatTime } = useTimestamp()
   const router = useRouter()
@@ -582,7 +584,7 @@ const DocumentList: FC<IDocumentListProps> = ({
       else { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
     }
   }
-  const ExamineMap = {
+  const ExamineMap: any = {
     1: '待审核',
     2: '审核不通过',
     3: '无',
@@ -632,7 +634,7 @@ const DocumentList: FC<IDocumentListProps> = ({
             <tr>
               <td className='w-12'>
                 <div className='flex items-center' onClick={e => e.stopPropagation()}>
-                  {embeddingAvailable && (
+                  {isOperation && embeddingAvailable && (
                     <Checkbox
                       className='mr-2 shrink-0'
                       checked={isAllSelected}
@@ -659,7 +661,11 @@ const DocumentList: FC<IDocumentListProps> = ({
               </td>
               <td className='w-40'>{t('datasetDocuments.list.table.header.status')}</td>
               <td className='w-40'>审核状态</td>
-              <td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
+              {
+                isOperation && (
+                  <td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
+                )
+              }
             </tr>
           </thead>
           <tbody className="text-text-secondary">
@@ -668,50 +674,58 @@ const DocumentList: FC<IDocumentListProps> = ({
               const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
               return <tr
                 key={doc.id}
-                className={'h-8 cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover'}
+                className={`h-8 border-b border-divider-subtle ${isOperation && (doc.check_status === 2 || doc.check_status === 3) ? 'cursor-pointer  hover:bg-background-default-hover' : ''}`}
                 onClick={() => {
-                  (doc.check_status === 2 || doc.check_status === 3) && router.push(`/datasets/${datasetId}/documents/${doc.id}`)
+                  isOperation && (doc.check_status === 2 || doc.check_status === 3) && router.push(`/datasets/${datasetId}/documents/${doc.id}`)
                 }}>
                 <td className='text-left align-middle text-xs text-text-tertiary'>
                   <div className='flex items-center' onClick={e => e.stopPropagation()}>
-                    <Checkbox
-                      className='mr-2 shrink-0'
-                      checked={selectedIds.includes(doc.id)}
-                      onCheck={() => {
-                        onSelectedIdChange(
-                          selectedIds.includes(doc.id)
-                            ? selectedIds.filter(id => id !== doc.id)
-                            : [...selectedIds, doc.id],
-                        )
-                      }}
-                    />
+                    {
+                      isOperation && (
+                        <Checkbox
+                          className='mr-2 shrink-0'
+                          checked={selectedIds.includes(doc.id)}
+                          onCheck={() => {
+                            onSelectedIdChange(
+                              selectedIds.includes(doc.id)
+                                ? selectedIds.filter(id => id !== doc.id)
+                                : [...selectedIds, doc.id],
+                            )
+                          }}
+                        />
+                      )
+                    }
                     {/* {doc.position} */}
                     {index + 1}
                   </div>
                 </td>
                 <td>
-                  <div className={'group mr-6 flex max-w-[460px] items-center hover:mr-0'}>
+                  <div className={`group mr-6 flex max-w-[460px] items-center ${isOperation ? 'hover:mr-0' : ''}`}>
                     <div className='shrink-0'>
                       {doc?.data_source_type === DataSourceType.NOTION && <NotionIcon className='mr-1.5 mt-[-3px] inline-flex align-middle' type='page' src={doc.data_source_info.notion_page_icon} />}
                       {doc?.data_source_type === DataSourceType.FILE && <FileTypeIcon type={extensionToFileType(doc?.data_source_info?.upload_file?.extension ?? fileType)} className='mr-1.5' />}
                       {doc?.data_source_type === DataSourceType.WEB && <Globe01 className='mr-1.5 mt-[-3px] inline-flex align-middle' />}
                     </div>
                     <span className='grow-1 truncate text-sm'>{doc.name}</span>
-                    <div className='hidden shrink-0 group-hover:ml-auto group-hover:flex'>
-                      <Tooltip
-                        popupContent={t('datasetDocuments.list.table.rename')}
-                      >
-                        <div
-                          className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
-                          onClick={(e) => {
-                            e.stopPropagation()
-                            handleShowRenameModal(doc)
-                          }}
-                        >
-                          <RiEditLine className='h-4 w-4 text-text-tertiary' />
+                    {
+                      isOperation && (
+                        <div className='hidden shrink-0 group-hover:ml-auto group-hover:flex'>
+                          <Tooltip
+                            popupContent={t('datasetDocuments.list.table.rename')}
+                          >
+                            <div
+                              className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
+                              onClick={(e) => {
+                                e.stopPropagation()
+                                handleShowRenameModal(doc)
+                              }}
+                            >
+                              <RiEditLine className='h-4 w-4 text-text-tertiary' />
+                            </div>
+                          </Tooltip>
                         </div>
-                      </Tooltip>
-                    </div>
+                      )
+                    }
                   </div>
                 </td>
                 <td>
@@ -734,43 +748,53 @@ const DocumentList: FC<IDocumentListProps> = ({
                 </td>
                 <td>
                   {
-                    doc.check_status === 1 && (
-                      ['owner', 'admin'].includes(appContext.currentWorkspace.role)
-                        ? <div className="cursor-pointer text-[#155aef]" onClick={(e) => {
-                          e.stopPropagation()
-                          setRow(doc)
-                          setConfirmExamineHandleTitle(`${doc.enabled ? '下线' : '上线'}审核`)
-                          setConfirmExamineHandleContent(`用户“${doc.enable_application}”申请将该知识${doc.enabled ? '下线' : '上线'},请审核!`)
-                          setShowConfirmExamineHandle(true)
-                        }}>{ExamineMap[doc.check_status]}</div>
-                        : <div>{ExamineMap[doc.check_status]}</div>
-                    )
-                  }
-                  {
-                    doc.check_status === 2 && (
-                      <div className="cursor text-[#155aef]" onClick={(e) => {
-                        e.stopPropagation()
-                        setConfirmExamineResultContent(`用户“${doc.check_by}”不同意将该知识上线!`)
-                        setShowConfirmExamineResult(true)
-                      }}>{ExamineMap[doc.check_status]}</div>
-                    )
-                  }
-                  {
-                    doc.check_status === 3 && (
+                    isOperation ? (<>
+                      {
+                        doc.check_status === 1 && (
+                          ['owner', 'admin'].includes(appContext.currentWorkspace.role)
+                            ? <div className="cursor-pointer text-[#155aef]" onClick={(e) => {
+                              e.stopPropagation()
+                              setRow(doc)
+                              setConfirmExamineHandleTitle(`${doc.enabled ? '下线' : '上线'}审核`)
+                              setConfirmExamineHandleContent(`用户“${doc.enable_application}”申请将该知识${doc.enabled ? '下线' : '上线'},请审核!`)
+                              setShowConfirmExamineHandle(true)
+                            }}>{ExamineMap[doc.check_status]}</div>
+                            : <div>{ExamineMap[doc.check_status]}</div>
+                        )
+                      }
+                      {
+                        doc.check_status === 2 && (
+                          <div className="cursor text-[#155aef]" onClick={(e) => {
+                            e.stopPropagation()
+                            setConfirmExamineResultContent(`用户“${doc.check_by}”不同意将该知识上线!`)
+                            setShowConfirmExamineResult(true)
+                          }}>{ExamineMap[doc.check_status]}</div>
+                        )
+                      }
+                      {
+                        doc.check_status === 3 && (
+                          <div>{ExamineMap[doc.check_status]}</div>
+                        )
+                      }
+                    </>) : (
                       <div>{ExamineMap[doc.check_status]}</div>
                     )
                   }
                 </td>
-                <td>
-                  {
-                    (doc.check_status === 2 || doc.check_status === 3) && (<OperationAction
-                      embeddingAvailable={embeddingAvailable}
-                      datasetId={datasetId}
-                      detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'check_status'])}
-                      onUpdate={onUpdate}
-                    />)
-                  }
-                </td>
+                {
+                  isOperation && (
+                    <td>
+                      {
+                        (doc.check_status === 2 || doc.check_status === 3) && (<OperationAction
+                          embeddingAvailable={embeddingAvailable}
+                          datasetId={datasetId}
+                          detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'check_status'])}
+                          onUpdate={onUpdate}
+                        />)
+                      }
+                    </td>
+                  )
+                }
               </tr>
             })}
           </tbody>

+ 22 - 15
web/app/components/datasets/rename-modal/index.tsx

@@ -24,6 +24,7 @@ import {
   setDatasetsPermission,
 } from '@/service/common'
 import { TreeSelect as AntdTreeSelect } from 'antd'
+import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
 
 type RenameDatasetModalProps = {
   show: boolean
@@ -33,6 +34,7 @@ type RenameDatasetModalProps = {
 }
 
 const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDatasetModalProps) => {
+  const { isCreate, isEdit, isOperation } = GetDatasetAuth(dataset)
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
   const [loading, setLoading] = useState(false)
@@ -219,21 +221,26 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
             />
           </div>
         </div>
-        <div className='pt-2'>
-          <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑权限</div>
-          <div className="h-[32px]">
-            <SimpleSelect
-              className="h-[32px]"
-              defaultValue={editAuth}
-              onSelect={(i: any) => {
-                setEditAuth(i.value)
-              }}
-              items={optionsEditAuth}
-              allowSearch={false}
-              placeholder="请选择编辑权限"
-            />
-          </div>
-        </div>
+        {
+          isCreate && (
+            <div className='pt-2'>
+              <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑权限</div>
+              <div className="h-[32px]">
+                <SimpleSelect
+                  className="h-[32px]"
+                  defaultValue={editAuth}
+                  onSelect={(i: any) => {
+                    setEditAuth(i.value)
+                  }}
+                  items={optionsEditAuth}
+                  allowSearch={false}
+                  placeholder="请选择编辑权限"
+                  notClearable={true}
+                />
+              </div>
+            </div>
+          )
+        }
         <div className='pt-2'>
           <div className='py-2 text-sm font-medium leading-[20px] text-text-primary'>编辑授权</div>
           <AntdTreeSelect

+ 10 - 11
web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx

@@ -4,7 +4,6 @@ import React, { useCallback, useState } from 'react'
 import { useBoolean } from 'ahooks'
 import {
   RiDeleteBinLine,
-  RiEditLine,
 } from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
 import type { DataSet } from '@/models/datasets'
@@ -77,16 +76,16 @@ const DatasetItem: FC<Props> = ({
       </div>
       {!readonly && (
         <div className='ml-2 hidden shrink-0 items-center  space-x-1 group-hover/dataset-item:flex'>
-          {
-            editable && <ActionButton
-              onClick={(e) => {
-                e.stopPropagation()
-                showSettingsModal()
-              }}
-            >
-              <RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' />
-            </ActionButton>
-          }
+          {/* { */}
+          {/*  editable && <ActionButton */}
+          {/*    onClick={(e) => { */}
+          {/*      e.stopPropagation() */}
+          {/*      showSettingsModal() */}
+          {/*    }} */}
+          {/*  > */}
+          {/*    <RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' /> */}
+          {/*  </ActionButton> */}
+          {/* } */}
           <ActionButton
             onClick={handleRemove}
             state={ActionButtonState.Destructive}

+ 1 - 1
web/context/app-context.tsx

@@ -100,7 +100,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
   const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
   const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
   const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
-  const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
+  const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor', 'leader'].includes(currentWorkspace.role), [currentWorkspace.role])
   const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
   const updateUserProfileAndVersion = useCallback(async () => {
     if (userProfileResponse && !userProfileResponse.bodyUsed) {