Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8197cc5aa3 | |||
| 605e04871e | |||
| 3ebac6ed03 | |||
| cfe56264ea | |||
| 66ffa9679d | |||
| 9c38f9ed9a |
@@ -25,7 +25,9 @@
|
||||
"Bash(git -C /d/3.Project/HTY1024/ai-app-database commit -m 'fix\\(ci\\): 将 Secret 名从 GITEA_TOKEN 改为 REGISTRY_TOKEN:*)",
|
||||
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -3)",
|
||||
"Bash(git -C /d/3.Project/HTY1024/ai-app-database add scripts/ .gitattributes)",
|
||||
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -4)"
|
||||
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -4)",
|
||||
"Bash(xargs ls:*)",
|
||||
"Bash(find /d/3.Project/HTY1024/ai-app-database/app -name \"__init__.py\" -exec head -30 {} \\\\;)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+29
-3
@@ -24,6 +24,11 @@ on:
|
||||
- '.gitignore'
|
||||
- '.env*.example'
|
||||
|
||||
# 同分支只保留最新一次构建,旧的自动取消,避免 runner 上多份 buildx 同时跑导致 OOM
|
||||
concurrency:
|
||||
group: ci-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: git.hty1024.com
|
||||
|
||||
@@ -31,6 +36,8 @@ jobs:
|
||||
build-and-push:
|
||||
name: Build & Push Image
|
||||
runs-on: ubuntu-latest
|
||||
# 防止 buildx 卡死导致 docker daemon 被一直占用
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# ── 1. 检出代码 ──────────────────────────────────────────
|
||||
@@ -50,16 +57,25 @@ jobs:
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
|
||||
# ── 4. 设置 Docker Buildx ────────────────────────────────
|
||||
# 限制 buildkitd 并行度,避免在小内存 runner 上同时编译过多步骤导致 OOM
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=host
|
||||
buildkitd-flags: --debug --oci-worker-gc-keepstorage 5000
|
||||
|
||||
# ── 5. 登录 Gitea 镜像仓库 ──────────────────────────────
|
||||
# logout: false 禁用 Post 步骤的 docker logout —— 避免 act_runner 在
|
||||
# Post 阶段加载 action 的 dist/index.js 时 "Cannot find module" 报错。
|
||||
# job 容器跑完即销毁,凭据不会泄漏,无需主动 logout。
|
||||
- name: 登录 Gitea 镜像仓库
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
logout: false
|
||||
|
||||
# ── 6. 提取镜像元数据(自动生成 tags 和 labels)─────────
|
||||
- name: 提取镜像元数据
|
||||
@@ -80,6 +96,8 @@ jobs:
|
||||
org.opencontainers.image.vendor=HTY1024
|
||||
|
||||
# ── 7. 构建并推送镜像 ────────────────────────────────────
|
||||
# cache-to 用 mode=min(仅导出最终层引用),避免每次构建把所有中间层
|
||||
# 都推到 registry 造成大量磁盘 I/O 和带宽占用 —— 这是 runner 卡死的主因之一
|
||||
- name: 构建并推送镜像
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
@@ -89,11 +107,19 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# 利用镜像层缓存加速构建(buildcache tag 仅用于缓存)
|
||||
provenance: false
|
||||
cache-from: type=registry,ref=${{ steps.image.outputs.name }}:buildcache
|
||||
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max
|
||||
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=min
|
||||
|
||||
# ── 8. 输出构建摘要 ──────────────────────────────────────
|
||||
# ── 8. 清理 buildx 缓存(防止 runner 磁盘被撑满)─────────
|
||||
# always() 保证即使前面失败也清理,避免反复失败把磁盘吃光
|
||||
- name: 清理 buildx 构建缓存
|
||||
if: always()
|
||||
run: |
|
||||
docker buildx prune -f --keep-storage 2GB || true
|
||||
docker image prune -f || true
|
||||
|
||||
# ── 9. 输出构建摘要 ──────────────────────────────────────
|
||||
- name: 输出构建信息
|
||||
run: |
|
||||
echo "### 🐳 镜像构建成功" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -41,6 +41,12 @@ on:
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
# 部署互斥:避免新旧两次 deploy 同时操作同一目录与同一组容器,
|
||||
# 也避免 CI 刚结束就有第二次 push 触发的 deploy 与本次抢资源
|
||||
concurrency:
|
||||
group: deploy-demo
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: git.hty1024.com
|
||||
# Demo 部署目录(需与服务器实际路径一致)
|
||||
@@ -58,6 +64,7 @@ jobs:
|
||||
deploy-demo:
|
||||
name: Deploy to Demo
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 20
|
||||
|
||||
# workflow_run 触发时,仅在 CI 成功时继续
|
||||
if: >
|
||||
@@ -88,12 +95,16 @@ jobs:
|
||||
echo "镜像: ${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# ── 3. 登录 Gitea 镜像仓库 ──────────────────────────────────
|
||||
# logout: false —— 避免 act_runner Post 阶段 "Cannot find module" 报错。
|
||||
# 注意:deploy 跑在 self-hosted runner(宿主机),凭据会落到 ~/.docker/config.json。
|
||||
# 如担心残留,可在最后一步用 `docker logout` 显式清理(见 13 步)。
|
||||
- name: 登录 Gitea 镜像仓库
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
logout: false
|
||||
|
||||
# ── 4. 检测 Docker Compose 命令 ─────────────────────────────
|
||||
- name: 检测 Compose 命令
|
||||
@@ -208,6 +219,23 @@ jobs:
|
||||
echo "Caddy 配置未变更,跳过重载"
|
||||
fi
|
||||
|
||||
# ── 12.5 清理旧镜像(避免长期运行后磁盘被旧版本镜像撑满)─────
|
||||
- name: 清理悬挂镜像
|
||||
if: always()
|
||||
run: |
|
||||
# 清理 dangling 镜像(被新版本顶替的旧 latest)
|
||||
docker image prune -f || true
|
||||
# 清理 24 小时以上没用的镜像(保留最近一份)
|
||||
docker image prune -af --filter "until=24h" || true
|
||||
|
||||
# ── 12.6 手动登出(替代被禁用的 login-action Post 步骤)──────
|
||||
# 因为 self-hosted runner 是宿主机,凭据会留在 ~/.docker/config.json,
|
||||
# 这里显式清理,避免凭据残留。
|
||||
- name: 登出镜像仓库
|
||||
if: always()
|
||||
run: |
|
||||
docker logout ${{ env.REGISTRY }} || true
|
||||
|
||||
# ── 13. 输出部署摘要 ─────────────────────────────────────────
|
||||
- name: 输出部署摘要
|
||||
run: |
|
||||
|
||||
@@ -17,6 +17,11 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
# release 工作流互斥:同时只允许一次 release 跑,避免与 ci.yml 争抢 buildx
|
||||
concurrency:
|
||||
group: release-build
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: git.hty1024.com
|
||||
|
||||
@@ -24,6 +29,7 @@ jobs:
|
||||
release:
|
||||
name: Release Image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
# ── 1. 检出代码(完整历史,用于生成 changelog)──────────
|
||||
@@ -56,14 +62,20 @@ jobs:
|
||||
# ── 4. 设置 Docker Buildx ────────────────────────────────
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=host
|
||||
buildkitd-flags: --debug --oci-worker-gc-keepstorage 5000
|
||||
|
||||
# ── 5. 登录 Gitea 镜像仓库 ──────────────────────────────
|
||||
# logout: false —— 避免 act_runner Post 阶段 "Cannot find module" 报错
|
||||
- name: 登录 Gitea 镜像仓库
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
logout: false
|
||||
|
||||
# ── 6. 提取镜像元数据 ────────────────────────────────────
|
||||
# metadata-action 对 semver tag 会自动生成多级标签:
|
||||
@@ -100,13 +112,22 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
provenance: false
|
||||
# 复用 CI 构建的缓存层,加速 release 构建
|
||||
# mode=min:只导出最终层引用,减小磁盘 I/O 与 registry 带宽
|
||||
cache-from: type=registry,ref=${{ steps.image.outputs.name }}:buildcache
|
||||
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=max
|
||||
cache-to: type=registry,ref=${{ steps.image.outputs.name }}:buildcache,mode=min
|
||||
# 构建参数:写入版本号到镜像内
|
||||
build-args: |
|
||||
APP_VERSION=${{ steps.image.outputs.version }}
|
||||
|
||||
# ── 7.5 清理 buildx 缓存 ─────────────────────────────────
|
||||
- name: 清理 buildx 构建缓存
|
||||
if: always()
|
||||
run: |
|
||||
docker buildx prune -f --keep-storage 2GB || true
|
||||
docker image prune -f || true
|
||||
|
||||
# ── 8. 生成两次 tag 之间的变更日志 ───────────────────────
|
||||
- name: 生成变更日志
|
||||
id: changelog
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 个人资料库 (Personal Resource Library)
|
||||
# 个人资料库 (Personal Resource Library)
|
||||
|
||||
基于 Flask + MySQL 的个人多媒体资料管理系统。支持文本、图片、音频、视频的上传、URL 下载、磁力下载及在线预览。
|
||||
|
||||
|
||||
+55
-1
@@ -404,7 +404,7 @@ def progress(resource_id):
|
||||
@resources_bp.route('/<int:resource_id>/preview')
|
||||
@login_required
|
||||
def preview(resource_id):
|
||||
"""直接返回文件内容(用于 iframe 文本预览)"""
|
||||
"""直接返回文件内容(用于文本/Word 文档预览)"""
|
||||
res = db.get_or_404(Resource, resource_id)
|
||||
if res.user_id != current_user.id and not current_user.is_admin:
|
||||
abort(403)
|
||||
@@ -413,6 +413,60 @@ def preview(resource_id):
|
||||
abs_path = os.path.join(current_app.root_path, 'static', res.file_path)
|
||||
if not os.path.exists(abs_path):
|
||||
abort(404)
|
||||
|
||||
ext = (res.original_name or res.filename or '').rsplit('.', 1)[-1].lower()
|
||||
mime = (res.mime_type or '').lower()
|
||||
|
||||
DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
DOC_MIME = 'application/msword'
|
||||
|
||||
# 部分来源(如 URL 下载)文件名没有扩展名,需用 MIME 与文件头兜底判断
|
||||
is_docx = ext == 'docx' or mime == DOCX_MIME
|
||||
is_doc = ext == 'doc' or mime == DOC_MIME
|
||||
if not (is_docx or is_doc):
|
||||
try:
|
||||
with open(abs_path, 'rb') as f:
|
||||
head = f.read(8)
|
||||
# docx/xlsx/pptx 实际上是 zip:PK\x03\x04
|
||||
if head.startswith(b'PK\x03\x04') and (
|
||||
'wordprocessingml' in mime or 'officedocument' in mime
|
||||
):
|
||||
is_docx = True
|
||||
# 旧版 .doc 二进制头:D0 CF 11 E0 A1 B1 1A E1
|
||||
elif head.startswith(b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1') and \
|
||||
('msword' in mime or mime == ''):
|
||||
is_doc = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Word 文档(.docx)使用 mammoth 转换为 HTML
|
||||
if is_docx:
|
||||
try:
|
||||
import mammoth
|
||||
with open(abs_path, 'rb') as f:
|
||||
result = mammoth.convert_to_html(f)
|
||||
return Response(result.value, mimetype='text/html; charset=utf-8')
|
||||
except ImportError:
|
||||
return Response(
|
||||
'<div style="padding:1rem;color:#dc3545">'
|
||||
'服务器未安装 mammoth 库,无法预览 .docx 文件。请运行 '
|
||||
'<code>pip install mammoth</code> 后重试。</div>',
|
||||
mimetype='text/html; charset=utf-8'
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
f'<div style="padding:1rem;color:#dc3545">无法解析 Word 文档:{e}</div>',
|
||||
mimetype='text/html; charset=utf-8'
|
||||
)
|
||||
|
||||
# 旧版 .doc 二进制格式不支持在线预览
|
||||
if is_doc:
|
||||
return Response(
|
||||
'<div style="padding:1rem;color:#6c757d">'
|
||||
'不支持在线预览旧版 .doc 文件,请下载后查看,或将文件转换为 .docx 格式。</div>',
|
||||
mimetype='text/html; charset=utf-8'
|
||||
)
|
||||
|
||||
try:
|
||||
with open(abs_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
@@ -56,6 +56,20 @@
|
||||
#sidebar .nav-link:hover { background: rgba(255,255,255,.08); }
|
||||
#sidebar .nav-link.active { background: rgba(255,255,255,.15); color: #fff; }
|
||||
|
||||
/* 新建一级文件夹按钮:默认稍微淡化,hover 高亮 */
|
||||
#sidebar .folder-add-root {
|
||||
opacity: .75;
|
||||
border-radius: 4px;
|
||||
transition: opacity var(--transition), background var(--transition);
|
||||
}
|
||||
#sidebar .folder-add-root:hover,
|
||||
#sidebar .folder-add-root:focus {
|
||||
opacity: 1;
|
||||
background: rgba(255,255,255,.12);
|
||||
}
|
||||
/* 折叠态隐藏(避免 60px 宽时挤变形) */
|
||||
#sidebar.collapsed .folder-add-root { display: none; }
|
||||
|
||||
/* 主内容区 */
|
||||
#main-content {
|
||||
min-width: 0;
|
||||
|
||||
@@ -91,17 +91,18 @@
|
||||
|
||||
<!-- 文件夹区块 -->
|
||||
<li class="mt-2">
|
||||
<div class="d-flex align-items-center justify-content-between px-1 py-1">
|
||||
<button class="nav-link text-white text-start d-flex align-items-center gap-1 p-0 border-0 bg-transparent"
|
||||
<div class="d-flex align-items-center justify-content-between px-1 py-1 gap-2">
|
||||
<button class="btn btn-sm text-white text-start d-flex align-items-center gap-1 p-0 border-0 bg-transparent flex-grow-1"
|
||||
type="button" data-bs-toggle="collapse" data-bs-target="#folderTree"
|
||||
aria-expanded="true" style="font-size:.9rem">
|
||||
<i class="bi bi-folder2 me-1"></i>文件夹
|
||||
<i class="bi bi-chevron-down small ms-1" id="folderChevron"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm p-0 text-white opacity-75 border-0 bg-transparent"
|
||||
title="新建文件夹"
|
||||
data-bs-toggle="modal" data-bs-target="#createFolderModal"
|
||||
onclick="openCreateFolder(null)">
|
||||
<button class="btn btn-sm py-0 px-1 text-white border-0 bg-transparent flex-shrink-0 folder-add-root"
|
||||
type="button"
|
||||
title="新建一级文件夹"
|
||||
style="font-size:1rem;line-height:1"
|
||||
onclick="event.stopPropagation();openCreateFolder(null);">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -155,6 +155,24 @@
|
||||
</div>
|
||||
|
||||
{% elif resource.resource_type == 'text' %}
|
||||
{%- set _name = (resource.original_name or resource.filename or '') -%}
|
||||
{%- set _ext = _name.rsplit('.', 1)[-1].lower() if '.' in _name else '' -%}
|
||||
{%- set _mime = (resource.mime_type or '')|lower -%}
|
||||
{%- set _is_word = _ext in ('doc', 'docx')
|
||||
or 'wordprocessingml' in _mime
|
||||
or _mime == 'application/msword' -%}
|
||||
{% if _is_word %}
|
||||
<div class="text-preview-toolbar p-2 border-bottom d-flex gap-2 align-items-center">
|
||||
<i class="bi bi-file-earmark-word text-primary"></i>
|
||||
<span class="small text-muted">Word 文档预览</span>
|
||||
</div>
|
||||
<div class="word-preview p-4 bg-white" id="textContent"
|
||||
style="max-height:75vh;overflow:auto;line-height:1.7">
|
||||
<div class="text-center py-4 text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>加载中…
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-preview-toolbar p-2 border-bottom d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="changeFont(1)">A+</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="changeFont(-1)">A-</button>
|
||||
@@ -165,6 +183,7 @@
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>加载中…
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="d-flex flex-column align-items-center justify-content-center p-5 text-muted">
|
||||
@@ -195,6 +214,34 @@
|
||||
<script>
|
||||
// ── 文本内容加载 ────────────────────────────────────────────────────────────
|
||||
{% if resource.resource_type == 'text' and resource.file_path and resource.download_status in ('na','done') %}
|
||||
{%- set _name = (resource.original_name or resource.filename or '') -%}
|
||||
{%- set _ext = _name.rsplit('.', 1)[-1].lower() if '.' in _name else '' -%}
|
||||
{%- set _mime = (resource.mime_type or '')|lower -%}
|
||||
{%- set _is_word = _ext in ('doc', 'docx')
|
||||
or 'wordprocessingml' in _mime
|
||||
or _mime == 'application/msword' -%}
|
||||
{% if _is_word %}
|
||||
// Word 文档:服务端已转换为 HTML,直接渲染
|
||||
fetch("{{ url_for('resources.preview', resource_id=resource.id) }}")
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const container = document.getElementById('textContent');
|
||||
container.innerHTML = html;
|
||||
// 让文档内的图片自适应宽度
|
||||
container.querySelectorAll('img').forEach(img => {
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.height = 'auto';
|
||||
});
|
||||
// 表格基础样式
|
||||
container.querySelectorAll('table').forEach(t => {
|
||||
t.classList.add('table', 'table-bordered');
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('textContent').innerHTML =
|
||||
'<div class="text-danger p-3">文档加载失败</div>';
|
||||
});
|
||||
{% else %}
|
||||
fetch("{{ url_for('resources.preview', resource_id=resource.id) }}")
|
||||
.then(r => r.text())
|
||||
.then(text => {
|
||||
@@ -222,6 +269,7 @@ function toggleWrap() {
|
||||
pre.style.whiteSpace = pre.style.whiteSpace === 'pre' ? 'pre-wrap' : 'pre';
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
// ── 下载进度轮询 ────────────────────────────────────────────────────────────
|
||||
{% if resource.download_status in ('pending','downloading') %}
|
||||
|
||||
@@ -15,6 +15,8 @@ MIME_TYPE_MAP = {
|
||||
'text/xml': 'text',
|
||||
'application/json': 'text',
|
||||
'application/xml': 'text',
|
||||
'application/msword': 'text',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'text',
|
||||
# 图片
|
||||
'image/jpeg': 'image',
|
||||
'image/png': 'image',
|
||||
@@ -45,6 +47,7 @@ MIME_TYPE_MAP = {
|
||||
EXT_TYPE_MAP = {
|
||||
'txt': 'text', 'md': 'text', 'csv': 'text', 'json': 'text',
|
||||
'xml': 'text', 'log': 'text', 'html': 'text', 'htm': 'text',
|
||||
'doc': 'text', 'docx': 'text',
|
||||
'jpg': 'image', 'jpeg': 'image', 'png': 'image', 'gif': 'image',
|
||||
'webp': 'image', 'bmp': 'image', 'svg': 'image', 'ico': 'image',
|
||||
'mp3': 'audio', 'wav': 'audio', 'ogg': 'audio', 'flac': 'audio',
|
||||
|
||||
@@ -29,7 +29,7 @@ class Config:
|
||||
WTF_CSRF_TIME_LIMIT = 3600
|
||||
|
||||
# 允许的文件类型
|
||||
ALLOWED_TEXT_EXT = {'txt', 'md', 'csv', 'json', 'xml', 'log', 'html', 'htm'}
|
||||
ALLOWED_TEXT_EXT = {'txt', 'md', 'csv', 'json', 'xml', 'log', 'html', 'htm', 'doc', 'docx'}
|
||||
ALLOWED_IMAGE_EXT = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'}
|
||||
ALLOWED_AUDIO_EXT = {'mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma'}
|
||||
ALLOWED_VIDEO_EXT = {'mp4', 'webm', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'm4v'}
|
||||
|
||||
@@ -13,5 +13,6 @@ requests==2.32.3
|
||||
python-dotenv==1.0.1
|
||||
python-magic-bin==0.4.14; sys_platform == 'win32'
|
||||
python-magic==0.4.27; sys_platform != 'win32'
|
||||
mammoth==1.8.0
|
||||
# WSGI 服务器(容器生产环境)
|
||||
gunicorn==23.0.0
|
||||
|
||||
Reference in New Issue
Block a user