# 功能更新 - 批量任务进度保存和恢复 v3.12.0

**版本**: v3.12.0  
**日期**: 2025-10-08  
**类型**: 功能更新

## 功能概述

新增批量任务执行进度的自动保存和恢复功能。当批量任务执行过程中刷新页面或浏览器意外关闭后，可以从上次中断的位置继续执行，无需重新开始。

### 用户需求

> "突然发现批量自动化没有记录上次跑到哪一个token了，我需要记录一下这次跑到第几个token，刷新网页之后，还可以继续下面进行做任务"

## 功能特性

### 1. 自动保存进度

- **实时保存**：每完成一个Token的任务后，自动保存进度到 `localStorage`
- **保存内容**：
  - 已完成的Token ID列表
  - 所有Token ID列表
  - 任务列表
  - 执行统计（成功、失败、跳过数量）
  - 保存时间戳

### 2. 智能进度恢复

- **刷新检测**：页面刷新后自动检测是否有未完成的任务
- **用户选择**：
  - ✅ **继续上次进度** - 只执行剩余未完成的Token
  - 🔄 **重新开始** - 清除进度，从头开始执行所有Token
- **进度过期**：超过24小时的进度自动清除

### 3. 进度信息展示

弹窗显示详细进度信息：
- 总计Token数量
- 已完成Token数量
- 剩余Token数量（高亮显示）

## 技术实现

### 1. 数据结构

#### 保存的进度数据

```javascript
{
  completedTokenIds: ['token1', 'token2', ...],  // 已完成的Token ID列表
  allTokenIds: ['token1', 'token2', 'token3', ...],  // 所有Token ID列表
  tasks: ['dailyFix', 'legionSignIn', ...],  // 任务列表
  timestamp: 1728374400000,  // 保存时间戳
  stats: {
    total: 100,      // 总数
    success: 45,     // 成功数
    failed: 3,       // 失败数
    skipped: 0       // 跳过数
  }
}
```

### 2. 核心函数

#### src/stores/batchTaskStore.js

**保存进度**：
```javascript
const saveExecutionProgress = (completedTokenIds, allTokenIds, tasks) => {
  const progress = {
    completedTokenIds: completedTokenIds,
    allTokenIds: allTokenIds,
    tasks: tasks,
    timestamp: Date.now(),
    stats: {
      total: executionStats.value.total,
      success: executionStats.value.success,
      failed: executionStats.value.failed,
      skipped: executionStats.value.skipped
    }
  }
  localStorage.setItem('batchTaskProgress', JSON.stringify(progress))
  savedProgress.value = progress
}
```

**清除进度**：
```javascript
const clearSavedProgress = () => {
  localStorage.removeItem('batchTaskProgress')
  savedProgress.value = null
}
```

**检查进度**：
```javascript
const hasSavedProgress = computed(() => {
  if (!savedProgress.value) return false
  
  // 检查进度是否过期（超过24小时）
  const elapsed = Date.now() - savedProgress.value.timestamp
  const isExpired = elapsed > 24 * 60 * 60 * 1000
  
  if (isExpired) {
    clearSavedProgress()
    return false
  }
  
  // 检查是否还有未完成的token
  const remaining = savedProgress.value.allTokenIds.length - savedProgress.value.completedTokenIds.length
  return remaining > 0
})
```

**修改启动函数**：
```javascript
const startBatchExecution = async (tokenIds = null, tasks = null, isRetry = false, continueFromSaved = false) => {
  // ... 省略其他代码
  
  let targetTokens = tokenIds || tokenStore.gameTokens.map(t => t.id)
  let targetTasks = tasks || currentTemplateTasks.value
  let completedTokenIds = []
  
  // 检查是否要从保存的进度继续
  if (continueFromSaved && savedProgress.value && hasSavedProgress.value) {
    console.log('📂 从上次保存的进度继续执行')
    targetTokens = savedProgress.value.allTokenIds
    targetTasks = savedProgress.value.tasks
    completedTokenIds = savedProgress.value.completedTokenIds
    
    // 恢复统计数据
    executionStats.value = {
      ...executionStats.value,
      total: savedProgress.value.stats.total,
      success: savedProgress.value.stats.success,
      failed: savedProgress.value.stats.failed,
      skipped: savedProgress.value.stats.skipped
    }
    
    console.log(`✅ 已完成: ${completedTokenIds.length}/${targetTokens.length} 个Token`)
    console.log(`🔄 继续执行剩余 ${targetTokens.length - completedTokenIds.length} 个Token`)
  }
  
  // ... 省略其他代码
  
  // 过滤出需要执行的token（排除已完成的）
  const tokensToExecute = targetTokens.filter(id => !completedTokenIds.includes(id))
  
  // 执行批量任务
  await executeBatchWithConcurrency(tokensToExecute, targetTasks, targetTokens, completedTokenIds)
}
```

