fix: persist image assets under /data

This commit is contained in:
meoacgx committed 2026-03-10 13:00:58 +08:00
1 parent fb08a5806c
commit 7ce4f42927
5 files changed
+41 -16

No files matched your search

+17 -11
View File
@@ -31,21 +31,27 @@
## 近期新增功能
- 🧠 任务中心增强:持续任务支持暂停、编辑、重新运行;任务列表区分执行中与历史任务;支持“仅清理历史任务”与“历史任务保留上限”自动清理
- 💬 持续活跃任务升级:新增账号持续活跃(群组/频道)能力,支持多分类账号、随机文案、秒级发送间隔、持续运行配置
- 🔄 同步体验优化:手动“立即同步”改为后台任务执行,可在任务中心跟踪进度;同步链路补充超时保护与状态持久化
- 👤 账号列表增强:新增注册时间(估算,非百分百正确)展示,并可直接从账号操作中查看该账号已加入的频道和群组
- 📺 频道管理升级:频道列表改为面向“已加入频道”视角,不再只显示系统创建频道;支持按账号、角色、分类筛选,支持查看频道内的本系统账号
- 👥 群组管理补齐:新增群组创建页、群组分类、批量操作与列表能力补齐;群组列表同样支持按账号、角色、分类筛选,并支持查看群组内的本系统账号
- 🔗 多账号关系可视化:同一频道/群组可绑定多个系统账号,频道/群组列表与详情页都可查看当前关联的系统账号、角色与状态
- 🚪 真实退出/解散能力:频道与群组支持单个/批量退出;支持解散频道/群组,且解散前需后台密码确认
- 🧹 数据准确性修正:修复频道列表混入群组的问题,并优化账号、频道、群组关系同步后的本地展示逻辑
- ♻️ 同步残留自动清理:同步完成后自动清理失效关联、创建者残留和孤儿频道/群组记录,避免数据库持续臃肿
- ⚡ 数据层优化:补充频道/群组查询与关系索引,提升几百账号、上千频道/群组场景下的筛选与加载性能
- 🧠 AI 验证接入:持续活跃任务支持识别验证消息后自动点击按钮或文本作答
- ⚙️ AI 设置增强:OpenAI 兼容端点、API Key、默认/预设模型、一键连通测试
- 🔁 AI 稳定性:支持配置失败重试次数,AI 决策/作答/连通测试统一复用
- 📚 数据字典能力:支持文本字典/图片字典与模板变量
- 🕒 定时任务能力:新增定时频道/群组相关任务(创建/公开等)
- 🧠 任务中心增强:持续任务支持暂停、编辑、重新运行;任务列表区分执行中与历史任务;支持自动清理
- 💬 持续活跃任务升级:支持多分类账号、随机文案、秒级发送间隔、持续运行配置
- 🔄 同步体验优化:手动“立即同步”改为后台任务执行,可在任务中心跟踪进度
- 👤 账号列表增强:新增注册时间(估算)展示,并可查看账号已加入的频道/群组
- 📺 频道管理升级:频道列表改为“已加入频道”视角,支持多条件筛选与关联账号展示
- 👥 群组管理补齐:新增群组创建、分类、批量操作与列表能力
- 🔗 多账号关系可视化:频道/群组可绑定多个系统账号,列表与详情可查看关联状态
- 🚪 真实退出/解散能力:频道与群组支持单个/批量退出与解散
- 🧹 数据准确性修正:修复频道列表混入群组的问题,优化关系同步后的展示
- ♻️ 同步残留清理:同步完成后自动清理失效关联与孤儿记录
- ⚡ 数据层优化:补充查询与关系索引,提升大量账号/频道/群组场景的筛选性能
## TODO(规划)
- [x] 一键退群/退订、订阅(频道/群组)
- [x] 批量自动签到
- [ ] 一键清空联系人
- [ ] 批量手机号验证码重新登录(用于刷新会话 session)
- [ ] 手机号注册:未注册号支持完整注册流程(姓名/可选邮箱/邮箱验证码等)
+1
View File
@@ -61,6 +61,7 @@ docker compose up -d
- Sessions:`./docker-data/sessions/`
- 系统设置本地覆盖:`./docker-data/appsettings.local.json`
- 后台登录凭据文件:`./docker-data/admin_auth.json`
- 图片资产:`./docker-data/uploads/`
更多说明见:[配置与数据目录](../reference/configuration.md)。
+1
View File
@@ -15,6 +15,7 @@
- `/data/sessions/`:账号 session 文件
- `/data/appsettings.local.json`:UI 保存后的本地覆盖配置
- `/data/admin_auth.json`:后台登录账号/密码(首次会用初始默认值生成)
- `/data/uploads/`:图片资产(数据字典图片、头像素材等)
## 后台任务(刷新页面不影响)
+11
View File
@@ -16,6 +16,7 @@ using TelegramPanel.Web.Services;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.FileProviders;
using System.Diagnostics;
// 诊断:对某个目录下的 *.json/*.session 做一次“可转换/可校验”检查(不写数据库)
@@ -726,6 +727,16 @@ if (hasHttpsEndpoint)
// 静态文件(包括 MudBlazor 等 NuGet 包的静态 Web 资源)
app.UseStaticFiles();
if (Directory.Exists("/data"))
{
var uploadsRoot = "/data/uploads";
Directory.CreateDirectory(uploadsRoot);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(uploadsRoot),
RequestPath = "/uploads"
});
}
app.UseAntiforgery();
@@ -50,7 +50,7 @@ public sealed class ImageAssetStorageService
var extension = ".jpg";
var relativeDir = Path.Combine("uploads", scope).Replace('\\', '/');
var root = GetWebRootPath();
var root = GetStorageRootPath();
var fullDir = Path.Combine(root, "uploads", scope.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(fullDir);
@@ -84,7 +84,7 @@ public sealed class ImageAssetStorageService
public Task<Stream> OpenReadAsync(string assetPath, CancellationToken cancellationToken = default)
{
assetPath = NormalizeAssetPath(assetPath);
var fullPath = Path.Combine(GetWebRootPath(), assetPath.Replace('/', Path.DirectorySeparatorChar));
var fullPath = Path.Combine(GetStorageRootPath(), assetPath.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(fullPath))
throw new FileNotFoundException("图片资产不存在", fullPath);
@@ -98,7 +98,7 @@ public sealed class ImageAssetStorageService
if (assetPath.Length == 0)
return Task.CompletedTask;
var fullPath = Path.Combine(GetWebRootPath(), assetPath.Replace('/', Path.DirectorySeparatorChar));
var fullPath = Path.Combine(GetStorageRootPath(), assetPath.Replace('/', Path.DirectorySeparatorChar));
try
{
if (File.Exists(fullPath))
@@ -118,7 +118,7 @@ public sealed class ImageAssetStorageService
if (scope.Length == 0)
return Task.CompletedTask;
var fullDir = Path.Combine(GetWebRootPath(), "uploads", scope.Replace('/', Path.DirectorySeparatorChar));
var fullDir = Path.Combine(GetStorageRootPath(), "uploads", scope.Replace('/', Path.DirectorySeparatorChar));
try
{
if (Directory.Exists(fullDir))
@@ -132,8 +132,14 @@ public sealed class ImageAssetStorageService
return Task.CompletedTask;
}
private string GetWebRootPath()
private string GetStorageRootPath()
{
if (Directory.Exists("/data"))
{
Directory.CreateDirectory("/data/uploads");
return "/data";
}
var path = _environment.WebRootPath;
if (string.IsNullOrWhiteSpace(path))
path = Path.Combine(AppContext.BaseDirectory, "wwwroot");