优化维修状态管理
痛点:
有维修时30s轮询没必要,GUI关闭无法持久化维修状态
以下是DS给的建议:
目录
- 消费
repair_seconds + 超时保护
- 动态延迟计算 + 回退策略
- 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
}
修改清单
修改 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;
}
}
}
}
关键变化:
- 先占位再发请求 → 防止并发重复送修
- 从
resp.data.repair_seconds 提取维修秒数
- 通过
validateRepairSeconds() 校验合理性
- 可信 → 设置
repairEndTime;不可信 → 保持 0
- 失败时不删除记录,保留
requestSent=false 供下次轮询重试
- 成功后调用
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 可能是 undefined,repair_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);
}
修改清单
修改 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 秒:将决策权交给调用方,保持单一职责
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, // ← 新增
);
注意:bathingShips 是 RepairManager 的 private 属性。需要将其改为 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 持久化 + 启动恢复
当前状态
RepairManager 的 bathingShips 是纯内存 Map:
private bathingShips: Map<string, BathingShip> = new Map();
GUI 重启后所有记录丢失,导致:
- 已送入泡澡的船被重新送修(后端会再次导航到浴室)
- 维修时间信息丢失,回退到 30 秒轮询
- 编队预设轮换状态丢失
修改清单
修改 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()。
修改文件汇总
实现顺序建议
第 1 步: BathingShip 接口 + validateRepairSeconds() + sendToBath() 重写
└→ 这是核心改动,其他都依赖它
第 2 步: TaskQueue 动态延迟
└→ 依赖第 1 步的 repairEndTime 字段
第 3 步: localStorage 持久化
└→ 依赖第 1 步的 sendToBath() 重写
└→ 可以在第 1 步之后独立进行
第 4 步: Scheduler 调用点修改
└→ 依赖第 2 步的 scheduleDeferredRetry 新签名
每个步骤都是独立可测试的,可以分步合并到主分支。
优化维修状态管理
痛点:
有维修时30s轮询没必要,GUI关闭无法持久化维修状态
repair_seconds未使用RepairManager.ts:134-154repair_seconds,前端完全忽略bathingShips不持久化RepairManager.ts:44TaskQueue.ts:165-173RepairManager.ts:134-154sendToBath()中 API 调用失败时删除记录,但未重试try/catch+delete以下是DS给的建议:
目录
repair_seconds+ 超时保护1. 消费
repair_seconds+ 超时保护当前状态
后端
/api/repair/ship已返回:但前端
RepairManager.sendToBath()完全忽略了响应:同时
BathingShip接口缺少repairEndTime字段:修改清单
文件 1:
AutoWSGR-GUI/src/model/scheduler/RepairManager.ts修改 A —
BathingShip接口(第 22-29 行)BathingShip中新增repairEndTime字段requestSent: boolean之后添加repairEndTime: number设计理由:
repairEndTime = 0作为哨兵值,语义为"维修时间不可信,回退到轮询模式"。这与Date.now()返回的正整数天然不冲突。修改 B —
sendToBath()方法(第 134-154 行)sendToBath()方法,消费repair_seconds关键变化:
resp.data.repair_seconds提取维修秒数validateRepairSeconds()校验合理性repairEndTime;不可信 → 保持0requestSent=false供下次轮询重试saveToStorage()持久化修改 C — 新增
validateRepairSeconds()静态方法needsRepair()方法之后(第 129 行之后)设计理由:
resp.data可能是undefined,repair_seconds可能是null或字符串修改 D —
refreshBathingStatus()方法(第 159-190 行)refreshBathingStatus()中增加repairEndTime的清理逻辑saveToStorage()完整修改后的
refreshBathingStatus()相关段落(第 182-186 行):修改 E —
clearAll()方法(第 224-226 行)clearAll()中增加持久化清理this.bathingShips.clear()后调用this.saveToStorage()2. 动态延迟计算 + 回退策略
当前状态
TaskQueue.scheduleDeferredRetry()使用固定 30 秒:修改清单
文件 2:
AutoWSGR-GUI/src/model/scheduler/TaskQueue.ts修改 A —
scheduleDeferredRetry()方法签名(第 165 行)bathingShips参数需要在文件顶部引入
BathingShip类型:修改 B —
scheduleDeferredRetry()方法体(第 166-173 行)修改 C — 新增
calculateDynamicWait()方法clearDeferredTimer()方法之后(第 191 行之后)设计理由:
repairEndTime在几小时后)导致定时器过长,期间用户可能手动操作-1而非直接设 30 秒:将决策权交给调用方,保持单一职责文件 3:
AutoWSGR-GUI/src/model/scheduler/Scheduler.tsscheduleDeferredRetry()的调用点有两处,都需要传递bathingShips。修改 D —
consumeNext()中的调用(第 274-277 行)bathingShips参数this.repairManager.bathingShips注意:
bathingShips是RepairManager的private属性。需要将其改为public或新增一个 getter。修改 E —
RepairManager.bathingShips访问权限(第 44 行)private改为public或新增 getterpublic getBathingShips()方法在
RepairManager中新增:然后在
Scheduler中调用:修改 F —
deferCurrentTask()中的调用(第 532-535 行)bathingShips参数3. localStorage 持久化 + 启动恢复
当前状态
RepairManager的bathingShips是纯内存Map:GUI 重启后所有记录丢失,导致:
修改清单
文件:
AutoWSGR-GUI/src/model/scheduler/RepairManager.ts修改 A — 新增常量(第 41 行,
class RepairManager内部)STORAGE_KEY常量private bathingShips之后(第 44 行之后)修改 B —
constructor()(第 46-48 行)restoreFromStorage()this.api = api;之后添加修改 C — 新增
saveToStorage()方法clearAll()方法之后(第 226 行之后)设计理由:
filter排除已过期的记录,避免重启后加载过时的维修状态repairEndTime === 0的记录也要保存,因为需要继续轮询确认removeItem而非写入空数组,避免存储脏数据try/catch包裹,因为localStorage在隐私模式下可能抛出异常修改 D — 新增
restoreFromStorage()方法saveToStorage()方法之后设计理由:
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.tsBathingShip新增repairEndTime: numberRepairManager.tssendToBath()消费repair_seconds,失败不删除记录RepairManager.tsvalidateRepairSeconds()静态校验方法RepairManager.tssaveToStorage()/restoreFromStorage()持久化RepairManager.tsgetBathingShips()只读访问器RepairManager.tsrefreshBathingStatus()删除记录后调用saveToStorage()RepairManager.tsclearAll()清除后调用saveToStorage()RepairManager.tsrestoreFromStorage()TaskQueue.tsscheduleDeferredRetry()新增bathingShips参数,动态延迟TaskQueue.tscalculateDynamicWait()动态等待时间计算TaskQueue.tsBathingShip类型Scheduler.tsconsumeNext()和deferCurrentTask()中传递bathingShips实现顺序建议
每个步骤都是独立可测试的,可以分步合并到主分支。