**修改并发执行函数**：
```javascript
const executeBatchWithConcurrency = async (tokenIds, tasks, allTokenIds = null, completedTokenIds = []) => {
  const queue = [...tokenIds]
  const executing = []
  const completed = [...completedTokenIds]  // 已完成的token列表
  const all = allTokenIds || tokenIds  // 所有token列表
  
  while (queue.length > 0 || executing.length > 0) {
    // ... 省略其他代码
    
    const promise = (async () => {
      // 执行任务
      return executeTokenTasks(tokenId, tasks)
    })()
      .then(() => {
        // 从执行队列中移除
        const index = executing.indexOf(promise)
        if (index > -1) {
          executing.splice(index, 1)
        }
        executingTokens.value.delete(tokenId)
        
        // 保存进度：记录此token已完成
        completed.push(tokenId)
        saveExecutionProgress(completed, all, tasks)
      })
      .catch(error => {
        // ... 省略错误处理
        
        // 保存进度：即使失败也记录为已完成（避免下次重复执行）
        completed.push(tokenId)
        saveExecutionProgress(completed, all, tasks)
      })
  }
}
```

**完成时清除进度**：
```javascript
const finishBatchExecution = async () => {
  // ... 省略其他代码
  
  // 清除保存的进度（任务已全部完成）
  clearSavedProgress()
  
  // ... 省略其他代码
}
```

#### src/components/BatchTaskPanel.vue

**修改开始执行函数**：
```javascript
const handleStart = () => {
  // 检查是否有保存的进度
  if (batchStore.hasSavedProgress && batchStore.savedProgress) {
    const completed = batchStore.savedProgress.completedTokenIds.length
    const total = batchStore.savedProgress.allTokenIds.length
    const remaining = total - completed
    
    dialog.warning({
      title: '发现未完成的任务',
      content: () => {
        return h('div', [
          h('p', `检测到上次有未完成的批量任务：`),
          h('ul', { style: 'margin: 12px 0; padding-left: 24px;' }, [
            h('li', `总计：${total} 个Token`),
            h('li', `已完成：${completed} 个`),
            h('li', { style: 'color: #ff6b6b; font-weight: bold;' }, `剩余：${remaining} 个`)
          ]),
          h('p', { style: 'margin-top: 16px; font-weight: bold;' }, '您想要：')
        ])
      },
      positiveText: '继续上次进度',
      negativeText: '重新开始',
      onPositiveClick: () => {
        batchStore.startBatchExecution(null, null, false, true)
        message.success(`继续执行剩余 ${remaining} 个Token`)
      },
      onNegativeClick: () => {
        dialog.warning({
          title: '确认重新开始',
          content: `确定要重新开始执行所有 ${tokenStore.gameTokens.length} 个角色的任务吗？\n\n已完成的 ${completed} 个Token将会重新执行。`,
          positiveText: '确定重新开始',
          negativeText: '取消',
          onPositiveClick: () => {
            batchStore.clearSavedProgress()
            batchStore.startBatchExecution()
            message.success('批量任务已启动（重新开始）')
          }
        })
      }
    })
  } else {
    // 没有保存的进度，正常开始
    dialog.warning({
      title: '确认执行',
      content: `即将对 ${tokenStore.gameTokens.length} 个角色执行批量任务，是否继续？`,
      positiveText: '确定',
      negativeText: '取消',
      onPositiveClick: () => {
        batchStore.startBatchExecution()
        message.success('批量任务已启动')
      }
    })
  }
}
```

### 3. 新增导出

#### src/stores/batchTaskStore.js

```javascript
return {
  // 状态
  savedProgress,        // 新增：保存的进度数据
  
  // 计算属性
  hasSavedProgress,     // 新增：是否有保存的进度
  
  // 方法
  clearSavedProgress,   // 新增：清除保存的进度
  
  // ... 其他已有的导出
}
```

## 使用场景

### 场景1：正常完成

```
用户操作：
1. 开始执行批量任务（100个Token）
2. 任务全部执行完成
3. 系统自动清除保存的进度

结果：
✅ 所有任务完成
✅ 进度已清除
```

### 场景2：中途刷新（继续执行）

```
用户操作：
1. 开始执行批量任务（100个Token）
2. 执行到第50个时，刷新页面
3. 点击"开始执行"按钮
4. 弹窗显示：已完成50个，剩余50个
5. 选择"继续上次进度"

结果：
✅ 从第51个Token继续执行
✅ 已完成的50个Token不会重复执行
✅ 统计数据累加（成功、失败数量保持）
```

### 场景3：中途刷新（重新开始）

