6 Commits

Author SHA1 Message Date
huty 8197cc5aa3 修复展开文件夹后,顶层文件夹无法新增的问题
CI — Docker Build & Push / Build & Push Image (push) Failing after 13m20s
2026-04-28 15:26:50 +09:00
huty 605e04871e 测试 Gitea 工作流 2026-04-28 14:40:06 +09:00
huty 3ebac6ed03 修复 Gitea 工作流问题
CI — Docker Build & Push / Build & Push Image (push) Failing after 2m12s
2026-04-28 14:13:20 +09:00
huty cfe56264ea 修复 Gitea 工作流问题
CI — Docker Build & Push / Build & Push Image (push) Failing after 2m6s
2026-04-28 12:56:51 +09:00
huty 66ffa9679d 修复 word 文件预览问题
CI — Docker Build & Push / Build & Push Image (push) Failing after 2m28s
2026-04-28 12:45:54 +09:00
huty 9c38f9ed9a 新增 word 文件预览功能
CI — Docker Build & Push / Build & Push Image (push) Failing after 14m37s
2026-04-28 11:11:12 +09:00
12 changed files with 212 additions and 14 deletions
+3 -1
View File
@@ -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
View File
@@ -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
+28
View File
@@ -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: |
+22 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
# 个人资料库 (Personal Resource Library)
# 个人资料库 (Personal Resource Library)
基于 Flask + MySQL 的个人多媒体资料管理系统。支持文本、图片、音频、视频的上传、URL 下载、磁力下载及在线预览。
+55 -1
View File
@@ -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 实际上是 zipPK\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()
+14
View File
@@ -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;
+7 -6
View File
@@ -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>
+48
View File
@@ -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') %}
+3
View File
@@ -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',
+1 -1
View File
@@ -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'}
+1
View File
@@ -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