From 5e1039694e93783f80238a121a07ce111bc596f4 Mon Sep 17 00:00:00 2001
From: zstar <65890619+zstar1003@users.noreply.github.com>
Date: Sat, 5 Apr 2025 22:04:05 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0=E8=87=B3?=
=?UTF-8?q?v0.1.1=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=9B=A2=E9=98=9F=E7=AE=A1?=
=?UTF-8?q?=E7=90=86=EF=BC=8C=E7=94=A8=E6=88=B7=E9=85=8D=E7=BD=AE=E4=B8=A4?=
=?UTF-8?q?=E9=A1=B9=E5=8A=9F=E8=83=BD=20(#13)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/ISSUE_TEMPLATE/bug_report.yml | 70 +--
.github/ISSUE_TEMPLATE/question.yml | 21 +-
.github/ISSUE_TEMPLATE/subtask.yml | 2 +-
.github/pull_request_template.md | 18 +-
README.md | 7 -
api/apps/document_app.py | 17 +-
api/db/services/document_service.py | 1 +
api/db/services/task_service.py | 62 +++
deepdoc/parser/docx_parser.py | 102 ++++-
deepdoc/parser/pdf_parser.py | 38 ++
deepdoc/vision/ocr.py | 28 ++
management/server/app.py | 79 +---
management/server/database.py | 258 +----------
management/server/routes/__init__.py | 18 +
management/server/routes/teams/__init__.py | 0
management/server/routes/teams/routes.py | 178 ++++++++
management/server/routes/tenants/__init__.py | 0
management/server/routes/tenants/routes.py | 53 +++
management/server/routes/users/__init__.py | 0
management/server/routes/users/routes.py | 74 +++
management/server/services/teams/__init__.py | 0
management/server/services/teams/service.py | 271 +++++++++++
.../server/services/tenants/__init__.py | 0
management/server/services/tenants/service.py | 121 +++++
management/server/services/users/__init__.py | 0
management/server/services/users/service.py | 260 +++++++++++
management/web/public/favicon.ico | Bin 8627 -> 70814 bytes
.../web/src/common/apis/configs/index.ts | 20 +
.../web/src/common/apis/configs/type.ts | 29 ++
management/web/src/common/apis/teams/index.ts | 44 ++
management/web/src/common/apis/teams/type.ts | 30 ++
.../common/assets/icons/team-management.svg | 9 +
.../src/common/assets/icons/user-config.svg | 9 +
.../common/assets/icons/user-management.svg | 5 +
.../src/common/assets/images/docs/preview.png | Bin 503620 -> 0 bytes
.../src/common/assets/images/layouts/logo.png | Bin 3990 -> 7250 bytes
management/web/src/layouts/config.ts | 2 +-
.../composable-demo/apis/use-fetch-select.ts | 39 --
.../apis/use-fullscreen-loading.ts | 26 --
.../demo/composable-demo/use-fetch-select.vue | 30 --
.../use-fullscreen-loading.vue | 60 ---
.../demo/composable-demo/use-watermark.vue | 59 ---
.../web/src/pages/demo/level2/index.vue | 30 --
.../src/pages/demo/level2/level3/index.vue | 15 -
.../pages/demo/permission/button-level.vue | 42 --
.../permission/components/SwitchRoles.vue | 37 --
.../src/pages/demo/permission/page-level.vue | 18 -
.../web/src/pages/demo/unocss/index.vue | 17 -
.../web/src/pages/demo/vxe-table/index.vue | 425 ------------------
.../demo/vxe-table/tsx/RoleColumnSlots.tsx | 9 -
.../demo/vxe-table/tsx/StatusColumnSlots.tsx | 9 -
.../web/src/pages/team-management/index.vue | 392 ++++++++++++++++
.../web/src/pages/user-config/index.vue | 206 +++++++++
.../index.vue | 2 +-
management/web/src/router/index.ts | 42 +-
management/web/types/auto/components.d.ts | 4 +
.../web/types/auto/svg-component-global.d.ts | 6 +-
management/web/types/auto/svg-component.d.ts | 10 +-
58 files changed, 2036 insertions(+), 1268 deletions(-)
create mode 100644 management/server/routes/__init__.py
create mode 100644 management/server/routes/teams/__init__.py
create mode 100644 management/server/routes/teams/routes.py
create mode 100644 management/server/routes/tenants/__init__.py
create mode 100644 management/server/routes/tenants/routes.py
create mode 100644 management/server/routes/users/__init__.py
create mode 100644 management/server/routes/users/routes.py
create mode 100644 management/server/services/teams/__init__.py
create mode 100644 management/server/services/teams/service.py
create mode 100644 management/server/services/tenants/__init__.py
create mode 100644 management/server/services/tenants/service.py
create mode 100644 management/server/services/users/__init__.py
create mode 100644 management/server/services/users/service.py
create mode 100644 management/web/src/common/apis/configs/index.ts
create mode 100644 management/web/src/common/apis/configs/type.ts
create mode 100644 management/web/src/common/apis/teams/index.ts
create mode 100644 management/web/src/common/apis/teams/type.ts
create mode 100644 management/web/src/common/assets/icons/team-management.svg
create mode 100644 management/web/src/common/assets/icons/user-config.svg
create mode 100644 management/web/src/common/assets/icons/user-management.svg
delete mode 100644 management/web/src/common/assets/images/docs/preview.png
delete mode 100644 management/web/src/pages/demo/composable-demo/apis/use-fetch-select.ts
delete mode 100644 management/web/src/pages/demo/composable-demo/apis/use-fullscreen-loading.ts
delete mode 100644 management/web/src/pages/demo/composable-demo/use-fetch-select.vue
delete mode 100644 management/web/src/pages/demo/composable-demo/use-fullscreen-loading.vue
delete mode 100644 management/web/src/pages/demo/composable-demo/use-watermark.vue
delete mode 100644 management/web/src/pages/demo/level2/index.vue
delete mode 100644 management/web/src/pages/demo/level2/level3/index.vue
delete mode 100644 management/web/src/pages/demo/permission/button-level.vue
delete mode 100644 management/web/src/pages/demo/permission/components/SwitchRoles.vue
delete mode 100644 management/web/src/pages/demo/permission/page-level.vue
delete mode 100644 management/web/src/pages/demo/unocss/index.vue
delete mode 100644 management/web/src/pages/demo/vxe-table/index.vue
delete mode 100644 management/web/src/pages/demo/vxe-table/tsx/RoleColumnSlots.tsx
delete mode 100644 management/web/src/pages/demo/vxe-table/tsx/StatusColumnSlots.tsx
create mode 100644 management/web/src/pages/team-management/index.vue
create mode 100644 management/web/src/pages/user-config/index.vue
rename management/web/src/pages/{demo/element-plus => user-management}/index.vue (99%)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 068baed..473b491 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,73 +1,15 @@
name: "🐞 Bug Report"
-description: Create a bug issue for RAGFlow
+description: Create a bug issue for RAGFlow-Plus
title: "[Bug]: "
labels: ["🐞 bug"]
body:
-- type: checkboxes
- attributes:
- label: Self Checks
- description: "Please check the following in order to be responded in time :)"
- options:
- - label: I have searched for existing issues [search for existing issues](https://github.com/infiniflow/ragflow/issues), including closed ones.
- required: true
- - label: I confirm that I am using English to submit this report ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- required: true
- - label: Non-english title submitions will be closed directly ( 非英文标题的提交将会被直接关闭 ) ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- required: true
- - label: "Please do not modify this template :) and fill in all the required fields."
- required: true
- type: markdown
attributes:
- value: "Please provide the following information to help us understand the issue."
-- type: input
- attributes:
- label: RAGFlow workspace code commit ID
- description: Enter the commit ID associated with the issue.
- placeholder: e.g., 26d3480e
- validations:
- required: true
-- type: input
- attributes:
- label: RAGFlow image version
- description: Enter the image version(shown in RAGFlow UI, `System` page) associated with the issue.
- placeholder: e.g., 26d3480e(v0.13.0~174)
- validations:
- required: true
+ value: |
+ 尽管问,不限语言。(All language is welcomed)
- type: textarea
attributes:
- label: Other environment information
- description: |
- Enter the environment details:
- value: |
- - Hardware parameters:
- - OS type:
- - Others:
- render: Markdown
+ label: 描述你的问题
+ description: 尽可能清晰地描述,如果是bug,建议附上详细步骤和日志。
validations:
- required: false
-- type: textarea
- attributes:
- label: Actual behavior
- description: Describe what you encountered.
- validations:
- required: true
-- type: textarea
- attributes:
- label: Expected behavior
- description: Describe what you expected.
- validations:
- required: false
-- type: textarea
- attributes:
- label: Steps to reproduce
- description: Steps to reproduce what you encountered.
- render: Markdown
- validations:
- required: true
-- type: textarea
- attributes:
- label: Additional information
- description: |
- Log, error message, or any other information can help find the root cause.
- validations:
- required: false
\ No newline at end of file
+ required: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index f50fc33..1470335 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -1,28 +1,15 @@
name: "🙋♀️ Question"
-description: Ask questions on RAGFlow
+description: Ask questions on RAGFlow-Plus
title: "[Question]: "
labels: ["🙋♀️ question"]
body:
-- type: checkboxes
- attributes:
- label: Self Checks
- description: "Please check the following in order to be responded in time :)"
- options:
- - label: I have searched for existing issues [search for existing issues](https://github.com/infiniflow/ragflow/issues), including closed ones.
- required: true
- - label: I confirm that I am using English to submit this report ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- required: true
- - label: Non-english title submitions will be closed directly ( 非英文标题的提交将会被直接关闭 ) ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- required: true
- - label: "Please do not modify this template :) and fill in all the required fields."
- required: true
- type: markdown
attributes:
value: |
- If the previous templates don't fit with what you'd like to report or ask, please use this general question template to file issue.
+ 尽管问,不限语言。(All language is welcomed)
- type: textarea
attributes:
- label: Describe your problem
- description: A clear and concise description of your problem.
+ label: 描述你的问题
+ description: 尽可能清晰地描述,如果是bug,建议附上详细步骤和日志。
validations:
required: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/subtask.yml b/.github/ISSUE_TEMPLATE/subtask.yml
index e2cac09..2378fed 100644
--- a/.github/ISSUE_TEMPLATE/subtask.yml
+++ b/.github/ISSUE_TEMPLATE/subtask.yml
@@ -1,5 +1,5 @@
name: Subtask
-description: "Propose a subtask for RAGFlow"
+description: "Propose a subtask for RAGFlow-Plus"
title: "[Subtask]: "
labels: [subtask]
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8257a2d..aabc2dd 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,12 +1,8 @@
-### What problem does this PR solve?
+### 选择一个类型
-_Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._
-
-### Type of change
-
-- [ ] Bug Fix (non-breaking change which fixes an issue)
-- [ ] New Feature (non-breaking change which adds functionality)
-- [ ] Documentation Update
-- [ ] Refactoring
-- [ ] Performance Improvement
-- [ ] Other (please describe):
\ No newline at end of file
+- [ ] Bug修复(Bug Fix)
+- [ ] 新功能(New Feature)
+- [ ] 文档更新(Documentation Update)
+- [ ] 重构(Refactoring)
+- [ ] 性能优化(Performance Improvement)
+- [ ] 其他(Other)
diff --git a/README.md b/README.md
index 2981652..4cf5c1b 100644
--- a/README.md
+++ b/README.md
@@ -113,13 +113,6 @@ pnpm dev
-## License
-
-版权说明:本项目在 Ragflow 项目基础上进行二开,需要遵守 Ragflow 的开源协议,如下
-
-This repository is available under the [Ragflow
- Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions.
-
## 鸣谢
本项目基于以下开源项目开发:
diff --git a/api/apps/document_app.py b/api/apps/document_app.py
index ec7db5f..c27a4d9 100644
--- a/api/apps/document_app.py
+++ b/api/apps/document_app.py
@@ -349,6 +349,8 @@ def rm():
@validate_request("doc_ids", "run")
def run():
req = request.json
+
+ # 检查用户是否有权限操作这些文档
for doc_id in req["doc_ids"]:
if not DocumentService.accessible(doc_id, current_user.id):
return get_json_result(
@@ -357,29 +359,42 @@ def run():
code=settings.RetCode.AUTHENTICATION_ERROR
)
try:
+ # 遍历所有需要处理的文档ID
for id in req["doc_ids"]:
+ # 准备更新文档状态的信息
info = {"run": str(req["run"]), "progress": 0}
+ # 如果是运行状态且需要删除旧数据,则重置统计信息
if str(req["run"]) == TaskStatus.RUNNING.value and req.get("delete", False):
info["progress_msg"] = ""
info["chunk_num"] = 0
info["token_num"] = 0
+
+ # 更新文档状态
DocumentService.update_by_id(id, info)
+ # 获取租户ID
tenant_id = DocumentService.get_tenant_id(id)
if not tenant_id:
return get_data_error_result(message="Tenant not found!")
+ # 获取文档详情
e, doc = DocumentService.get_by_id(id)
if not e:
return get_data_error_result(message="Document not found!")
+ # 如果需要删除旧数据
if req.get("delete", False):
+ # 删除相关任务记录
TaskService.filter_delete([Task.doc_id == id])
+ # 如果索引存在,则删除索引中的文档数据
if settings.docStoreConn.indexExist(search.index_name(tenant_id), doc.kb_id):
settings.docStoreConn.delete({"doc_id": id}, search.index_name(tenant_id), doc.kb_id)
-
+
+ # 如果是运行状态,则创建解析任务
if str(req["run"]) == TaskStatus.RUNNING.value:
e, doc = DocumentService.get_by_id(id)
doc = doc.to_dict()
doc["tenant_id"] = tenant_id
+ # 获取文档存储位置
bucket, name = File2DocumentService.get_storage_address(doc_id=doc["id"])
+ # 将任务加入队列
queue_tasks(doc, bucket, name)
return get_json_result(data=True)
diff --git a/api/db/services/document_service.py b/api/db/services/document_service.py
index 16482c8..470f24a 100644
--- a/api/db/services/document_service.py
+++ b/api/db/services/document_service.py
@@ -515,6 +515,7 @@ def doc_upload_and_parse(conversation_id, file_objs, user_id):
ParserType.EMAIL.value: email
}
parser_config = {"chunk_token_num": 4096, "delimiter": "\n!?;。;!?", "layout_recognize": "Plain Text"}
+ # 使用线程池执行解析任务
exe = ThreadPoolExecutor(max_workers=12)
threads = []
doc_nm = {}
diff --git a/api/db/services/task_service.py b/api/db/services/task_service.py
index 2d3147c..d61feef 100644
--- a/api/db/services/task_service.py
+++ b/api/db/services/task_service.py
@@ -202,76 +202,138 @@ class TaskService(CommonService):
def queue_tasks(doc: dict, bucket: str, name: str):
+ """
+ 将文档解析任务分割并加入队列处理。
+
+ 该函数根据文档类型(PDF、表格等)将文档分割成多个子任务,计算任务摘要,
+ 检查是否可以重用之前的任务结果,并将未完成的任务加入Redis队列进行处理。
+
+ 参数:
+ doc (dict): 文档信息字典,包含id、type、parser_id、parser_config等信息
+ bucket (str): 存储桶名称
+ name (str): 文件名称
+
+ 流程:
+ 1. 根据文档类型(PDF/表格)将文档分割成多个子任务
+ 2. 为每个任务生成唯一摘要(digest)
+ 3. 尝试重用之前任务的处理结果
+ 4. 清理旧任务并更新文档状态
+ 5. 将新任务批量插入数据库
+ 6. 将未完成的任务加入Redis队列
+ """
def new_task():
+ """
+ 创建一个新的任务字典,包含基本任务信息。
+
+ 返回:
+ dict: 包含任务ID、文档ID、进度和页面范围的任务字典
+ """
return {"id": get_uuid(), "doc_id": doc["id"], "progress": 0.0, "from_page": 0, "to_page": 100000000}
+ # 存储所有解析任务的数组
parse_task_array = []
+ # PDF文档处理逻辑
if doc["type"] == FileType.PDF.value:
+ # 从存储中获取文件内容
file_bin = STORAGE_IMPL.get(bucket, name)
+ # 获取布局识别方式,默认为"DeepDOC"
do_layout = doc["parser_config"].get("layout_recognize", "DeepDOC")
+ # 获取PDF总页数
pages = PdfParser.total_page_number(doc["name"], file_bin)
+ # 获取每个任务处理的页数,默认为12页
page_size = doc["parser_config"].get("task_page_size", 12)
+ # 对于学术论文类型,默认任务页数为22
if doc["parser_id"] == "paper":
page_size = doc["parser_config"].get("task_page_size", 22)
+ # 对于特定解析器或非DeepDOC布局识别,将整个文档作为一个任务处理
if doc["parser_id"] in ["one", "knowledge_graph"] or do_layout != "DeepDOC":
page_size = 10 ** 9
+ # 获取需要处理的页面范围,默认为全部页面
page_ranges = doc["parser_config"].get("pages") or [(1, 10 ** 5)]
+ # 根据页面范围和任务页数分割任务
for s, e in page_ranges:
+ # 调整页码(从0开始)
s -= 1
s = max(0, s)
+ # 确保结束页不超过文档总页数
e = min(e - 1, pages)
+ # 按照任务页数分割任务
for p in range(s, e, page_size):
task = new_task()
task["from_page"] = p
task["to_page"] = min(p + page_size, e)
parse_task_array.append(task)
+ # 表格文档处理逻辑
elif doc["parser_id"] == "table":
+ # 从存储中获取文件内容
file_bin = STORAGE_IMPL.get(bucket, name)
+ # 获取表格总行数
rn = RAGFlowExcelParser.row_number(doc["name"], file_bin)
+ # 每3000行作为一个任务
for i in range(0, rn, 3000):
task = new_task()
task["from_page"] = i
task["to_page"] = min(i + 3000, rn)
parse_task_array.append(task)
+ # 其他类型文档,整个文档作为一个任务处理
else:
parse_task_array.append(new_task())
+ # 获取文档的分块配置
chunking_config = DocumentService.get_chunking_config(doc["id"])
+ # 为每个任务生成唯一摘要(digest)
for task in parse_task_array:
+ # 创建哈希对象
hasher = xxhash.xxh64()
+ # 对分块配置中的每个字段进行哈希
for field in sorted(chunking_config.keys()):
if field == "parser_config":
+ # 移除不需要参与哈希计算的特定配置项
for k in ["raptor", "graphrag"]:
if k in chunking_config[field]:
del chunking_config[field][k]
+ # 将配置字段添加到哈希计算中
hasher.update(str(chunking_config[field]).encode("utf-8"))
+ # 将任务特定字段添加到哈希计算中
for field in ["doc_id", "from_page", "to_page"]:
hasher.update(str(task.get(field, "")).encode("utf-8"))
+ # 生成任务摘要并设置初始进度
task_digest = hasher.hexdigest()
task["digest"] = task_digest
task["progress"] = 0.0
+ # 获取文档之前的任务记录
prev_tasks = TaskService.get_tasks(doc["id"])
+ # 记录重用的块数量
ck_num = 0
if prev_tasks:
+ # 尝试重用之前任务的处理结果
for task in parse_task_array:
ck_num += reuse_prev_task_chunks(task, prev_tasks, chunking_config)
+ # 删除文档之前的任务记录
TaskService.filter_delete([Task.doc_id == doc["id"]])
+ # 收集需要删除的块ID
chunk_ids = []
for task in prev_tasks:
if task["chunk_ids"]:
chunk_ids.extend(task["chunk_ids"].split())
+ # 从文档存储中删除这些块
if chunk_ids:
settings.docStoreConn.delete({"id": chunk_ids}, search.index_name(chunking_config["tenant_id"]),
chunking_config["kb_id"])
+ # 更新文档的块数量
DocumentService.update_by_id(doc["id"], {"chunk_num": ck_num})
+ # 将新任务批量插入数据库
bulk_insert_into_db(Task, parse_task_array, True)
+ # 开始解析文档
DocumentService.begin2parse(doc["id"])
+ # 筛选出未完成的任务
unfinished_task_array = [task for task in parse_task_array if task["progress"] < 1.0]
+ # 将未完成的任务加入Redis队列
for unfinished_task in unfinished_task_array:
assert REDIS_CONN.queue_product(
SVR_QUEUE_NAME, message=unfinished_task
diff --git a/deepdoc/parser/docx_parser.py b/deepdoc/parser/docx_parser.py
index dfe3f37..a15acca 100644
--- a/deepdoc/parser/docx_parser.py
+++ b/deepdoc/parser/docx_parser.py
@@ -23,16 +23,65 @@ from io import BytesIO
class RAGFlowDocxParser:
+ """
+ Word文档(.docx)解析器,用于提取文档中的文本内容和表格。
+
+ 该解析器能够:
+ 1. 按页面范围提取文档中的段落文本及其样式
+ 2. 识别文档中的表格并将其转换为结构化文本
+ 3. 智能处理表格头部和内容,生成语义化的文本描述
+ """
def __extract_table_content(self, tb):
+ """
+ 从Word表格对象中提取内容并转换为DataFrame
+
+ 参数:
+ tb: docx库的Table对象
+
+ 返回:
+ 处理后的表格内容文本列表
+ """
df = []
for row in tb.rows:
df.append([c.text for c in row.cells])
return self.__compose_table_content(pd.DataFrame(df))
def __compose_table_content(self, df):
+ """
+ 将表格DataFrame转换为语义化的文本描述
+
+ 通过识别表格的结构特征(如表头、数据类型等),将表格转换为更易于理解的文本形式
+
+ 参数:
+ df: 包含表格内容的DataFrame
+
+ 返回:
+ 表格内容的文本表示列表
+ """
def blockType(b):
+ """
+ 识别单元格内容的类型
+
+ 通过正则表达式和文本特征分析,将单元格内容分类为不同类型:
+ - Dt: 日期类型
+ - Nu: 数字类型
+ - Ca: 代码/ID类型
+ - En: 英文文本
+ - NE: 数字和文本混合
+ - Sg: 单字符
+ - Tx: 短文本
+ - Lx: 长文本
+ - Nr: 人名
+ - Ot: 其他类型
+
+ 参数:
+ b: 单元格文本内容
+
+ 返回:
+ 内容类型的字符串标识
+ """
patt = [
("^(20|19)[0-9]{2}[年/-][0-9]{1,2}[月/-][0-9]{1,2}日*$", "Dt"),
(r"^(20|19)[0-9]{2}年$", "Dt"),
@@ -62,14 +111,21 @@ class RAGFlowDocxParser:
return "Ot"
+ # 表格至少需要两行才能处理
if len(df) < 2:
return []
+
+ # 统计表格中最常见的内容类型
max_type = Counter([blockType(str(df.iloc[i, j])) for i in range(
1, len(df)) for j in range(len(df.iloc[i, :]))])
max_type = max(max_type.items(), key=lambda x: x[1])[0]
+ # 获取表格列数
colnm = len(df.iloc[0, :])
- hdrows = [0] # header is not nessesarily appear in the first line
+ # 默认第一行为表头
+ hdrows = [0] # 表头不一定出现在第一行
+
+ # 如果表格主要是数字类型,则识别非数字行作为表头
if max_type == "Nu":
for r in range(1, len(df)):
tys = Counter([blockType(str(df.iloc[r, j]))
@@ -78,18 +134,26 @@ class RAGFlowDocxParser:
if tys != max_type:
hdrows.append(r)
+ # 处理表格内容,将每行转换为文本
lines = []
for i in range(1, len(df)):
+ # 跳过表头行
if i in hdrows:
continue
+
+ # 计算当前行之前的表头行
hr = [r - i for r in hdrows]
hr = [r for r in hr if r < 0]
+
+ # 找到最近的连续表头行
t = len(hr) - 1
while t > 0:
if hr[t] - hr[t - 1] > 1:
hr = hr[t:]
break
t -= 1
+
+ # 为每列构建表头描述
headers = []
for j in range(len(df.iloc[i, :])):
t = []
@@ -102,6 +166,8 @@ class RAGFlowDocxParser:
if t:
t += ": "
headers.append(t)
+
+ # 构建每行的文本表示
cells = []
for j in range(len(df.iloc[i, :])):
if not str(df.iloc[i, j]):
@@ -109,31 +175,53 @@ class RAGFlowDocxParser:
cells.append(headers[j] + str(df.iloc[i, j]))
lines.append(";".join(cells))
+ # 根据列数决定返回格式
if colnm > 3:
return lines
return ["\n".join(lines)]
def __call__(self, fnm, from_page=0, to_page=100000000):
+ """
+ 解析Word文档,提取指定页面范围内的文本和表格
+
+ 参数:
+ fnm: 文件名或二进制内容
+ from_page: 起始页码(从0开始)
+ to_page: 结束页码
+
+ 返回:
+ 元组(secs, tbls),其中:
+ - secs: 段落内容列表,每项为(文本内容, 样式名称)的元组
+ - tbls: 表格内容列表
+ """
+ # 根据输入类型创建Document对象
self.doc = Document(fnm) if isinstance(
fnm, str) else Document(BytesIO(fnm))
- pn = 0 # parsed page
- secs = [] # parsed contents
+ pn = 0 # 当前解析页码
+ secs = [] # 存储解析的段落内容
+
+ # 遍历文档中的所有段落
for p in self.doc.paragraphs:
+ # 如果超出指定页码范围,停止解析
if pn > to_page:
break
- runs_within_single_paragraph = [] # save runs within the range of pages
+ runs_within_single_paragraph = [] # 保存在页面范围内的文本片段
+ # 遍历段落中的所有文本片段(run)
for run in p.runs:
if pn > to_page:
break
+ # 如果当前页码在指定范围内且段落有内容,则添加文本
if from_page <= pn < to_page and p.text.strip():
- runs_within_single_paragraph.append(run.text) # append run.text first
+ runs_within_single_paragraph.append(run.text) # 先添加文本片段
- # wrap page break checker into a static method
+ # 检查页面分隔符
if 'lastRenderedPageBreak' in run._element.xml:
pn += 1
- secs.append(("".join(runs_within_single_paragraph), p.style.name if hasattr(p.style, 'name') else '')) # then concat run.text as part of the paragraph
+ # 将段落文本和样式添加到结果列表
+ secs.append(("".join(runs_within_single_paragraph), p.style.name if hasattr(p.style, 'name') else '')) # 然后将文本片段连接为段落的一部分
+ # 提取所有表格内容
tbls = [self.__extract_table_content(tb) for tb in self.doc.tables]
return secs, tbls
diff --git a/deepdoc/parser/pdf_parser.py b/deepdoc/parser/pdf_parser.py
index 30a30f5..b8a14ca 100644
--- a/deepdoc/parser/pdf_parser.py
+++ b/deepdoc/parser/pdf_parser.py
@@ -284,6 +284,24 @@ class RAGFlowPdfParser:
b["SP"] = ii
def __ocr(self, pagenum, img, chars, ZM=3):
+ """
+ 对PDF页面图像进行OCR处理,识别文本内容并合并PDF提取的字符
+
+ 该方法完成以下任务:
+ 1. 使用OCR模型检测图像中的文本框
+ 2. 将PDF提取的字符与OCR检测的文本框匹配合并
+ 3. 对没有文本的框进行文本识别
+ 4. 计算文本高度统计信息
+
+ 参数:
+ pagenum: 当前处理的页码
+ img: 页面图像对象
+ chars: 从PDF中提取的字符列表
+ ZM: 缩放因子,用于坐标转换
+
+ 返回:
+ 无直接返回值,但会更新self.boxes列表
+ """
start = timer()
bxs = self.ocr.detect(np.array(img))
logging.info(f"__ocr detecting boxes of a image cost ({timer() - start}s)")
@@ -965,6 +983,26 @@ class RAGFlowPdfParser:
def __images__(self, fnm, zoomin=3, page_from=0,
page_to=299, callback=None):
+ """
+ 处理PDF文件并提取图像和文本内容
+
+ 该方法是PDF解析的核心,负责:
+ 1. 将PDF页面转换为高分辨率图像
+ 2. 提取页面中的字符和布局信息
+ 3. 使用OCR识别文本内容
+ 4. 分析文档语言(英文或中文)
+ 5. 提取文档大纲结构
+
+ 参数:
+ fnm: 文件名或二进制内容
+ zoomin: 图像缩放因子,用于提高OCR精度
+ page_from: 起始页码(从0开始)
+ page_to: 结束页码
+ callback: 进度回调函数,用于报告处理进度
+
+ 返回:
+ 无直接返回值,但会更新类的内部状态
+ """
self.lefted_chars = []
self.mean_height = []
self.mean_width = []
diff --git a/deepdoc/vision/ocr.py b/deepdoc/vision/ocr.py
index 87ba2b6..ddd0834 100644
--- a/deepdoc/vision/ocr.py
+++ b/deepdoc/vision/ocr.py
@@ -123,7 +123,19 @@ def load_model(model_dir, nm):
class TextRecognizer:
+ """
+ 文本识别器类,用于识别检测到的文本区域中的具体文字内容
+
+ 该类使用基于CTC(Connectionist Temporal Classification)的文本识别模型,
+ 能够将图像中的文本区域转换为文字内容
+ """
def __init__(self, model_dir):
+ """
+ 初始化文本识别器
+
+ 参数:
+ model_dir: 模型文件所在目录
+ """
self.rec_image_shape = [int(v) for v in "3, 48, 320".split(",")]
self.rec_batch_num = 16
postprocess_params = {
@@ -136,6 +148,16 @@ class TextRecognizer:
self.input_tensor = self.predictor.get_inputs()[0]
def resize_norm_img(self, img, max_wh_ratio):
+ """
+ 调整图像大小并进行归一化处理,保持宽高比
+
+ 参数:
+ img: 输入图像
+ max_wh_ratio: 最大宽高比
+
+ 返回:
+ 处理后的图像张量
+ """
imgC, imgH, imgW = self.rec_image_shape
assert imgC == img.shape[2]
@@ -394,6 +416,12 @@ class TextRecognizer:
class TextDetector:
+ """
+ 文本检测器类,用于检测图像中的文本区域
+
+ 该类使用基于DB(Differentiable Binarization)的文本检测模型,
+ 能够准确定位图像中的文本区域,并返回文本框的坐标信息
+ """
def __init__(self, model_dir):
pre_process_list = [{
'DetResizeForTest': {
diff --git a/management/server/app.py b/management/server/app.py
index 918e098..206d875 100644
--- a/management/server/app.py
+++ b/management/server/app.py
@@ -1,89 +1,20 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
import database
+from routes import register_routes
app = Flask(__name__)
# 启用CORS,允许前端访问
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
-# 添加 v1 前缀
+# 注册所有路由
+register_routes(app)
+
+# 登录路由保留在主文件中
@app.route('/api/v1/auth/login', methods=['POST'])
def login():
# 实现登录逻辑
return {"code": 0, "data": {"token": "your-token"}, "message": "登录成功"}
-
-
-@app.route('/api/v1/users/me', methods=['GET'])
-def get_current_user():
- return jsonify({
- "code": 0,
- "data": {
- "username": "admin",
- "roles": ["admin"]
- },
- "message": "获取用户信息成功"
- })
-
-
-@app.route('/api/v1/users', methods=['GET'])
-def get_users():
- """获取用户的API端点,支持分页和条件查询"""
- try:
- # 获取查询参数
- current_page = int(request.args.get('currentPage', 1))
- page_size = int(request.args.get('size', 10))
- username = request.args.get('username', '')
- email = request.args.get('email', '')
-
- # 调用数据库函数获取分页和筛选后的用户数据
- users, total = database.get_users_with_pagination(current_page, page_size, username, email)
-
- # 返回符合前端期望格式的数据
- return jsonify({
- "code": 0, # 成功状态码
- "data": {
- "list": users,
- "total": total
- },
- "message": "获取用户列表成功"
- })
- except Exception as e:
- # 错误处理
- return jsonify({
- "code": 500,
- "message": f"获取用户列表失败: {str(e)}"
- }), 500
-
-@app.route('/api/v1/users/', methods=['DELETE'])
-def delete_user(user_id):
- """删除用户的API端点"""
- database.delete_user(user_id)
- return jsonify({
- "code": 0,
- "message": f"用户 {user_id} 删除成功"
- })
-
-@app.route('/api/v1/users', methods=['POST'])
-def create_user():
- """创建用户的API端点"""
- data = request.json
- # 创建用户
- database.create_user(user_data=data)
- return jsonify({
- "code": 0,
- "message": "用户创建成功"
- })
-
-@app.route('/api/v1/users/', methods=['PUT'])
-def update_user(user_id):
- """更新用户的API端点"""
- data = request.json
- user_id = data.get('id')
- database.update_user(user_id=user_id, user_data=data)
- return jsonify({
- "code": 0,
- "message": f"用户 {user_id} 更新成功"
- })
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
\ No newline at end of file
diff --git a/management/server/database.py b/management/server/database.py
index c3accf3..d2aa5e3 100644
--- a/management/server/database.py
+++ b/management/server/database.py
@@ -10,260 +10,4 @@ db_config = {
"user": "root",
"password": "infini_rag_flow",
"database": "rag_flow",
-}
-
-def get_users_with_pagination(current_page, page_size, username='', email=''):
- """查询用户信息,支持分页和条件筛选"""
- try:
- # 建立数据库连接
- conn = mysql.connector.connect(**db_config)
- cursor = conn.cursor(dictionary=True)
-
- # 构建WHERE子句和参数
- where_clauses = []
- params = []
-
- if username:
- where_clauses.append("nickname LIKE %s")
- params.append(f"%{username}%")
-
- if email:
- where_clauses.append("email LIKE %s")
- params.append(f"%{email}%")
-
- # 组合WHERE子句
- where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
-
- # 查询总记录数
- count_sql = f"SELECT COUNT(*) as total FROM user WHERE {where_sql}"
- cursor.execute(count_sql, params)
- total = cursor.fetchone()['total']
-
- # 计算分页偏移量
- offset = (current_page - 1) * page_size
-
- # 执行分页查询
- query = f"""
- SELECT id, nickname, email, create_date, update_date, status, is_superuser
- FROM user
- WHERE {where_sql}
- ORDER BY id DESC
- LIMIT %s OFFSET %s
- """
- cursor.execute(query, params + [page_size, offset])
- results = cursor.fetchall()
-
- # 关闭连接
- cursor.close()
- conn.close()
-
- # 格式化结果
- formatted_users = []
- for user in results:
- formatted_users.append({
- "id": user["id"],
- "username": user["nickname"],
- "email": user["email"],
- "createTime": user["create_date"].strftime("%Y-%m-%d %H:%M:%S") if user["create_date"] else "",
- "updateTime": user["update_date"].strftime("%Y-%m-%d %H:%M:%S") if user["update_date"] else "",
- })
-
- return formatted_users, total
-
- except mysql.connector.Error as err:
- print(f"数据库错误: {err}")
- return [], 0
-
-def delete_user(user_id):
- """删除指定ID的用户"""
- try:
- conn = mysql.connector.connect(**db_config)
- cursor = conn.cursor()
-
- # 删除 user 表中的用户记录
- query = "DELETE FROM user WHERE id = %s"
- cursor.execute(query, (user_id,))
-
- # 删除 user_tenant 表中的关联记录
- user_tenant_query = "DELETE FROM user_tenant WHERE user_id = %s"
- cursor.execute(user_tenant_query, (user_id,))
-
- # 删除 tenant 表中的关联记录
- tenant_query = "DELETE FROM tenant WHERE id = %s"
- cursor.execute(tenant_query, (user_id,))
-
- # 删除 tenant_llm 表中的关联记录
- tenant_llm_query = "DELETE FROM tenant_llm WHERE tenant_id = %s"
- cursor.execute(tenant_llm_query, (user_id,))
-
- conn.commit()
- cursor.close()
- conn.close()
-
- return True
- except mysql.connector.Error as err:
- print(f"删除用户错误: {err}")
- return False
-
-def create_user(user_data):
- """创建新用户,并加入最早用户的团队,并使用相同的模型配置"""
- try:
- conn = mysql.connector.connect(**db_config)
- cursor = conn.cursor(dictionary=True)
-
- # 查询最早创建的tenant配置
- query_earliest_tenant = """
- SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit
- FROM tenant
- WHERE create_time = (SELECT MIN(create_time) FROM tenant)
- LIMIT 1
- """
- cursor.execute(query_earliest_tenant)
- earliest_tenant = cursor.fetchone()
-
- # 查询最早创建的tenant配置
- query_earliest_tenant_llm = """
- SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
- FROM tenant_llm
- WHERE create_time = (SELECT MIN(create_time) FROM tenant_llm)
- LIMIT 1
- """
- cursor.execute(query_earliest_tenant_llm)
- earliest_tenant_llm = cursor.fetchone()
-
- # 开始插入
- user_id = generate_uuid()
- # 获取基本信息
- username = user_data.get("username")
- email = user_data.get("email")
- password = user_data.get("password")
- # 加密密码
- encrypted_password = encrypt_password(password)
-
- current_datetime = datetime.now()
- create_time = int(current_datetime.timestamp() * 1000)
- current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
-
- # 插入用户表
- user_insert_query = """
- INSERT INTO user (
- id, create_time, create_date, update_time, update_date, access_token,
- nickname, password, email, avatar, language, color_schema, timezone,
- last_login_time, is_authenticated, is_active, is_anonymous, login_channel,
- status, is_superuser
- ) VALUES (
- %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s,
- %s, %s
- )
- """
- user_data = (
- user_id, create_time, current_date, create_time, current_date, None,
- username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai",
- current_date, 1, 1, 0, "password",
- 1, 0
- )
- cursor.execute(user_insert_query, user_data)
-
- # 插入租户表
- tenant_insert_query = """
- INSERT INTO tenant (
- id, create_time, create_date, update_time, update_date, name,
- public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id,
- parser_ids, credit, status
- ) VALUES (
- %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s,
- %s, %s, %s
- )
- """
- tenant_data = (
- user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom",
- None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']),
- str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']),
- str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']),
- str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1
- )
- cursor.execute(tenant_insert_query, tenant_data)
-
- # 插入用户租户关系表(owner角色)
- user_tenant_insert_owner_query = """
- INSERT INTO user_tenant (
- id, create_time, create_date, update_time, update_date, user_id,
- tenant_id, role, invited_by, status
- ) VALUES (
- %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s
- )
- """
- user_tenant_data_owner = (
- generate_uuid(), create_time, current_date, create_time, current_date, user_id,
- user_id, "owner", user_id, 1
- )
- cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner)
-
- # 插入用户租户关系表(normal角色)
- user_tenant_insert_normal_query = """
- INSERT INTO user_tenant (
- id, create_time, create_date, update_time, update_date, user_id,
- tenant_id, role, invited_by, status
- ) VALUES (
- %s, %s, %s, %s, %s, %s,
- %s, %s, %s, %s
- )
- """
- user_tenant_data_normal = (
- generate_uuid(), create_time, current_date, create_time, current_date, user_id,
- earliest_tenant['id'], "normal", earliest_tenant['id'], 1
- )
- cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal)
-
- # 插入租户LLM配置表
- tenant_llm_insert_query = """
- INSERT INTO tenant_llm (
- create_time, create_date, update_time, update_date, tenant_id,
- llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
- ) VALUES (
- %s, %s, %s, %s, %s,
- %s, %s, %s, %s, %s, %s, %s
- )
- """
- tenant_llm_data = (
- create_time, current_date, create_time, current_date, user_id,
- str(earliest_tenant_llm['llm_factory']), str(earliest_tenant_llm['model_type']), str(earliest_tenant_llm['llm_name']),
- str(earliest_tenant_llm['api_key']), str(earliest_tenant_llm['api_base']), str(earliest_tenant_llm['max_tokens']), 0
- )
- cursor.execute(tenant_llm_insert_query, tenant_llm_data)
-
- conn.commit()
- cursor.close()
- conn.close()
-
- return True
- except mysql.connector.Error as err:
- print(f"创建用户错误: {err}")
- return False
-
-def update_user(user_id, user_data):
- """更新用户信息"""
- try:
- conn = mysql.connector.connect(**db_config)
- cursor = conn.cursor()
-
- query = """
- UPDATE user SET nickname = %s WHERE id = %s
- """
- cursor.execute(query, (
- user_data.get("username"),
- user_id
- ))
- conn.commit()
-
- cursor.close()
- conn.close()
-
- return True
- except mysql.connector.Error as err:
- print(f"更新用户错误: {err}")
- return False
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/management/server/routes/__init__.py b/management/server/routes/__init__.py
new file mode 100644
index 0000000..c86b2b9
--- /dev/null
+++ b/management/server/routes/__init__.py
@@ -0,0 +1,18 @@
+# 路由模块初始化
+from flask import Blueprint
+
+# 创建蓝图
+users_bp = Blueprint('users', __name__, url_prefix='/api/v1/users')
+teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams')
+tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants')
+
+# 导入路由
+from .users.routes import *
+from .teams.routes import *
+from .tenants.routes import *
+
+def register_routes(app):
+ """注册所有路由蓝图到应用"""
+ app.register_blueprint(users_bp)
+ app.register_blueprint(teams_bp)
+ app.register_blueprint(tenants_bp)
\ No newline at end of file
diff --git a/management/server/routes/teams/__init__.py b/management/server/routes/teams/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/routes/teams/routes.py b/management/server/routes/teams/routes.py
new file mode 100644
index 0000000..b9afb2a
--- /dev/null
+++ b/management/server/routes/teams/routes.py
@@ -0,0 +1,178 @@
+from flask import jsonify, request
+from services.teams.service import get_teams_with_pagination, get_team_by_id, delete_team, get_team_members, add_team_member, remove_team_member
+from .. import teams_bp
+
+@teams_bp.route('', methods=['GET'])
+def get_teams():
+ """获取团队列表的API端点,支持分页和条件查询"""
+ try:
+ # 获取查询参数
+ current_page = int(request.args.get('currentPage', 1))
+ page_size = int(request.args.get('size', 10))
+ team_name = request.args.get('name', '')
+
+ # 调用服务函数获取分页和筛选后的团队数据
+ teams, total = get_teams_with_pagination(current_page, page_size, team_name)
+
+ # 返回符合前端期望格式的数据
+ return jsonify({
+ "code": 0,
+ "data": {
+ "list": teams,
+ "total": total
+ },
+ "message": "获取团队列表成功"
+ })
+ except Exception as e:
+ # 错误处理
+ return jsonify({
+ "code": 500,
+ "message": f"获取团队列表失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('/', methods=['GET'])
+def get_team(team_id):
+ """获取单个团队详情的API端点"""
+ try:
+ team = get_team_by_id(team_id)
+ if team:
+ return jsonify({
+ "code": 0,
+ "data": team,
+ "message": "获取团队详情成功"
+ })
+ else:
+ return jsonify({
+ "code": 404,
+ "message": f"团队 {team_id} 不存在"
+ }), 404
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"获取团队详情失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('', methods=['POST'])
+def create_team_route():
+ """创建团队的API端点"""
+ try:
+ data = request.json
+ team_id = create_team(team_data=data)
+ return jsonify({
+ "code": 0,
+ "data": {"id": team_id},
+ "message": "团队创建成功"
+ })
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"创建团队失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('/', methods=['PUT'])
+def update_team_route(team_id):
+ """更新团队的API端点"""
+ try:
+ data = request.json
+ success = update_team(team_id=team_id, team_data=data)
+ if success:
+ return jsonify({
+ "code": 0,
+ "message": f"团队 {team_id} 更新成功"
+ })
+ else:
+ return jsonify({
+ "code": 404,
+ "message": f"团队 {team_id} 不存在或更新失败"
+ }), 404
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"更新团队失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('/', methods=['DELETE'])
+def delete_team_route(team_id):
+ """删除团队的API端点"""
+ try:
+ success = delete_team(team_id)
+ if success:
+ return jsonify({
+ "code": 0,
+ "message": f"团队 {team_id} 删除成功"
+ })
+ else:
+ return jsonify({
+ "code": 404,
+ "message": f"团队 {team_id} 不存在或删除失败"
+ }), 404
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"删除团队失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('//members', methods=['GET'])
+def get_team_members_route(team_id):
+ """获取团队成员的API端点"""
+ try:
+ print(f"正在查询团队 {team_id} 的成员")
+ members = get_team_members(team_id)
+ print(f"查询结果: 找到 {len(members)} 个成员")
+
+ return jsonify({
+ "code": 0,
+ "data": members,
+ "message": "获取团队成员成功"
+ })
+ except Exception as e:
+ print(f"获取团队成员异常: {str(e)}")
+ return jsonify({
+ "code": 500,
+ "message": f"获取团队成员失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('//members', methods=['POST'])
+def add_team_member_route(team_id):
+ """添加团队成员的API端点"""
+ try:
+ data = request.json
+ user_id = data.get('userId')
+ role = data.get('role', 'member')
+ success = add_team_member(team_id, user_id, role)
+ if success:
+ return jsonify({
+ "code": 0,
+ "message": "添加团队成员成功"
+ })
+ else:
+ return jsonify({
+ "code": 400,
+ "message": "添加团队成员失败"
+ }), 400
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"添加团队成员失败: {str(e)}"
+ }), 500
+
+@teams_bp.route('//members/', methods=['DELETE'])
+def remove_team_member_route(team_id, user_id):
+ """移除团队成员的API端点"""
+ try:
+ success = remove_team_member(team_id, user_id)
+ if success:
+ return jsonify({
+ "code": 0,
+ "message": "移除团队成员成功"
+ })
+ else:
+ return jsonify({
+ "code": 400,
+ "message": "移除团队成员失败"
+ }), 400
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"移除团队成员失败: {str(e)}"
+ }), 500
\ No newline at end of file
diff --git a/management/server/routes/tenants/__init__.py b/management/server/routes/tenants/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/routes/tenants/routes.py b/management/server/routes/tenants/routes.py
new file mode 100644
index 0000000..a4747a8
--- /dev/null
+++ b/management/server/routes/tenants/routes.py
@@ -0,0 +1,53 @@
+from flask import jsonify, request
+from services.tenants.service import get_tenants_with_pagination, update_tenant
+from .. import tenants_bp
+
+@tenants_bp.route('', methods=['GET'])
+def get_tenants():
+ """获取租户列表的API端点,支持分页和条件查询"""
+ try:
+ # 获取查询参数
+ current_page = int(request.args.get('currentPage', 1))
+ page_size = int(request.args.get('size', 10))
+ username = request.args.get('username', '')
+
+ # 调用服务函数获取分页和筛选后的租户数据
+ tenants, total = get_tenants_with_pagination(current_page, page_size, username)
+
+ # 返回符合前端期望格式的数据
+ return jsonify({
+ "code": 0,
+ "data": {
+ "list": tenants,
+ "total": total
+ },
+ "message": "获取租户列表成功"
+ })
+ except Exception as e:
+ # 错误处理
+ return jsonify({
+ "code": 500,
+ "message": f"获取租户列表失败: {str(e)}"
+ }), 500
+
+@tenants_bp.route('/', methods=['PUT'])
+def update_tenant_route(tenant_id):
+ """更新租户的API端点"""
+ try:
+ data = request.json
+ success = update_tenant(tenant_id=tenant_id, tenant_data=data)
+ if success:
+ return jsonify({
+ "code": 0,
+ "message": f"租户 {tenant_id} 更新成功"
+ })
+ else:
+ return jsonify({
+ "code": 404,
+ "message": f"租户 {tenant_id} 不存在或更新失败"
+ }), 404
+ except Exception as e:
+ return jsonify({
+ "code": 500,
+ "message": f"更新租户失败: {str(e)}"
+ }), 500
\ No newline at end of file
diff --git a/management/server/routes/users/__init__.py b/management/server/routes/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/routes/users/routes.py b/management/server/routes/users/routes.py
new file mode 100644
index 0000000..68808f8
--- /dev/null
+++ b/management/server/routes/users/routes.py
@@ -0,0 +1,74 @@
+from flask import jsonify, request
+from services.users.service import get_users_with_pagination, delete_user, create_user, update_user
+from .. import users_bp
+
+@users_bp.route('', methods=['GET'])
+def get_users():
+ """获取用户的API端点,支持分页和条件查询"""
+ try:
+ # 获取查询参数
+ current_page = int(request.args.get('currentPage', 1))
+ page_size = int(request.args.get('size', 10))
+ username = request.args.get('username', '')
+ email = request.args.get('email', '')
+
+ # 调用服务函数获取分页和筛选后的用户数据
+ users, total = get_users_with_pagination(current_page, page_size, username, email)
+
+ # 返回符合前端期望格式的数据
+ return jsonify({
+ "code": 0, # 成功状态码
+ "data": {
+ "list": users,
+ "total": total
+ },
+ "message": "获取用户列表成功"
+ })
+ except Exception as e:
+ # 错误处理
+ return jsonify({
+ "code": 500,
+ "message": f"获取用户列表失败: {str(e)}"
+ }), 500
+
+@users_bp.route('/', methods=['DELETE'])
+def delete_user_route(user_id):
+ """删除用户的API端点"""
+ delete_user(user_id)
+ return jsonify({
+ "code": 0,
+ "message": f"用户 {user_id} 删除成功"
+ })
+
+@users_bp.route('', methods=['POST'])
+def create_user_route():
+ """创建用户的API端点"""
+ data = request.json
+ # 创建用户
+ create_user(user_data=data)
+ return jsonify({
+ "code": 0,
+ "message": "用户创建成功"
+ })
+
+@users_bp.route('/', methods=['PUT'])
+def update_user_route(user_id):
+ """更新用户的API端点"""
+ data = request.json
+ user_id = data.get('id')
+ update_user(user_id=user_id, user_data=data)
+ return jsonify({
+ "code": 0,
+ "message": f"用户 {user_id} 更新成功"
+ })
+
+@users_bp.route('/me', methods=['GET'])
+def get_current_user():
+ return jsonify({
+ "code": 0,
+ "data": {
+ "username": "admin",
+ "roles": ["admin"]
+ },
+ "message": "获取用户信息成功"
+ })
\ No newline at end of file
diff --git a/management/server/services/teams/__init__.py b/management/server/services/teams/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/services/teams/service.py b/management/server/services/teams/service.py
new file mode 100644
index 0000000..f4f88bb
--- /dev/null
+++ b/management/server/services/teams/service.py
@@ -0,0 +1,271 @@
+import mysql.connector
+from datetime import datetime
+from utils import generate_uuid
+from database import db_config
+
+def get_teams_with_pagination(current_page, page_size, name=''):
+ """查询团队信息,支持分页和条件筛选"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ # 构建WHERE子句和参数
+ where_clauses = []
+ params = []
+
+ if name:
+ where_clauses.append("t.name LIKE %s")
+ params.append(f"%{name}%")
+
+ # 组合WHERE子句
+ where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
+
+ # 查询总记录数
+ count_sql = f"SELECT COUNT(*) as total FROM tenant t WHERE {where_sql}"
+ cursor.execute(count_sql, params)
+ total = cursor.fetchone()['total']
+
+ # 计算分页偏移量
+ offset = (current_page - 1) * page_size
+
+ # 执行分页查询,包含负责人信息和成员数量
+ query = f"""
+ SELECT
+ t.id,
+ t.name,
+ t.create_date,
+ t.update_date,
+ t.status,
+ (SELECT u.nickname FROM user_tenant ut JOIN user u ON ut.user_id = u.id
+ WHERE ut.tenant_id = t.id AND ut.role = 'owner' LIMIT 1) as owner_name,
+ (SELECT COUNT(*) FROM user_tenant ut WHERE ut.tenant_id = t.id AND ut.status = 1) as member_count
+ FROM
+ tenant t
+ WHERE
+ {where_sql}
+ ORDER BY
+ t.create_date DESC
+ LIMIT %s OFFSET %s
+ """
+ cursor.execute(query, params + [page_size, offset])
+ results = cursor.fetchall()
+
+ # 关闭连接
+ cursor.close()
+ conn.close()
+
+ # 格式化结果
+ formatted_teams = []
+ for team in results:
+ owner_name = team["owner_name"] if team["owner_name"] else "未指定"
+ formatted_teams.append({
+ "id": team["id"],
+ "name": f"{owner_name}的团队",
+ "ownerName": owner_name,
+ "memberCount": team["member_count"],
+ "createTime": team["create_date"].strftime("%Y-%m-%d %H:%M:%S") if team["create_date"] else "",
+ "updateTime": team["update_date"].strftime("%Y-%m-%d %H:%M:%S") if team["update_date"] else "",
+ "status": team["status"]
+ })
+
+ return formatted_teams, total
+
+ except mysql.connector.Error as err:
+ print(f"数据库错误: {err}")
+ return [], 0
+
+
+def get_team_by_id(team_id):
+ """根据ID获取团队详情"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ query = """
+ SELECT id, name, create_date, update_date, status, credit
+ FROM tenant
+ WHERE id = %s
+ """
+ cursor.execute(query, (team_id,))
+ team = cursor.fetchone()
+
+ cursor.close()
+ conn.close()
+
+ if team:
+ return {
+ "id": team["id"],
+ "name": team["name"],
+ "createTime": team["create_date"].strftime("%Y-%m-%d %H:%M:%S") if team["create_date"] else "",
+ "updateTime": team["update_date"].strftime("%Y-%m-%d %H:%M:%S") if team["update_date"] else "",
+ "status": team["status"],
+ "credit": team["credit"]
+ }
+ return None
+
+ except mysql.connector.Error as err:
+ print(f"数据库错误: {err}")
+ return None
+
+def delete_team(team_id):
+ """删除指定ID的团队"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ # 删除团队成员关联
+ member_query = "DELETE FROM user_tenant WHERE tenant_id = %s"
+ cursor.execute(member_query, (team_id,))
+
+ # 删除团队
+ team_query = "DELETE FROM tenant WHERE id = %s"
+ cursor.execute(team_query, (team_id,))
+
+ affected_rows = cursor.rowcount
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ return affected_rows > 0
+
+ except mysql.connector.Error as err:
+ print(f"删除团队错误: {err}")
+ return False
+
+def get_team_members(team_id):
+ """获取团队成员列表"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ query = """
+ SELECT ut.user_id, u.nickname, u.email, ut.role, ut.create_date
+ FROM user_tenant ut
+ JOIN user u ON ut.user_id = u.id
+ WHERE ut.tenant_id = %s AND ut.status = 1
+ ORDER BY ut.create_date DESC
+ """
+ cursor.execute(query, (team_id,))
+ results = cursor.fetchall()
+
+ cursor.close()
+ conn.close()
+
+ # 格式化结果
+ formatted_members = []
+ for member in results:
+ # 将 role 转换为前端需要的格式
+ role = "管理员" if member["role"] == "owner" else "普通成员"
+
+ formatted_members.append({
+ "userId": member["user_id"],
+ "username": member["nickname"],
+ "role": member["role"], # 保持原始角色值 "owner" 或 "normal"
+ "joinTime": member["create_date"].strftime("%Y-%m-%d %H:%M:%S") if member["create_date"] else ""
+ })
+
+ return formatted_members
+
+ except mysql.connector.Error as err:
+ print(f"获取团队成员错误: {err}")
+ return []
+
+def add_team_member(team_id, user_id, role="member"):
+ """添加团队成员"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ # 检查用户是否已经是团队成员
+ check_query = """
+ SELECT id FROM user_tenant
+ WHERE tenant_id = %s AND user_id = %s
+ """
+ cursor.execute(check_query, (team_id, user_id))
+ existing = cursor.fetchone()
+
+ if existing:
+ # 如果已经是成员,更新角色
+ update_query = """
+ UPDATE user_tenant SET role = %s, status = 1
+ WHERE tenant_id = %s AND user_id = %s
+ """
+ cursor.execute(update_query, (role, team_id, user_id))
+ else:
+ # 如果不是成员,添加新记录
+ current_datetime = datetime.now()
+ create_time = int(current_datetime.timestamp() * 1000)
+ current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
+
+ insert_query = """
+ INSERT INTO user_tenant (
+ id, create_time, create_date, update_time, update_date, user_id,
+ tenant_id, role, invited_by, status
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s
+ )
+ """
+ # 假设邀请者是系统管理员
+ invited_by = "system"
+
+ user_tenant_data = (
+ generate_uuid(), create_time, current_date, create_time, current_date, user_id,
+ team_id, role, invited_by, 1
+ )
+ cursor.execute(insert_query, user_tenant_data)
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ return True
+
+ except mysql.connector.Error as err:
+ print(f"添加团队成员错误: {err}")
+ return False
+
+def remove_team_member(team_id, user_id):
+ """移除团队成员"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ # 检查是否是团队的唯一所有者
+ check_owner_query = """
+ SELECT COUNT(*) as owner_count FROM user_tenant
+ WHERE tenant_id = %s AND role = 'owner'
+ """
+ cursor.execute(check_owner_query, (team_id,))
+ owner_count = cursor.fetchone()[0]
+
+ # 检查当前用户是否是所有者
+ check_user_role_query = """
+ SELECT role FROM user_tenant
+ WHERE tenant_id = %s AND user_id = %s
+ """
+ cursor.execute(check_user_role_query, (team_id, user_id))
+ user_role = cursor.fetchone()
+
+ # 如果是唯一所有者,不允许移除
+ if owner_count == 1 and user_role and user_role[0] == 'owner':
+ return False
+
+ # 移除成员
+ delete_query = """
+ DELETE FROM user_tenant
+ WHERE tenant_id = %s AND user_id = %s
+ """
+ cursor.execute(delete_query, (team_id, user_id))
+ affected_rows = cursor.rowcount
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ return affected_rows > 0
+
+ except mysql.connector.Error as err:
+ print(f"移除团队成员错误: {err}")
+ return False
\ No newline at end of file
diff --git a/management/server/services/tenants/__init__.py b/management/server/services/tenants/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/services/tenants/service.py b/management/server/services/tenants/service.py
new file mode 100644
index 0000000..999bef8
--- /dev/null
+++ b/management/server/services/tenants/service.py
@@ -0,0 +1,121 @@
+import mysql.connector
+from datetime import datetime
+from database import db_config
+
+def get_tenants_with_pagination(current_page, page_size, username=''):
+ """查询租户信息,支持分页和条件筛选"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ # 构建WHERE子句和参数
+ where_clauses = []
+ params = []
+
+ if username:
+ where_clauses.append("""
+ EXISTS (
+ SELECT 1 FROM user_tenant ut
+ JOIN user u ON ut.user_id = u.id
+ WHERE ut.tenant_id = t.id AND u.nickname LIKE %s
+ )
+ """)
+ params.append(f"%{username}%")
+
+ # 组合WHERE子句
+ where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
+
+ # 查询总记录数
+ count_sql = f"""
+ SELECT COUNT(*) as total
+ FROM tenant t
+ WHERE {where_sql}
+ """
+ cursor.execute(count_sql, params)
+ total = cursor.fetchone()['total']
+
+ # 计算分页偏移量
+ offset = (current_page - 1) * page_size
+
+ # 执行分页查询
+ query = f"""
+ SELECT
+ t.id,
+ (SELECT u.nickname FROM user_tenant ut JOIN user u ON ut.user_id = u.id
+ WHERE ut.tenant_id = t.id AND ut.role = 'owner' LIMIT 1) as username,
+ t.llm_id as chat_model,
+ t.embd_id as embedding_model,
+ t.create_date,
+ t.update_date
+ FROM
+ tenant t
+ WHERE
+ {where_sql}
+ ORDER BY
+ t.create_date DESC
+ LIMIT %s OFFSET %s
+ """
+ cursor.execute(query, params + [page_size, offset])
+ results = cursor.fetchall()
+
+ # 关闭连接
+ cursor.close()
+ conn.close()
+
+ # 格式化结果
+ formatted_tenants = []
+ for tenant in results:
+ formatted_tenants.append({
+ "id": tenant["id"],
+ "username": tenant["username"] if tenant["username"] else "未指定",
+ "chatModel": tenant["chat_model"] if tenant["chat_model"] else "",
+ "embeddingModel": tenant["embedding_model"] if tenant["embedding_model"] else "",
+ "createTime": tenant["create_date"].strftime("%Y-%m-%d %H:%M:%S") if tenant["create_date"] else "",
+ "updateTime": tenant["update_date"].strftime("%Y-%m-%d %H:%M:%S") if tenant["update_date"] else ""
+ })
+
+ return formatted_tenants, total
+
+ except mysql.connector.Error as err:
+ print(f"数据库错误: {err}")
+ return [], 0
+
+def update_tenant(tenant_id, tenant_data):
+ """更新租户信息"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ # 更新租户表
+ current_datetime = datetime.now()
+ update_time = int(current_datetime.timestamp() * 1000)
+ current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
+
+ query = """
+ UPDATE tenant
+ SET update_time = %s,
+ update_date = %s,
+ llm_id = %s,
+ embd_id = %s
+ WHERE id = %s
+ """
+
+ cursor.execute(query, (
+ update_time,
+ current_date,
+ tenant_data.get("chatModel", ""),
+ tenant_data.get("embeddingModel", ""),
+ tenant_id
+ ))
+
+ affected_rows = cursor.rowcount
+ conn.commit()
+
+ cursor.close()
+ conn.close()
+
+ return affected_rows > 0
+
+ except mysql.connector.Error as err:
+ print(f"更新租户错误: {err}")
+ return False
\ No newline at end of file
diff --git a/management/server/services/users/__init__.py b/management/server/services/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/management/server/services/users/service.py b/management/server/services/users/service.py
new file mode 100644
index 0000000..4ec9480
--- /dev/null
+++ b/management/server/services/users/service.py
@@ -0,0 +1,260 @@
+import mysql.connector
+from datetime import datetime
+from utils import generate_uuid, encrypt_password
+from database import db_config
+
+def get_users_with_pagination(current_page, page_size, username='', email=''):
+ """查询用户信息,支持分页和条件筛选"""
+ try:
+ # 建立数据库连接
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ # 构建WHERE子句和参数
+ where_clauses = []
+ params = []
+
+ if username:
+ where_clauses.append("nickname LIKE %s")
+ params.append(f"%{username}%")
+
+ if email:
+ where_clauses.append("email LIKE %s")
+ params.append(f"%{email}%")
+
+ # 组合WHERE子句
+ where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
+
+ # 查询总记录数
+ count_sql = f"SELECT COUNT(*) as total FROM user WHERE {where_sql}"
+ cursor.execute(count_sql, params)
+ total = cursor.fetchone()['total']
+
+ # 计算分页偏移量
+ offset = (current_page - 1) * page_size
+
+ # 执行分页查询
+ query = f"""
+ SELECT id, nickname, email, create_date, update_date, status, is_superuser
+ FROM user
+ WHERE {where_sql}
+ ORDER BY id DESC
+ LIMIT %s OFFSET %s
+ """
+ cursor.execute(query, params + [page_size, offset])
+ results = cursor.fetchall()
+
+ # 关闭连接
+ cursor.close()
+ conn.close()
+
+ # 格式化结果
+ formatted_users = []
+ for user in results:
+ formatted_users.append({
+ "id": user["id"],
+ "username": user["nickname"],
+ "email": user["email"],
+ "createTime": user["create_date"].strftime("%Y-%m-%d %H:%M:%S") if user["create_date"] else "",
+ "updateTime": user["update_date"].strftime("%Y-%m-%d %H:%M:%S") if user["update_date"] else "",
+ })
+
+ return formatted_users, total
+
+ except mysql.connector.Error as err:
+ print(f"数据库错误: {err}")
+ return [], 0
+
+def delete_user(user_id):
+ """删除指定ID的用户"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ # 删除 user 表中的用户记录
+ query = "DELETE FROM user WHERE id = %s"
+ cursor.execute(query, (user_id,))
+
+ # 删除 user_tenant 表中的关联记录
+ user_tenant_query = "DELETE FROM user_tenant WHERE user_id = %s"
+ cursor.execute(user_tenant_query, (user_id,))
+
+ # 删除 tenant 表中的关联记录
+ tenant_query = "DELETE FROM tenant WHERE id = %s"
+ cursor.execute(tenant_query, (user_id,))
+
+ # 删除 tenant_llm 表中的关联记录
+ tenant_llm_query = "DELETE FROM tenant_llm WHERE tenant_id = %s"
+ cursor.execute(tenant_llm_query, (user_id,))
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ return True
+ except mysql.connector.Error as err:
+ print(f"删除用户错误: {err}")
+ return False
+
+def create_user(user_data):
+ """创建新用户,并加入最早用户的团队,并使用相同的模型配置"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor(dictionary=True)
+
+ # 查询最早创建的tenant配置
+ query_earliest_tenant = """
+ SELECT id, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id, parser_ids, credit
+ FROM tenant
+ WHERE create_time = (SELECT MIN(create_time) FROM tenant)
+ LIMIT 1
+ """
+ cursor.execute(query_earliest_tenant)
+ earliest_tenant = cursor.fetchone()
+
+ # 查询最早创建的tenant配置
+ query_earliest_tenant_llm = """
+ SELECT llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
+ FROM tenant_llm
+ WHERE create_time = (SELECT MIN(create_time) FROM tenant_llm)
+ LIMIT 1
+ """
+ cursor.execute(query_earliest_tenant_llm)
+ earliest_tenant_llm = cursor.fetchone()
+
+ # 开始插入
+ user_id = generate_uuid()
+ # 获取基本信息
+ username = user_data.get("username")
+ email = user_data.get("email")
+ password = user_data.get("password")
+ # 加密密码
+ encrypted_password = encrypt_password(password)
+
+ current_datetime = datetime.now()
+ create_time = int(current_datetime.timestamp() * 1000)
+ current_date = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
+
+ # 插入用户表
+ user_insert_query = """
+ INSERT INTO user (
+ id, create_time, create_date, update_time, update_date, access_token,
+ nickname, password, email, avatar, language, color_schema, timezone,
+ last_login_time, is_authenticated, is_active, is_anonymous, login_channel,
+ status, is_superuser
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s, %s,
+ %s, %s
+ )
+ """
+ user_data_tuple = (
+ user_id, create_time, current_date, create_time, current_date, None,
+ username, encrypted_password, email, None, "Chinese", "Bright", "UTC+8 Asia/Shanghai",
+ current_date, 1, 1, 0, "password",
+ 1, 0
+ )
+ cursor.execute(user_insert_query, user_data_tuple)
+
+ # 插入租户表
+ tenant_insert_query = """
+ INSERT INTO tenant (
+ id, create_time, create_date, update_time, update_date, name,
+ public_key, llm_id, embd_id, asr_id, img2txt_id, rerank_id, tts_id,
+ parser_ids, credit, status
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s, %s, %s, %s,
+ %s, %s, %s
+ )
+ """
+ tenant_data = (
+ user_id, create_time, current_date, create_time, current_date, username + "'s Kingdom",
+ None, str(earliest_tenant['llm_id']), str(earliest_tenant['embd_id']),
+ str(earliest_tenant['asr_id']), str(earliest_tenant['img2txt_id']),
+ str(earliest_tenant['rerank_id']), str(earliest_tenant['tts_id']),
+ str(earliest_tenant['parser_ids']), str(earliest_tenant['credit']), 1
+ )
+ cursor.execute(tenant_insert_query, tenant_data)
+
+ # 插入用户租户关系表(owner角色)
+ user_tenant_insert_owner_query = """
+ INSERT INTO user_tenant (
+ id, create_time, create_date, update_time, update_date, user_id,
+ tenant_id, role, invited_by, status
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s
+ )
+ """
+ user_tenant_data_owner = (
+ generate_uuid(), create_time, current_date, create_time, current_date, user_id,
+ user_id, "owner", user_id, 1
+ )
+ cursor.execute(user_tenant_insert_owner_query, user_tenant_data_owner)
+
+ # 插入用户租户关系表(normal角色)
+ user_tenant_insert_normal_query = """
+ INSERT INTO user_tenant (
+ id, create_time, create_date, update_time, update_date, user_id,
+ tenant_id, role, invited_by, status
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s
+ )
+ """
+ user_tenant_data_normal = (
+ generate_uuid(), create_time, current_date, create_time, current_date, user_id,
+ earliest_tenant['id'], "normal", earliest_tenant['id'], 1
+ )
+ cursor.execute(user_tenant_insert_normal_query, user_tenant_data_normal)
+
+ # 插入租户LLM配置表
+ tenant_llm_insert_query = """
+ INSERT INTO tenant_llm (
+ create_time, create_date, update_time, update_date, tenant_id,
+ llm_factory, model_type, llm_name, api_key, api_base, max_tokens, used_tokens
+ ) VALUES (
+ %s, %s, %s, %s, %s,
+ %s, %s, %s, %s, %s, %s, %s
+ )
+ """
+ tenant_llm_data = (
+ create_time, current_date, create_time, current_date, user_id,
+ str(earliest_tenant_llm['llm_factory']), str(earliest_tenant_llm['model_type']), str(earliest_tenant_llm['llm_name']),
+ str(earliest_tenant_llm['api_key']), str(earliest_tenant_llm['api_base']), str(earliest_tenant_llm['max_tokens']), 0
+ )
+ cursor.execute(tenant_llm_insert_query, tenant_llm_data)
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ return True
+ except mysql.connector.Error as err:
+ print(f"创建用户错误: {err}")
+ return False
+
+def update_user(user_id, user_data):
+ """更新用户信息"""
+ try:
+ conn = mysql.connector.connect(**db_config)
+ cursor = conn.cursor()
+
+ query = """
+ UPDATE user SET nickname = %s WHERE id = %s
+ """
+ cursor.execute(query, (
+ user_data.get("username"),
+ user_id
+ ))
+ conn.commit()
+
+ cursor.close()
+ conn.close()
+
+ return True
+ except mysql.connector.Error as err:
+ print(f"更新用户错误: {err}")
+ return False
\ No newline at end of file
diff --git a/management/web/public/favicon.ico b/management/web/public/favicon.ico
index c641cd002918bdb009192b6b74f55e42bb97adc9..69a2f107e71c2be1b0947d1b3f3a19ed737d4a17 100644
GIT binary patch
literal 70814
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
R=UV|lPD)v_M%*;ye*o|wcIE&8
diff --git a/management/web/src/common/apis/configs/index.ts b/management/web/src/common/apis/configs/index.ts
new file mode 100644
index 0000000..9d53fbb
--- /dev/null
+++ b/management/web/src/common/apis/configs/index.ts
@@ -0,0 +1,20 @@
+import type * as Tables from "./type"
+import { request } from "@/http/axios"
+
+/** 改 */
+export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
+ return request({
+ url: `api/v1/tenants/${data.id}`,
+ method: "put",
+ data
+ })
+}
+
+/** 查 */
+export function getTableDataApi(params: Tables.TableRequestData) {
+ return request({
+ url: "api/v1/tenants",
+ method: "get",
+ params
+ })
+}
diff --git a/management/web/src/common/apis/configs/type.ts b/management/web/src/common/apis/configs/type.ts
new file mode 100644
index 0000000..f70917e
--- /dev/null
+++ b/management/web/src/common/apis/configs/type.ts
@@ -0,0 +1,29 @@
+export interface CreateOrUpdateTableRequestData {
+ id?: number
+ username: string
+ chatModel?: string
+ embeddingModel?: string
+}
+
+export interface TableRequestData {
+ /** 当前页码 */
+ currentPage: number
+ /** 查询条数 */
+ size: number
+ /** 查询参数:用户名 */
+ username?: string
+}
+
+export interface TableData {
+ id: number
+ username: string
+ chatModel: string
+ embeddingModel: string
+ createTime: string
+ updateTime: string
+}
+
+export type TableResponseData = ApiResponseData<{
+ list: TableData[]
+ total: number
+}>
diff --git a/management/web/src/common/apis/teams/index.ts b/management/web/src/common/apis/teams/index.ts
new file mode 100644
index 0000000..d7e0ffd
--- /dev/null
+++ b/management/web/src/common/apis/teams/index.ts
@@ -0,0 +1,44 @@
+import type * as Tables from "./type"
+import { request } from "@/http/axios"
+
+// 查询团队整体数据
+export function getTableDataApi(params: Tables.TableRequestData) {
+ return request({
+ url: "api/v1/teams",
+ method: "get",
+ params
+ })
+}
+
+// 获取团队成员列表
+export function getTeamMembersApi(teamId: number) {
+ return request({
+ url: `api/v1/teams/${teamId}/members`,
+ method: "get"
+ })
+}
+
+// 添加团队成员
+export function addTeamMemberApi(data: { teamId: number, userId: number, role: string }) {
+ return request({
+ url: `api/v1/teams/${data.teamId}/members`,
+ method: "post",
+ data
+ })
+}
+
+// 移除团队成员
+export function removeTeamMemberApi(data: { teamId: number, memberId: number }) {
+ return request({
+ url: `api/v1/teams/${data.teamId}/members/${data.memberId}`,
+ method: "delete"
+ })
+}
+
+// 获取用户列表
+export function getUsersApi() {
+ return request({
+ url: "api/v1/users",
+ method: "get"
+ })
+}
diff --git a/management/web/src/common/apis/teams/type.ts b/management/web/src/common/apis/teams/type.ts
new file mode 100644
index 0000000..52d3ba6
--- /dev/null
+++ b/management/web/src/common/apis/teams/type.ts
@@ -0,0 +1,30 @@
+export interface CreateOrUpdateTableRequestData {
+ id?: number
+ username: string
+ email?: string
+ password?: string
+}
+
+export interface TableRequestData {
+ /** 当前页码 */
+ currentPage: number
+ /** 查询条数 */
+ size: number
+ /** 查询参数:用户名 */
+ username?: string
+ /** 查询参数:邮箱 */
+ email?: string
+}
+
+export interface TableData {
+ id: number
+ username: string
+ email: string
+ createTime: string
+ updateTime: string
+}
+
+export type TableResponseData = ApiResponseData<{
+ list: TableData[]
+ total: number
+}>
diff --git a/management/web/src/common/assets/icons/team-management.svg b/management/web/src/common/assets/icons/team-management.svg
new file mode 100644
index 0000000..ccaa694
--- /dev/null
+++ b/management/web/src/common/assets/icons/team-management.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/management/web/src/common/assets/icons/user-config.svg b/management/web/src/common/assets/icons/user-config.svg
new file mode 100644
index 0000000..2d417dc
--- /dev/null
+++ b/management/web/src/common/assets/icons/user-config.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/management/web/src/common/assets/icons/user-management.svg b/management/web/src/common/assets/icons/user-management.svg
new file mode 100644
index 0000000..0b8fd3f
--- /dev/null
+++ b/management/web/src/common/assets/icons/user-management.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/management/web/src/common/assets/images/docs/preview.png b/management/web/src/common/assets/images/docs/preview.png
deleted file mode 100644
index 860ea0023b5f25faad019e01bfe8d45d435ab6ac..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 503620
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