```
用户操作：
1. 开始执行批量任务（100个Token）
2. 执行到第50个时，刷新页面
3. 点击"开始执行"按钮
4. 弹窗显示：已完成50个，剩余50个
5. 选择"重新开始"
6. 二次确认弹窗
7. 确认"确定重新开始"

结果：
✅ 清除保存的进度
✅ 从第1个Token重新执行所有100个
✅ 统计数据重置
```

### 场景4：进度过期

```
用户操作：
1. 开始执行批量任务（100个Token）
2. 执行到第50个时，关闭浏览器
3. 24小时后重新打开页面
4. 点击"开始执行"按钮

结果：
✅ 进度已自动过期清除
✅ 直接弹出正常开始确认框
✅ 从头开始执行所有Token
```

## 用户界面

### 弹窗1：发现未完成的任务

```
┌─────────────────────────────────────┐
│ ⚠ 发现未完成的任务                  │
├─────────────────────────────────────┤
│ 检测到上次有未完成的批量任务：       │
│                                     │
│ • 总计：100 个Token                 │
│ • 已完成：50 个                     │
│ • 剩余：50 个  ← 红色高亮           │
│                                     │
│ 您想要：                            │
│                                     │
│  [继续上次进度]  [重新开始]         │
└─────────────────────────────────────┘
```

### 弹窗2：确认重新开始

```
┌─────────────────────────────────────┐
│ ⚠ 确认重新开始                      │
├─────────────────────────────────────┤
│ 确定要重新开始执行所有 100 个角色的 │
│ 任务吗？                            │
│                                     │
│ 已完成的 50 个Token将会重新执行。   │
│                                     │
│  [确定重新开始]  [取消]             │
└─────────────────────────────────────┘
```

### 控制台日志

**继续执行时**：
```
📂 从上次保存的进度继续执行
✅ 已完成: 50/100 个Token
🔄 继续执行剩余 50 个Token
🚀 开始批量执行任务
📋 Token数量: 100
📋 任务列表: ['dailyFix', 'legionSignIn', ...]
```

**每完成一个Token**：
```
💾 已保存进度: 51/100 个Token已完成
💾 已保存进度: 52/100 个Token已完成
...
```

**全部完成时**：
```
🎉 批量任务执行完成
🗑️ 已清除保存的进度
✅ 所有任务成功完成！
```

## 数据存储

### localStorage 键名

```
batchTaskProgress
```

### 数据示例

```json
{
  "completedTokenIds": [
    "10601服-0-7145...",
    "10601服-1-7146...",
    "10602服-0-7147..."
  ],
  "allTokenIds": [
    "10601服-0-7145...",
    "10601服-1-7146...",
    "10602服-0-7147...",
    "10603服-0-7148...",
    "10604服-0-7149..."
  ],
  "tasks": [
    "dailyFix",
    "legionSignIn",
    "autoStudy",
    "claimHangupReward",
    "addClock",
    "sendCar",
    "climbTower"
  ],
  "timestamp": 1728374400000,
  "stats": {
    "total": 5,
    "success": 3,
    "failed": 0,
    "skipped": 0
  }
}
```

## 技术要点

### 1. 进度保存时机

- ✅ **成功完成**：Token任务执行成功后保存
- ✅ **失败**：Token任务执行失败后也保存（避免重复执行失败的任务）
- ❌ **执行中**：不保存（避免状态不一致）

### 2. 进度恢复逻辑

```javascript
// 初始化每个Token的进度
targetTokens.forEach(tokenId => {
  if (completedTokenIds.includes(tokenId)) {
    // 已完成的token标记为completed状态
    taskProgress.value[tokenId] = {
      status: 'completed',
      progress: 100,
      // ...
    }
  } else {
    // 未完成的token初始化为pending状态
    taskProgress.value[tokenId] = {
      status: 'pending',
      progress: 0,
      // ...
    }
  }
})

// 过滤出需要执行的token
const tokensToExecute = targetTokens.filter(id => !completedTokenIds.includes(id))

// 只执行未完成的token
await executeBatchWithConcurrency(tokensToExecute, targetTasks, targetTokens, completedTokenIds)
```

### 3. 统计数据累加

```javascript
if (continueFromSaved && savedProgress.value) {
  // 恢复之前的统计数据
  executionStats.value = {
    ...executionStats.value,
    total: savedProgress.value.stats.total,
    success: savedProgress.value.stats.success,  // 保持之前的成功数
    failed: savedProgress.value.stats.failed,    // 保持之前的失败数
    skipped: savedProgress.value.stats.skipped   // 保持之前的跳过数
  }
}
```

### 4. 进度过期检查

