Skip to content

优化维修状态管理 #10

Description

@ilevalser

优化维修状态管理

痛点:
有维修时30s轮询没必要,GUI关闭无法持久化维修状态

脆弱点 位置 风险描述 当前处理
repair_seconds 未使用 RepairManager.ts:134-154 后端返回了 repair_seconds,前端完全忽略 无处理
bathingShips 不持久化 RepairManager.ts:44 GUI 重启后所有泡澡记录丢失 无处理
固定 30 秒轮询 TaskQueue.ts:165-173 无论维修时长都固定 30 秒重试 无处理
无超时保护 RepairManager.ts:134-154 sendToBath() 中 API 调用失败时删除记录,但未重试 简单 try/catch + delete

以下是DS给的建议:

目录

  1. 消费 repair_seconds + 超时保护
  2. 动态延迟计算 + 回退策略
  3. localStorage 持久化 + 启动恢复

1. 消费 repair_seconds + 超时保护

当前状态

后端 /api/repair/ship 已返回:

return ApiResponse(
    success=True,
    data={'ship_name': request.ship_name, 'repair_seconds': repair_secs},
    message=f'{request.ship_name} 已送入泡澡修理 ({repair_secs}s)',
)

但前端 RepairManager.sendToBath() 完全忽略了响应:

// 当前代码 — 第 144-148 行
await this.api.repairShip(name);
const entry = this.bathingShips.get(key);
if (entry) entry.requestSent = true;

同时 BathingShip 接口缺少 repairEndTime 字段:

export interface BathingShip {
  name: string;
  startTime: number;       // 已记录
  requestSent: boolean;
  // ❌ 缺少 repairEndTime
}

修改清单

文件 1: AutoWSGR-GUI/src/model/scheduler/RepairManager.ts

修改 A — BathingShip 接口(第 22-29 行)
操作 BathingShip 中新增 repairEndTime 字段
位置 第 22-29 行
改动 requestSent: boolean 之后添加 repairEndTime: number
export interface BathingShip {
  name: string;
  startTime: number;
  /** 预计维修完成时间 (Date.now() 时间戳)。0 表示未知,需回退到轮询模式 */
  repairEndTime: number;
  requestSent: boolean;
}

设计理由repairEndTime = 0 作为哨兵值,语义为"维修时间不可信,回退到轮询模式"。这与 Date.now() 返回的正整数天然不冲突。

修改 B — sendToBath() 方法(第 134-154 行)
操作 重写 sendToBath() 方法,消费 repair_seconds
位置 第 134-154 行
改动 见下方完整替换代码
async sendToBath(shipNames: string[]): Promise<void> {
  for (const name of shipNames) {
    const key = toBackendName(name);
    if (this.bathingShips.has(key)) continue;

    // 先占位标记(防止并发重复送修)
    this.bathingShips.set(key, {
      name,
      startTime: Date.now(),
      repairEndTime: 0,   // 未知,待 API 返回后更新
      requestSent: false,
    });

    try {
      const resp = await this.api.repairShip(name);
      const entry = this.bathingShips.get(key);
      if (!entry) continue; // 已被外部清除

      // ── 核心改动:消费 repair_seconds ──
      const rawSeconds: unknown = (resp.data as any)?.repair_seconds;
      const validatedSeconds = RepairManager.validateRepairSeconds(rawSeconds);

      if (validatedSeconds > 0) {
        entry.repairEndTime = Date.now() + validatedSeconds * 1000;
        Logger.info(
          `舰船「${name}」已送入泡澡修理,预计 ${validatedSeconds} 秒后完成`,
          'repair',
        );
      } else {
        entry.repairEndTime = 0; // 不可信,回退轮询
        Logger.info(
          `舰船「${name}」已送入泡澡修理(维修时间未知,将轮询检查)`,
          'repair',
        );
      }

      entry.requestSent = true;
      this.saveToStorage(); // 持久化(见 2.2.3)
    } catch (e) {
      Logger.error(`舰船「${name}」送入泡澡失败: ${e}`, 'repair');
      // 不删除记录,保留 repairEndTime=0 以便后续重试
      const entry = this.bathingShips.get(key);
      if (entry) {
        entry.requestSent = false;
      }
    }
  }
}

