ソースを参照

feat(logging): 增强系统日志记录和管理功能

- 在多个网络请求和服务模块中集成 ServerLogUtil 进行详细日志记录
- 新增远程缓存文件下载过程的完整日志追踪
- 在设置页面增加日志上传开关和日志清除功能
- 实现关于页面连续点击Logo触发日志收集与上传机制
- 增加Meitu云存储日志上传配置及处理逻辑
- 提供本地日志文件的递归删除和分类清理能力
- 支持日志上传状态管理和上传处理器动态注册
- 增加缓存目录权限验证和测试文件写入检测
- 完善Navidrome相关操作的日志记录覆盖
- 添加WebDav文件下载进度跟踪和错误堆栈记录
chendeben 8 ヶ月 前
コミット
9927c4041d

+ 15 - 2
entry/src/main/ets/common/network/NavidromeApi.ets

@@ -2,6 +2,7 @@ import { http } from '@kit.NetworkKit';
 import { MD5 } from '@pura/harmony-utils';
 import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import Logger from '../util/Logger';
+import { ServerLogUtil } from '../util/ServerLogUtil';
 
 const TAG = 'heanup NavidromeApi';
 const API_VERSION = '1.16.1';
@@ -225,7 +226,10 @@ export class NavidromeApi {
     this.appendParams(params, await this.buildAuthParams(account));
     this.appendCommonParams(params);
     const query = this.buildQueryString(params);
-    return `${baseUrl}/stream?${query}`;
+    const url = `${baseUrl}/stream?${query}`;
+    void ServerLogUtil.info(TAG, `构建播放流: ${url}`);
+    void ServerLogUtil.debug(TAG, `stream auth参数 count=${params.length}`);
+    return url;
   }
 
   async buildCoverArtUrl(account: WebDavAccount, coverId: string | undefined, size?: number): Promise<string | undefined> {
@@ -241,7 +245,10 @@ export class NavidromeApi {
     this.appendParams(params, await this.buildAuthParams(account));
     this.appendCommonParams(params);
     const query = this.buildQueryString(params);
-    return `${baseUrl}/getCoverArt?${query}`;
+    const url = `${baseUrl}/getCoverArt?${query}`;
+    void ServerLogUtil.debug(TAG, `构建封面: ${url}`);
+    void ServerLogUtil.debug(TAG, `cover params: ${JSON.stringify(params)}`);
+    return url;
   }
 
   private async request(account: WebDavAccount, endpoint: string, extraParams: Array<QueryParam>): Promise<SubsonicBody> {
@@ -252,6 +259,8 @@ export class NavidromeApi {
     this.appendCommonParams(params);
     const query = this.buildQueryString(params);
     const url = `${baseUrl}/${endpoint}?${query}`;
+    void ServerLogUtil.info(TAG, `请求 ${endpoint} -> ${url}`);
+    void ServerLogUtil.debug(TAG, `endpoint params: ${JSON.stringify(extraParams)}`);
 
     const httpRequest = http.createHttp();
     try {
@@ -263,6 +272,7 @@ export class NavidromeApi {
       };
       const response = await httpRequest.request(url, options);
       if (response.responseCode !== 200) {
+        void ServerLogUtil.error(TAG, `${endpoint} HTTP ${response.responseCode}`);
         throw new Error(`Navidrome 请求失败: HTTP ${response.responseCode}`);
       }
       const result = response.result as string;
@@ -274,12 +284,15 @@ export class NavidromeApi {
       if (body.status !== 'ok') {
         const errorInfo = body.error;
         const message = errorInfo && errorInfo.message ? errorInfo.message : 'Navidrome 返回错误';
+        void ServerLogUtil.warn(TAG, `${endpoint} 返回错误: ${message}`);
         throw new Error(message);
       }
+      void ServerLogUtil.debug(TAG, `${endpoint} 成功`);
       return body;
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `Navidrome 请求失败: ${err.message}`);
+      void ServerLogUtil.error(TAG, `${endpoint} 异常: ${err.message}`);
       throw err;
     } finally {
       httpRequest.destroy();

+ 11 - 0
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -1,6 +1,7 @@
 import { http } from '@kit.NetworkKit';
 import { WebDavAccount } from '../../viewmodel/WebDavAccount';
 import Logger from '../util/Logger';
+import { ServerLogUtil } from '../util/ServerLogUtil';
 
 const TAG = 'heanup NavidromeRestApi';
 
@@ -121,6 +122,8 @@ export class NavidromeRestApi {
       const auth = await this.ensureAuth(account);
       const query = this.buildQueryString(params);
       const url = `${this.buildRootBase(account)}${path}${query}`;
+      void ServerLogUtil.info(TAG, `GET ${url}`);
+      void ServerLogUtil.debug(TAG, `请求参数: ${JSON.stringify(params ?? [])}`)
       const response = await httpRequest.request(url, {
         method: http.RequestMethod.GET,
         connectTimeout: 10000,
@@ -131,11 +134,14 @@ export class NavidromeRestApi {
 
       if (response.responseCode === 401 && retry) {
         this.invalidateAuth(account);
+        void ServerLogUtil.warn(TAG, `401需要重试: ${url}`)
         return this.get(account, path, params, false);
       }
       if (response.responseCode !== 200) {
+        void ServerLogUtil.error(TAG, `GET ${url} 失败 code=${response.responseCode}`);
         throw new Error(`Navidrome API 请求失败: HTTP ${response.responseCode}`);
       }
+      void ServerLogUtil.info(TAG, `GET ${url} 成功 code=${response.responseCode}`);
       return JSON.parse(response.result as string) as T;
     } finally {
       httpRequest.destroy();
@@ -170,6 +176,7 @@ export class NavidromeRestApi {
       if (!username || !password) {
         throw new Error('Navidrome账号缺少用户名或密码');
       }
+      void ServerLogUtil.info(TAG, `登录 ${ServerLogUtil.sanitizeAccount(account)}`)
       const response = await httpRequest.request(url, {
         method: http.RequestMethod.POST,
         connectTimeout: 10000,
@@ -182,12 +189,14 @@ export class NavidromeRestApi {
       });
 
       if (response.responseCode !== 200) {
+        void ServerLogUtil.error(TAG, `登录失败 code=${response.responseCode}`);
         throw new Error(`Navidrome 登录失败: HTTP ${response.responseCode}`);
       }
       const body = JSON.parse(response.result as string) as NavidromeLoginResponse;
       if (!body.token || !body.id) {
         throw new Error('Navidrome 登录响应缺少 token 信息');
       }
+      void ServerLogUtil.info(TAG, '登录成功,已获取token');
       return {
         token: body.token,
         clientId: body.id
@@ -195,6 +204,7 @@ export class NavidromeRestApi {
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `Navidrome 登录异常: ${err.message}`);
+      void ServerLogUtil.error(TAG, `登录异常: ${err.message}`);
       throw err;
     } finally {
       httpRequest.destroy();
@@ -237,6 +247,7 @@ export class NavidromeRestApi {
     }
     const port = account.port && account.port > 0 ? `:${account.port}` : '';
     const prefix = this.resolveRootPath(account.navidromeBasePath);
+    void ServerLogUtil.debug(TAG, `buildRootBase => ${protocol}://${host}${port}${prefix}`)
     return `${protocol}://${host}${port}${prefix}`;
   }
 

+ 33 - 0
entry/src/main/ets/common/network/RemoteSongCache.ets

@@ -5,6 +5,8 @@ import Logger from '../util/Logger';
 import { fileIo } from '@kit.CoreFileKit';
 import { common } from '@kit.AbilityKit';
 import { FileUtil } from '@pura/harmony-utils';
+import { ServerLogUtil } from '../util/ServerLogUtil';
+import { buffer } from '@kit.ArkTS';
 
 const TAG = 'RemoteSongCache';
 const CACHE_ROOT_DIR = 'remote_cache';
@@ -36,7 +38,26 @@ async function ensureCacheRoot(): Promise<string> {
     throw new Error('未找到可用的缓存根目录');
   }
   const cacheRoot = merge2paths(baseDir, CACHE_ROOT_DIR);
+
+  await ServerLogUtil.info('CacheRoot', `缓存根目录: ${baseDir}`);
+  await ServerLogUtil.info('CacheRoot', `远程缓存目录: ${cacheRoot}`);
+
   await FileManager.createDir(cacheRoot);
+
+  // 验证目录创建成功且有写入权限
+  try {
+    const testFile = cacheRoot + '/.cache_test';
+    const file = fileIo.openSync(testFile, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
+    const testData = new Uint8Array([116, 101, 115, 116]); // "test" in bytes
+    await fileIo.write(file.fd, testData.buffer);
+    fileIo.closeSync(file);
+    await FileManager.deleteFile(testFile);
+    await ServerLogUtil.info('CacheRoot', `缓存根目录权限验证通过`);
+  } catch (error) {
+    await ServerLogUtil.error('CacheRoot', `缓存根目录权限验证失败: ${(error as Error).message}`);
+    throw new Error(`缓存目录无写入权限: ${cacheRoot}`);
+  }
+
   return cacheRoot;
 }
 
@@ -99,10 +120,22 @@ export async function resolveCacheFilePath(
   accountId: string | number | undefined,
   relativePath: string
 ): Promise<CachePathInfo> {
+  await ServerLogUtil.info('CachePath', `解析缓存文件路径:`);
+  await ServerLogUtil.info('CachePath', `- 类型: ${type}`);
+  await ServerLogUtil.info('CachePath', `- 账户ID: ${accountId || 'default'}`);
+  await ServerLogUtil.info('CachePath', `- 相对路径: ${relativePath}`);
+
   const accountDir = await ensureAccountDir(type, accountId);
   const normalizedRelative = normalizeCacheRelativePath(relativePath);
   const cacheFileName = await buildCacheFileName(normalizedRelative);
   const cachePath = merge2paths(accountDir, cacheFileName);
+
+  await ServerLogUtil.info('CachePath', `路径解析结果:`);
+  await ServerLogUtil.info('CachePath', `- 账户目录: ${accountDir}`);
+  await ServerLogUtil.info('CachePath', `- 规范化相对路径: ${normalizedRelative}`);
+  await ServerLogUtil.info('CachePath', `- 缓存文件名: ${cacheFileName}`);
+  await ServerLogUtil.info('CachePath', `- 完整缓存路径: ${cachePath}`);
+
   return {
     cacheDir: accountDir,
     cachePath,

+ 142 - 0
entry/src/main/ets/common/network/Uploader.ets

@@ -0,0 +1,142 @@
+import { http } from '@kit.NetworkKit'
+import util from '@ohos.util'
+import fs from '@ohos.file.fs'
+import { Base64Util, DateUtil, FileUtil, LogUtil } from '@pura/harmony-utils'
+
+export interface UploadConfig {
+  uploadToken: string; // UpToken
+  domain: string; // 例如 https://your.cdn.domain
+  keyPrefix?: string; // 例如 logs/ttmusic
+  uploadHost?: string; // 可覆盖上传Host,不传使用默认美图云 Host
+}
+
+export class Uploader {
+  private static readonly DEFAULT_UPLOAD_HOST = 'https://up.meitudata.com'
+  private static readonly BACKUP_UPLOAD_HOST = 'https://upload.meitudata.com'
+
+  /**
+   * 检查网络连接和DNS解析
+   */
+  private static async checkNetworkConnectivity(host: string): Promise<boolean> {
+    try {
+      const httpRequest = http.createHttp()
+      const response = await httpRequest.request(`${host}/`, {
+        method: http.RequestMethod.GET,
+        connectTimeout: 8000,
+        readTimeout: 10000,
+        expectDataType: http.HttpDataType.STRING,
+        header: {
+          'User-Agent': 'TTMusic-LogCollector/1.0'
+        }
+      })
+      httpRequest.destroy()
+      return response.responseCode >= 200 && response.responseCode < 500
+    } catch (error) {
+      LogUtil.warn('Uploader', `网络连接检查失败(${host}): ${(error as Error).message}`)
+      return false
+    }
+  }
+
+  static async uploadFile(filePath: string, config: UploadConfig): Promise<string> {
+    const stat = FileUtil.lstatSync(filePath)
+    const file = FileUtil.openSync(filePath, fs.OpenMode.READ_ONLY)
+    try {
+      const buffer = new ArrayBuffer(stat.size)
+      FileUtil.readSync(file.fd, buffer, { offset: 0, length: stat.size })
+      const u8 = new Uint8Array(buffer)
+      const contentBase64 = Base64Util.encodeToStrSync(u8)
+      const key = Uploader.buildKey(filePath, config)
+      const keyBase64 = Uploader.toUrlSafeBase64(key)
+      const mainHost = config.uploadHost ?? Uploader.DEFAULT_UPLOAD_HOST
+
+      LogUtil.info('Uploader', `开始上传文件: ${filePath}, 大小: ${stat.size} bytes`)
+      LogUtil.info('Uploader', `使用上传主机: ${mainHost}`)
+
+      // 对于 401 错误,先尝试直接上传不做网络预检查
+      try {
+        LogUtil.info('Uploader', `直接尝试主主机上传: ${mainHost}`)
+        return await Uploader.doUpload(mainHost, stat.size, key, keyBase64, contentBase64, config)
+      } catch (err) {
+        LogUtil.warn('Uploader', `主上传Host失败(${mainHost}): ${(err as Error).message},尝试预检查后使用备用`)
+
+        // 预检查主机的网络连接性
+        const backupHostReachable = await Uploader.checkNetworkConnectivity(Uploader.BACKUP_UPLOAD_HOST)
+        if (!backupHostReachable) {
+          throw new Error(`上传失败且备用主机不可达: ${(err as Error).message}`)
+        }
+        LogUtil.info('Uploader', `使用备用主机上传: ${Uploader.BACKUP_UPLOAD_HOST}`)
+        return await Uploader.doUpload(Uploader.BACKUP_UPLOAD_HOST, stat.size, key, keyBase64, contentBase64, config)
+      }
+    } catch (err) {
+      LogUtil.error('Uploader', `上传异常: ${(err as Error).message}`)
+      throw new Error((err as Error).message)
+    } finally {
+      FileUtil.closeSync(file.fd)
+    }
+  }
+
+  private static buildKey(filePath: string, config: UploadConfig): string {
+    const prefix = config.keyPrefix ?? 'ttmusic-logs'
+    const filename = FileUtil.getFileName(filePath)
+    const timestamp = DateUtil.getTodayStr('yyyyMMdd_HHmmss')
+    return `${prefix}/${timestamp}_${filename}`
+  }
+
+  private static toUrlSafeBase64(text: string): string {
+    const encoder = new util.TextEncoder()
+    const u8 = encoder.encode(text)
+    const base64 = Base64Util.encodeToStrSync(u8)
+    return base64.replace(/\+/g, '-').replace(/\//g, '_')
+  }
+
+  private static normalizeDomain(domain: string): string {
+    if (domain.endsWith('/')) {
+      return domain.slice(0, -1)
+    }
+    return domain
+  }
+
+  private static async doUpload(host: string, size: number, key: string, keyBase64: string, contentBase64: string, config: UploadConfig): Promise<string> {
+    const url = `${host}/putb64/${size}/key/${encodeURIComponent(keyBase64)}`
+
+    // 调试信息
+    LogUtil.info('Uploader', `上传URL: ${url}`)
+    LogUtil.info('Uploader', `上传Key: ${key}`)
+    LogUtil.info('Uploader', `Token前缀: ${config.uploadToken.substring(0, 20)}...`)
+
+    const httpRequest = http.createHttp()
+    try {
+      const requestOptions: http.HttpRequestOptions = {
+        method: http.RequestMethod.POST,
+        connectTimeout: 10000,
+        readTimeout: 20000,
+        expectDataType: http.HttpDataType.STRING,
+        header: {
+          'Content-Type': 'application/octet-stream',
+          'Authorization': `UpToken ${config.uploadToken}`,
+          'User-Agent': 'TTMusic-LogCollector/1.0'
+        },
+        extraData: contentBase64
+      }
+
+      LogUtil.info('Uploader', `请求头: Authorization = UpToken ${config.uploadToken.substring(0, 20)}...`)
+
+      const response = await httpRequest.request(url, requestOptions)
+
+      LogUtil.info('Uploader', `响应状态: ${response.responseCode}`)
+      if (response.result) {
+        LogUtil.info('Uploader', `响应内容: ${response.result}`)
+      }
+
+      if (response.responseCode !== 200) {
+        throw new Error(`上传失败: HTTP ${response.responseCode}`)
+      }
+      const body = JSON.parse(response.result as string) as Record<string, string>
+      const savedKey = body['key'] ?? key
+      const domain = Uploader.normalizeDomain(config.domain)
+      return `${domain}/${savedKey}`
+    } finally {
+      httpRequest.destroy()
+    }
+  }
+}

+ 74 - 11
entry/src/main/ets/common/network/WebDavFileCache.ets

@@ -10,6 +10,7 @@ import {
   findExistingCacheFile,
   normalizeCacheRelativePath
 } from './RemoteSongCache';
+import { ServerLogUtil } from '../util/ServerLogUtil';
 
 const TAG = 'WebDavFileCache';
 const ongoingDownloads: Map<string, Promise<string | null>> = new Map();
@@ -44,6 +45,12 @@ export async function triggerWebDavCacheDownload(
   relativePath: string,
   fullUrl: string
 ): Promise<string | null> {
+  // 记录账号信息(脱敏处理)
+  const sanitizedAccount = ServerLogUtil.sanitizeAccount(account);
+  await ServerLogUtil.info('WebDavCache', `开始缓存下载 - 账号: ${sanitizedAccount}`);
+  await ServerLogUtil.info('WebDavCache', `请求文件: ${relativePath}`);
+  await ServerLogUtil.info('WebDavCache', `完整URL: ${fullUrl}`);
+
   const pathInfo = await resolveCacheFilePath(
     RemoteCacheType.WEBDAV,
     account.id?.toString(),
@@ -52,18 +59,21 @@ export async function triggerWebDavCacheDownload(
   const cachePath = pathInfo.cachePath;
   const normalizedRelative = pathInfo.normalizedRelative;
 
+  await ServerLogUtil.info('WebDavCache', `缓存路径: ${cachePath}`);
+
   const exists = await FileManager.isExist(cachePath);
   if (exists) {
     const validSize = await FileManager.getFileSize(cachePath);
     if (validSize > 0) {
-      Logger.info(TAG, `WebDAV 缓存已存在,跳过下载: ${normalizedRelative}`);
+      await ServerLogUtil.info('WebDavCache', `缓存文件已存在,跳过下载: ${normalizedRelative} (${validSize} bytes)`);
       return null;
     }
     await FileManager.deleteFile(cachePath);
+    await ServerLogUtil.warn('WebDavCache', `删除无效缓存文件: ${normalizedRelative}`);
   }
 
   if (ongoingDownloads.has(cachePath)) {
-    Logger.info(TAG, `WebDAV 缓存下载进行中,跳过重复任务: ${normalizedRelative}`);
+    await ServerLogUtil.info('WebDavCache', `缓存下载进行中,跳过重复任务: ${normalizedRelative}`);
     const existingTask = ongoingDownloads.get(cachePath);
     if (existingTask) {
       return existingTask;
@@ -80,15 +90,20 @@ export async function triggerWebDavCacheDownload(
     if (authHeader) {
       headers['Authorization'] = authHeader;
     }
+
+    const hasAuth = authHeader ? '是' : '否';
+    await ServerLogUtil.info('WebDavCache', `下载配置 - 认证: ${hasAuth}`);
+
     try {
-      await downloadWebDavToFile(fullUrl, authHeader, cachePath);
-      Logger.info(TAG, `WebDAV 缓存完成: ${normalizedRelative}`);
+      await downloadWebDavToFile(fullUrl, authHeader, cachePath, normalizedRelative, sanitizedAccount);
+      await ServerLogUtil.info('WebDavCache', `缓存下载完成: ${normalizedRelative}`);
       return cachePath;
     } catch (error) {
       const err = error as Error;
-      Logger.error(TAG, `WebDAV 缓存异常: ${err.message}`);
+      await ServerLogUtil.error('WebDavCache', `缓存下载异常: ${err.message}`);
+      await ServerLogUtil.error('WebDavCache', `错误堆栈: ${err.stack || '无堆栈信息'}`);
       await FileManager.deleteFile(cachePath);
-      throw err;
+      throw new Error(`缓存下载异常: ${err.message}`);
     } finally {
       ongoingDownloads.delete(cachePath);
     }
@@ -101,9 +116,16 @@ export async function triggerWebDavCacheDownload(
 async function downloadWebDavToFile(
   url: string,
   authHeader: string | undefined,
-  cachePath: string
+  cachePath: string,
+  normalizedRelative: string,
+  sanitizedAccount: string
 ): Promise<void> {
+  await ServerLogUtil.info('WebDavDownload', `开始文件下载 - 账号: ${sanitizedAccount}`);
+  await ServerLogUtil.info('WebDavDownload', `下载文件: ${normalizedRelative}`);
+
   const fileHandle: fileIo.File = await fileIo.open(cachePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
+  await ServerLogUtil.debug('WebDavDownload', `文件句柄创建成功: ${fileHandle.fd}`);
+
   const headers: rcp.RequestHeaders = {
     'User-Agent': 'TTMusic-WebDAV/1.0',
     'Accept': '*/*',
@@ -112,6 +134,7 @@ async function downloadWebDavToFile(
   if (authHeader) {
     headers['Authorization'] = authHeader;
   }
+
   const tracingConfig: rcp.TracingConfiguration = {
     verbose: false,
     infoToCollect: {
@@ -123,26 +146,66 @@ async function downloadWebDavToFile(
     },
     collectTimeInfo: false
   };
+
   const session = rcp.createSession({
     requestConfiguration: { tracing: tracingConfig },
     headers: headers
   });
+
+  await ServerLogUtil.debug('WebDavDownload', `网络会话创建成功`);
+
+  let totalBytesWritten = 0;
   const streamData: rcp.WriteStream = {
     async write(buffer: ArrayBuffer): Promise<number> {
-      await fileIo.write(fileHandle.fd, buffer);
-      return buffer.byteLength;
+      try {
+        await fileIo.write(fileHandle.fd, buffer);
+        totalBytesWritten += buffer.byteLength;
+
+        // 每写入1MB记录一次进度
+        if (totalBytesWritten % (1024 * 1024) === 0) {
+          await ServerLogUtil.debug('WebDavDownload', `下载进度: ${totalBytesWritten} 字节`);
+        }
+
+        return buffer.byteLength;
+      } catch (writeError) {
+        await ServerLogUtil.error('WebDavDownload', `写入文件失败: ${(writeError as Error).message}`);
+        await ServerLogUtil.error('WebDavDownload', `文件句柄: ${fileHandle.fd}, 缓冲区大小: ${buffer.byteLength}`);
+        throw new Error(`写入文件失败: ${(writeError as Error).message}`);
+      }
     }
   };
+
   const downloadToStream: rcp.DownloadToStream = {
     kind: 'stream',
     stream: streamData
   };
+
+  await ServerLogUtil.debug('WebDavDownload', `开始流式下载...`);
+
   return new Promise<void>((resolve, reject) => {
-    session.downloadToStream(url, downloadToStream).then(() => {
+    session.downloadToStream(url, downloadToStream).then(async () => {
+      await ServerLogUtil.info('WebDavDownload', `下载完成,总写入字节数: ${totalBytesWritten}`);
       fileIo.closeSync(fileHandle);
       session.close();
+
+      // 验证文件大小
+      try {
+        const size = await FileManager.getFileSize(cachePath);
+        await ServerLogUtil.info('WebDavDownload', `最终文件大小: ${size} 字节`);
+        if (size === 0) {
+          await ServerLogUtil.warn('WebDavDownload', `警告: 下载的文件大小为0`);
+        }
+      } catch (sizeError) {
+        await ServerLogUtil.warn('WebDavDownload', `无法获取文件大小: ${(sizeError as Error).message}`);
+      }
+
       resolve();
-    }).catch((error: Error) => {
+    }).catch(async (error: Error) => {
+      await ServerLogUtil.error('WebDavDownload', `下载失败: ${error.message}`);
+      await ServerLogUtil.error('WebDavDownload', `错误详情: ${error.stack || '无堆栈信息'}`);
+      await ServerLogUtil.error('WebDavDownload', `文件句柄状态: ${fileHandle.fd}`);
+      await ServerLogUtil.error('WebDavDownload', `已写入字节数: ${totalBytesWritten}`);
+
       fileIo.closeSync(fileHandle);
       session.close();
       reject(error);

+ 258 - 0
entry/src/main/ets/common/util/LogCollector.ets

@@ -0,0 +1,258 @@
+import { AppUtil, DateUtil, DeviceUtil, FileUtil, LogUtil, StrUtil } from '@pura/harmony-utils'
+import { LogPackager } from './LogPackager'
+import fs from '@ohos.file.fs'
+import util from '@ohos.util'
+
+export interface LogCollectResult {
+  targetDir: string;
+  mergedLogPath: string;
+  sourceFiles: string[];
+}
+
+type UploadHandler = (filePath: string) => Promise<string | void>
+
+export class LogCollector {
+  private static readonly LOG_DIR: string = 'LogUpload'
+  private static readonly ALLOW_EXT: string[] = ['.log', '.txt', '.json']
+  private static uploadHandler?: UploadHandler
+
+  static registerUploadHandler(handler: UploadHandler): void {
+    LogCollector.uploadHandler = handler
+  }
+
+  static async collectAllLogs(customDir?: string): Promise<LogCollectResult> {
+    const timestamp = DateUtil.getTodayStr('yyyyMMdd_HHmmss')
+    const baseDir = StrUtil.isNotEmpty(customDir) ? customDir! : FileUtil.getCacheDirPath(LogCollector.LOG_DIR)
+    if (!FileUtil.accessSync(baseDir)) {
+      FileUtil.mkdirSync(baseDir)
+    }
+    const targetDir = `${baseDir}${FileUtil.separator}${timestamp}`
+    if (!FileUtil.accessSync(targetDir)) {
+      FileUtil.mkdirSync(targetDir)
+    }
+    const mergedLogPath = `${targetDir}${FileUtil.separator}logs_${timestamp}.txt`
+    const sources = LogCollector.collectExistingLogs(targetDir)
+    await LogCollector.writeMergedLog(mergedLogPath, sources)
+    return { targetDir, mergedLogPath, sourceFiles: sources }
+  }
+
+  static async uploadLogs(filePath: string): Promise<string | void> {
+    if (LogCollector.uploadHandler) {
+      const bundle = await LogPackager.pack(filePath)
+      return await LogCollector.uploadHandler(bundle)
+    }
+    LogUtil.info('LogCollector', `未配置上传接口,跳过上传。文件路径: ${filePath}`)
+  }
+
+  private static collectExistingLogs(targetDir: string): string[] {
+    const collected: string[] = []
+    const context = AppUtil.getContext()
+
+    // 1. 收集预设的错误日志文件
+    const presetFiles: string[] = [
+      FileUtil.getFilesDirPath('ErrorLog', 'errorLog.txt'),
+    ]
+    presetFiles.forEach((filePath) => LogCollector.copyIfLogFile(filePath, targetDir, collected))
+
+    // 2. 收集主要目录中的日志文件,但排除重复的收集目录
+    const candidateDirs: string[] = [context.filesDir, context.cacheDir, context.tempDir]
+    candidateDirs.forEach((dir) => LogCollector.copyDirLogs(dir, targetDir, collected))
+
+    // 3. 记录收集统计信息
+    LogUtil.info('LogCollector', `日志收集完成 - 预设文件: ${presetFiles.length}, 目录扫描文件: ${collected.length - presetFiles.length}`)
+
+    return collected
+  }
+
+  private static copyDirLogs(dirPath: string, targetDir: string, collected: string[]): void {
+    if (!FileUtil.accessSync(dirPath)) {
+      return
+    }
+    try {
+      const files = FileUtil.listFileSync(dirPath, { recursion: true })
+      files.forEach((name: string) => {
+        const fullPath = `${dirPath}${FileUtil.separator}${name}`
+        if (FileUtil.isDirectory(fullPath)) {
+          return
+        }
+        if (!LogCollector.shouldCollect(fullPath)) {
+          return
+        }
+
+        // 跳过已收集的日志目录中的文件,避免重复收集
+        if (LogCollector.isInCollectedLogDir(fullPath)) {
+          return
+        }
+
+        const normalizedName = name.replace(/[\\\\/]/g, '_')
+        const destPath = `${targetDir}${FileUtil.separator}${normalizedName}`
+        FileUtil.copyFileSync(fullPath, destPath)
+        collected.push(destPath)
+      })
+    } catch (err) {
+      LogUtil.warn('LogCollector', `遍历日志目录失败: ${JSON.stringify(err)}`)
+    }
+  }
+
+  private static copyIfLogFile(filePath: string, targetDir: string, collected: string[]): void {
+    if (!FileUtil.accessSync(filePath)) {
+      return
+    }
+    if (!LogCollector.shouldCollect(filePath)) {
+      return
+    }
+    const fileName = FileUtil.getFileName(filePath)
+    const destPath = `${targetDir}${FileUtil.separator}${fileName}`
+    try {
+      FileUtil.copyFileSync(filePath, destPath)
+      collected.push(destPath)
+    } catch (err) {
+      LogUtil.warn('LogCollector', `复制日志文件失败: ${filePath}`)
+    }
+  }
+
+  private static shouldCollect(filePath: string): boolean {
+    const ext = FileUtil.getFileExtention(filePath)
+    return LogCollector.ALLOW_EXT.includes(`.${ext.toLowerCase()}`)
+  }
+
+  /**
+   * 检查文件是否已经在收集的日志目录中,避免重复收集
+   */
+  private static isInCollectedLogDir(filePath: string): boolean {
+    // 检查路径中是否包含 LogUpload 目录
+    if (filePath.includes(LogCollector.LOG_DIR)) {
+      return true
+    }
+
+    // 检查是否是嵌套的收集目录中的文件
+    // 这些模式表明文件已经被之前收集过
+    const collectedLogPatterns = [
+      '_LogUpload_',           // 嵌套的LogUpload目录
+      'ttmusic_logs_',         // 已收集的应用日志文件
+      'logs_202',              // 已收集的合并日志文件
+      '_navidrome.log',        // 已收集的Navidrome日志文件
+      '_errorLog.txt'          // 已收集的错误日志文件
+    ]
+
+    return collectedLogPatterns.some(pattern => filePath.includes(pattern))
+  }
+
+  private static async writeMergedLog(targetPath: string, sources: string[]): Promise<void> {
+    const header = [
+      '====== 日志采集 ======',
+      `时间: ${DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')}`,
+      `版本: ${AppUtil.getVersionName()} (${AppUtil.getVersionCode()})`,
+      `包名: ${AppUtil.getBundleName()}`,
+      `设备: ${DeviceUtil.getBrand()} ${DeviceUtil.getProductModel()} / ${DeviceUtil.getOsReleaseType()}`,
+      `来源文件数量: ${sources.length}`,
+      '==============================',
+      '',
+    ].join('\n')
+
+    // 先写入文件头
+    await FileUtil.writeEasy(targetPath, header, false)
+
+    if (sources.length === 0) {
+      await FileUtil.writeEasy(targetPath, '未找到可用日志文件。\n', true)
+      return
+    }
+
+    // 限制处理的日志文件数量和大小,避免内存溢出
+    const maxFiles = 20; // 最多处理20个日志文件
+    const maxFileSize = 10 * 1024 * 1024; // 单个文件最大10MB
+    const processedSources = sources.slice(0, maxFiles)
+
+    LogUtil.info('LogCollector', `准备处理 ${processedSources.length} 个日志文件(总数: ${sources.length})`)
+
+    for (const source of processedSources) {
+      try {
+        // 检查文件大小
+        const stat = FileUtil.lstatSync(source)
+        if (stat.size > maxFileSize) {
+          const fileSizeStr = LogCollector.formatFileSize(stat.size)
+          await FileUtil.writeEasy(targetPath, `\n\n===== ${source} =====\n文件过大 (${fileSizeStr}),跳过内容读取\n`, true)
+          LogUtil.warn('LogCollector', `日志文件过大,跳过: ${source} (${fileSizeStr})`)
+          continue
+        }
+
+        // 对于较大的文件,分块读取以避免内存溢出
+        if (stat.size > 1024 * 1024) { // 如果文件大于1MB
+          await LogCollector.writeLargeFileInChunks(targetPath, source)
+        } else {
+          // 小文件直接读取
+          const content = await FileUtil.readText(source)
+          await FileUtil.writeEasy(targetPath, `\n\n===== ${source} =====\n${content}\n`, true)
+        }
+
+        LogUtil.info('LogCollector', `成功处理日志文件: ${source}`)
+      } catch (err) {
+        const errorMessage = err instanceof Error ? err.message : '未知错误'
+        await FileUtil.writeEasy(targetPath, `\n\n===== ${source} =====\n读取失败: ${errorMessage}\n`, true)
+        LogUtil.warn('LogCollector', `读取日志文件失败: ${source} - ${errorMessage}`)
+      }
+    }
+
+    // 如果还有未处理的文件,在日志中记录
+    if (sources.length > maxFiles) {
+      const skippedCount = sources.length - maxFiles
+      await FileUtil.writeEasy(targetPath, `\n\n===== 信息 =====\n由于文件数量过多,跳过了 ${skippedCount} 个日志文件的处理。\n`, true)
+      LogUtil.info('LogCollector', `跳过了 ${skippedCount} 个日志文件以避免内存溢出`)
+    }
+  }
+
+  /**
+   * 格式化文件大小显示
+   */
+  private static formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 B'
+    const k = 1024
+    const sizes = ['B', 'KB', 'MB', 'GB']
+    const i = Math.floor(Math.log(bytes) / Math.log(k))
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+  }
+
+  /**
+   * 分块读取大文件并写入
+   */
+  private static async writeLargeFileInChunks(targetPath: string, filePath: string): Promise<void> {
+    try {
+      const chunkSize = 64 * 1024 // 64KB chunks
+      const file = FileUtil.openSync(filePath, fs.OpenMode.READ_ONLY)
+      const stat = FileUtil.lstatSync(filePath)
+      let offset = 0
+      const textDecoder = new util.TextDecoder()
+
+      await FileUtil.writeEasy(targetPath, `\n\n===== ${filePath} =====\n文件内容(分块处理):\n`, true)
+
+      while (offset < stat.size) {
+        const buffer = new ArrayBuffer(Math.min(chunkSize, stat.size - offset))
+        const bytesRead = FileUtil.readSync(file.fd, buffer, { offset })
+
+        if (bytesRead > 0) {
+          const chunk = new Uint8Array(buffer, 0, bytesRead)
+          // 转换为字符串,限制每个chunk的内容长度
+          const maxChunkLength = 50 * 1024 // 50KB
+          let content = textDecoder.decodeWithStream(chunk.slice(0, Math.min(bytesRead, maxChunkLength)))
+
+          // 如果内容被截断,添加提示
+          if (bytesRead === chunkSize && offset + bytesRead < stat.size) {
+            content += '\n... [内容被截断]'
+          }
+
+          await FileUtil.writeEasy(targetPath, content, true)
+        }
+
+        offset += bytesRead
+      }
+
+      FileUtil.closeSync(file.fd)
+
+      await FileUtil.writeEasy(targetPath, `\n===== 文件处理完成 =====\n`, true)
+    } catch (err) {
+      const errorMessage = (err instanceof Error) ? err.message : '未知错误'
+      LogUtil.error('LogCollector', `分块读取大文件失败: ${filePath} - ${errorMessage}`)
+      await FileUtil.writeEasy(targetPath, `\n===== ${filePath} =====\n分块读取失败: ${errorMessage}\n`, true)
+    }
+  }
+}

+ 11 - 0
entry/src/main/ets/common/util/LogPackager.ets

@@ -0,0 +1,11 @@
+import { DateUtil, FileUtil } from '@pura/harmony-utils'
+
+/**
+ * 简易日志打包器:将合并后的日志复制为带时间戳的包文件,便于上传。
+ */
+export class LogPackager {
+  static async pack(mergedLogPath: string): Promise<string> {
+    // 直接返回原始日志文件,避免假装压缩导致无法识别
+    return mergedLogPath
+  }
+}

+ 8 - 0
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -22,6 +22,7 @@ import { NavidromeApi, NavidromeAlbumDetail, NavidromeArtist, NavidromeArtistDet
 import { WebDavUrlUtil } from './WebDavUrlUtil';
 import MediaTable from './MediaTable';
 import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
+import { ServerLogUtil } from './ServerLogUtil';
 
 const TAG = 'heanup RemoteDriveManager';
 
@@ -1066,6 +1067,7 @@ export class RemoteDriveManager {
     const normalized = this.normalizeFullPath(fullPath);
     this.registerPathLabel('/', '根目录');
     const segments = normalized.split('/').filter(part => part.length > 0);
+    void ServerLogUtil.info(TAG, `浏览 Navidrome 路径: ${normalized} account=${ServerLogUtil.sanitizeAccount(account)}`);
 
     if (normalized === '/' || segments.length === 0) {
       const artists: NavidromeArtist[] = await this.navidromeApi.getArtists(account);
@@ -1077,6 +1079,8 @@ export class RemoteDriveManager {
       });
       this.webDavSongs = [];
       Logger.info(TAG, `从Navidrome获取到 ${artists.length} 位艺术家`);
+      void ServerLogUtil.info(TAG, `获取艺术家列表,数量: ${artists.length}`);
+      void ServerLogUtil.debug(TAG, `artists ids: ${artists.map(a => a.id).join(',')}`)
       return;
     }
 
@@ -1091,6 +1095,8 @@ export class RemoteDriveManager {
       });
       this.webDavSongs = [];
       Logger.info(TAG, `Navidrome 艺术家 ${detail.name} 包含 ${detail.albums.length} 张专辑`);
+      void ServerLogUtil.info(TAG, `进入艺术家 ${detail.name},专辑数: ${detail.albums.length}`);
+      void ServerLogUtil.debug(TAG, `artist albums: ${detail.albums.map(a => a.id).join(',')}`)
       return;
     }
 
@@ -1104,6 +1110,8 @@ export class RemoteDriveManager {
       this.webDavSongs = detail.songs.map(song => this.buildNavidromeVideoItem(song, account, detail, albumCoverUrl));
       await this.enrichSongsWithDatabase(this.webDavSongs);
       Logger.info(TAG, `Navidrome 专辑 ${detail.name} 包含 ${detail.songs.length} 首歌曲`);
+      void ServerLogUtil.info(TAG, `进入专辑 ${detail.name},歌曲数: ${detail.songs.length}`);
+      void ServerLogUtil.debug(TAG, `album songs ids: ${detail.songs.map(s => s.id).join(',')}`)
       return;
     }
 

+ 245 - 0
entry/src/main/ets/common/util/ServerLogUtil.ets

@@ -0,0 +1,245 @@
+import { DateUtil, FileUtil, LogUtil, PreferencesUtil } from '@pura/harmony-utils'
+import FileManager from '../util/FileManager'
+
+const LOG_DIR = 'ServerLog'
+const LOG_FILE = 'server.log'
+const DEFAULT_TAG = 'ServerLog'
+const LOG_UPLOAD_ENABLED = 'logUploadEnabled' // 与设置页面保持一致
+
+function getLogFilePath(): string {
+  return FileUtil.getFilesDirPath(LOG_DIR, LOG_FILE)
+}
+
+async function appendLine(line: string) {
+  await FileUtil.writeEasy(getLogFilePath(), `${line}\n`, true)
+}
+
+function formatLine(level: string, tag: string, message: string): string {
+  const time = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')
+  return `[${time}][${level}][${tag}] ${message}`
+}
+
+// 定义账号信息接口
+interface ServerAccount {
+  host?: string;
+  port?: number;
+  enableHttps?: boolean;
+  account?: string;
+  basePath?: string;
+  navidromeBasePath?: string; // Navidrome 特有路径
+  name?: string; // 账号名称
+  id?: number | string; // 账号ID
+  serverType?: string; // 服务器类型字段
+  webType?: number; // 网络类型枚举
+}
+
+export class ServerLogUtil {
+  /**
+   * 检查是否启用了日志上传功能
+   * @returns 是否启用日志上传
+   */
+  private static isLogUploadEnabled(): boolean {
+    try {
+      return PreferencesUtil.getBooleanSync(LOG_UPLOAD_ENABLED, false)
+    } catch (error) {
+      LogUtil.warn('ServerLogUtil', `检查日志上传开关失败: ${(error as Error).message}`)
+      return false
+    }
+  }
+
+  /**
+   * 清理账号敏感信息,只保留必要的调试信息
+   * @param account 账号信息对象
+   * @returns 脱敏后的账号信息字符串
+   */
+  static sanitizeAccount(account: ServerAccount | object): string {
+    // 确保 account 是一个对象
+    if (!account || typeof account !== 'object') {
+      return 'unknown-account'
+    }
+
+    // 安全地访问属性
+    const obj = account as ServerAccount;
+    const host = typeof obj.host === 'string' ? obj.host : 'unknown'
+    const port = typeof obj.port === 'number' ? obj.port : 0
+    const enableHttps = Boolean(obj.enableHttps)
+    const https = enableHttps ? 'https' : 'http'
+
+    // 处理不同的账号字段
+    let userField = ''
+    let displayName = ''
+
+    if (typeof obj.account === 'string' && obj.account.length > 0) {
+      userField = `user(${obj.account.length} chars)`
+    }
+
+    if (typeof obj.name === 'string' && obj.name.length > 0) {
+      displayName = obj.name.length > 20 ? `${obj.name.substring(0, 20)}...` : obj.name
+    }
+
+    // 处理路径信息
+    let path = ''
+    if (typeof obj.navidromeBasePath === 'string' && obj.navidromeBasePath.length > 0) {
+      path = obj.navidromeBasePath
+    } else if (typeof obj.basePath === 'string' && obj.basePath.length > 0) {
+      path = obj.basePath
+    }
+
+    // 确定服务器类型
+    let serverType = 'Unknown'
+    if (typeof obj.webType === 'number') {
+      // 如果是枚举值,可以根据需要映射到具体的类型名称
+      serverType = `Type(${obj.webType})`
+    } else if (typeof obj.serverType === 'string') {
+      serverType = obj.serverType
+    }
+
+    const user = userField.length > 0 ? userField : 'no-user'
+    const name = displayName.length > 0 ? `(${displayName})` : ''
+    const serverInfo = `${serverType} ${https}://${host}${port ? ':' + port : ''}${path}`
+
+    return `${serverInfo} ${user}${name}`
+  }
+
+  /**
+   * 记录信息级别日志
+   * @param tag 日志标签,默认为 'ServerLog'
+   * @param message 日志消息
+   */
+  static async info(tag: string = DEFAULT_TAG, message: string) {
+    // 始终输出到IDE控制台
+    LogUtil.info(tag, message)
+
+    // 只有启用日志上传时才写入文件
+    if (ServerLogUtil.isLogUploadEnabled()) {
+      try {
+        await appendLine(formatLine('INFO', tag, message))
+      } catch (error) {
+        LogUtil.warn('ServerLogUtil', `写入INFO日志失败: ${(error as Error).message}`)
+      }
+    }
+  }
+
+  /**
+   * 记录警告级别日志
+   * @param tag 日志标签,默认为 'ServerLog'
+   * @param message 日志消息
+   */
+  static async warn(tag: string = DEFAULT_TAG, message: string) {
+    // 始终输出到IDE控制台
+    LogUtil.warn(tag, message)
+
+    // 只有启用日志上传时才写入文件
+    if (ServerLogUtil.isLogUploadEnabled()) {
+      try {
+        await appendLine(formatLine('WARN', tag, message))
+      } catch (error) {
+        LogUtil.warn('ServerLogUtil', `写入WARN日志失败: ${(error as Error).message}`)
+      }
+    }
+  }
+
+  /**
+   * 记录错误级别日志
+   * @param tag 日志标签,默认为 'ServerLog'
+   * @param message 日志消息
+   */
+  static async error(tag: string = DEFAULT_TAG, message: string) {
+    // 始终输出到IDE控制台
+    LogUtil.error(tag, message)
+
+    // 只有启用日志上传时才写入文件
+    if (ServerLogUtil.isLogUploadEnabled()) {
+      try {
+        await appendLine(formatLine('ERROR', tag, message))
+      } catch (error) {
+        LogUtil.warn('ServerLogUtil', `写入ERROR日志失败: ${(error as Error).message}`)
+      }
+    }
+  }
+
+  /**
+   * 记录调试级别日志
+   * @param tag 日志标签,默认为 'ServerLog'
+   * @param message 日志消息
+   */
+  static async debug(tag: string = DEFAULT_TAG, message: string) {
+    // 始终输出到IDE控制台
+    LogUtil.debug(tag, message)
+
+    // 只有启用日志上传时才写入文件
+    if (ServerLogUtil.isLogUploadEnabled()) {
+      try {
+        await appendLine(formatLine('DEBUG', tag, message))
+      } catch (error) {
+        LogUtil.warn('ServerLogUtil', `写入DEBUG日志失败: ${(error as Error).message}`)
+      }
+    }
+  }
+
+  /**
+   * 获取日志文件路径
+   * @returns 日志文件的完整路径
+   */
+  static getLogFilePath(): string {
+    return getLogFilePath()
+  }
+
+  /**
+   * 清空日志文件
+   */
+  static async clearLog(): Promise<void> {
+    try {
+      await FileUtil.writeEasy(getLogFilePath(), '', false)
+      LogUtil.info('ServerLogUtil', '日志文件已清空')
+
+      // 如果启用了日志上传,则记录到文件
+      if (ServerLogUtil.isLogUploadEnabled()) {
+        await appendLine(formatLine('INFO', 'ServerLogUtil', '日志文件已清空'))
+      }
+    } catch (error) {
+      LogUtil.error('ServerLogUtil', `清空日志文件失败: ${(error as Error).message}`)
+    }
+  }
+
+  /**
+   * 获取日志文件大小
+   * @returns 日志文件大小(字节)
+   */
+  static async getLogFileSize(): Promise<number> {
+    try {
+      return await FileManager.getFileSize(getLogFilePath())
+    } catch (error) {
+      LogUtil.error('ServerLogUtil', `获取日志文件大小失败: ${(error as Error).message}`)
+      return 0
+    }
+  }
+
+  /**
+   * 检查日志文件是否存在
+   * @returns 是否存在日志文件
+   */
+  static logFileExists(): boolean {
+    return FileUtil.accessSync(getLogFilePath())
+  }
+
+  /**
+   * 公开方法:检查是否启用了日志上传功能
+   * @returns 是否启用日志上传
+   */
+  static isLogCollectionEnabled(): boolean {
+    return ServerLogUtil.isLogUploadEnabled()
+  }
+
+  /**
+   * 公开方法:获取日志上传开关状态描述
+   * @returns 开关状态描述字符串
+   */
+  static getLogUploadStatus(): string {
+    const enabled = ServerLogUtil.isLogUploadEnabled()
+    const filePath = getLogFilePath()
+    const exists = FileUtil.accessSync(filePath)
+
+    return `日志上传: ${enabled ? '已启用' : '已禁用'}, 文件: ${exists ? '存在' : '不存在'} (${filePath})`
+  }
+}

+ 115 - 0
entry/src/main/ets/common/util/UpTokenUtil.ets

@@ -0,0 +1,115 @@
+import util from '@ohos.util'
+import { Base64Util } from '@pura/harmony-utils'
+
+function sha1(message: Uint8Array): Uint8Array {
+  const K = [0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6]
+  const ml = message.length * 8
+  const withOne = new Uint8Array(message.length + 1)
+  withOne.set(message)
+  withOne[message.length] = 0x80
+  let zeroPad = (56 - (withOne.length % 64) + 64) % 64
+  if (zeroPad === 0) zeroPad = 64
+  const padded = new Uint8Array(withOne.length + zeroPad + 8)
+  padded.set(withOne)
+  const view = new DataView(padded.buffer)
+  view.setUint32(padded.length - 8, Math.floor(ml / 0x100000000))
+  view.setUint32(padded.length - 4, ml >>> 0)
+
+  let h0 = 0x67452301
+  let h1 = 0xEFCDAB89
+  let h2 = 0x98BADCFE
+  let h3 = 0x10325476
+  let h4 = 0xC3D2E1F0
+
+  const w = new Uint32Array(80)
+  for (let i = 0; i < padded.length; i += 64) {
+    for (let t = 0; t < 16; t++) {
+      w[t] = view.getUint32(i + t * 4)
+    }
+    for (let t = 16; t < 80; t++) {
+      const n = w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16]
+      w[t] = (n << 1) | (n >>> 31)
+    }
+    let a = h0, b = h1, c = h2, d = h3, e = h4
+    for (let t = 0; t < 80; t++) {
+      let temp = ((a << 5) | (a >>> 27)) + e + w[t] + K[Math.floor(t / 20)]
+      if (t < 20) {
+        temp += (b & c) | (~b & d)
+      } else if (t < 40) {
+        temp += b ^ c ^ d
+      } else if (t < 60) {
+        temp += (b & c) | (b & d) | (c & d)
+      } else {
+        temp += b ^ c ^ d
+      }
+      e = d
+      d = c
+      c = (b << 30) | (b >>> 2)
+      b = a
+      a = temp >>> 0
+    }
+    h0 = (h0 + a) >>> 0
+    h1 = (h1 + b) >>> 0
+    h2 = (h2 + c) >>> 0
+    h3 = (h3 + d) >>> 0
+    h4 = (h4 + e) >>> 0
+  }
+  const out = new Uint8Array(20)
+  const outView = new DataView(out.buffer)
+  outView.setUint32(0, h0)
+  outView.setUint32(4, h1)
+  outView.setUint32(8, h2)
+  outView.setUint32(12, h3)
+  outView.setUint32(16, h4)
+  return out
+}
+
+function hmacSha1(key: Uint8Array, message: Uint8Array): Uint8Array {
+  const blockSize = 64
+  if (key.length > blockSize) {
+    key = sha1(key)
+  }
+  const oKeyPad = new Uint8Array(blockSize)
+  const iKeyPad = new Uint8Array(blockSize)
+  for (let i = 0; i < blockSize; i++) {
+    const b = i < key.length ? key[i] : 0
+    oKeyPad[i] = 0x5c ^ b
+    iKeyPad[i] = 0x36 ^ b
+  }
+  const inner = sha1(concat(iKeyPad, message))
+  return sha1(concat(oKeyPad, inner))
+}
+
+function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
+  const res = new Uint8Array(a.length + b.length)
+  res.set(a)
+  res.set(b, a.length)
+  return res
+}
+
+function toUrlSafeBase64(data: Uint8Array): string {
+  const base = Base64Util.encodeToStrSync(data)
+  return base.replace(/\+/g, '-').replace(/\//g, '_')
+}
+
+export class UpTokenUtil {
+  static generateUpToken(accessKey: string, secretKey: string, bucket: string, expireSeconds: number = 3600): string {
+    const currentTime = Math.floor(Date.now() / 1000)
+    const deadline = currentTime + expireSeconds
+    const putPolicy = JSON.stringify({ scope: bucket, deadline })
+
+    // 调试信息
+    console.info(`UpToken生成 - 当前时间: ${currentTime}, 过期时间: ${deadline}, 有效期: ${expireSeconds}秒`)
+    console.info(`UpToken生成 - Bucket: ${bucket}`)
+    console.info(`UpToken生成 - Policy: ${putPolicy}`)
+
+    const policyBytes = new util.TextEncoder().encode(putPolicy)
+    const encodedPolicy = toUrlSafeBase64(policyBytes)
+    const signBytes = hmacSha1(new util.TextEncoder().encode(secretKey), new util.TextEncoder().encode(encodedPolicy))
+    const encodedSign = toUrlSafeBase64(signBytes)
+    const token = `${accessKey}:${encodedSign}:${encodedPolicy}`
+
+    console.info(`UpToken生成完成 - Token长度: ${token.length}`)
+    return token
+  }
+}

+ 85 - 1
entry/src/main/ets/pages/AboutPage.ets

@@ -1,7 +1,7 @@
 import TitleBar from '../view/TitleBar'
 import { router } from '@kit.ArkUI'
 import { CommonConstants } from '../common/constants/CommonConstants'
-import { AppUtil, DeviceUtil, DisplayUtil, ToastUtil } from '@pura/harmony-utils'
+import { AppUtil, LogUtil, ToastUtil, DateUtil, FileUtil } from '@pura/harmony-utils'
 import { Utility } from '../common/util/Utility'
 import { common, ConfigurationConstant } from '@kit.AbilityKit'
 import { BreakpointTypeEnum } from '../common/util/BreakpointSystem'
@@ -11,6 +11,8 @@ import { hilog } from '@kit.PerformanceAnalysisKit'
 import { BusinessError } from '@kit.BasicServicesKit'
 import { systemShare } from '@kit.ShareKit'
 import { uniformTypeDescriptor } from '@kit.ArkData'
+import { http } from '@kit.NetworkKit'
+import { LogCollector } from '../common/util/LogCollector'
 
 @Preview
 // @Entry
@@ -30,6 +32,10 @@ export struct AboutPage{
   @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
 
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
+  private logoClickCount: number = 0
+  private lastLogoClickTime: number = 0
+  private static readonly LOGO_TRIGGER_COUNT: number = 8
+  private static readonly LOGO_RESET_INTERVAL: number = 2000
 
   onColorModeChange() {
     this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
@@ -155,6 +161,9 @@ export struct AboutPage{
             .height(120)
             .borderRadius('100%')
             .clip(true)
+            .onClick(() => {
+              this.handleLogoClick()
+            })
           Text(this.appName+'V:'+this.verName)
             .fontColor(this.isDarkMode ? Color.White : Color.Black)
             .fontWeight(480)
@@ -190,6 +199,81 @@ export struct AboutPage{
     .height('100%')
   }
 
+  private async handleLogoClick() {
+    let now = Date.now()
+    if (now - this.lastLogoClickTime > AboutPage.LOGO_RESET_INTERVAL) {
+      this.logoClickCount = 0
+    }
+    this.logoClickCount += 1
+    this.lastLogoClickTime = now
+    if (this.logoClickCount >= AboutPage.LOGO_TRIGGER_COUNT) {
+      this.logoClickCount = 0
+      await this.collectAndUploadLogs()
+    }
+  }
+
+  private async collectAndUploadLogs() {
+    try {
+      ToastUtil.showToast('正在收集日志…')
+
+      const result = await LogCollector.collectAllLogs()
+
+      // 检查文件是否成功创建
+      if (!result.mergedLogPath || !FileUtil.accessSync(result.mergedLogPath)) {
+        throw new Error('日志文件创建失败')
+      }
+
+      const url = await LogCollector.uploadLogs(result.mergedLogPath)
+      if (url && typeof url === 'string') {
+        const timeStr = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')
+        await this.submitLogUrl(url)
+        ToastUtil.showToast('日志已上传')
+        LogUtil.info('AboutPage', `日志采集完成并上报,时间: ${timeStr}`)
+      } else {
+        ToastUtil.showToast('日志已收集并触发上传')
+        LogUtil.info('AboutPage', `日志采集完成,路径: ${result.mergedLogPath}`)
+      }
+    } catch (error) {
+      const errorMessage = (error as Error).message
+      LogUtil.error('AboutPage', `日志收集失败: ${errorMessage}`)
+
+      // 根据错误类型提供不同的用户提示
+      if (errorMessage.includes('DNS') || errorMessage.includes('resolve') || errorMessage.includes('host') || errorMessage.includes('域名')) {
+        ToastUtil.showToast('网络连接异常,请检查网络设置或稍后重试')
+      } else if (errorMessage.includes('网络') || errorMessage.includes('connect')) {
+        ToastUtil.showToast('网络连接失败,请检查网络状态')
+      } else if (errorMessage.includes('存储') || errorMessage.includes('storage') || errorMessage.includes('disk')) {
+        ToastUtil.showToast('存储空间不足,请清理后重试')
+      } else if (errorMessage.includes('不可达') || errorMessage.includes('unreachable')) {
+        ToastUtil.showToast('服务器暂时不可达,请稍后重试')
+      } else {
+        ToastUtil.showToast('日志收集失败,请稍后重试')
+      }
+
+      }
+  }
+
+  private async submitLogUrl(url: string) {
+    try {
+      const httpRequest = http.createHttp()
+      await httpRequest.request('https://pay.ss5.xyz/mussy/log_collect', {
+        method: http.RequestMethod.POST,
+        connectTimeout: 8000,
+        readTimeout: 8000,
+        expectDataType: http.HttpDataType.STRING,
+        header: {
+          'Content-Type': 'application/json'
+        },
+        extraData: JSON.stringify({
+          url: url
+        })
+      })
+      httpRequest.destroy()
+    } catch (err) {
+      LogUtil.warn('AboutPage', `日志上报接口调用失败: ${(err as Error).message}`)
+    }
+  }
+
   @Builder
   aboutItemView(){
     Column() {

+ 279 - 1
entry/src/main/ets/pages/SettingPage.ets

@@ -2,7 +2,7 @@ import TitleBar from '../view/TitleBar'
 import { promptAction, router } from '@kit.ArkUI'
 import { CommonConstants } from '../common/constants/CommonConstants'
 import { EventConstants } from '../common/constants/EventConstants'
-import { AppUtil, DeviceUtil, DisplayUtil, FileUtil, MD5, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'
+import { AppUtil, DeviceUtil, DisplayUtil, FileUtil, LogUtil, MD5, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'
 import { CustomContentDialog } from '@kit.ArkUI'
 import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant';
 import common from '@ohos.app.ability.common';
@@ -22,6 +22,9 @@ import { CustomizeICON, Icon } from '../view/CustomizeICON'
 import { appInfoManager } from '@kit.StoreKit'
 import { clearWebDavCacheByAccount, clearWebDavCaches } from '../common/network/RemoteSongCache';
 import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { LogCollector } from '../common/util/LogCollector';
+import { Uploader, UploadConfig } from '../common/network/Uploader';
+import { UpTokenUtil } from '../common/util/UpTokenUtil';
 
 @Preview
 // @Entry
@@ -77,6 +80,13 @@ export struct SettingPage {
   static readonly IS_COVER_TOP: string = 'IS_COVER_TOP';
   static readonly IS_SHOW_HEADER: string = 'IS_SHOW_HEADER';
   static readonly IS_COVER_TOP_BIG: string = 'IS_COVER_TOP_BIG';
+  static readonly LOG_UPLOAD_ENABLED: string = 'logUploadEnabled';
+  static readonly MEITU_UPLOAD_TOKEN_KEY: string = 'meituUploadToken';
+  static readonly MEITU_UPLOAD_DOMAIN_KEY: string = 'meituUploadDomain';
+  static readonly MEITU_AK: string = '7msMIyQ9xOjOwqO9JSPl';
+  static readonly MEITU_SK: string = 'JU8kLByQ1U1Zgkfni95IC9ttvfv5NLC3qO642R1Z';
+  static readonly MEITU_BUCKET: string = 'makeup-magic';
+  static readonly MEITU_DOMAIN: string = 'https://makeup-magic.zone1.meitudata.com';
   public static THEME_COLOR_KEY: string = 'THEME_COLOR';
   public static TWO_FINGER_TYPE: string = 'twoFingerType';
   public static LONG_PRESS_SPEED: string = 'longPressSpeed';
@@ -133,6 +143,8 @@ export struct SettingPage {
   @State isShowPlayPageBack: boolean = false //是否显示播放页返回键
   @State isShowPrecious: boolean = false //播控条显示上一首按钮
   @State isShowSingleLineLyric: boolean = false //是否显示单行歌词
+  @State isLogUploadEnabled: boolean = false //日志上传开关
+  @State isClearingLogs: boolean = false //是否正在清除日志
   @State isShowHeader: boolean = true
   @State isCustomizeBg: boolean = false //自定义背景界面
   @State isCustomizeBgSheet: boolean = false //自定义背景界面
@@ -291,6 +303,12 @@ export struct SettingPage {
     this.webdavUploadAutoClear = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_AUTO_CLEAR, false)
     this.webdavUploadAllowMobile = PreferencesUtil.getBooleanSync(SettingPage.WEBDAV_UPLOAD_ALLOW_MOBILE, false)
     this.webdavUploadRetryCount = PreferencesUtil.getNumberSync(SettingPage.WEBDAV_UPLOAD_RETRY_COUNT, 3)
+    this.isLogUploadEnabled = PreferencesUtil.getBooleanSync(SettingPage.LOG_UPLOAD_ENABLED, false)
+    if (this.isLogUploadEnabled) {
+      this.ensureMeituUploadHandler(true)
+    } else {
+      this.registerLogUploadDisabled()
+    }
 
     // 只用 themeMode 控制主题
     this.themeMode = PreferencesUtil.getNumberSync(SettingPage.THEME_MODE, 0)
@@ -372,6 +390,139 @@ export struct SettingPage {
     });
   }
 
+  private handleClearLogFiles() {
+    if (this.isClearingLogs) {
+      return;
+    }
+
+    // 显示确认对话框
+    AlertDialog.show({
+      title: '确认清除',
+      message: '确定要清除所有日志文件吗?此操作不可撤销。',
+      autoCancel: true,
+      alignment: DialogAlignment.Bottom,
+      primaryButton: {
+        value: '确定清除',
+        action: () => {
+          this.clearLogFiles();
+        }
+      },
+      secondaryButton: {
+        value: '取消',
+        action: () => {
+          // 取消操作
+        }
+      }
+    });
+  }
+
+  private deleteDirectoryRecursively(dirPath: string): void {
+    try {
+      if (!FileUtil.accessSync(dirPath)) {
+        return;
+      }
+
+      if (FileUtil.isDirectory(dirPath)) {
+        const files = FileUtil.listFileSync(dirPath);
+        files.forEach((file: string) => {
+          const filePath = `${dirPath}${FileUtil.separator}${file}`;
+          this.deleteDirectoryRecursively(filePath);
+        });
+        FileUtil.rmdirSync(dirPath);
+      } else {
+        FileUtil.unlinkSync(dirPath);
+      }
+    } catch (error) {
+      hilog.warn(0, 'SettingPage', `删除目录失败: ${dirPath}, 错误: ${JSON.stringify(error)}`);
+    }
+  }
+
+  private async clearLogFiles() {
+    this.isClearingLogs = true;
+    try {
+      let clearedCount = 0;
+
+      // 清除错误日志
+      const errorLogPath = FileUtil.getFilesDirPath('ErrorLog', 'errorLog.txt');
+      if (FileUtil.accessSync(errorLogPath)) {
+        FileUtil.unlinkSync(errorLogPath);
+        clearedCount++;
+      }
+
+      // 清除Navidrome日志
+      const navidromeLogDir = FileUtil.getFilesDirPath('NavidromeLog');
+      if (FileUtil.accessSync(navidromeLogDir)) {
+        try {
+          const files = FileUtil.listFileSync(navidromeLogDir);
+          files.forEach((file: string) => {
+            const filePath = `${navidromeLogDir}${FileUtil.separator}${file}`;
+            if (FileUtil.accessSync(filePath) && !FileUtil.isDirectory(filePath)) {
+              FileUtil.unlinkSync(filePath);
+              clearedCount++;
+            }
+          });
+        } catch (error) {
+          hilog.warn(0, 'SettingPage', `清除Navidrome日志目录时出错: ${JSON.stringify(error)}`);
+        }
+      }
+
+      // 清除日志上传缓存
+      const logUploadDir = FileUtil.getCacheDirPath('LogUpload');
+      if (FileUtil.accessSync(logUploadDir)) {
+        try {
+          // 递归删除目录及其内容
+          this.deleteDirectoryRecursively(logUploadDir);
+          clearedCount++;
+        } catch (error) {
+          hilog.warn(0, 'SettingPage', `清除日志上传缓存时出错: ${JSON.stringify(error)}`);
+        }
+      }
+
+      // 清除其他可能的日志文件
+      const logFilePatterns = [
+        'app.log',
+        'debug.log',
+        'info.log',
+        'warn.log',
+        'crash.log'
+      ];
+
+      logFilePatterns.forEach((fileName: string) => {
+        try {
+          const filesDir = AppUtil.getContext().filesDir;
+          const logFilePath = `${filesDir}${FileUtil.separator}${fileName}`;
+          if (FileUtil.accessSync(logFilePath)) {
+            FileUtil.unlinkSync(logFilePath);
+            clearedCount++;
+          }
+
+          // 检查cache目录
+          const cacheLogPath = `${AppUtil.getContext().cacheDir}${FileUtil.separator}${fileName}`;
+          if (FileUtil.accessSync(cacheLogPath)) {
+            FileUtil.unlinkSync(cacheLogPath);
+            clearedCount++;
+          }
+        } catch (error) {
+          hilog.warn(0, 'SettingPage', `清除日志文件 ${fileName} 时出错: ${JSON.stringify(error)}`);
+        }
+      });
+
+      if (clearedCount > 0) {
+        ToastUtil.showToast(`已清除 ${clearedCount} 个日志文件`);
+        hilog.info(0, 'SettingPage', `成功清除 ${clearedCount} 个日志文件`);
+      } else {
+        ToastUtil.showToast('未找到需要清除的日志文件');
+      }
+
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      hilog.error(0, 'SettingPage', `清除日志文件失败: ${errorMessage}`);
+      ToastUtil.showToast('清除失败,请稍后重试');
+    } finally {
+      this.isClearingLogs = false;
+    }
+  }
+
   @Builder
   pickerBuilder() {
     Column({ space: 20 }) {
@@ -1625,6 +1776,78 @@ export struct SettingPage {
             .height(55)
             .clickEffect({ level: ClickEffectLevel.HEAVY })
             Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+
+            // 日志上传开关
+            Row() {
+              SymbolGlyph($r('sys.symbol.cloud'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .margin({ left: 15 })
+              Column({ space: 4 }) {
+                Text('开启日志上传')
+                  .fontSize(15)
+                  .fontColor(Color.Gray)
+                  .fontWeight(480)
+                Text('开启后,在关于我们页连点Logo 8次将收集并上传日志')
+                  .fontSize(12)
+                  .fontColor(Color.Gray)
+              }.layoutWeight(1).margin({ left: 8 })
+              Toggle({ type: ToggleType.Switch, isOn: this.isLogUploadEnabled })
+                .selectedColor(this.themeColor)
+                .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.5})
+                .switchPointColor(Color.White)
+                .margin({ right: 18 })
+                .onChange((checked: boolean) => {
+                  this.isLogUploadEnabled = checked;
+                  PreferencesUtil.put(SettingPage.LOG_UPLOAD_ENABLED, this.isLogUploadEnabled)
+                  if (checked) {
+                    const ok = this.ensureMeituUploadHandler(false);
+                    if (ok) {
+                      ToastUtil.showToast('已开启日志上传,关于我们页连点Logo 8次将上报')
+                    }
+                  } else {
+                    this.registerLogUploadDisabled();
+                    ToastUtil.showToast('已关闭日志上传')
+                  }
+                })
+                .width(50)
+                .height(30);
+            }
+            .height(70)
+            .clickEffect({ level: ClickEffectLevel.HEAVY })
+            Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+
+            // 清除日志文件按钮 - 只在开启日志上传时显示
+            if (this.isLogUploadEnabled) {
+              Row() {
+                SymbolGlyph($r('sys.symbol.trash_fill'))
+                  .fontSize(20)
+                  .fontColor([this.themeColor])
+                  .alignSelf(ItemAlign.Center)
+                  .margin({ left: 15 })
+                Column({ space: 4 }) {
+                  Text('清除日志文件')
+                    .fontSize(15)
+                    .fontColor(Color.Gray)
+                    .fontWeight(480)
+                  Text('清除本地保存的所有日志文件')
+                    .fontSize(12)
+                    .fontColor(Color.Gray)
+                }.layoutWeight(1).margin({ left: 8 })
+                Text(this.isClearingLogs ? '清除中…' : '立即清除')
+                  .margin({ right: 18 })
+                  .fontSize(13)
+                  .fontColor(this.themeColor)
+              }
+              .height(55)
+              .clickEffect({ level: ClickEffectLevel.HEAVY })
+              .onClick(() => {
+                this.handleClearLogFiles();
+              })
+              Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
+            }
+
             Row() {
               SymbolGlyph($r('sys.symbol.trash'))
                 .fontSize(20)
@@ -2827,8 +3050,63 @@ export struct SettingPage {
     }
   }
 
+  private getMeituConfig(): UploadConfig | null {
+    // 尝试清理可能存在的旧token缓存,避免使用过期token
+    try {
+      PreferencesUtil.put(SettingPage.MEITU_UPLOAD_TOKEN_KEY, '')
+      LogUtil.info('SettingPage', '已清理旧的UpToken缓存')
+    } catch (error) {
+      LogUtil.debug('SettingPage', '清理token缓存失败,继续生成新token')
+    }
+
+    // 每次都生成新的UpToken,避免使用过期token
+    LogUtil.info('SettingPage', `生成新的UpToken,Bucket: ${SettingPage.MEITU_BUCKET}`)
+    const token = UpTokenUtil.generateUpToken(SettingPage.MEITU_AK, SettingPage.MEITU_SK, SettingPage.MEITU_BUCKET, 3600)
+    LogUtil.info('SettingPage', `UpToken生成完成,Token前缀: ${token.substring(0, 20)}...`)
+
+    // Domain可以缓存,因为通常不会变化
+    let domain = PreferencesUtil.getStringSync(SettingPage.MEITU_UPLOAD_DOMAIN_KEY, SettingPage.MEITU_DOMAIN)
+    if (!domain || domain.trim().length === 0) {
+      domain = SettingPage.MEITU_DOMAIN
+      PreferencesUtil.put(SettingPage.MEITU_UPLOAD_DOMAIN_KEY, domain)
+    }
+
+    if (!domain || domain.trim().length === 0) {
+      LogUtil.warn('SettingPage', '日志上传未配置域名')
+      return null
+    }
+
+    LogUtil.info('SettingPage', `上传配置 - Domain: ${domain}, Bucket: ${SettingPage.MEITU_BUCKET}`)
+    return {
+      uploadToken: token,
+      domain: domain,
+      keyPrefix: 'logs'
+    }
+  }
 
+  private ensureMeituUploadHandler(isInit: boolean): boolean {
+    const config = this.getMeituConfig()
+    if (!config) {
+      if (!isInit) {
+        ToastUtil.showToast('未配置日志上传token/域名,已关闭开关')
+        this.isLogUploadEnabled = false
+        PreferencesUtil.put(SettingPage.LOG_UPLOAD_ENABLED, false)
+      }
+      this.registerLogUploadDisabled()
+      return false
+    }
+    LogCollector.registerUploadHandler(async (filePath: string): Promise<string> => {
+      return await Uploader.uploadFile(filePath, config)
+    })
+    return true
+  }
 
+  private registerLogUploadDisabled() {
+    LogCollector.registerUploadHandler(async (filePath: string): Promise<void> => {
+      LogUtil.info('LogUpload', `日志上传开关关闭,跳过: ${filePath}`)
+      return
+    })
+  }
 }
 
 // 主题色类型声明

+ 4 - 0
entry/src/main/ets/view/LocalMusic.ets

@@ -86,6 +86,7 @@ import '../common/network/RemoteCacheRegistry';
 import { RemoteCacheType } from '../common/network/RemoteSongCache';
 import { findWebDavCacheIfExists, triggerWebDavCacheDownload } from '../common/network/WebDavFileCache';
 import { navidromeApi } from '../common/network/NavidromeApi';
+import { ServerLogUtil } from '../common/util/ServerLogUtil';
 
 import { HSBColorPicker, HSBColorPickerLayout } from '@keke/color-picker'
 import app from '@system.app';
@@ -509,10 +510,13 @@ async function setVideoUrlForSong(song: VideoItem, options?: MetadataExtractionO
       const navSongId = song.remote_rel_path || song.id || song.filePath;
       const streamUrl = await navidromeApi.buildStreamUrl(account, navSongId);
       Logger.info(TAG, `Navidrome 流地址构建成功: ${streamUrl}`);
+      void ServerLogUtil.info(TAG, `流地址构建成功: ${streamUrl}`);
+      void ServerLogUtil.debug(TAG, `播放songId=${navSongId}, account=${ServerLogUtil.sanitizeAccount(account)}`);
       return sanitizePlaybackUrl(streamUrl);
     } catch (error) {
       const err = error as Error;
       Logger.error(TAG, `Navidrome URL构建失败: ${err.message}`);
+      void ServerLogUtil.error(TAG, `URL构建失败: ${err.message}`);
       throw new Error(err.message);
     }
   }

+ 255 - 10
entry/src/main/ets/view/NavidromePage.ets

@@ -20,7 +20,8 @@ import { EventConstants } from '../common/constants/EventConstants';
 import { emitter } from '@kit.BasicServicesKit';
 import { setNavidromePlaylist } from '../common/util/NavidromePlaylistStore';
 import { navidromeApi } from '../common/network/NavidromeApi';
-
+import { ServerLogUtil } from '../common/util/ServerLogUtil';
+import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
 const NAVIDROME_PLAYLIST_ID = 'navidrome-playlist';
 
 interface PlaylistEventData {
@@ -123,6 +124,13 @@ export struct NavidromePage {
       }
       return;
     }
+
+    // 记录日志开关状态
+    void ServerLogUtil.info('NavidromeLoad', `日志状态: ${ServerLogUtil.getLogUploadStatus()}`);
+
+    // 记录账号信息和缓存路径
+    await this.logAccountCacheInfo(account);
+
     await this.loadNavidromeLibrary(account);
     this.doSortType(this.sortType)
   }
@@ -152,31 +160,55 @@ export struct NavidromePage {
     const ticket = ++this.loadTicket;
     this.loading = true;
     this.coverUrlCache.clear();
+
+    // 记录开始加载的信息
+    void ServerLogUtil.info('NavidromeLoad', '开始加载 Navidrome 数据库');
+    void ServerLogUtil.info('NavidromeLoad', `服务器: ${ServerLogUtil.sanitizeAccount(account)}`);
+    void ServerLogUtil.info('NavidromeLoad', `封面URL缓存已清理,当前缓存数量: ${this.coverUrlCache.size}`);
+
     try {
       const requestTasks: Promise<object>[] = [
         navidromeRestApi.fetchAllSongs(account),
         navidromeRestApi.fetchArtists(account),
         navidromeRestApi.fetchAlbums(account)
       ];
+
+      void ServerLogUtil.debug('NavidromeLoad', '并发请求歌曲、艺术家、专辑数据');
       const responses = await Promise.all(requestTasks);
       const songs = responses[0] as NavidromeRestSong[];
       const artistList = responses[1] as NavidromeRestArtist[];
       const albumList = responses[2] as NavidromeRestAlbum[];
+
       if (ticket !== this.loadTicket) {
         return;
       }
+
+      // 记录原始数据加载结果
+      void ServerLogUtil.info('NavidromeLoad', `原始数据加载完成: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
+
+      // 处理专辑封面
+      void ServerLogUtil.debug('NavidromeLoad', '开始构建专辑封面映射');
       const albumCoverMap = await this.buildAlbumCoverMap(albumList, account);
       if (ticket !== this.loadTicket) {
         return;
       }
+      void ServerLogUtil.info('NavidromeLoad', `专辑封面映射完成: ${albumCoverMap.size}/${albumList.length} 张专辑有封面`);
+
+      // 处理艺术家封面
+      void ServerLogUtil.debug('NavidromeLoad', '开始构建艺术家封面映射');
       const artistCoverMap = await this.buildArtistCoverMap(artistList, account);
       if (ticket !== this.loadTicket) {
         return;
       }
+      void ServerLogUtil.info('NavidromeLoad', `艺术家封面映射完成: ${artistCoverMap.size}/${artistList.length} 位艺术家有封面`);
+
+      // 转换歌曲数据
+      void ServerLogUtil.debug('NavidromeLoad', '开始转换歌曲为VideoItem对象');
       const videos = await this.convertSongsToVideoItems(songs, account, albumCoverMap);
       if (ticket !== this.loadTicket) {
         return;
       }
+
       this.allVideos = videos;
       this.artists = artistList.map(artist => {
         artist.coverUrl = artistCoverMap.get(artist.id);
@@ -186,15 +218,27 @@ export struct NavidromePage {
         album.coverUrl = albumCoverMap.get(album.id);
         return album;
       });
+
+      // 记录最终加载结果
       Logger.info('heanup', `Navidrome 已加载: 歌曲 ${songs.length} 首, 艺术家 ${artistList.length} 位, 专辑 ${albumList.length} 张`);
+      void ServerLogUtil.info('NavidromeLoad', `数据转换完成: 歌曲 ${songs.length} -> VideoItem ${videos.length}`);
+      void ServerLogUtil.info('NavidromePage', `加载完成: 歌曲 ${songs.length} / 艺术家 ${artistList.length} / 专辑 ${albumList.length}`);
+      void ServerLogUtil.debug('NavidromePage', `歌曲ID列表: ${songs.slice(0, 50).map(s => s.id).join(',')}${songs.length > 50 ? '...' : ''}`);
+
+      // 记录缓存统计信息
+      await this.logCacheStatistics(account);
+
     } catch (error) {
       if (ticket === this.loadTicket) {
         Logger.error('heanup', `Navidrome 数据加载失败: ${(error as Error).message}`);
+        void ServerLogUtil.error('NavidromeLoad', `数据加载失败: ${(error as Error).message}`);
+        void ServerLogUtil.error('NavidromeLoad', `失败时服务器信息: ${ServerLogUtil.sanitizeAccount(account)}`);
         ToastUtil.showToast((error as Error).message ?? 'Navidrome 数据加载失败');
       }
     } finally {
       if (ticket === this.loadTicket) {
         this.loading = false;
+        void ServerLogUtil.info('NavidromeLoad', '加载流程结束');
       }
     }
   }
@@ -202,71 +246,137 @@ export struct NavidromePage {
   private async buildAlbumCoverMap(albums: NavidromeRestAlbum[], account: WebDavAccount): Promise<Map<string, string>> {
     const map = new Map<string, string>();
     const tasks: Promise<void>[] = [];
+    let directCoverCount = 0;
+    let generatedCoverCount = 0;
+    let failedCoverCount = 0;
+
+    void ServerLogUtil.info('AlbumCover', `开始处理 ${albums.length} 张专辑的封面`);
+
     for (let i = 0; i < albums.length; i++) {
       const album = albums[i];
       tasks.push((async () => {
         try {
+          // 尝试获取直接嵌入的封面路径
           const directUrl = this.resolveEmbedCover(account, album.embedArtPath ?? album.coverArtPath);
           if (directUrl) {
             map.set(album.id, directUrl);
+            directCoverCount++;
+            void ServerLogUtil.debug('AlbumCover', `专辑 ${album.name} 使用直接封面: ${directUrl}`);
             return;
           }
+
+          // 生成封面URL
           const coverId = album.coverArt ?? album.coverArtId ?? (album.id ? `al-${album.id}` : undefined);
           const url = await this.buildCoverUrl(account, coverId);
           if (url) {
             map.set(album.id, url);
+            generatedCoverCount++;
+            void ServerLogUtil.debug('AlbumCover', `专辑 ${album.name} 生成封面URL: ${coverId} -> ${url}`);
+          } else {
+            failedCoverCount++;
+            void ServerLogUtil.warn('AlbumCover', `专辑 ${album.name} 无可用封面: ${JSON.stringify({coverArt: album.coverArt, coverArtId: album.coverArtId, id: album.id})}`);
           }
         } catch (error) {
+          failedCoverCount++;
           Logger.warn('heanup', `Navidrome 专辑封面解析失败: ${(error as Error).message}`);
+          void ServerLogUtil.warn('AlbumCover', `专辑 ${album.name} 封面解析失败: ${(error as Error).message}`);
         }
       })());
     }
+
     await Promise.all(tasks);
+
+    void ServerLogUtil.info('AlbumCover', `专辑封面处理完成: 直接嵌入 ${directCoverCount}, 生成URL ${generatedCoverCount}, 失败 ${failedCoverCount}`);
     return map;
   }
 
   private async buildArtistCoverMap(artists: NavidromeRestArtist[], account: WebDavAccount): Promise<Map<string, string>> {
     const map = new Map<string, string>();
     const tasks: Promise<void>[] = [];
+    let directCoverCount = 0;
+    let generatedCoverCount = 0;
+    let failedCoverCount = 0;
+
+    void ServerLogUtil.info('ArtistCover', `开始处理 ${artists.length} 位艺术家的封面`);
+
     for (let i = 0; i < artists.length; i++) {
       const artist = artists[i];
       tasks.push((async () => {
         try {
+          // 尝试获取已有图片URL
           const directUrl = artist.mediumImageUrl ?? artist.largeImageUrl ?? this.resolveEmbedCover(account, artist.coverArtPath);
           if (directUrl) {
             map.set(artist.id, directUrl);
+            directCoverCount++;
+            void ServerLogUtil.debug('ArtistCover', `艺术家 ${artist.name} 使用已有图片: ${directUrl}`);
             return;
           }
+
+          // 生成封面URL
           const coverId = artist.coverArt ?? artist.coverArtId ?? (artist.id ? `ar-${artist.id}` : undefined);
           const url = await this.buildCoverUrl(account, coverId, 256);
           if (url) {
             map.set(artist.id, url);
+            generatedCoverCount++;
+            void ServerLogUtil.debug('ArtistCover', `艺术家 ${artist.name} 生成封面URL: ${coverId} -> ${url}`);
+          } else {
+            failedCoverCount++;
+            void ServerLogUtil.warn('ArtistCover', `艺术家 ${artist.name} 无可用封面: ${JSON.stringify({coverArt: artist.coverArt, coverArtId: artist.coverArtId, id: artist.id})}`);
           }
         } catch (error) {
+          failedCoverCount++;
           Logger.warn('heanup', `Navidrome 艺术家封面解析失败: ${(error as Error).message}`);
+          void ServerLogUtil.warn('ArtistCover', `艺术家 ${artist.name} 封面解析失败: ${(error as Error).message}`);
         }
       })());
     }
+
     await Promise.all(tasks);
+
+    void ServerLogUtil.info('ArtistCover', `艺术家封面处理完成: 直接链接 ${directCoverCount}, 生成URL ${generatedCoverCount}, 失败 ${failedCoverCount}`);
     return map;
   }
 
   private async convertSongsToVideoItems(songs: NavidromeRestSong[], account: WebDavAccount,
     albumCoverMap: Map<string, string>): Promise<VideoItem[]> {
     const tasks: Promise<VideoItem>[] = [];
+    let successCount = 0;
+    let failedCount = 0;
+
+    void ServerLogUtil.info('SongConvert', `开始转换 ${songs.length} 首歌曲为 VideoItem`);
+
     for (let i = 0; i < songs.length; i++) {
       const song = songs[i];
       tasks.push((async () => {
-        let coverUrl: string | undefined;
         try {
-          coverUrl = await this.resolveSongCover(song, account, albumCoverMap);
+          let coverUrl: string | undefined;
+          try {
+            coverUrl = await this.resolveSongCover(song, account, albumCoverMap);
+          } catch (error) {
+            Logger.warn('heanup', `Navidrome 单曲封面解析失败: ${(error as Error).message}`);
+            void ServerLogUtil.warn('SongConvert', `歌曲 ${song.title} 封面解析失败: ${(error as Error).message}`);
+          }
+
+          const videoItem = this.convertSongToVideoItem(song, account, coverUrl);
+          successCount++;
+
+          // 记录前几首歌的详细信息用于调试
+          if (i < 3) {
+            void ServerLogUtil.debug('SongConvert', `歌曲 ${i + 1}: ${videoItem.name}, 封面: ${coverUrl ? '有' : '无'}, 路径: ${videoItem.filePath}`);
+          }
+
+          return videoItem;
         } catch (error) {
-          Logger.warn('heanup', `Navidrome 单曲封面解析失败: ${(error as Error).message}`);
+          failedCount++;
+          void ServerLogUtil.error('SongConvert', `歌曲 ${song.title} 转换失败: ${(error as Error).message}`);
+          throw new Error(`歌曲 ${song.title} 转换失败: ${(error as Error).message}`);
         }
-        return this.convertSongToVideoItem(song, account, coverUrl);
       })());
     }
-    return Promise.all(tasks);
+
+    const results = await Promise.all(tasks);
+    void ServerLogUtil.info('SongConvert', `歌曲转换完成: 成功 ${successCount}, 失败 ${failedCount}`);
+    return results;
   }
 
   private async resolveSongCover(song: NavidromeRestSong, account: WebDavAccount,
@@ -293,15 +403,79 @@ export struct NavidromePage {
     const cacheKey = `${normalizedId}_${size}`;
     const cached = this.coverUrlCache.get(cacheKey);
     if (cached) {
+      void ServerLogUtil.debug('CoverCache', `封面URL缓存命中: ${cacheKey} -> ${cached}`);
       return cached;
     }
+
+    void ServerLogUtil.debug('CoverCache', `生成封面URL: ${coverId} (尺寸: ${size})`);
     const url = await navidromeApi.buildCoverArtUrl(account, normalizedId, size);
     if (url) {
       this.coverUrlCache.set(cacheKey, url);
+      void ServerLogUtil.debug('CoverCache', `封面URL已缓存: ${cacheKey} -> ${url}`);
+    } else {
+      void ServerLogUtil.warn('CoverCache', `封面URL生成失败: ${coverId} (尺寸: ${size})`);
     }
     return url;
   }
 
+  /**
+   * 记录账号缓存信息
+   */
+  private async logAccountCacheInfo(account: WebDavAccount): Promise<void> {
+    try {
+      void ServerLogUtil.info('AccountInfo', `账号信息: ${ServerLogUtil.sanitizeAccount(account)}`);
+
+      // 记录缓存路径信息
+      const accountId = account.id?.toString() || 'unknown';
+
+      // 尝试获取Navidrome缓存路径信息
+      try {
+        const cachePathInfo = await resolveCacheFilePath(RemoteCacheType.WEBDAV, accountId, 'navidrome_test_path');
+        void ServerLogUtil.info('AccountInfo', `Navidrome缓存目录结构:`);
+        void ServerLogUtil.info('AccountInfo', `- 缓存根目录: ${cachePathInfo.cacheDir}`);
+        void ServerLogUtil.info('AccountInfo', `- 示例缓存路径: ${cachePathInfo.cachePath}`);
+      } catch (error) {
+        void ServerLogUtil.warn('AccountInfo', `缓存路径解析失败: ${(error as Error).message}`);
+      }
+
+      // 记录封面缓存状态
+      void ServerLogUtil.info('AccountInfo', `当前封面URL缓存数量: ${this.coverUrlCache.size}`);
+      if (this.coverUrlCache.size > 0) {
+        const cacheKeys = Array.from(this.coverUrlCache.keys()).slice(0, 10);
+        void ServerLogUtil.debug('AccountInfo', `封面缓存示例: ${cacheKeys.join(', ')}`);
+      }
+
+    } catch (error) {
+      void ServerLogUtil.error('AccountInfo', `记录账号信息失败: ${(error as Error).message}`);
+    }
+  }
+
+  /**
+   * 记录缓存统计信息
+   */
+  private async logCacheStatistics(account: WebDavAccount): Promise<void> {
+    try {
+      void ServerLogUtil.info('CacheStats', '=== Navidrome 缓存统计 ===');
+      void ServerLogUtil.info('CacheStats', `封面URL缓存: ${this.coverUrlCache.size} 个条目`);
+      void ServerLogUtil.info('CacheStats', `已加载歌曲: ${this.allVideos.length} 首`);
+      void ServerLogUtil.info('CacheStats', `已处理艺术家: ${this.artists.length} 位`);
+      void ServerLogUtil.info('CacheStats', `已处理专辑: ${this.albums.length} 张`);
+
+      // 计算封面统计
+      const songsWithCover = this.allVideos.filter(song => song.pixelMapPath && song.pixelMapPath.length > 0).length;
+      const albumsWithCover = this.albums.filter(album => album.coverUrl && album.coverUrl.length > 0).length;
+      const artistsWithCover = this.artists.filter(artist => artist.coverUrl && artist.coverUrl.length > 0).length;
+
+      void ServerLogUtil.info('CacheStats', `封面统计:`);
+      void ServerLogUtil.info('CacheStats', `- 歌曲有封面: ${songsWithCover}/${this.allVideos.length} (${Math.round(songsWithCover / this.allVideos.length * 100)}%)`);
+      void ServerLogUtil.info('CacheStats', `- 专辑有封面: ${albumsWithCover}/${this.albums.length} (${Math.round(albumsWithCover / this.albums.length * 100)}%)`);
+      void ServerLogUtil.info('CacheStats', `- 艺术家有封面: ${artistsWithCover}/${this.artists.length} (${Math.round(artistsWithCover / this.artists.length * 100)}%)`);
+
+    } catch (error) {
+      void ServerLogUtil.error('CacheStats', `记录缓存统计失败: ${(error as Error).message}`);
+    }
+  }
+
   private resolveEmbedCover(account: WebDavAccount, path?: string): string | undefined {
     return navidromeRestApi.resolveResourceUrl(account, path);
   }
@@ -552,6 +726,14 @@ export struct NavidromePage {
   doSortType(index: number) {
     this.sortType = index;
     const songs = this.isSearchMode ? this.filteredList :this.allVideos;
+    const sortTypeNames = ['名称升序', '名称降序', '艺术家升序', '艺术家降序', '专辑升序', '专辑降序'];
+    const sortTypeName = sortTypeNames[index] || '未知排序';
+
+    // 记录排序操作
+    void ServerLogUtil.info('NavidromeSort', `应用排序: ${sortTypeName} (${index})`);
+    void ServerLogUtil.info('NavidromeSort', `排序范围: ${songs.length} 首歌曲 (${this.isSearchMode ? '搜索结果' : '全部歌曲'})`);
+
+    const startTime = Date.now();
 
     // 对歌曲列表进行排序
     switch (index) {
@@ -593,10 +775,17 @@ export struct NavidromePage {
         break;
     }
 
+    const sortTime = Date.now() - startTime;
+
     // 更新显示列表
     if (this.isSearchMode) {
       this.filteredList = [...songs];
     }
+
+    // 记录排序结果
+    void ServerLogUtil.info('NavidromeSort', `排序完成: ${sortTypeName}`);
+    void ServerLogUtil.info('NavidromeSort', `- 排序耗时: ${sortTime}ms`);
+    void ServerLogUtil.debug('NavidromeSort', `排序结果示例: ${songs.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`);
   }
 
   // 实时搜索逻辑
@@ -604,10 +793,16 @@ export struct NavidromePage {
     this.searchText = value.trim();
     let mSearchList: Array<VideoItem> = [...this.allVideos];
 
+    // 记录搜索操作
+    void ServerLogUtil.debug('NavidromeSearch', `搜索输入: "${value}" -> "${this.searchText}"`);
+    void ServerLogUtil.debug('NavidromeSearch', `搜索范围: ${mSearchList.length} 首歌曲`);
+
     // 新增条件判断:空输入时显示所有数据
     if (this.searchText === '') {
       this.filteredList = [...mSearchList]; // 创建新数组以触发状态更新
+      void ServerLogUtil.info('NavidromeSearch', '搜索已清空,显示所有歌曲');
     } else {
+      const startTime = Date.now();
       this.filteredList = mSearchList.filter((item: VideoItem) => {
         // 支持模糊匹配和艺术家 专辑匹配
         const regex = new RegExp(this.searchText.toLowerCase().replace(/\s+/g, '.*'), 'i');
@@ -616,8 +811,18 @@ export struct NavidromePage {
           regex.test(item.artist?.toLowerCase() ?? "") ||
           regex.test(item.album?.toLowerCase() ?? "")
       });
-    }
+      const searchTime = Date.now() - startTime;
 
+      // 记录搜索结果
+      void ServerLogUtil.info('NavidromeSearch', `搜索完成: "${this.searchText}"`);
+      void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
+      void ServerLogUtil.info('NavidromeSearch', `- 匹配结果: ${this.filteredList.length}/${mSearchList.length}`);
+      void ServerLogUtil.debug('NavidromeSearch', `搜索结果示例: ${this.filteredList.slice(0, 3).map(s => `${s.name} (${s.artist})`).join(', ')}`);
+
+      if (this.filteredList.length === 0) {
+        void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${this.searchText}"`);
+      }
+    }
   }
 
   private getCurrentCount(): number {
@@ -904,7 +1109,14 @@ export struct NavidromePage {
 
   private applyFilter(type: NavFilterType, id: string, label: string): void {
     Logger.info('heanup', `应用筛选: type=${type}, id=${id}, label=${label}`);
-    
+
+    // 记录筛选操作
+    void ServerLogUtil.info('NavidromeFilter', `应用筛选:`);
+    void ServerLogUtil.info('NavidromeFilter', `- 类型: ${type} (${type === NavFilterType.Artist ? '艺术家' : type === NavFilterType.Album ? '专辑' : '无'})`);
+    void ServerLogUtil.info('NavidromeFilter', `- ID: ${id}`);
+    void ServerLogUtil.info('NavidromeFilter', `- 标签: ${label}`);
+    void ServerLogUtil.info('NavidromeFilter', `- 筛选前总歌曲数: ${this.allVideos.length}`);
+
     // 先更新状态
     this.filterType = type;
     this.filterId = id;
@@ -913,14 +1125,22 @@ export struct NavidromePage {
     this.isSearchMode = false;
     this.searchText = '';
     this.filteredList = [];
-    
+
     // 调试日志:检查筛选结果
     const visibleSongs = this.getVisibleSongs();
     Logger.info('heanup', `筛选后歌曲数量: ${visibleSongs.length}, 总歌曲数: ${this.allVideos.length}`);
     if (visibleSongs.length > 0) {
       Logger.info('heanup', `第一首歌: ${visibleSongs[0].name}, albumId=${visibleSongs[0].navAlbumId}, album=${visibleSongs[0].album}`);
+
+      // 记录筛选结果详情
+      void ServerLogUtil.info('NavidromeFilter', `筛选结果统计:`);
+      void ServerLogUtil.info('NavidromeFilter', `- 筛选后歌曲数: ${visibleSongs.length}`);
+      void ServerLogUtil.info('NavidromeFilter', `- 筛选成功率: ${Math.round(visibleSongs.length / this.allVideos.length * 100)}%`);
+      void ServerLogUtil.debug('NavidromeFilter', `筛选结果示例: ${visibleSongs.slice(0, 5).map(s => `${s.name} (${s.artist})`).join(', ')}`);
+    } else {
+      void ServerLogUtil.warn('NavidromeFilter', `筛选结果为空: 未找到匹配的歌曲`);
     }
-    
+
     // 然后执行动画切换标签页
     this.getUIContext().animateTo({ duration: 555 }, () => {
       this.selectedTab = 0;
@@ -945,10 +1165,26 @@ export struct NavidromePage {
         ToastUtil.showToast('Navidrome账号信息不完整,无法播放');
         return;
       }
+
+      // 记录播放详细信息
       Logger.info('heanup', `Navidrome 播放歌曲: ${song.name}, 索引: ${index}`);
+      void ServerLogUtil.info('NavidromePlay', `开始播放歌曲: ${song.name} (#${index})`);
+      void ServerLogUtil.info('NavidromePlay', `歌曲信息:`);
+      void ServerLogUtil.info('NavidromePlay', `- ID: ${song.id}`);
+      void ServerLogUtil.info('NavidromePlay', `- 艺术家: ${song.artist || '未知'}`);
+      void ServerLogUtil.info('NavidromePlay', `- 专辑: ${song.album || '未知'}`);
+      void ServerLogUtil.info('NavidromePlay', `- 文件大小: ${song.size || '未知'}`);
+      void ServerLogUtil.info('NavidromePlay', `- 时长: ${song.duration || '未知'}`);
+      void ServerLogUtil.info('NavidromePlay', `- 封面: ${song.pixelMapPath ? '有' : '无'}`);
+      void ServerLogUtil.info('NavidromePlay', `- 播放路径: ${song.filePath}`);
+
       const targetIndex = this.allVideos.findIndex(item => item.id === song.id);
       const startIndex = targetIndex >= 0 ? targetIndex : index;
+
+      void ServerLogUtil.info('NavidromePlay', `播放列表设置: 起始索引 ${startIndex}, 总数 ${this.allVideos.length}`);
+
       setNavidromePlaylist(this.allVideos, startIndex);
+
       const playlistData: PlaylistEventData = {
         playlistId: NAVIDROME_PLAYLIST_ID,
         playlistName: `Navidrome - ${account.name ?? '未知账户'}`,
@@ -957,18 +1193,27 @@ export struct NavidromePage {
         isJump: isJump,//设置true会弹出播放页
         songFilePaths: this.allVideos.map(item => item.filePath)
       };
+
       const eventPlaylistPlay: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAYLIST_PLAY };
       emitter.emit(eventPlaylistPlay, { data: playlistData });
+
       Logger.info('heanup', `Navidrome 发送播放事件,歌曲数: ${this.allVideos.length}, 起始: ${index}`);
+      void ServerLogUtil.info('NavidromePlay', `播放事件已发送`);
+      void ServerLogUtil.debug('NavidromePlay', `播放事件详情: 列表ID=${playlistData.playlistId}, 列表名称=${playlistData.playlistName}, 歌曲数=${playlistData.songCount}, 起始索引=${playlistData.startIndex}, 是否跳转=${playlistData.isJump}`);
+
       if(!this.isNoJumpToHome){
+        void ServerLogUtil.debug('NavidromePlay', '跳转到首页播放器');
         // 跳转到首页播放器
         this.getUIContext()?.animateTo({ duration: 555 }, () => {
           this.mType = 0
         })
       }
+
     } catch (error) {
       const err = error as Error;
       Logger.error('heanup', '播放歌曲失败: ' + err.message);
+      void ServerLogUtil.error('NavidromePlay', `播放失败: ${err.message}`);
+      void ServerLogUtil.error('NavidromePlay', `失败歌曲信息: ${song.name} (${song.id})`);
       ToastUtil.showToast('播放失败');
     }
   }