```javascript
const hasSavedProgress = computed(() => {
  if (!savedProgress.value) return false
  
  const elapsed = Date.now() - savedProgress.value.timestamp
  const isExpired = elapsed > 24 * 60 * 60 * 1000  // 24小时
  
  if (isExpired) {
    clearSavedProgress()
    return false
  }
  
  const remaining = savedProgress.value.allTokenIds.length - savedProgress.value.completedTokenIds.length
  return remaining > 0
})
```

## 优势与特性

### 1. 可靠性

- ✅ 实时保存到 localStorage，不依赖网络
- ✅ 每完成一个Token就保存，最小化数据丢失
- ✅ 自动过期机制，避免过时数据干扰

### 2. 灵活性

- ✅ 用户可选择继续或重新开始
- ✅ 支持部分完成的恢复
- ✅ 失败的Token也记录为已完成（避免无限重试）

### 3. 用户体验

- ✅ 详细的进度信息展示
- ✅ 明确的操作选项
- ✅ 二次确认机制（重新开始时）
- ✅ 友好的提示消息

### 4. 性能优化

- ✅ 只执行未完成的Token，节省时间
- ✅ 避免重复执行，减少服务器压力
- ✅ localStorage 存储，无网络开销

## 注意事项

### 1. Token列表变化

如果Token列表发生变化（添加/删除Token），保存的进度可能不准确：
- **建议**：重新开始执行
- **未来改进**：可以添加Token指纹验证

### 2. 任务列表变化

如果任务模板发生变化，保存的进度仍使用旧的任务列表：
- **当前行为**：继续使用保存的任务列表
- **未来改进**：可以提示任务列表已变化

### 3. 数据大小

对于大量Token（如1000+），进度数据可能较大：
- **当前限制**：localStorage 通常 5-10MB
- **评估**：1000个Token ID × 20字节 ≈ 20KB（完全可接受）

### 4. 浏览器隐私模式

在隐私/无痕模式下，localStorage 可能不持久化：
- **影响**：刷新后进度丢失
- **解决**：建议使用正常模式执行长时间任务

## 测试用例

### 测试1：正常完成

1. 开始执行10个Token
2. 等待全部完成
3. 检查localStorage：`batchTaskProgress` 应该被删除
4. 再次点击"开始执行"：应该是正常的确认框

### 测试2：中途刷新继续

1. 开始执行10个Token
2. 完成5个后刷新页面
3. 点击"开始执行"：应该弹出"发现未完成的任务"
4. 选择"继续上次进度"
5. 应该从第6个Token开始执行
6. 统计数据应该累加

### 测试3：中途刷新重新开始

1. 开始执行10个Token
2. 完成5个后刷新页面
3. 点击"开始执行"：应该弹出"发现未完成的任务"
4. 选择"重新开始"
5. 二次确认后，应该清除进度
6. 从第1个Token重新执行所有10个

### 测试4：进度过期

1. 开始执行10个Token
2. 完成5个后关闭浏览器
3. 手动修改localStorage中的timestamp为25小时前
4. 重新打开页面，点击"开始执行"
5. 应该弹出正常的确认框（进度已自动清除）

### 测试5：失败Token处理

1. 开始执行10个Token，其中3个会失败
2. 完成5个（包括2个失败）后刷新
3. 继续执行：失败的2个不应该再次执行
4. 统计数据中的失败数应该保持

## 修改文件

- ✅ src/stores/batchTaskStore.js
  - 新增 `savedProgress` 状态
  - 新增 `saveExecutionProgress` 函数
  - 新增 `clearSavedProgress` 函数
  - 新增 `hasSavedProgress` 计算属性
  - 修改 `startBatchExecution` 函数支持 `continueFromSaved` 参数
  - 修改 `executeBatchWithConcurrency` 函数支持进度保存
  - 修改 `finishBatchExecution` 函数清除进度

- ✅ src/components/BatchTaskPanel.vue
  - 导入 `h` 函数
  - 修改 `handleStart` 函数支持进度恢复提示

## 相关版本

- **v3.11.x**: 批量任务优化和错误处理
- **v3.12.0**: 新增进度保存和恢复功能（本版本）

## 总结

**核心功能**：
- 💾 自动保存批量任务执行进度
- 🔄 刷新后可继续执行剩余任务
- 🎯 用户可选择继续或重新开始
- ⏰ 24小时自动过期机制

**用户获益**：
- ✅ 不怕浏览器意外关闭
- ✅ 不怕页面刷新中断
- ✅ 节省时间（继续执行而非重新开始）
- ✅ 灵活控制（可选择重新开始）

**技术优势**：
- ✅ 实时保存，可靠性高
- ✅ 自动过期，避免脏数据
- ✅ 累加统计，数据准确
- ✅ 用户友好，操作简单

---

**状态**: ✅ 已完成  
**版本**: v3.12.0