关键变化

  1. 先占位再发请求 → 防止并发重复送修
  2. resp.data.repair_seconds 提取维修秒数
  3. 通过 validateRepairSeconds() 校验合理性
  4. 可信 → 设置 repairEndTime;不可信 → 保持 0
  5. 失败时不删除记录,保留 requestSent=false 供下次轮询重试
  6. 成功后调用 saveToStorage() 持久化
修改 C — 新增 validateRepairSeconds() 静态方法
操作 新增私有静态方法
位置 needsRepair() 方法之后(第 129 行之后)
改动 新增以下方法
/**
 * 校验后端返回的维修秒数是否在合理范围内。
 *
 * 合理范围: 60 秒(最低维修时间)~ 86400 秒(24 小时,大破高等级船)。
 * 不合理时返回 0,调用方应回退到轮询模式。
 *
 * @param seconds 后端返回的原始值(可能为 null/undefined/非数字)
 * @returns 校验通过的秒数,或 0(不可信)
 */
private static validateRepairSeconds(seconds: unknown): number {
  if (typeof seconds !== 'number' || !Number.isFinite(seconds)) {
    return 0;
  }
  if (seconds >= 60 && seconds <= 86400) {
    return seconds;
  }
  // 超出合理范围 → 不可信
  Logger.warn(`维修秒数 ${seconds}s 超出合理范围 [60, 86400],回退到轮询模式`, 'repair');
  return 0;
}

设计理由

  • 防御性编程:resp.data 可能是 undefinedrepair_seconds 可能是 null 或字符串
  • 60 秒下限:游戏内最低维修时间(1 级船擦伤)约 60 秒
  • 86400 秒上限:24 小时,覆盖大破 100+ 级船的维修时间
  • 超出范围时记录警告日志,便于调试
修改 D — refreshBathingStatus() 方法(第 159-190 行)
操作 refreshBathingStatus() 中增加 repairEndTime 的清理逻辑
位置 第 159-190 行
改动 在删除已修好舰船记录后,调用 saveToStorage()
// 在现有代码第 184 行(this.bathingShips.delete(name))之后添加:
this.saveToStorage();

完整修改后的 refreshBathingStatus() 相关段落(第 182-186 行):

if (!this.needsRepair(ship, threshold)) {
  Logger.info(`舰船「${ship.name}」泡澡修理完成`, 'repair');
  this.bathingShips.delete(name);
  this.saveToStorage(); // ← 新增:持久化变更
}
修改 E — clearAll() 方法(第 224-226 行)
操作 clearAll() 中增加持久化清理
位置 第 224-226 行
改动 this.bathingShips.clear() 后调用 this.saveToStorage()
clearAll(): void {
  this.bathingShips.clear();
  this.saveToStorage(); // ← 新增:清除持久化数据
}

2. 动态延迟计算 + 回退策略

当前状态

TaskQueue.scheduleDeferredRetry() 使用固定 30 秒:

scheduleDeferredRetry(onRetry: () => void, emitLog: (level: string, msg: string) => void): void {
  if (this.deferredRetryTimer) return;
  emitLog('info', '所有任务因修理被阻塞,30 秒后重试...');
  this.deferredRetryTimer = setTimeout(() => {
    this.deferredRetryTimer = null;
    this.retryDeferredTasks(emitLog);
    onRetry();
  }, 30_000);
}

修改清单

文件 2: AutoWSGR-GUI/src/model/scheduler/TaskQueue.ts

