diff --git a/Directory.Build.props b/Directory.Build.props
index 67acfc4..21c35ba 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,10 +5,10 @@
- 1.31.7
- 1.31.7.0
- 1.31.7.0
- 1.31.7
+ 1.31.8
+ 1.31.8.0
+ 1.31.8.0
+ 1.31.8
Telegram Panel Contributors
diff --git a/README.md b/README.md
index 6c5a98e..cfb09d9 100644
--- a/README.md
+++ b/README.md
@@ -31,21 +31,27 @@
## 近期新增功能
-- 🧠 任务中心增强:持续任务支持暂停、编辑、重新运行;任务列表区分执行中与历史任务;支持“仅清理历史任务”与“历史任务保留上限”自动清理
-- 💬 持续活跃任务升级:新增账号持续活跃(群组/频道)能力,支持多分类账号、随机文案、秒级发送间隔、持续运行配置
-- 🔄 同步体验优化:手动“立即同步”改为后台任务执行,可在任务中心跟踪进度;同步链路补充超时保护与状态持久化
-- 👤 账号列表增强:新增注册时间(估算,非百分百正确)展示,并可直接从账号操作中查看该账号已加入的频道和群组
-- 📺 频道管理升级:频道列表改为面向“已加入频道”视角,不再只显示系统创建频道;支持按账号、角色、分类筛选,支持查看频道内的本系统账号
-- 👥 群组管理补齐:新增群组创建页、群组分类、批量操作与列表能力补齐;群组列表同样支持按账号、角色、分类筛选,并支持查看群组内的本系统账号
-- 🔗 多账号关系可视化:同一频道/群组可绑定多个系统账号,频道/群组列表与详情页都可查看当前关联的系统账号、角色与状态
-- 🚪 真实退出/解散能力:频道与群组支持单个/批量退出;支持解散频道/群组,且解散前需后台密码确认
-- 🧹 数据准确性修正:修复频道列表混入群组的问题,并优化账号、频道、群组关系同步后的本地展示逻辑
-- ♻️ 同步残留自动清理:同步完成后自动清理失效关联、创建者残留和孤儿频道/群组记录,避免数据库持续臃肿
-- ⚡ 数据层优化:补充频道/群组查询与关系索引,提升几百账号、上千频道/群组场景下的筛选与加载性能
+- 🧠 AI 验证接入:持续活跃任务支持识别验证消息后自动点击按钮或文本作答
+- ⚙️ AI 设置增强:OpenAI 兼容端点、API Key、默认/预设模型、一键连通测试
+- 🔁 AI 稳定性:支持配置失败重试次数,AI 决策/作答/连通测试统一复用
+- 📚 数据字典能力:支持文本字典/图片字典与模板变量
+- 🕒 定时任务能力:新增定时频道/群组相关任务(创建/公开等)
+- 🧠 任务中心增强:持续任务支持暂停、编辑、重新运行;任务列表区分执行中与历史任务;支持自动清理
+- 💬 持续活跃任务升级:支持多分类账号、随机文案、秒级发送间隔、持续运行配置
+- 🔄 同步体验优化:手动“立即同步”改为后台任务执行,可在任务中心跟踪进度
+- 👤 账号列表增强:新增注册时间(估算)展示,并可查看账号已加入的频道/群组
+- 📺 频道管理升级:频道列表改为“已加入频道”视角,支持多条件筛选与关联账号展示
+- 👥 群组管理补齐:新增群组创建、分类、批量操作与列表能力
+- 🔗 多账号关系可视化:频道/群组可绑定多个系统账号,列表与详情可查看关联状态
+- 🚪 真实退出/解散能力:频道与群组支持单个/批量退出与解散
+- 🧹 数据准确性修正:修复频道列表混入群组的问题,优化关系同步后的展示
+- ♻️ 同步残留清理:同步完成后自动清理失效关联与孤儿记录
+- ⚡ 数据层优化:补充查询与关系索引,提升大量账号/频道/群组场景的筛选性能
## TODO(规划)
- [x] 一键退群/退订、订阅(频道/群组)
+- [x] 批量自动签到
- [ ] 一键清空联系人
- [ ] 批量手机号验证码重新登录(用于刷新会话 session)
- [ ] 手机号注册:未注册号支持完整注册流程(姓名/可选邮箱/邮箱验证码等)
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index abe960b..5e94380 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -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)。
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 02534b0..f07243b 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -15,6 +15,7 @@
- `/data/sessions/`:账号 session 文件
- `/data/appsettings.local.json`:UI 保存后的本地覆盖配置
- `/data/admin_auth.json`:后台登录账号/密码(首次会用初始默认值生成)
+- `/data/uploads/`:图片资产(数据字典图片、头像素材等)
## 后台任务(刷新页面不影响)
diff --git a/src/TelegramPanel.Web/Program.cs b/src/TelegramPanel.Web/Program.cs
index 85345c6..3339a56 100644
--- a/src/TelegramPanel.Web/Program.cs
+++ b/src/TelegramPanel.Web/Program.cs
@@ -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();
diff --git a/src/TelegramPanel.Web/Services/ImageAssetStorageService.cs b/src/TelegramPanel.Web/Services/ImageAssetStorageService.cs
index f5705a0..9a2e8d4 100644
--- a/src/TelegramPanel.Web/Services/ImageAssetStorageService.cs
+++ b/src/TelegramPanel.Web/Services/ImageAssetStorageService.cs
@@ -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 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");