修改 A — scheduleDeferredRetry() 方法签名(第 165 行)
操作 新增 bathingShips 参数
位置 第 165 行
改动 方法签名增加第三个参数
scheduleDeferredRetry(
  onRetry: () => void,
  emitLog: (level: string, msg: string) => void,
  bathingShips?: Map<string, BathingShip>,  // ← 新增
): void {

需要在文件顶部引入 BathingShip 类型:

import type { BathingShip } from './RepairManager';
修改 B — scheduleDeferredRetry() 方法体(第 166-173 行)
操作 重写方法体,实现动态延迟计算
位置 第 166-173 行
改动 见下方完整替换代码
scheduleDeferredRetry(
  onRetry: () => void,
  emitLog: (level: string, msg: string) => void,
  bathingShips?: Map<string, BathingShip>,
): void {
  if (this.deferredRetryTimer) return;

  const waitMs = this.calculateDynamicWait(bathingShips);

  if (waitMs <= 0) {
    // 所有船 repairEndTime 都不可信 → 使用保守的 30 秒轮询
    emitLog('info', '维修时间未知,30 秒后重试...');
    this.deferredRetryTimer = setTimeout(() => {
      this.deferredRetryTimer = null;
      this.retryDeferredTasks(emitLog);
      onRetry();
    }, 30_000);
  } else {
    const waitSeconds = Math.ceil(waitMs / 1000);
    emitLog('info', `预计 ${waitSeconds} 秒后维修完成,等待中...`);
    this.deferredRetryTimer = setTimeout(() => {
      this.deferredRetryTimer = null;
      this.retryDeferredTasks(emitLog);
      onRetry();
    }, waitMs);
  }
}
修改 C — 新增 calculateDynamicWait() 方法
操作 新增私有方法
位置 clearDeferredTimer() 方法之后(第 191 行之后)
改动 新增以下方法
/**
 * 根据泡澡中舰船的 repairEndTime 计算动态等待时间。
 *
 * 策略:
 * - 有可信 repairEndTime → 取最早完成时间 + 5 秒缓冲,最多 30 秒
 * - 所有 repairEndTime 都不可信(0 或已过期)→ 返回 -1,由调用方回退到 30 秒
 * - 已过预期完成时间 → 5 秒后立即检查
 *
 * @param bathingShips 泡澡中舰船列表
 * @returns 等待毫秒数,或 -1(全部不可信,应使用默认 30 秒)
 */
private calculateDynamicWait(bathingShips?: Map<string, BathingShip>): number {
  if (!bathingShips || bathingShips.size === 0) {
    return -1; // 无泡澡中舰船,回退到 30 秒
  }

  let minEndTime = Infinity;
  let hasValidTime = false;

  for (const ship of bathingShips.values()) {
    if (ship.repairEndTime && ship.repairEndTime > 0) {
      minEndTime = Math.min(minEndTime, ship.repairEndTime);
      hasValidTime = true;
    }
  }

  if (!hasValidTime) return -1; // 全部不可信,回退到 30 秒

  const now = Date.now();
  if (minEndTime <= now) {
    // 已过预期时间,5 秒后立即检查(给游戏动画留缓冲)
    return 5_000;
  }

  // 修好后额外等 5 秒缓冲,但不超过 30 秒
  const rawWait = minEndTime - now + 5_000;
  return Math.min(rawWait, 30_000);
}

设计理由

  • 5 秒缓冲:游戏动画延迟可能导致实际修好时间略晚于 OCR 识别时间
  • 30 秒上限:防止极端情况(如 repairEndTime 在几小时后)导致定时器过长,期间用户可能手动操作
  • 返回 -1 而非直接设 30 秒:将决策权交给调用方,保持单一职责

文件 3: AutoWSGR-GUI/src/model/scheduler/Scheduler.ts

scheduleDeferredRetry() 的调用点有两处,都需要传递 bathingShips

修改 D — consumeNext() 中的调用(第 274-277 行)
操作 传递 bathingShips 参数
位置 第 274-277 行
改动 在第三个参数传入 this.repairManager.bathingShips
// 修改前(第 274-277 行):
this._taskQueue.scheduleDeferredRetry(
  () => this.consumeNext(),
  (level, msg) => this.emitLog(level, msg),
);

// 修改后:
this._taskQueue.scheduleDeferredRetry(
  () => this.consumeNext(),
  (level, msg) => this.emitLog(level, msg),
  this.repairManager.bathingShips,  // ← 新增
);

注意bathingShipsRepairManagerprivate 属性。需要将其改为 public 或新增一个 getter。

修改 E — RepairManager.bathingShips 访问权限(第 44 行)
操作 private 改为 public 或新增 getter
位置 第 44 行
改动 推荐方案:新增 public getBathingShips() 方法

RepairManager 中新增:

/** 获取泡澡中舰船列表(只读引用,供 TaskQueue 计算动态延迟) */
getBathingShips(): ReadonlyMap<string, BathingShip> {
  return this.bathingShips;
}

然后在 Scheduler 中调用:

this._taskQueue.scheduleDeferredRetry(
  () => this.consumeNext(),
  (level, msg) => this.emitLog(level, msg),
  this.repairManager.getBathingShips() as Map<string, BathingShip>,
);
修改 F — deferCurrentTask() 中的调用(第 532-535 行)
操作 传递 bathingShips 参数
位置 第 532-535 行
改动 同上,在第三个参数传入
// 修改前(第 532-535 行):
this._taskQueue.scheduleDeferredRetry(
  () => this.consumeNext(),
  (level, msg) => this.emitLog(level, msg),
);

// 修改后:
this._taskQueue.scheduleDeferredRetry(
  () => this.consumeNext(),
  (level, msg) => this.emitLog(level, msg),
  this.repairManager.getBathingShips() as Map<string, BathingShip>,
);

3. localStorage 持久化 + 启动恢复

当前状态

RepairManagerbathingShips 是纯内存 Map

private bathingShips: Map<string, BathingShip> = new Map();

GUI 重启后所有记录丢失,导致:

  1. 已送入泡澡的船被重新送修(后端会再次导航到浴室)
  2. 维修时间信息丢失,回退到 30 秒轮询
  3. 编队预设轮换状态丢失

修改清单

文件: AutoWSGR-GUI/src/model/scheduler/RepairManager.ts

修改 A — 新增常量(第 41 行,class RepairManager 内部)
操作 新增 STORAGE_KEY 常量
位置 private bathingShips 之后(第 44 行之后)
改动 新增一行
private static readonly STORAGE_KEY = 'autowsgr_bathing_ships';
修改 B — constructor()(第 46-48 行)
操作 在构造函数末尾调用 restoreFromStorage()
位置 第 46-48 行
改动 this.api = api; 之后添加
constructor(api: ApiClient) {
  this.api = api;
  this.restoreFromStorage();  // ← 新增:启动时恢复持久化状态
}
修改 C — 新增 saveToStorage() 方法
操作 新增私有方法
位置 clearAll() 方法之后(第 226 行之后)
改动 新增以下方法
/**
 * 将 bathingShips 持久化到 localStorage。
 *
 * 仅保存尚未完成的维修记录:
 * - 有 repairEndTime 且未过期 → 保存
 * - 无 repairEndTime(=0,未知)→ 保存(需要继续轮询)
 * - 有 repairEndTime 但已过期 → 不保存(已修好)
 */
private saveToStorage(): void {
  try {
    const now = Date.now();
    const data = Array.from(this.bathingShips.entries())
      .filter(([, ship]) => {
        // repairEndTime=0 表示未知,需要保存以继续轮询
        if (ship.repairEndTime === 0) return true;
        // repairEndTime 未过期 → 仍在维修中
        return ship.repairEndTime > now;
      })
      .map(([key, ship]) => ({
        key,
        name: ship.name,
        startTime: ship.startTime,
        repairEndTime: ship.repairEndTime,
        requestSent: ship.requestSent,
      }));

    if (data.length === 0) {
      // 没有需要保存的记录 → 清除存储(避免残留脏数据)
      localStorage.removeItem(RepairManager.STORAGE_KEY);
    } else {
      localStorage.setItem(RepairManager.STORAGE_KEY, JSON.stringify(data));
    }
  } catch (e) {
    Logger.warn(`保存泡澡状态失败: ${e}`, 'repair');
  }
}

设计理由

  • filter 排除已过期的记录,避免重启后加载过时的维修状态
  • repairEndTime === 0 的记录也要保存,因为需要继续轮询确认
  • 无有效记录时 removeItem 而非写入空数组,避免存储脏数据
  • 整个方法用 try/catch 包裹,因为 localStorage 在隐私模式下可能抛出异常
修改 D — 新增 restoreFromStorage() 方法
操作 新增私有方法
位置 saveToStorage() 方法之后
改动 新增以下方法
/**
 * 从 localStorage 恢复泡澡状态。
 *
 * 在构造函数中调用,用于 GUI 重启后恢复维修记录。
 * 恢复后会自动调用 refreshBathingStatus 确认实际状态(由 Scheduler 触发)。
 *
 * 恢复规则:
 * - repairEndTime 已过期 → 跳过(船已修好)
 * - repairEndTime 未过期 → 恢复
 * - repairEndTime = 0(未知)→ 恢复,继续轮询
 */
private restoreFromStorage(): void {
  try {
    const raw = localStorage.getItem(RepairManager.STORAGE_KEY);
    if (!raw) return;

    const data: Array<{
      key: string;
      name: string;
      startTime: number;
      repairEndTime: number;
      requestSent: boolean;
    }> = JSON.parse(raw);

    if (!Array.isArray(data)) {
      Logger.warn('泡澡状态数据格式异常,已清空', 'repair');
      localStorage.removeItem(RepairManager.STORAGE_KEY);
      return;
    }

    const now = Date.now();
    let restoredCount = 0;

    for (const item of data) {
      // 数据完整性校验
      if (!item.key || !item.name) continue;

      // 已过期的记录不恢复
      if (item.repairEndTime > 0 && item.repairEndTime <= now) continue;

      this.bathingShips.set(item.key, {
        name: item.name,
        startTime: item.startTime,
        repairEndTime: item.repairEndTime,
        requestSent: item.requestSent,
      });
      restoredCount++;
    }

    if (restoredCount > 0) {
      Logger.info(
        `从本地存储恢复了 ${restoredCount} 艘泡澡中的舰船,将在下次任务前确认状态`,
        'repair',
      );
    }
  } catch (e) {
    Logger.warn(`恢复泡澡状态失败,已清空: ${e}`, 'repair');
    // 恢复失败时清空,避免损坏数据导致循环报错
    this.bathingShips.clear();
    localStorage.removeItem(RepairManager.STORAGE_KEY);
  }
}

设计理由

  • 数据完整性校验:防止 localStorage 被外部篡改或格式不兼容
  • 恢复失败时 clear() + removeItem():防止损坏数据导致每次操作都报错
  • 恢复后不立即调用 refreshBathingStatus():由 Scheduler 在下次 consumeNext() 时自然触发
修改 E — 在 sendToBath() 中调用 saveToStorage()

已在 1. 修改 B 中覆盖:API 成功后调用 this.saveToStorage()

修改 F — 在 refreshBathingStatus() 中调用 saveToStorage()

已在 2. 修改 D 中覆盖:删除已修好记录后调用 this.saveToStorage()

修改 G — 在 clearAll() 中调用 saveToStorage()

已在 3. 修改 E 中覆盖:clear() 后调用 this.saveToStorage()


修改文件汇总

文件 修改类型 改动内容
RepairManager.ts 接口扩展 BathingShip 新增 repairEndTime: number
RepairManager.ts 方法重写 sendToBath() 消费 repair_seconds,失败不删除记录
RepairManager.ts 新增方法 validateRepairSeconds() 静态校验方法
RepairManager.ts 新增方法 saveToStorage() / restoreFromStorage() 持久化
RepairManager.ts 新增方法 getBathingShips() 只读访问器
RepairManager.ts 方法修改 refreshBathingStatus() 删除记录后调用 saveToStorage()
RepairManager.ts 方法修改 clearAll() 清除后调用 saveToStorage()
RepairManager.ts 构造函数 末尾调用 restoreFromStorage()
TaskQueue.ts 方法重写 scheduleDeferredRetry() 新增 bathingShips 参数,动态延迟
TaskQueue.ts 新增方法 calculateDynamicWait() 动态等待时间计算
TaskQueue.ts 新增 import 引入 BathingShip 类型
Scheduler.ts 调用修改 consumeNext()deferCurrentTask() 中传递 bathingShips

实现顺序建议

第 1 步: BathingShip 接口 + validateRepairSeconds() + sendToBath() 重写
  └→ 这是核心改动,其他都依赖它

第 2 步: TaskQueue 动态延迟
  └→ 依赖第 1 步的 repairEndTime 字段

第 3 步: localStorage 持久化
  └→ 依赖第 1 步的 sendToBath() 重写
  └→ 可以在第 1 步之后独立进行

第 4 步: Scheduler 调用点修改
  └→ 依赖第 2 步的 scheduleDeferredRetry 新签名

每个步骤都是独立可测试的,可以分步合并到主分支。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions