├── .github ├── last_activity.md └── workflows │ ├── keep-alive.yml │ ├── build.yml │ ├── xmsport.yml │ └── sync-fork.yml ├── dist ├── types │ ├── main.types.js │ ├── notify.types.js │ ├── apiService.types.js │ ├── dataProcessor.types.js │ └── index.js ├── utils.js ├── local-test.js ├── dataProcessor.js ├── index.js ├── apiService.js ├── notify.js └── data.txt ├── src ├── types │ ├── index.ts │ ├── dataProcessor.types.ts │ ├── main.types.ts │ ├── notify.types.ts │ └── apiService.types.ts ├── utils.ts ├── local-test.ts ├── dataProcessor.ts ├── index.ts ├── apiService.ts ├── notify.ts └── data.txt ├── .env.example ├── js-backup ├── local-test.js ├── utils.js ├── dataProcessor.js ├── index.js ├── apiService.js └── notify.js ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── tsconfig.json /.github/last_activity.md: -------------------------------------------------------------------------------- 1 | 上次活动时间: 2025-12-01 10:00:01 (北京时间) 2 | -------------------------------------------------------------------------------- /dist/types/main.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * 主程序相关类型定义 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | -------------------------------------------------------------------------------- /dist/types/notify.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * 通知服务相关类型定义 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | -------------------------------------------------------------------------------- /dist/types/apiService.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * API 接口相关类型定义 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | -------------------------------------------------------------------------------- /dist/types/dataProcessor.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * 数据处理相关类型定义 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 统一导出所有类型定义 3 | */ 4 | 5 | // API服务相关类型 6 | export * from './apiService.types'; 7 | 8 | // 通知服务相关类型 9 | export * from './notify.types'; 10 | 11 | // 数据处理相关类型 12 | export * from './dataProcessor.types'; 13 | 14 | // 主程序相关类型 15 | export * from './main.types'; -------------------------------------------------------------------------------- /src/types/dataProcessor.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据处理相关类型定义 3 | */ 4 | 5 | /** 6 | * 步数数据配置接口 7 | */ 8 | export interface StepDataConfig { 9 | minStep: number; 10 | maxStep: number; 11 | } 12 | 13 | /** 14 | * 步数数据处理结果接口 15 | */ 16 | export interface ProcessDataResult { 17 | success: boolean; 18 | message: string; 19 | dataJson?: string; 20 | step?: number; 21 | } -------------------------------------------------------------------------------- /src/types/main.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 主程序相关类型定义 3 | */ 4 | 5 | /** 6 | * 程序执行环境配置 7 | */ 8 | export interface ExecutionEnvironment { 9 | isGitHubAction: boolean; 10 | isLocalTest: boolean; 11 | } 12 | 13 | /** 14 | * 程序执行结果 15 | */ 16 | export interface ExecutionResult { 17 | status: 'success' | 'failure'; 18 | message: string; 19 | step?: number; 20 | executionTime?: number; 21 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 必要环境变量 2 | PHONE_NUMBER=你的手机号 3 | PASSWORD=你的密码 4 | 5 | # 可选环境变量 6 | xmSportMinStep=10000 7 | xmSportMaxStep=19999 8 | ENABLE_NOTIFY=true 9 | 10 | # 通知相关环境变量(可选) 11 | # Server酱 12 | # SERVERCHAN_KEY= 13 | 14 | # Bark 15 | # BARK_KEY= 16 | 17 | # Telegram 18 | # TG_BOT_TOKEN= 19 | # TG_CHAT_ID= 20 | 21 | # 钉钉 22 | # DINGTALK_WEBHOOK= 23 | # DINGTALK_SECRET= 24 | 25 | # 企业微信 26 | # WECOM_KEY= 27 | 28 | # PushPlus 29 | # PUSHPLUS_TOKEN= -------------------------------------------------------------------------------- /src/types/notify.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知服务相关类型定义 3 | */ 4 | 5 | /** 6 | * 通知选项接口 7 | */ 8 | export interface NotificationOptions { 9 | title: string; 10 | content: string; 11 | platform?: string; 12 | } 13 | 14 | /** 15 | * 通知结果接口 16 | */ 17 | export interface NotificationResult { 18 | success: boolean; 19 | message: string; 20 | platform: string; 21 | } 22 | 23 | /** 24 | * 各平台通知配置接口 25 | */ 26 | export interface NotificationConfig { 27 | enabled: boolean; 28 | key?: string; 29 | token?: string; 30 | webhook?: string; 31 | chatId?: string; 32 | } -------------------------------------------------------------------------------- /js-backup/local-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地测试脚本 - 自动读取data.txt并设置环境变量 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const { spawn } = require('child_process'); 7 | 8 | // 加载.env文件 9 | require('dotenv').config(); 10 | 11 | // 读取data.txt文件 12 | try { 13 | const dataFilePath = path.join(__dirname, 'src', 'data.txt'); 14 | const dataContent = fs.readFileSync(dataFilePath, 'utf8'); 15 | 16 | console.log('✅ 已读取data.txt文件'); 17 | 18 | // 设置DATA_JSON环境变量 19 | process.env.DATA_JSON = dataContent; 20 | 21 | // 启动主程序 22 | console.log('🚀 正在启动主程序...'); 23 | 24 | const childProcess = spawn('node', ['src/index.js'], { 25 | stdio: 'inherit', 26 | env: process.env 27 | }); 28 | 29 | childProcess.on('exit', (code) => { 30 | console.log(`⏹️ 程序执行结束,退出码: ${code}`); 31 | }); 32 | 33 | } catch (error) { 34 | console.error(`❌ 读取data.txt文件失败: ${error.message}`); 35 | process.exit(1); 36 | } -------------------------------------------------------------------------------- /src/types/apiService.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API 接口相关类型定义 3 | */ 4 | 5 | /** 6 | * 登录响应接口 7 | */ 8 | export interface LoginResponse { 9 | token_info: { 10 | login_token: string; 11 | app_token: string; 12 | user_id: string; 13 | }; 14 | result: string; 15 | } 16 | 17 | /** 18 | * App Token 响应接口 19 | */ 20 | export interface AppTokenResponse { 21 | token_info: { 22 | app_token: string; 23 | }; 24 | result: string; 25 | } 26 | 27 | /** 28 | * 通用 API 响应接口 29 | */ 30 | export interface ApiResponse { 31 | message?: string; 32 | [key: string]: any; 33 | } 34 | 35 | /** 36 | * 获取登录 Token 和用户 ID 的结果接口 37 | */ 38 | export interface LoginTokenAndUserIdResult { 39 | loginToken: string; 40 | userId: string; 41 | } 42 | 43 | /** 44 | * API 请求选项接口 45 | */ 46 | export interface ApiRequestOptions { 47 | method: string; 48 | url: string; 49 | headers: Record; 50 | data?: string | Record; 51 | operationName?: string; 52 | [key: string]: any; 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 chiupam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dist/types/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * 统一导出所有类型定义 4 | */ 5 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | var desc = Object.getOwnPropertyDescriptor(m, k); 8 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 9 | desc = { enumerable: true, get: function() { return m[k]; } }; 10 | } 11 | Object.defineProperty(o, k2, desc); 12 | }) : (function(o, m, k, k2) { 13 | if (k2 === undefined) k2 = k; 14 | o[k2] = m[k]; 15 | })); 16 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 17 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 18 | }; 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | // API服务相关类型 21 | __exportStar(require("./apiService.types"), exports); 22 | // 通知服务相关类型 23 | __exportStar(require("./notify.types"), exports); 24 | // 数据处理相关类型 25 | __exportStar(require("./dataProcessor.types"), exports); 26 | // 主程序相关类型 27 | __exportStar(require("./main.types"), exports); 28 | -------------------------------------------------------------------------------- /.github/workflows/keep-alive.yml: -------------------------------------------------------------------------------- 1 | name: 保持仓库活跃 2 | 3 | on: 4 | schedule: 5 | # 每月1日运行一次 6 | - cron: '0 0 1 * *' 7 | # 允许手动触发 8 | workflow_dispatch: 9 | 10 | # 设置权限 11 | permissions: 12 | contents: write # 需要写入权限才能提交 13 | 14 | jobs: 15 | keep-alive: 16 | runs-on: ubuntu-latest 17 | 18 | # 设置任务级环境变量 19 | env: 20 | TZ: Asia/Shanghai # 设置时区为中国标准时间(北京时间) 21 | 22 | steps: 23 | - name: 检出代码 24 | uses: actions/checkout@v3 25 | 26 | - name: 获取当前时间 27 | id: date 28 | run: echo "NOW=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT 29 | 30 | - name: 创建临时文件 31 | run: | 32 | mkdir -p .github 33 | echo "上次活动时间: ${{ steps.date.outputs.NOW }} (北京时间)" > .github/last_activity.md 34 | 35 | - name: 提交更改 36 | run: | 37 | git config --global user.name 'github-actions[bot]' 38 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 39 | git add .github/last_activity.md 40 | git commit -m "保持仓库活跃: ${{ steps.date.outputs.NOW }}" || echo "没有更改,无需提交" 41 | git push -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmsport", 3 | "version": "1.0.0", 4 | "description": "小米运动健康修改步数", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "node dist/index.js", 8 | "build": "tsc", 9 | "dev": "tsc --watch", 10 | "test": "npm run build && node dist/local-test.js", 11 | "test:env": "node -r dotenv/config dist/index.js", 12 | "test:fail": "cross-env PHONE_NUMBER=10000000000 PASSWORD=wrongpassword ENABLE_NOTIFY=true npm test", 13 | "prepare": "npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/chiupam/xmSport.git" 18 | }, 19 | "keywords": [ 20 | "xiaomi", 21 | "sport", 22 | "steps" 23 | ], 24 | "author": "", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/chiupam/xmSport/issues" 28 | }, 29 | "homepage": "https://github.com/chiupam/xmSport#readme", 30 | "dependencies": { 31 | "@actions/core": "^1.10.0", 32 | "axios": "^1.5.0", 33 | "axios-retry": "^4.5.0", 34 | "cross-env": "^7.0.3", 35 | "dotenv": "^16.3.1" 36 | }, 37 | "engines": { 38 | "node": ">=16" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^18.x", 42 | "typescript": "^5.x" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /js-backup/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具函数集合 3 | */ 4 | 5 | /** 6 | * 将对象转换为URL查询字符串 7 | * @param {Object} obj - 要转换的对象 8 | * @returns {string} URL查询字符串 9 | */ 10 | function toQueryString(obj) { 11 | return Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'); 12 | } 13 | 14 | /** 15 | * 获取指定范围内的随机整数 16 | * @param {number} min - 最小值 17 | * @param {number} max - 最大值 18 | * @returns {number} 随机整数 19 | */ 20 | function getRandomInt(min, max) { 21 | min = Math.ceil(min); 22 | max = Math.floor(max); 23 | return Math.floor(Math.random() * (max - min + 1)) + min; 24 | } 25 | 26 | /** 27 | * 格式化时间 28 | * @param {Date} date - 日期对象 29 | * @param {string} timezone - 时区 30 | * @param {string} offset - 时区偏移显示值 31 | * @returns {string} 格式化后的时间字符串 32 | */ 33 | function formatDate(date, timezone, offset) { 34 | const options = { 35 | year: 'numeric', 36 | month: 'numeric', 37 | day: 'numeric', 38 | hour: '2-digit', 39 | minute: '2-digit', 40 | second: '2-digit', 41 | hour12: false, 42 | timeZone: timezone 43 | }; 44 | 45 | const formattedDate = date.toLocaleString('zh-CN', options); 46 | return `${formattedDate} (${offset})`; 47 | } 48 | 49 | module.exports = { 50 | toQueryString, 51 | getRandomInt, 52 | formatDate 53 | }; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将对象转换为URL查询字符串 3 | * @param {object} obj - 要转换的对象 4 | * @returns {string} 转换后的查询字符串 5 | */ 6 | export function toQueryString(obj: Record): string { 7 | return Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'); 8 | } 9 | 10 | /** 11 | * 获取包含最小值和最大值在内的随机整数 12 | * @param {number} min - 最小值 13 | * @param {number} max - 最大值 14 | * @returns {number} 随机整数 15 | */ 16 | export function getRandomInt(min: number, max: number): number { 17 | min = Math.ceil(min); 18 | max = Math.floor(max); 19 | return Math.floor(Math.random() * (max - min + 1)) + min; 20 | } 21 | 22 | /** 23 | * 格式化日期为指定格式 24 | * @param {Date} date - 日期对象 25 | * @param {string} timezone - 时区,例如 'UTC', 'Asia/Shanghai' 26 | * @param {string} offset - 与UTC的偏移,例如 '+8' 27 | * @returns {string} 格式化后的日期字符串 28 | */ 29 | export function formatDate(date: Date, timezone: string, offset: string): string { 30 | const options: Intl.DateTimeFormatOptions = { 31 | timeZone: timezone, 32 | year: 'numeric', 33 | month: '2-digit', 34 | day: '2-digit', 35 | hour: '2-digit', 36 | minute: '2-digit', 37 | second: '2-digit', 38 | hour12: false 39 | }; 40 | 41 | return new Intl.DateTimeFormat('zh-CN', options).format(date) + ` (UTC${offset})`; 42 | } -------------------------------------------------------------------------------- /src/local-test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地测试脚本 - 自动读取data.txt并设置环境变量 3 | */ 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import { spawn } from 'child_process'; 7 | import dotenv from 'dotenv'; 8 | 9 | // 加载.env文件 10 | dotenv.config(); 11 | 12 | // 读取data.txt文件 13 | try { 14 | // 先尝试从dist目录读取 15 | let dataPath = path.join(__dirname, '..', 'dist', 'data.txt'); 16 | 17 | // 如果dist中不存在,则从src目录读取 18 | if (!fs.existsSync(dataPath)) { 19 | dataPath = path.join(__dirname, 'data.txt'); 20 | } 21 | 22 | if (!fs.existsSync(dataPath)) { 23 | console.error('❌ 错误: 找不到data.txt文件,请确保src或dist目录中存在此文件'); 24 | process.exit(1); 25 | } 26 | 27 | const dataContent = fs.readFileSync(dataPath, 'utf8'); 28 | 29 | // 设置DATA_JSON环境变量 30 | process.env.DATA_JSON = dataContent; 31 | 32 | console.log('✅ 已读取data.txt并设置为环境变量'); 33 | } catch (err: any) { 34 | console.error(`❌ 读取data.txt出错: ${err.message}`); 35 | process.exit(1); 36 | } 37 | 38 | // 编译后的脚本路径 39 | const scriptPath = path.join(__dirname, '..', 'dist', 'index.js'); 40 | 41 | console.log(`🚀 运行脚本: ${scriptPath}`); 42 | 43 | // 运行实际脚本 44 | const script = spawn('node', [scriptPath], { 45 | stdio: 'inherit', 46 | env: process.env 47 | }); 48 | 49 | script.on('close', (code) => { 50 | console.log(`脚本退出,退出码: ${code || 0}`); 51 | }); -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.toQueryString = toQueryString; 4 | exports.getRandomInt = getRandomInt; 5 | exports.formatDate = formatDate; 6 | /** 7 | * 将对象转换为URL查询字符串 8 | * @param {object} obj - 要转换的对象 9 | * @returns {string} 转换后的查询字符串 10 | */ 11 | function toQueryString(obj) { 12 | return Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'); 13 | } 14 | /** 15 | * 获取包含最小值和最大值在内的随机整数 16 | * @param {number} min - 最小值 17 | * @param {number} max - 最大值 18 | * @returns {number} 随机整数 19 | */ 20 | function getRandomInt(min, max) { 21 | min = Math.ceil(min); 22 | max = Math.floor(max); 23 | return Math.floor(Math.random() * (max - min + 1)) + min; 24 | } 25 | /** 26 | * 格式化日期为指定格式 27 | * @param {Date} date - 日期对象 28 | * @param {string} timezone - 时区,例如 'UTC', 'Asia/Shanghai' 29 | * @param {string} offset - 与UTC的偏移,例如 '+8' 30 | * @returns {string} 格式化后的日期字符串 31 | */ 32 | function formatDate(date, timezone, offset) { 33 | const options = { 34 | timeZone: timezone, 35 | year: 'numeric', 36 | month: '2-digit', 37 | day: '2-digit', 38 | hour: '2-digit', 39 | minute: '2-digit', 40 | second: '2-digit', 41 | hour12: false 42 | }; 43 | return new Intl.DateTimeFormat('zh-CN', options).format(date) + ` (UTC${offset})`; 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 构建TypeScript 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/**' 8 | - 'tsconfig.json' 9 | - 'package.json' 10 | - '.github/workflows/build.yml' 11 | workflow_dispatch: 12 | 13 | # 添加权限配置 14 | permissions: 15 | contents: write # 需要写入权限才能提交 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | # 确保获取完整历史以便于推送 25 | fetch-depth: 0 26 | 27 | - name: 设置Node.js环境 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: '16' 31 | cache: 'npm' 32 | 33 | - name: 安装依赖 34 | run: npm ci 35 | 36 | - name: 构建TypeScript 37 | run: npm run build 38 | 39 | - name: 复制数据文件 40 | run: | 41 | mkdir -p dist 42 | if [ -f src/data.txt ]; then 43 | cp src/data.txt dist/ 44 | echo "✅ 数据文件已复制到dist目录" 45 | else 46 | echo "⚠️ 数据文件不存在,跳过复制" 47 | fi 48 | 49 | - name: 提交编译后的文件 50 | run: | 51 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 52 | git config --local user.name "github-actions[bot]" 53 | git add -f dist/ 54 | if git diff --staged --quiet; then 55 | echo "没有变更需要提交" 56 | else 57 | git commit -m "自动构建: 更新编译后的JavaScript文件" 58 | # 添加调试信息 59 | echo "开始推送更改..." 60 | git push 61 | echo "✅ 编译后的文件已提交" 62 | fi -------------------------------------------------------------------------------- /.github/workflows/xmsport.yml: -------------------------------------------------------------------------------- 1 | name: 小米运动修改步数 2 | 3 | on: 4 | schedule: 5 | - cron: '00 12,13 * * *' # UTC时间12:00和13:00运行,对应北京时间20:00和21:00(但是存在严重的延迟) 6 | workflow_dispatch: # 允许手动触发,不再需要输入参数 7 | 8 | jobs: 9 | xmSport: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 15 # 设置超时限制 12 | 13 | # 设置任务级环境变量 14 | env: 15 | TZ: Asia/Shanghai # 设置时区为中国标准时间(北京时间) 16 | 17 | # 避免并发执行 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | steps: 23 | - name: 检出代码 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 1 # 浅克隆,提高速度 27 | 28 | - name: 设置Node.js环境 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: '16' 32 | cache: 'npm' # 启用npm缓存 33 | 34 | - name: 安装依赖 35 | run: npm ci # 使用ci而不是install,更快更可靠 36 | 37 | - name: 读取数据模板文件 38 | id: data-json 39 | run: | 40 | DATA_CONTENT=$(cat dist/data.txt) 41 | echo "DATA_JSON<> $GITHUB_ENV 42 | echo "$DATA_CONTENT" >> $GITHUB_ENV 43 | echo "EOF" >> $GITHUB_ENV 44 | echo "✅ 已读取数据模板文件" 45 | 46 | - name: 运行脚本 47 | env: 48 | PHONE_NUMBER: ${{ secrets.PHONE_NUMBER }} 49 | PASSWORD: ${{ secrets.PASSWORD }} 50 | # DATA_JSON由上一步自动设置,不再需要用户手动配置 51 | xmSportMinStep: ${{ secrets.xmSportMinStep || '20000' }} 52 | xmSportMaxStep: ${{ secrets.xmSportMaxStep || '22000' }} 53 | ENABLE_NOTIFY: ${{ secrets.ENABLE_NOTIFY || 'false' }} 54 | # 通知相关环境变量 55 | SERVERCHAN_KEY: ${{ secrets.SERVERCHAN_KEY }} 56 | BARK_KEY: ${{ secrets.BARK_KEY }} 57 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 58 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 59 | DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} 60 | DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} 61 | WECOM_KEY: ${{ secrets.WECOM_KEY }} 62 | PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} 63 | run: node dist/index.js 64 | 65 | - name: 处理结果 66 | if: always() 67 | run: | 68 | echo "当前时间: $(date "+%Y/%m/%d %H:%M:%S")" 69 | echo "工作流执行状态: ${{ job.status }}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # 运行时数据 10 | runtime/ 11 | *.lock 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # 项目特定文件 25 | .env 26 | .env.local 27 | .env.*.local 28 | config.js 29 | config.json 30 | .history/ 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | *.lcov 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (https://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # TypeScript v1 declaration files 59 | typings/ 60 | 61 | # TypeScript cache 62 | *.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # IDE files 86 | .idea/ 87 | .vscode/ 88 | *.swp 89 | *.swo 90 | 91 | # 本地开发文件 92 | .DS_Store 93 | *.log 94 | *.pid 95 | *.seed 96 | *.pid.lock 97 | 98 | # 测试文件 99 | coverage/ 100 | test/ 101 | __tests__/ 102 | 103 | # 临时文件 104 | tmp/ 105 | temp/ 106 | 107 | # 配置文件 108 | config.local.js 109 | config.local.json 110 | 111 | # 其他 112 | *.bak 113 | *.tmp 114 | *.temp 115 | 116 | # 忽略 node_modules 目录 117 | node_modules/ 118 | 119 | # 忽略日志文件 120 | *.log 121 | npm-debug.log* 122 | yarn-debug.log* 123 | yarn-error.log* 124 | 125 | # 忽略环境变量文件 126 | .env 127 | 128 | # 忽略编辑器/IDE配置文件 129 | .idea/ 130 | .vscode/ 131 | *.swp 132 | *.swo 133 | .DS_Store 134 | 135 | # 其他忽略文件 136 | .cache/ 137 | dist/ 138 | -------------------------------------------------------------------------------- /.github/workflows/sync-fork.yml: -------------------------------------------------------------------------------- 1 | name: 自动同步上游仓库 2 | 3 | on: 4 | schedule: 5 | # 每天凌晨2点执行(UTC时间,对应北京时间10点) 6 | - cron: '0 2 * * *' 7 | # 允许手动触发 8 | workflow_dispatch: 9 | 10 | # 添加权限配置 11 | permissions: 12 | contents: write # 需要写入权限才能提交 13 | 14 | jobs: 15 | sync-upstream: 16 | runs-on: ubuntu-latest 17 | name: 同步上游仓库更新 18 | 19 | # 仅在Fork仓库中运行此工作流 20 | if: github.repository != 'chiupam/xmSport' 21 | 22 | steps: 23 | - name: 检出代码 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | # 使用默认的GITHUB_TOKEN,它对当前仓库有写权限 28 | token: ${{ github.token }} 29 | 30 | - name: 配置Git 31 | run: | 32 | git config --global user.name 'github-actions[bot]' 33 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 34 | 35 | - name: 添加上游仓库 36 | run: | 37 | # 添加chiupam/xmSport作为上游仓库 38 | git remote add upstream https://github.com/chiupam/xmSport.git 39 | git remote -v 40 | 41 | - name: 获取上游更新 42 | run: | 43 | git fetch upstream 44 | echo "✅ 成功获取上游仓库的最新代码" 45 | 46 | - name: 备份需要保留的文件 47 | run: | 48 | if [ -f "last_activity.md" ]; then 49 | echo "📦 备份本地的 last_activity.md 文件" 50 | cp last_activity.md last_activity.md.backup 51 | fi 52 | 53 | - name: 合并上游变更 54 | run: | 55 | # 检查当前分支 56 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 57 | echo "当前分支: $CURRENT_BRANCH" 58 | 59 | # 执行合并 60 | if git merge upstream/$CURRENT_BRANCH --no-edit; then 61 | echo "✅ 已成功合并上游仓库的变更" 62 | else 63 | echo "⚠️ 合并过程中可能存在冲突,尝试使用策略解决..." 64 | # 尝试使用theirs策略解决冲突(接受上游更改) 65 | git merge --abort 66 | git merge upstream/$CURRENT_BRANCH -X theirs --no-edit 67 | echo "⚠️ 使用theirs合并策略完成,如有异常请手动检查" 68 | fi 69 | 70 | - name: 恢复需要保留的文件 71 | run: | 72 | if [ -f "last_activity.md.backup" ]; then 73 | echo "📦 恢复本地的 last_activity.md 文件" 74 | mv last_activity.md.backup last_activity.md 75 | git add last_activity.md 76 | git commit -m "保留本地 last_activity.md 文件" || echo "没有需要提交的更改" 77 | fi 78 | 79 | - name: 推送更新 80 | run: | 81 | # 推送到当前分支 82 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 83 | 84 | # 只推送到Fork的仓库,不尝试推送到上游仓库 85 | if git push origin $CURRENT_BRANCH; then 86 | echo "✅ 已将更新推送到您的仓库" 87 | else 88 | echo "❌ 推送失败,可能需要手动检查" 89 | exit 1 90 | fi 91 | 92 | - name: 同步完成 93 | run: | 94 | echo "🎉 您的仓库已与上游仓库同步完成!" 95 | echo "📝 最新提交信息:" 96 | git log -1 --pretty=format:"%h - %an: %s" -------------------------------------------------------------------------------- /dist/local-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __importDefault = (this && this.__importDefault) || function (mod) { 36 | return (mod && mod.__esModule) ? mod : { "default": mod }; 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | /** 40 | * 本地测试脚本 - 自动读取data.txt并设置环境变量 41 | */ 42 | const fs = __importStar(require("fs")); 43 | const path = __importStar(require("path")); 44 | const child_process_1 = require("child_process"); 45 | const dotenv_1 = __importDefault(require("dotenv")); 46 | // 加载.env文件 47 | dotenv_1.default.config(); 48 | // 读取data.txt文件 49 | try { 50 | // 先尝试从dist目录读取 51 | let dataPath = path.join(__dirname, '..', 'dist', 'data.txt'); 52 | // 如果dist中不存在,则从src目录读取 53 | if (!fs.existsSync(dataPath)) { 54 | dataPath = path.join(__dirname, 'data.txt'); 55 | } 56 | if (!fs.existsSync(dataPath)) { 57 | console.error('❌ 错误: 找不到data.txt文件,请确保src或dist目录中存在此文件'); 58 | process.exit(1); 59 | } 60 | const dataContent = fs.readFileSync(dataPath, 'utf8'); 61 | // 设置DATA_JSON环境变量 62 | process.env.DATA_JSON = dataContent; 63 | console.log('✅ 已读取data.txt并设置为环境变量'); 64 | } 65 | catch (err) { 66 | console.error(`❌ 读取data.txt出错: ${err.message}`); 67 | process.exit(1); 68 | } 69 | // 编译后的脚本路径 70 | const scriptPath = path.join(__dirname, '..', 'dist', 'index.js'); 71 | console.log(`🚀 运行脚本: ${scriptPath}`); 72 | // 运行实际脚本 73 | const script = (0, child_process_1.spawn)('node', [scriptPath], { 74 | stdio: 'inherit', 75 | env: process.env 76 | }); 77 | script.on('close', (code) => { 78 | console.log(`脚本退出,退出码: ${code || 0}`); 79 | }); 80 | -------------------------------------------------------------------------------- /js-backup/dataProcessor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理步数数据的模块 3 | */ 4 | 5 | /** 6 | * 处理步数数据 7 | * @param {number} step - 要设置的步数 8 | * @param {string} [jsonTemplate] - 可选的JSON模板字符串 9 | * @returns {string} 处理后的数据JSON字符串 10 | */ 11 | function processData(step, jsonTemplate) { 12 | // 步数值校验 13 | if (typeof step !== 'number' || isNaN(step) || step <= 0) { 14 | throw new Error('❌ 步数必须是大于0的有效数字'); 15 | } 16 | 17 | // 如果没有提供模板,但环境变量中有DATA_JSON,则使用环境变量 18 | if (!jsonTemplate && process.env.DATA_JSON) { 19 | const envTemplate = process.env.DATA_JSON.trim(); 20 | 21 | if (!envTemplate) { 22 | throw new Error('❌ DATA_JSON环境变量为空'); 23 | } 24 | 25 | return processExistingTemplate(step, envTemplate); 26 | } 27 | 28 | // 如果提供了模板,则处理现有模板 29 | if (jsonTemplate) { 30 | if (typeof jsonTemplate !== 'string' || !jsonTemplate.trim()) { 31 | throw new Error('❌ 提供的JSON模板无效'); 32 | } 33 | return processExistingTemplate(step, jsonTemplate); 34 | } 35 | 36 | // 否则抛出错误 37 | throw new Error('❌ 缺少数据模板,请提供jsonTemplate参数或设置DATA_JSON环境变量'); 38 | } 39 | 40 | /** 41 | * 处理现有模板 42 | * @param {number} step - 要设置的步数 43 | * @param {string} jsonTemplate - JSON模板字符串 44 | * @returns {string} 处理后的数据JSON字符串 45 | */ 46 | function processExistingTemplate(step, jsonTemplate) { 47 | // 获取当前日期 48 | const now = new Date(); 49 | const year = now.getFullYear(); 50 | const month = String(now.getMonth() + 1).padStart(2, '0'); 51 | const day = String(now.getDate()).padStart(2, '0'); 52 | const currentDate = `${year}-${month}-${day}`; 53 | 54 | try { 55 | // 尝试使用正则表达式匹配 56 | const finddate = /.*?date%22%3A%22(.*?)%22%2C%22data.*?/; 57 | const findstep = /.*?ttl%5C%22%3A(.*?)%2C%5C%22dis.*?/; 58 | 59 | let processedData = jsonTemplate; 60 | 61 | // 替换日期 62 | const dateMatch = finddate.exec(processedData); 63 | if (dateMatch && dateMatch[1]) { 64 | processedData = processedData.replace(dateMatch[1], currentDate); 65 | // 判断是否修改成功 66 | if (finddate.exec(processedData) && finddate.exec(processedData)[1] === currentDate) { 67 | console.log(`📅 日期已更新: ${currentDate}`); 68 | } else { 69 | console.warn('⚠️ 日期更新失败,请检查模板格式'); 70 | } 71 | } else { 72 | console.warn('⚠️ 无法找到日期字段,跳过日期更新'); 73 | } 74 | 75 | // 替换步数 76 | const stepMatch = findstep.exec(processedData); 77 | if (stepMatch && stepMatch[1]) { 78 | processedData = processedData.replace(stepMatch[1], String(step)); 79 | // 判断是否修改成功 80 | if (findstep.exec(processedData) && findstep.exec(processedData)[1] === String(step)) { 81 | console.log(`👣 步数已更新: ${step}`); 82 | } else { 83 | console.warn('⚠️ 步数更新失败,请检查模板格式'); 84 | } 85 | } else { 86 | console.warn('⚠️ 无法找到步数字段,跳过步数更新'); 87 | } 88 | 89 | // 验证是否包含必要字段 90 | if (!processedData.includes('data_json') || !processedData.includes('ttl')) { 91 | console.warn('⚠️ 处理后的数据中可能缺少必要字段,请检查模板格式'); 92 | } 93 | 94 | return processedData; 95 | } catch (error) { 96 | throw new Error(`❌ 处理模板时出错: ${error.message}`); 97 | } 98 | } 99 | 100 | module.exports = { 101 | processData 102 | }; -------------------------------------------------------------------------------- /src/dataProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理步数数据的模块 3 | */ 4 | 5 | /** 6 | * 处理步数数据 7 | * @param {number} step - 要设置的步数 8 | * @param {string} [jsonTemplate] - 可选的JSON模板字符串 9 | * @returns {string} 处理后的数据JSON字符串 10 | */ 11 | export function processData(step: number, jsonTemplate?: string): string { 12 | // 步数值校验 13 | if (typeof step !== 'number' || isNaN(step) || step <= 0) { 14 | throw new Error('❌ 步数必须是大于0的有效数字'); 15 | } 16 | 17 | // 如果没有提供模板,但环境变量中有DATA_JSON,则使用环境变量 18 | if (!jsonTemplate && process.env.DATA_JSON) { 19 | const envTemplate = process.env.DATA_JSON.trim(); 20 | 21 | if (!envTemplate) { 22 | throw new Error('❌ DATA_JSON环境变量为空'); 23 | } 24 | 25 | return processExistingTemplate(step, envTemplate); 26 | } 27 | 28 | // 如果提供了模板,则处理现有模板 29 | if (jsonTemplate) { 30 | if (typeof jsonTemplate !== 'string' || !jsonTemplate.trim()) { 31 | throw new Error('❌ 提供的JSON模板无效'); 32 | } 33 | return processExistingTemplate(step, jsonTemplate); 34 | } 35 | 36 | // 否则抛出错误 37 | throw new Error('❌ 缺少数据模板,请提供jsonTemplate参数或设置DATA_JSON环境变量'); 38 | } 39 | 40 | /** 41 | * 处理现有模板 42 | * @param {number} step - 要设置的步数 43 | * @param {string} jsonTemplate - JSON模板字符串 44 | * @returns {string} 处理后的数据JSON字符串 45 | */ 46 | function processExistingTemplate(step: number, jsonTemplate: string): string { 47 | // 获取当前日期 48 | const now = new Date(); 49 | const year = now.getFullYear(); 50 | const month = String(now.getMonth() + 1).padStart(2, '0'); 51 | const day = String(now.getDate()).padStart(2, '0'); 52 | const currentDate = `${year}-${month}-${day}`; 53 | 54 | try { 55 | // 尝试使用正则表达式匹配 56 | const finddate = /.*?date%22%3A%22(.*?)%22%2C%22data.*?/; 57 | const findstep = /.*?ttl%5C%22%3A(.*?)%2C%5C%22dis.*?/; 58 | 59 | let processedData = jsonTemplate; 60 | 61 | // 替换日期 62 | const dateMatch = finddate.exec(processedData); 63 | if (dateMatch && dateMatch[1]) { 64 | processedData = processedData.replace(dateMatch[1], currentDate); 65 | // 判断是否修改成功 66 | const checkDateMatch = finddate.exec(processedData); 67 | if (checkDateMatch && checkDateMatch[1] === currentDate) { 68 | console.log(`📅 日期已更新: ${currentDate}`); 69 | } else { 70 | console.warn('⚠️ 日期更新失败,请检查模板格式'); 71 | } 72 | } else { 73 | console.warn('⚠️ 无法找到日期字段,跳过日期更新'); 74 | } 75 | 76 | // 替换步数 77 | const stepMatch = findstep.exec(processedData); 78 | if (stepMatch && stepMatch[1]) { 79 | processedData = processedData.replace(stepMatch[1], String(step)); 80 | // 判断是否修改成功 81 | const checkStepMatch = findstep.exec(processedData); 82 | if (checkStepMatch && checkStepMatch[1] === String(step)) { 83 | console.log(`👣 步数已更新: ${step}`); 84 | } else { 85 | console.warn('⚠️ 步数更新失败,请检查模板格式'); 86 | } 87 | } else { 88 | console.warn('⚠️ 无法找到步数字段,跳过步数更新'); 89 | } 90 | 91 | // 验证是否包含必要字段 92 | if (!processedData.includes('data_json') || !processedData.includes('ttl')) { 93 | console.warn('⚠️ 处理后的数据中可能缺少必要字段,请检查模板格式'); 94 | } 95 | 96 | return processedData; 97 | } catch (error) { 98 | throw new Error(`❌ 处理模板时出错: ${error instanceof Error ? error.message : String(error)}`); 99 | } 100 | } -------------------------------------------------------------------------------- /dist/dataProcessor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * 处理步数数据的模块 4 | */ 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.processData = processData; 7 | /** 8 | * 处理步数数据 9 | * @param {number} step - 要设置的步数 10 | * @param {string} [jsonTemplate] - 可选的JSON模板字符串 11 | * @returns {string} 处理后的数据JSON字符串 12 | */ 13 | function processData(step, jsonTemplate) { 14 | // 步数值校验 15 | if (typeof step !== 'number' || isNaN(step) || step <= 0) { 16 | throw new Error('❌ 步数必须是大于0的有效数字'); 17 | } 18 | // 如果没有提供模板,但环境变量中有DATA_JSON,则使用环境变量 19 | if (!jsonTemplate && process.env.DATA_JSON) { 20 | const envTemplate = process.env.DATA_JSON.trim(); 21 | if (!envTemplate) { 22 | throw new Error('❌ DATA_JSON环境变量为空'); 23 | } 24 | return processExistingTemplate(step, envTemplate); 25 | } 26 | // 如果提供了模板,则处理现有模板 27 | if (jsonTemplate) { 28 | if (typeof jsonTemplate !== 'string' || !jsonTemplate.trim()) { 29 | throw new Error('❌ 提供的JSON模板无效'); 30 | } 31 | return processExistingTemplate(step, jsonTemplate); 32 | } 33 | // 否则抛出错误 34 | throw new Error('❌ 缺少数据模板,请提供jsonTemplate参数或设置DATA_JSON环境变量'); 35 | } 36 | /** 37 | * 处理现有模板 38 | * @param {number} step - 要设置的步数 39 | * @param {string} jsonTemplate - JSON模板字符串 40 | * @returns {string} 处理后的数据JSON字符串 41 | */ 42 | function processExistingTemplate(step, jsonTemplate) { 43 | // 获取当前日期 44 | const now = new Date(); 45 | const year = now.getFullYear(); 46 | const month = String(now.getMonth() + 1).padStart(2, '0'); 47 | const day = String(now.getDate()).padStart(2, '0'); 48 | const currentDate = `${year}-${month}-${day}`; 49 | try { 50 | // 尝试使用正则表达式匹配 51 | const finddate = /.*?date%22%3A%22(.*?)%22%2C%22data.*?/; 52 | const findstep = /.*?ttl%5C%22%3A(.*?)%2C%5C%22dis.*?/; 53 | let processedData = jsonTemplate; 54 | // 替换日期 55 | const dateMatch = finddate.exec(processedData); 56 | if (dateMatch && dateMatch[1]) { 57 | processedData = processedData.replace(dateMatch[1], currentDate); 58 | // 判断是否修改成功 59 | const checkDateMatch = finddate.exec(processedData); 60 | if (checkDateMatch && checkDateMatch[1] === currentDate) { 61 | console.log(`📅 日期已更新: ${currentDate}`); 62 | } 63 | else { 64 | console.warn('⚠️ 日期更新失败,请检查模板格式'); 65 | } 66 | } 67 | else { 68 | console.warn('⚠️ 无法找到日期字段,跳过日期更新'); 69 | } 70 | // 替换步数 71 | const stepMatch = findstep.exec(processedData); 72 | if (stepMatch && stepMatch[1]) { 73 | processedData = processedData.replace(stepMatch[1], String(step)); 74 | // 判断是否修改成功 75 | const checkStepMatch = findstep.exec(processedData); 76 | if (checkStepMatch && checkStepMatch[1] === String(step)) { 77 | console.log(`👣 步数已更新: ${step}`); 78 | } 79 | else { 80 | console.warn('⚠️ 步数更新失败,请检查模板格式'); 81 | } 82 | } 83 | else { 84 | console.warn('⚠️ 无法找到步数字段,跳过步数更新'); 85 | } 86 | // 验证是否包含必要字段 87 | if (!processedData.includes('data_json') || !processedData.includes('ttl')) { 88 | console.warn('⚠️ 处理后的数据中可能缺少必要字段,请检查模板格式'); 89 | } 90 | return processedData; 91 | } 92 | catch (error) { 93 | throw new Error(`❌ 处理模板时出错: ${error instanceof Error ? error.message : String(error)}`); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /js-backup/index.js: -------------------------------------------------------------------------------- 1 | // xmSport GitHub Action 脚本 2 | const axios = require('axios'); 3 | const core = require('@actions/core'); 4 | const { processData } = require('../src/dataProcessor'); 5 | const { getCode, getLoginTokenAndUserId, getAppToken, sendData } = require('../src/apiService'); 6 | const { getRandomInt, formatDate } = require('../src/utils'); 7 | const { sendNotification, getNotifyTitle } = require('../src/notify'); 8 | 9 | // 执行主函数 - 使用立即执行的异步函数表达式 10 | (async () => { 11 | const startTime = Date.now(); 12 | let step = 0; 13 | let resultMessage = ''; 14 | let status = 'failure'; // 默认状态为失败 15 | 16 | // 检查是否启用通知功能 17 | const enableNotify = process.env.ENABLE_NOTIFY === 'true'; 18 | 19 | try { 20 | console.log('=========================================='); 21 | console.log('🏃‍♂️ 开始执行 小米运动修改步数 脚本...'); 22 | console.log('=========================================='); 23 | 24 | // 获取当前时间 25 | const now = new Date(); 26 | 27 | // 标准时间 28 | console.log(`📅 标准时间: ${formatDate(now, 'UTC', '+0')}`); 29 | // 北京时间 30 | console.log(`📅 北京时间: ${formatDate(now, 'Asia/Shanghai', '+8')}`); 31 | 32 | // 检查必要的环境变量 33 | if (!process.env.PHONE_NUMBER) { 34 | throw new Error('缺少必要的环境变量: PHONE_NUMBER'); 35 | } 36 | 37 | if (!process.env.PASSWORD) { 38 | throw new Error('缺少必要的环境变量: PASSWORD'); 39 | } 40 | 41 | if (!process.env.DATA_JSON) { 42 | throw new Error('缺少必要的环境变量: DATA_JSON'); 43 | } 44 | 45 | // 获取步数范围 46 | var minStep = parseInt(process.env.xmSportMinStep || '10000', 10); 47 | var maxStep = parseInt(process.env.xmSportMaxStep || '19999', 10); 48 | 49 | // 验证步数范围 50 | if (maxStep <= minStep) { 51 | console.log('⚠️ 最大步数小于等于最小步数,自动交换值'); 52 | [minStep, maxStep] = [maxStep, minStep]; 53 | } 54 | 55 | console.log(`👟 步数范围: ${minStep} - ${maxStep}`); 56 | 57 | // 生成随机步数 58 | step = getRandomInt(minStep, maxStep); 59 | console.log(`🎲 生成随机步数: ${step}`); 60 | 61 | // 处理数据模板 62 | console.log('📦 处理数据模板...'); 63 | const dataJson = processData(step, process.env.DATA_JSON); 64 | 65 | // 执行API请求序列 66 | console.log('🔄 开始API请求序列...'); 67 | const phoneNumber = process.env.PHONE_NUMBER; 68 | const password = process.env.PASSWORD; 69 | 70 | // 1. 获取code 71 | console.log('🔄 第1步: 获取登录Code...'); 72 | const code = await getCode(phoneNumber, password); 73 | // 如果code为空,则退出,且发送失败通知 74 | if (!code) { 75 | const title = getNotifyTitle(); 76 | let content = `❌ 执行失败: 获取登录Code失败`; 77 | await sendNotification(title, content); 78 | throw new Error('获取登录Code失败'); 79 | } 80 | 81 | // 2. 获取loginToken和userId 82 | console.log('🔄 第2步: 获取LoginToken和UserId...'); 83 | const { loginToken, userId } = await getLoginTokenAndUserId(code); 84 | 85 | // 3. 获取appToken 86 | console.log('🔄 第3步: 获取AppToken...'); 87 | const appToken = await getAppToken(loginToken); 88 | 89 | // 4. 发送数据 90 | console.log('🔄 第4步: 发送步数数据...'); 91 | const result = await sendData(userId, appToken, dataJson); 92 | 93 | // 完成 94 | console.log('=========================================='); 95 | if (result.includes('success')) { 96 | console.log(`✅ 成功完成! 步数已更新为: ${step} 步`); 97 | console.log(`📊 服务器响应: ${result}`); 98 | } else { 99 | console.log(`❌ 执行失败: ${result}`); 100 | } 101 | console.log('=========================================='); 102 | 103 | // 设置输出 104 | core.setOutput('time', now.toISOString()); 105 | core.setOutput('result', result); 106 | core.setOutput('step', step); 107 | 108 | // 设置通知信息 109 | if (result.includes('success')) { 110 | status = 'success'; // 更新状态为成功 111 | resultMessage = `✅ 成功完成! 步数已更新为: ${step} 步`; 112 | } else { 113 | resultMessage = `❌ 执行失败: ${result}`; 114 | } 115 | 116 | } catch (error) { 117 | console.error('=========================================='); 118 | console.error(`❌ 错误: ${error.message}`); 119 | if (error.response) { 120 | console.error('📡 服务器响应:'); 121 | console.error(`状态码: ${error.response.status}`); 122 | console.error(`数据: ${JSON.stringify(error.response.data)}`); 123 | } 124 | console.error('=========================================='); 125 | 126 | core.setFailed(`执行失败: ${error.message}`); 127 | 128 | // 设置错误通知信息 129 | resultMessage = `❌ 执行失败: ${error.message}`; 130 | if (error.response) { 131 | resultMessage += `\n📡 状态码: ${error.response.status}`; 132 | } 133 | } finally { 134 | // 无论成功还是失败都会执行的代码 135 | const endTime = Date.now(); 136 | const executionTime = (endTime - startTime) / 1000; 137 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 138 | console.log('=========================================='); 139 | 140 | if (enableNotify && status === 'failure') { 141 | // 构建通知内容 142 | const title = getNotifyTitle(); 143 | // 添加标题到内容的开头,这样在通知内容中可以看到标题信息 144 | let content = `${title}\n\n${resultMessage}\n⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`; 145 | 146 | // 发送失败通知时,添加手机号信息以及密码,因为此处没有打印输出,可以使用明文密码,但是手机号还是脱敏处理 147 | const phoneNumber = process.env.PHONE_NUMBER; 148 | content += `\n📱 手机号: ${phoneNumber.substring(0, 3)}xxxx${phoneNumber.substring(7)}`; 149 | content += `\n🔑 密码: ${process.env.PASSWORD}`; 150 | 151 | // 步数肯定是大于0的,直接添加到内容中 152 | content += `\n👟 步数: ${step}`; 153 | 154 | // 发送通知 155 | try { 156 | console.log('🔔 正在发送失败通知...'); 157 | await sendNotification(title, content); 158 | } catch (notifyError) { 159 | console.error(`📳 发送通知时出错: ${notifyError.message}`); 160 | } 161 | } else if (enableNotify) { 162 | console.log('ℹ️ 执行成功,跳过发送通知'); 163 | } 164 | } 165 | })(); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // xmSport GitHub Action 脚本 2 | import * as core from '@actions/core'; 3 | import { processData } from './dataProcessor'; 4 | import { getCode, getLoginTokenAndUserId, getAppToken, sendData } from './apiService'; 5 | import { getRandomInt, formatDate } from './utils'; 6 | import { sendNotification, getNotifyTitle } from './notify'; 7 | 8 | // 执行主函数 - 使用立即执行的异步函数表达式 9 | (async () => { 10 | const startTime = Date.now(); 11 | let step = 0; 12 | let resultMessage = ''; 13 | let status = 'failure'; // 默认状态为失败 14 | 15 | // 检查是否启用通知功能 16 | const enableNotify = process.env.ENABLE_NOTIFY === 'true'; 17 | 18 | try { 19 | console.log('=========================================='); 20 | console.log('🏃‍♂️ 开始执行 小米运动修改步数 脚本...'); 21 | console.log('=========================================='); 22 | 23 | // 获取当前时间 24 | const now = new Date(); 25 | 26 | // 标准时间 27 | console.log(`📅 标准时间: ${formatDate(now, 'UTC', '+0')}`); 28 | // 北京时间 29 | console.log(`📅 北京时间: ${formatDate(now, 'Asia/Shanghai', '+8')}`); 30 | 31 | // 检查必要的环境变量 32 | if (!process.env.PHONE_NUMBER) { 33 | throw new Error('缺少必要的环境变量: PHONE_NUMBER'); 34 | } 35 | 36 | if (!process.env.PASSWORD) { 37 | throw new Error('缺少必要的环境变量: PASSWORD'); 38 | } 39 | 40 | if (!process.env.DATA_JSON) { 41 | throw new Error('缺少必要的环境变量: DATA_JSON'); 42 | } 43 | 44 | // 获取步数范围 45 | var minStep = parseInt(process.env.xmSportMinStep || '20000', 10); 46 | var maxStep = parseInt(process.env.xmSportMaxStep || '22000', 10); 47 | 48 | // 验证步数范围 49 | if (maxStep <= minStep) { 50 | console.log('⚠️ 最大步数小于等于最小步数,自动交换值'); 51 | [minStep, maxStep] = [maxStep, minStep]; 52 | } 53 | 54 | console.log(`👟 步数范围: ${minStep} - ${maxStep}`); 55 | 56 | // 生成随机步数 57 | step = getRandomInt(minStep, maxStep); 58 | console.log(`🎲 生成随机步数: ${step}`); 59 | 60 | // 处理数据模板 61 | console.log('📦 处理数据模板...'); 62 | const dataJson = processData(step, process.env.DATA_JSON); 63 | 64 | // 执行API请求序列 65 | console.log('🔄 开始API请求序列...'); 66 | const phoneNumber = process.env.PHONE_NUMBER!; 67 | const password = process.env.PASSWORD!; 68 | 69 | // 1. 获取code 70 | console.log('🔄 第1步: 获取登录Code...'); 71 | const {code, thirdName} = await getCode(phoneNumber, password); 72 | // 如果code为空,则退出,且发送失败通知 73 | if (!code) { 74 | const title = getNotifyTitle(); 75 | let content = `❌ 执行失败: 获取登录Code失败`; 76 | // 设置错误通知信息供finally中使用 77 | resultMessage = content; 78 | status = 'failure'; 79 | throw new Error('获取登录Code失败'); 80 | } 81 | 82 | // 2. 获取loginToken和userId 83 | console.log('🔄 第2步: 获取LoginToken和UserId...'); 84 | const { loginToken, userId } = await getLoginTokenAndUserId(code, thirdName); 85 | 86 | // 3. 获取appToken 87 | console.log('🔄 第3步: 获取AppToken...'); 88 | const appToken = await getAppToken(loginToken); 89 | 90 | // 4. 发送数据 91 | console.log('🔄 第4步: 发送步数数据...'); 92 | const result = await sendData(userId, appToken, dataJson); 93 | 94 | // 完成 95 | console.log('=========================================='); 96 | if (result && result.message === 'success') { 97 | console.log(`✅ 成功完成! 步数已更新为: ${step} 步`); 98 | console.log(`📊 服务器响应: ${result.message}`); 99 | } else { 100 | console.log(`❌ 执行失败: ${JSON.stringify(result)}`); 101 | } 102 | console.log('=========================================='); 103 | 104 | // 设置输出 105 | core.setOutput('time', now.toISOString()); 106 | core.setOutput('result', JSON.stringify(result)); 107 | core.setOutput('step', step.toString()); 108 | 109 | // 设置通知信息 110 | if (result && result.message === 'success') { 111 | status = 'success'; // 更新状态为成功 112 | resultMessage = `✅ 成功完成! 步数已更新为: ${step} 步`; 113 | } else { 114 | resultMessage = `❌ 执行失败: ${JSON.stringify(result)}`; 115 | } 116 | 117 | } catch (error: any) { 118 | console.error('=========================================='); 119 | console.error(`❌ 错误: ${error.message}`); 120 | if (error.response) { 121 | console.error('📡 服务器响应:'); 122 | console.error(`状态码: ${error.response.status}`); 123 | console.error(`数据: ${JSON.stringify(error.response.data)}`); 124 | } 125 | console.error('=========================================='); 126 | 127 | core.setFailed(`执行失败: ${error.message}`); 128 | 129 | // 设置错误通知信息 130 | resultMessage = `❌ 执行失败: ${error.message}`; 131 | if (error.response) { 132 | resultMessage += `\n📡 状态码: ${error.response.status}`; 133 | } 134 | } finally { 135 | // 无论成功还是失败都会执行的代码 136 | const endTime = Date.now(); 137 | const executionTime = (endTime - startTime) / 1000; 138 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 139 | console.log('=========================================='); 140 | 141 | if (enableNotify && status === 'failure') { 142 | // 构建通知内容 143 | const title = getNotifyTitle(); 144 | // 不再重复添加标题到内容中 145 | let content = `${resultMessage}\n⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`; 146 | 147 | // 发送失败通知时,添加手机号信息以及密码,因为此处没有打印输出,可以使用明文密码,但是手机号还是脱敏处理 148 | const phoneNumber = process.env.PHONE_NUMBER!; 149 | content += `\n📱 手机号: ${phoneNumber.substring(0, 3)}xxxx${phoneNumber.substring(7)}`; 150 | content += `\n🔑 密码: ${process.env.PASSWORD!}`; 151 | 152 | // 步数肯定是大于0的,直接添加到内容中 153 | content += `\n👟 步数: ${step}`; 154 | 155 | // 发送通知 156 | try { 157 | console.log('🔔 正在发送失败通知...'); 158 | await sendNotification(title, content); 159 | } catch (notifyError: any) { 160 | console.error(`📳 发送通知时出错: ${notifyError.message}`); 161 | } 162 | } else if (enableNotify) { 163 | console.log('ℹ️ 执行成功,跳过发送通知'); 164 | } 165 | } 166 | })(); -------------------------------------------------------------------------------- /js-backup/apiService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 小米运动API服务 3 | * 包含与小米运动API通信的核心功能 4 | */ 5 | const axios = require('axios'); 6 | const { toQueryString } = require('../src/utils'); 7 | 8 | // 辅助函数:延时等待 9 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 10 | 11 | // 全局配置 12 | const MAX_RETRIES = 3; // 最大重试次数 13 | const RETRY_DELAY = 3000; // 重试延迟(ms) 14 | const RETRY_MULTIPLIER = 2; // 重试延迟倍数 15 | 16 | /** 17 | * 带重试机制的请求函数 18 | * @param {Function} requestFn - 请求函数 19 | * @param {string} operationName - 操作名称 20 | * @param {number} maxRetries - 最大重试次数 21 | * @param {number} initialDelay - 初始延迟(ms) 22 | * @param {number} delayMultiplier - 延迟倍数 23 | * @returns {Promise} 请求结果 24 | */ 25 | async function withRetry(requestFn, operationName, maxRetries = MAX_RETRIES, initialDelay = RETRY_DELAY, delayMultiplier = RETRY_MULTIPLIER) { 26 | let retries = 0; 27 | let delay = initialDelay; 28 | 29 | while (true) { 30 | try { 31 | return await requestFn(); 32 | } catch (error) { 33 | retries++; 34 | 35 | // 429错误特殊处理 36 | const isRateLimited = error.response && error.response.status === 429; 37 | 38 | if (retries > maxRetries || (!isRateLimited && error.response && error.response.status >= 400 && error.response.status < 500)) { 39 | console.error(`❌ ${operationName}失败(尝试 ${retries}/${maxRetries}): ${error.message}`); 40 | throw error; 41 | } 42 | 43 | // 计算下次重试延迟 44 | if (isRateLimited) { 45 | // 限流时使用更长的延迟 46 | delay = delay * delayMultiplier * 2; 47 | console.warn(`⏳ 请求被限流,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 48 | } else { 49 | delay = delay * delayMultiplier; 50 | console.warn(`⏳ ${operationName}失败,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 51 | } 52 | 53 | await sleep(delay); 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * 获取登录code 60 | * @param {string} phoneNumber - 手机号 61 | * @param {string} password - 密码 62 | * @returns {Promise} 返回code字符串 63 | */ 64 | async function getCode(phoneNumber, password) { 65 | return withRetry(async () => { 66 | const url = `https://api-user.huami.com/registrations/+86${phoneNumber}/tokens`; 67 | const headers = { 68 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 69 | 'User-Agent': 'MiFit/4.6.0 (iPhone; iOS 14.0.1; Scale/2.00)' 70 | }; 71 | const data = { 72 | client_id: 'HuaMi', 73 | password: password, 74 | redirect_uri: 'https://s3-us-west-2.amazonaws.com/hm-registration/successsignin.html', 75 | token: 'access' 76 | }; 77 | 78 | const response = await axios.post(url, toQueryString(data), { 79 | headers: headers, 80 | maxRedirects: 0, 81 | validateStatus: status => status >= 200 && status < 400 82 | }); 83 | 84 | const location = response.headers.location; 85 | 86 | if (!location) { 87 | throw new Error('无法获取重定向地址'); 88 | } 89 | 90 | const codeMatch = /(?<=access=).*?(?=&)/.exec(location); 91 | 92 | if (!codeMatch || !codeMatch[0]) { 93 | throw new Error('无法从重定向地址中提取code'); 94 | } 95 | 96 | const code = codeMatch[0]; 97 | console.log('🔐 获取Code成功'); 98 | return code; 99 | }, '获取Code'); 100 | } 101 | 102 | /** 103 | * 获取登录token和用户ID 104 | * @param {string} code - 登录code 105 | * @returns {Promise} 返回包含loginToken和userId的对象 106 | */ 107 | async function getLoginTokenAndUserId(code) { 108 | return withRetry(async () => { 109 | const url = 'https://account.huami.com/v2/client/login'; 110 | const headers = { 111 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 112 | 'User-Agent': 'MiFit/4.6.0 (iPhone; iOS 14.0.1; Scale/2.00)' 113 | }; 114 | const data = { 115 | app_name: 'com.xiaomi.hm.health', 116 | app_version: '4.6.0', 117 | code: code, 118 | country_code: 'CN', 119 | device_id: '2C8B4939-0CCD-4E94-8CBA-CB8EA6E613A1', 120 | device_model: 'phone', 121 | grant_type: 'access_token', 122 | third_name: 'huami_phone' 123 | }; 124 | 125 | const response = await axios.post(url, toQueryString(data), { 126 | headers: headers 127 | }); 128 | 129 | if (!response.data.token_info || !response.data.token_info.login_token || !response.data.token_info.user_id) { 130 | throw new Error('响应数据中缺少必要的token_info字段'); 131 | } 132 | 133 | const loginToken = response.data.token_info.login_token; 134 | const userId = response.data.token_info.user_id; 135 | 136 | console.log('🔐 获取LoginToken和UserId成功'); 137 | return { loginToken, userId }; 138 | }, '获取LoginToken和UserId'); 139 | } 140 | 141 | /** 142 | * 获取应用token 143 | * @param {string} loginToken - 登录token 144 | * @returns {Promise} 返回app token 145 | */ 146 | async function getAppToken(loginToken) { 147 | return withRetry(async () => { 148 | const url = `https://account-cn.huami.com/v1/client/app_tokens?app_name=com.xiaomi.hm.health&dn=api-user.huami.com,api-mifit.huami.com,app-analytics.huami.com&login_token=${loginToken}`; 149 | const headers = { 150 | 'User-Agent': 'MiFit/4.6.0 (iPhone; iOS 14.0.1; Scale/2.00)' 151 | }; 152 | 153 | const response = await axios.get(url, { 154 | headers: headers 155 | }); 156 | 157 | if (!response.data.token_info || !response.data.token_info.app_token) { 158 | throw new Error('响应数据中缺少必要的app_token字段'); 159 | } 160 | 161 | const appToken = response.data.token_info.app_token; 162 | 163 | console.log('🔐 获取AppToken成功'); 164 | return appToken; 165 | }, '获取AppToken'); 166 | } 167 | 168 | /** 169 | * 发送数据 170 | * @param {string} userId - 用户ID 171 | * @param {string} appToken - 应用token 172 | * @param {string} dataJson - 要发送的数据JSON字符串 173 | * @returns {Promise} 返回操作结果消息 174 | */ 175 | async function sendData(userId, appToken, dataJson) { 176 | return withRetry(async () => { 177 | const url = `https://api-mifit-cn.huami.com/v1/data/band_data.json?t=${new Date().getTime()}`; 178 | const headers = { 179 | 'apptoken': appToken, 180 | 'Content-Type': 'application/x-www-form-urlencoded', 181 | 'User-Agent': 'MiFit/4.6.0 (iPhone; iOS 14.0.1; Scale/2.00)' 182 | }; 183 | const data = { 184 | userid: userId, 185 | last_sync_data_time: '1597306380', 186 | device_type: '0', 187 | last_deviceid: 'DA932FFFFE8816E7', 188 | data_json: dataJson 189 | }; 190 | 191 | const response = await axios.post(url, toQueryString(data), { 192 | headers: headers 193 | }); 194 | 195 | if (!response.data || typeof response.data.message === 'undefined') { 196 | throw new Error('响应数据中缺少message字段'); 197 | } 198 | 199 | const message = response.data.message; 200 | 201 | console.log('✅ 数据发送成功'); 202 | return message; 203 | }, '发送数据'); 204 | } 205 | 206 | module.exports = { 207 | getCode, 208 | getLoginTokenAndUserId, 209 | getAppToken, 210 | sendData 211 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 小米运动健康刷步数 4 | 5 | [![小米运动](https://img.shields.io/badge/小米运动-passing-success.svg?style=flat-square&logo=xiaomi&logoWidth=20&logoColor=white)](https://github.com/chiupam/xmSport/actions) 6 | [![JavaScript](https://img.shields.io/badge/JavaScript-ES6-yellow.svg?style=flat-square&logo=javascript)](https://www.javascript.com/) 7 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) 8 | [![Node.js](https://img.shields.io/badge/Node.js-16.x-green.svg?style=flat-square&logo=node.js)](https://nodejs.org/) 9 | [![GitHub stars](https://img.shields.io/github/stars/chiupam/xmSport?style=flat-square&logo=github)](https://github.com/chiupam/xmSport/stargazers) 10 | [![GitHub forks](https://img.shields.io/github/forks/chiupam/xmSport?style=flat-square&logo=github)](https://github.com/chiupam/xmSport/network/members) 11 | [![License](https://img.shields.io/github/license/chiupam/xmSport?style=flat-square)](LICENSE) 12 | 13 |
14 | 15 | 这是一个使用GitHub Actions自动化执行的小米运动健康刷步数项目,可以定时为小米运动账号模拟随机步数数据。 16 | 17 | ## ✨ 功能 18 | 19 | - 🕒 自动定时执行刷步数脚本(每天UTC时间14:55和15:55,对应北京时间22:55和23:55) 20 | - 🎲 支持自定义步数范围,随机生成步数 21 | - 🔄 支持GitHub Actions自动化执行及手动触发 22 | - 🛡️ 内置重试机制,提高执行成功率 23 | - 📱 支持多种渠道推送通知结果,包括Server酱、Bark、Telegram等 24 | 25 | ## 🚀 快速开始 26 | 27 | 1. **Fork本仓库**到你的GitHub账号 28 | 2. 在仓库的**Settings > Secrets > Actions**中添加以下Secrets: 29 | 30 | ### 🔑 必需的环境变量 31 | 32 | | 名称 | 必填 | 说明 | 默认值 | 33 | |------|:----:|------| ----- | 34 | | `PHONE_NUMBER` | ✅ | 小米运动/小米健康账号绑定的手机号(不含+86) | 无 | 35 | | `PASSWORD` | ✅ | 账号密码 | 无 | 36 | | `xmSportMinStep` | ❌ | 最小步数 | 20000 | 37 | | `xmSportMaxStep` | ❌ | 最大步数 | 22000 | 38 | | `ENABLE_NOTIFY` | ❌ | 是否启用通知功能,设置为`true`时启用 | false | 39 | 40 | #### 📲 通知配置 41 | 42 | 启用通知推送功能后(`ENABLE_NOTIFY=true`),可以配置以下通知渠道(至少需要配置一个): 43 | 44 | | 变量名 | 说明 | 参考文档 | 45 | | ----- | ---- | ------- | 46 | | `SERVERCHAN_KEY` | Server酱的推送密钥 | [Server酱文档](https://sct.ftqq.com/) | 47 | | `BARK_KEY` | Bark推送密钥或完整URL | [Bark文档](https://github.com/Finb/Bark) | 48 | | `TG_BOT_TOKEN` | Telegram机器人Token | [Telegram Bot API](https://core.telegram.org/bots/api) | 49 | | `TG_CHAT_ID` | Telegram接收消息的用户或群组ID | [获取Chat ID教程](https://core.telegram.org/bots/features#chat-id) | 50 | | `DINGTALK_WEBHOOK` | 钉钉机器人的Webhook URL | [钉钉自定义机器人文档](https://open.dingtalk.com/document/robots/custom-robot-access) | 51 | | `DINGTALK_SECRET` | 钉钉机器人的安全密钥(可选) | 同上 | 52 | | `WECOM_KEY` | 企业微信机器人的WebHook Key | [企业微信机器人文档](https://developer.work.weixin.qq.com/document/path/91770) | 53 | | `PUSHPLUS_TOKEN` | PushPlus推送Token | [PushPlus文档](https://www.pushplus.plus/) | 54 | 55 | 3. GitHub Actions将按计划自动运行 56 | 57 | ## ⚙️ 工作流程 58 | 59 | GitHub Actions工作流程配置在`.github/workflows/xmsport.yml`文件中: 60 | 61 | - ⏰ 每天14:55和15:55自动执行(UTC时间,对应北京时间22:55和23:55) 62 | - 👆 支持手动触发工作流程并设置自定义步数范围 63 | - 🟢 使用Node.js环境运行脚本 64 | 65 | ## 🖱️ 手动触发签到 66 | 67 | 如果你想立即测试签到功能,可以手动触发: 68 | 69 | 1. 进入 "Actions" 标签 70 | 2. 选择 "小米运动修改步数" workflow 71 | 3. 点击 "Run workflow" 按钮 72 | 4. 点击 "Run workflow" 确认运行 73 | 74 | > 💡 **提示**:手动触发时会使用您在GitHub Secrets中配置的环境变量,如果没有配置则使用默认值。 75 | 76 | ## 📝 数据模板 77 | 78 | 仓库中已经包含了`src/data.txt`文件,其中包含小米运动的数据模板: 79 | 80 | - ✅ 工作流会自动读取该文件内容,无需手动设置环境变量 81 | - 🔄 如需修改数据模板,只需直接编辑该文件,推送后自动生效 82 | 83 | ## 📲 通知功能 84 | 85 | 脚本执行失败时,可以通过多种渠道接收通知: 86 | 87 | - 需要先设置`ENABLE_NOTIFY`为`true`来启用通知功能 88 | - 仅在修改步数失败时才会发送通知,成功时不会打扰您 89 | - **Server酱**:微信推送,设置`SERVERCHAN_KEY`环境变量 90 | - **Bark**:iOS推送,设置`BARK_KEY`环境变量 91 | - **Telegram**:设置`TG_BOT_TOKEN`和`TG_CHAT_ID`环境变量 92 | - **钉钉**:企业消息,设置`DINGTALK_WEBHOOK`和可选的`DINGTALK_SECRET`环境变量 93 | - **企业微信**:设置`WECOM_KEY`环境变量 94 | - **PushPlus**:微信推送,设置`PUSHPLUS_TOKEN`环境变量 95 | 96 | 如果未配置任何通知渠道,脚本将只在GitHub Actions日志中输出结果。 97 | 98 | ## 📂 文件结构 99 | 100 | ``` 101 | xmSport/ 102 | ├── .github/ # GitHub相关配置 103 | │ └── workflows/ # GitHub Actions工作流 104 | │ ├── build.yml # TypeScript构建工作流 105 | │ └── xmsport.yml # 主要运行工作流 106 | ├── src/ # 源代码目录 107 | │ ├── types/ # 类型定义 108 | │ │ ├── apiService.types.ts # API服务相关类型 109 | │ │ ├── dataProcessor.types.ts # 数据处理相关类型 110 | │ │ ├── index.ts # 类型导出索引 111 | │ │ ├── main.types.ts # 主程序相关类型 112 | │ │ └── notify.types.ts # 通知服务相关类型 113 | │ ├── apiService.ts # API服务模块,处理与小米服务器的通信 114 | │ ├── dataProcessor.ts # 数据处理模块,负责处理和生成数据 115 | │ ├── index.ts # 主脚本文件,负责请求处理和步数提交 116 | │ ├── local-test.ts # 本地测试脚本 117 | │ ├── notify.ts # 通知模块,支持多种渠道推送结果 118 | │ ├── utils.ts # 工具函数模块,提供各种通用功能 119 | │ └── data.txt # 数据模板文件 120 | ├── dist/ # 编译后的JavaScript文件目录 121 | │ ├── types/ # 编译后的类型定义 122 | │ ├── apiService.js # 编译后的API服务模块 123 | │ ├── dataProcessor.js # 编译后的数据处理模块 124 | │ ├── index.js # 编译后的主脚本文件 125 | │ ├── local-test.js # 编译后的本地测试脚本 126 | │ ├── notify.js # 编译后的通知模块 127 | │ ├── utils.js # 编译后的工具函数模块 128 | │ └── data.txt # 复制的数据模板文件 129 | ├── js-backup/ # JavaScript原始文件备份 130 | ├── .env.example # 环境变量示例文件 131 | ├── .gitignore # Git忽略文件配置 132 | ├── LICENSE # 开源协议 133 | ├── package.json # 项目依赖和脚本配置 134 | ├── README.md # 项目说明文档 135 | └── tsconfig.json # TypeScript配置文件 136 | 137 | ``` 138 | 139 | ### 主要文件说明 140 | 141 | - **src/index.ts**: 程序入口点,处理环境变量,调用API服务和通知服务 142 | - **src/apiService.ts**: 包含与小米运动API交互的所有函数,包括登录、获取token和发送步数数据 143 | - **src/dataProcessor.ts**: 负责处理数据模板,替换步数和日期 144 | - **src/notify.ts**: 负责发送通知到多个平台(Server酱、Bark、Telegram等) 145 | - **src/utils.ts**: 包含通用工具函数,如时间格式化、随机数生成和URL参数转换 146 | - **src/types/**: 按模块组织的类型定义文件,提高代码的类型安全性 147 | 148 | ## 🔧 开发说明 149 | 150 | 本项目使用TypeScript开发,提供更好的类型安全和开发体验。源代码在`src`目录下,编译后的JavaScript文件在`dist`目录下。 151 | 152 | ### 自动构建 153 | 154 | 当你推送TypeScript源文件变更到GitHub仓库时,会自动触发构建工作流,将TypeScript编译为JavaScript并提交到`dist`目录。GitHub Actions工作流运行时使用的是编译后的JavaScript文件。 155 | 156 | ### 本地开发 157 | 158 | 如果你想在本地进行开发,请按照以下步骤: 159 | 160 | 1. 克隆仓库到本地 161 | ```bash 162 | git clone https://github.com/chiupam/xmSport.git 163 | cd xmSport 164 | ``` 165 | 166 | 2. 安装依赖 167 | ```bash 168 | npm install 169 | ``` 170 | 171 | 3. 编译TypeScript 172 | ```bash 173 | npm run build 174 | ``` 175 | 176 | 4. 编辑源代码后重新构建 177 | ```bash 178 | npm run build 179 | ``` 180 | 181 | 5. 或者启用开发模式,自动监视文件变化 182 | ```bash 183 | npm run dev 184 | ``` 185 | 186 | ## ⚠️ 免责声明 187 | 188 | **请仔细阅读以下声明:** 189 | 190 | 1. 本项目仅供学习和研究目的使用,不得用于商业或非法用途 191 | 2. 使用本项目可能违反小米运动/小米健康的服务条款,请自行评估使用风险 192 | 3. 本项目不保证功能的可用性,也不保证不会被小米官方检测或封禁 193 | 4. 使用本项目造成的任何问题,包括但不限于账号被封禁、数据丢失等,项目作者概不负责 194 | 5. 用户需自行承担使用本项目的全部风险和法律责任 195 | 196 | ## 📜 许可证 197 | 198 | 本项目采用 [MIT 许可证](LICENSE) 进行许可。 199 | 200 | ## 🧪 本地测试 201 | 202 | 如果你想在本地测试脚本,可以按照以下步骤操作: 203 | 204 | 1. 克隆仓库到本地 205 | ```bash 206 | git clone https://github.com/chipam/xmSport.git 207 | cd xmSport 208 | ``` 209 | 210 | 2. 安装依赖 211 | ```bash 212 | npm install 213 | ``` 214 | 215 | 3. 创建环境变量文件 216 | ```bash 217 | cp .env.example .env 218 | ``` 219 | 220 | 4. 编辑`.env`文件,填入你的个人信息 221 | - 修改`PHONE_NUMBER`和`PASSWORD`为你的账号信息 222 | - `DATA_JSON`会通过测试脚本自动读取,无需手动设置 223 | 224 | 5. 运行测试脚本(推荐) 225 | ```bash 226 | npm test 227 | ``` 228 | 这个命令会自动读取`src/data.txt`文件内容并设置为环境变量 229 | 230 | 6. 调试失败通知(可选) 231 | ```bash 232 | npm run test:fail 233 | ``` 234 | 这个命令使用错误的账号密码来触发失败通知 235 | 236 | ### 手动方式测试(可选) 237 | 238 | 如果你希望手动控制环境变量,可以: 239 | 240 | 1. 打开`src/data.txt`文件,复制其内容 241 | 2. 将内容粘贴到`.env`文件的`DATA_JSON=`后面(注意转义特殊字符) 242 | 3. 使用以下命令运行: 243 | ```bash 244 | npm run test:env 245 | ``` 246 | 247 | ## 🔄 Fork仓库自动同步 248 | 249 | 如果你Fork了此仓库,可以启用自动同步功能,保持与上游仓库的更新一致: 250 | 251 | 1. 在你Fork的仓库中,进入**Actions**标签页 252 | 2. 你会看到一个名为**自动同步上游仓库**的工作流 253 | 3. 点击**I understand my workflows, go ahead and enable them**启用工作流 254 | 4. 现在,你的Fork仓库将每天自动与此上游仓库同步 255 | 256 | 你也可以随时手动触发同步: 257 | 1. 在你Fork的仓库中,进入**Actions**标签页 258 | 2. 在左侧选择**自动同步上游仓库**工作流 259 | 3. 点击**Run workflow**按钮,然后点击绿色的**Run workflow**按钮确认 260 | 261 | 这样你的Fork仓库就会立即与上游仓库同步,获取最新的功能和修复。 262 | 263 | > **注意**:此工作流仅在Fork的仓库中运行,不会在原始仓库中执行。 -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 36 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 37 | return new (P || (P = Promise))(function (resolve, reject) { 38 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 39 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 40 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 41 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 42 | }); 43 | }; 44 | Object.defineProperty(exports, "__esModule", { value: true }); 45 | // xmSport GitHub Action 脚本 46 | const core = __importStar(require("@actions/core")); 47 | const dataProcessor_1 = require("./dataProcessor"); 48 | const apiService_1 = require("./apiService"); 49 | const utils_1 = require("./utils"); 50 | const notify_1 = require("./notify"); 51 | // 执行主函数 - 使用立即执行的异步函数表达式 52 | (() => __awaiter(void 0, void 0, void 0, function* () { 53 | const startTime = Date.now(); 54 | let step = 0; 55 | let resultMessage = ''; 56 | let status = 'failure'; // 默认状态为失败 57 | // 检查是否启用通知功能 58 | const enableNotify = process.env.ENABLE_NOTIFY === 'true'; 59 | try { 60 | console.log('=========================================='); 61 | console.log('🏃‍♂️ 开始执行 小米运动修改步数 脚本...'); 62 | console.log('=========================================='); 63 | // 获取当前时间 64 | const now = new Date(); 65 | // 标准时间 66 | console.log(`📅 标准时间: ${(0, utils_1.formatDate)(now, 'UTC', '+0')}`); 67 | // 北京时间 68 | console.log(`📅 北京时间: ${(0, utils_1.formatDate)(now, 'Asia/Shanghai', '+8')}`); 69 | // 检查必要的环境变量 70 | if (!process.env.PHONE_NUMBER) { 71 | throw new Error('缺少必要的环境变量: PHONE_NUMBER'); 72 | } 73 | if (!process.env.PASSWORD) { 74 | throw new Error('缺少必要的环境变量: PASSWORD'); 75 | } 76 | if (!process.env.DATA_JSON) { 77 | throw new Error('缺少必要的环境变量: DATA_JSON'); 78 | } 79 | // 获取步数范围 80 | var minStep = parseInt(process.env.xmSportMinStep || '20000', 10); 81 | var maxStep = parseInt(process.env.xmSportMaxStep || '22000', 10); 82 | // 验证步数范围 83 | if (maxStep <= minStep) { 84 | console.log('⚠️ 最大步数小于等于最小步数,自动交换值'); 85 | [minStep, maxStep] = [maxStep, minStep]; 86 | } 87 | console.log(`👟 步数范围: ${minStep} - ${maxStep}`); 88 | // 生成随机步数 89 | step = (0, utils_1.getRandomInt)(minStep, maxStep); 90 | console.log(`🎲 生成随机步数: ${step}`); 91 | // 处理数据模板 92 | console.log('📦 处理数据模板...'); 93 | const dataJson = (0, dataProcessor_1.processData)(step, process.env.DATA_JSON); 94 | // 执行API请求序列 95 | console.log('🔄 开始API请求序列...'); 96 | const phoneNumber = process.env.PHONE_NUMBER; 97 | const password = process.env.PASSWORD; 98 | // 1. 获取code 99 | console.log('🔄 第1步: 获取登录Code...'); 100 | const { code, thirdName } = yield (0, apiService_1.getCode)(phoneNumber, password); 101 | // 如果code为空,则退出,且发送失败通知 102 | if (!code) { 103 | const title = (0, notify_1.getNotifyTitle)(); 104 | let content = `❌ 执行失败: 获取登录Code失败`; 105 | // 设置错误通知信息供finally中使用 106 | resultMessage = content; 107 | status = 'failure'; 108 | throw new Error('获取登录Code失败'); 109 | } 110 | // 2. 获取loginToken和userId 111 | console.log('🔄 第2步: 获取LoginToken和UserId...'); 112 | const { loginToken, userId } = yield (0, apiService_1.getLoginTokenAndUserId)(code, thirdName); 113 | // 3. 获取appToken 114 | console.log('🔄 第3步: 获取AppToken...'); 115 | const appToken = yield (0, apiService_1.getAppToken)(loginToken); 116 | // 4. 发送数据 117 | console.log('🔄 第4步: 发送步数数据...'); 118 | const result = yield (0, apiService_1.sendData)(userId, appToken, dataJson); 119 | // 完成 120 | console.log('=========================================='); 121 | if (result && result.message === 'success') { 122 | console.log(`✅ 成功完成! 步数已更新为: ${step} 步`); 123 | console.log(`📊 服务器响应: ${result.message}`); 124 | } 125 | else { 126 | console.log(`❌ 执行失败: ${JSON.stringify(result)}`); 127 | } 128 | console.log('=========================================='); 129 | // 设置输出 130 | core.setOutput('time', now.toISOString()); 131 | core.setOutput('result', JSON.stringify(result)); 132 | core.setOutput('step', step.toString()); 133 | // 设置通知信息 134 | if (result && result.message === 'success') { 135 | status = 'success'; // 更新状态为成功 136 | resultMessage = `✅ 成功完成! 步数已更新为: ${step} 步`; 137 | } 138 | else { 139 | resultMessage = `❌ 执行失败: ${JSON.stringify(result)}`; 140 | } 141 | } 142 | catch (error) { 143 | console.error('=========================================='); 144 | console.error(`❌ 错误: ${error.message}`); 145 | if (error.response) { 146 | console.error('📡 服务器响应:'); 147 | console.error(`状态码: ${error.response.status}`); 148 | console.error(`数据: ${JSON.stringify(error.response.data)}`); 149 | } 150 | console.error('=========================================='); 151 | core.setFailed(`执行失败: ${error.message}`); 152 | // 设置错误通知信息 153 | resultMessage = `❌ 执行失败: ${error.message}`; 154 | if (error.response) { 155 | resultMessage += `\n📡 状态码: ${error.response.status}`; 156 | } 157 | } 158 | finally { 159 | // 无论成功还是失败都会执行的代码 160 | const endTime = Date.now(); 161 | const executionTime = (endTime - startTime) / 1000; 162 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 163 | console.log('=========================================='); 164 | if (enableNotify && status === 'failure') { 165 | // 构建通知内容 166 | const title = (0, notify_1.getNotifyTitle)(); 167 | // 不再重复添加标题到内容中 168 | let content = `${resultMessage}\n⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`; 169 | // 发送失败通知时,添加手机号信息以及密码,因为此处没有打印输出,可以使用明文密码,但是手机号还是脱敏处理 170 | const phoneNumber = process.env.PHONE_NUMBER; 171 | content += `\n📱 手机号: ${phoneNumber.substring(0, 3)}xxxx${phoneNumber.substring(7)}`; 172 | content += `\n🔑 密码: ${process.env.PASSWORD}`; 173 | // 步数肯定是大于0的,直接添加到内容中 174 | content += `\n👟 步数: ${step}`; 175 | // 发送通知 176 | try { 177 | console.log('🔔 正在发送失败通知...'); 178 | yield (0, notify_1.sendNotification)(title, content); 179 | } 180 | catch (notifyError) { 181 | console.error(`📳 发送通知时出错: ${notifyError.message}`); 182 | } 183 | } 184 | else if (enableNotify) { 185 | console.log('ℹ️ 执行成功,跳过发送通知'); 186 | } 187 | } 188 | }))(); 189 | -------------------------------------------------------------------------------- /src/apiService.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import axiosRetry from 'axios-retry'; 3 | import { toQueryString } from './utils'; 4 | import { 5 | LoginResponse, 6 | AppTokenResponse, 7 | ApiResponse, 8 | LoginTokenAndUserIdResult 9 | } from './types'; 10 | 11 | // 声明axios-retry模块 12 | declare module 'axios-retry'; 13 | 14 | // 重试配置 15 | const MAX_RETRIES = 3; 16 | const RETRY_DELAY = 1000; // 初始延迟时间(毫秒) 17 | const RETRY_MULTIPLIER = 1.5; // 延迟时间乘数 18 | 19 | /** 20 | * 通用重试函数 - 为任何异步操作添加重试逻辑 21 | * @param {Function} asyncFn - 异步函数 22 | * @param {any[]} args - 函数参数 23 | * @param {string} operationName - 操作名称(用于日志) 24 | * @param {number} maxRetries - 最大重试次数 25 | * @param {number} initialDelay - 初始延迟时间(毫秒) 26 | * @param {number} multiplier - 延迟时间乘数 27 | * @returns {Promise} 函数结果 28 | */ 29 | async function withRetry( 30 | asyncFn: (...args: any[]) => Promise, 31 | args: any[] = [], 32 | operationName: string = '请求操作', 33 | maxRetries: number = MAX_RETRIES, 34 | initialDelay: number = RETRY_DELAY, 35 | multiplier: number = RETRY_MULTIPLIER 36 | ): Promise { 37 | let retries = 0; 38 | let delay = initialDelay; 39 | 40 | while (true) { 41 | try { 42 | return await asyncFn(...args); 43 | } catch (error: any) { 44 | retries++; 45 | 46 | // 检查是否达到最大重试次数 47 | if (retries >= maxRetries) { 48 | console.error(`${operationName}失败,已达到最大重试次数(${maxRetries}次)`); 49 | throw error; 50 | } 51 | 52 | // 计算延迟时间 53 | delay = Math.floor(delay * multiplier); 54 | 55 | console.log(`${operationName}失败,${retries}秒后重试(${retries}/${maxRetries})...`); 56 | 57 | // 输出错误信息 58 | if (error.response) { 59 | console.error(`状态码: ${error.response.status}`); 60 | console.error(`响应数据: ${JSON.stringify(error.response.data)}`); 61 | } else if (error.request) { 62 | console.error('无响应'); 63 | } else { 64 | console.error(`错误: ${error.message}`); 65 | } 66 | 67 | // 等待延迟时间 68 | await new Promise(resolve => setTimeout(resolve, delay)); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * 执行Axios请求 75 | * @param {AxiosRequestConfig} config - Axios配置 76 | * @returns {Promise} 请求响应数据 77 | */ 78 | async function executeRequest(config: AxiosRequestConfig): Promise { 79 | const response = await axios(config); 80 | return response.data; 81 | } 82 | 83 | /** 84 | * 获取登录Code及thirdName 85 | * @param {string} phoneNumber - 手机号 86 | * @param {string} password - 密码 87 | * @returns {Promise<{ code: string, thirdName: string }>} 登录Code和thirdName 88 | */ 89 | export async function getCode(phoneNumber: string, password: string): Promise<{ code: string, thirdName: string }> { 90 | try { 91 | const getLoginCode = async (): Promise<{ code: string, thirdName: string }> => { 92 | // 构造请求配置 93 | const PHONE_PATTERN = /^(1)\d{10}$/; 94 | const isPhone = PHONE_PATTERN.test(phoneNumber); 95 | const processedPhone = isPhone ? `+86${phoneNumber}` : phoneNumber; 96 | const thirdName = isPhone ? 'huami_phone' : 'huami'; 97 | const url = `https://api-user.huami.com/registrations/${processedPhone}/tokens`; 98 | const headers = { 99 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 100 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)', 101 | "app_name": "com.xiaomi.hm.health", 102 | }; 103 | 104 | const data = { 105 | client_id: 'HuaMi', 106 | country_code: 'CN', 107 | json_response: 'true', 108 | name: processedPhone, 109 | password: password, 110 | redirect_uri: 'https://s3-us-west-2.amazonaws.com/hm-registration/successsignin.html', 111 | state: 'REDIRECTION', 112 | token: 'access' 113 | }; 114 | 115 | const response = await axios.post(url, toQueryString(data), { 116 | headers: headers, 117 | maxRedirects: 0, 118 | validateStatus: status => status >= 200 && status < 400 119 | }); 120 | 121 | // 返回 access code 和 thirdName 122 | if (response.data && response.data.access) { 123 | return { code: response.data.access, thirdName }; 124 | } else { 125 | throw new Error('未能获取到code'); 126 | } 127 | }; 128 | 129 | // 使用withRetry执行请求 130 | const result = await withRetry(getLoginCode, [], '获取登录Code'); 131 | console.log('🔐 获取Code成功'); 132 | return result; 133 | } catch (error: any) { 134 | console.error(`获取登录Code出错: ${error.message}`); 135 | if (error.response) { 136 | console.error(`状态码: ${error.response.status}`); 137 | console.error(`响应数据: ${JSON.stringify(error.response.data)}`); 138 | } 139 | return { code: '', thirdName: '' }; 140 | } 141 | } 142 | 143 | /** 144 | * 获取登录Token和用户ID 145 | * @param {string} code - 登录Code 146 | * @returns {Promise} 登录Token和用户ID 147 | */ 148 | export async function getLoginTokenAndUserId(code: string, thirdName: string): Promise { 149 | const getTokenAndUserId = async (): Promise => { 150 | const url = 'https://account.huami.com/v2/client/login'; 151 | const headers = { 152 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 153 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)' 154 | }; 155 | // 按照新要求重写data 156 | const data = { 157 | app_name: 'com.xiaomi.hm.health', 158 | country_code: 'CN', 159 | code: code, 160 | device_id: '02:00:00:00:00:00', 161 | device_model: 'android_phone', 162 | app_version: '6.12.0', 163 | grant_type: 'access_token', 164 | allow_registration: 'false', 165 | source: 'com.xiaomi.hm.health', 166 | third_name: thirdName 167 | }; 168 | 169 | const response = await axios.post(url, toQueryString(data), { 170 | headers: headers 171 | }); 172 | 173 | if (!response.data.token_info || !response.data.token_info.login_token || !response.data.token_info.user_id) { 174 | throw new Error('响应数据中缺少必要的token_info字段'); 175 | } 176 | 177 | const loginToken = response.data.token_info.login_token; 178 | const userId = response.data.token_info.user_id; 179 | 180 | return { loginToken, userId }; 181 | }; 182 | 183 | // 使用withRetry执行请求 184 | const result = await withRetry(getTokenAndUserId, [], '获取登录Token和用户ID'); 185 | console.log('🔐 获取LoginToken和UserId成功'); 186 | return result; 187 | } 188 | 189 | /** 190 | * 获取App Token 191 | * @param {string} loginToken - 登录Token 192 | * @returns {Promise} App Token 193 | */ 194 | export async function getAppToken(loginToken: string): Promise { 195 | const fetchAppToken = async (): Promise => { 196 | const url = `https://account-cn.huami.com/v1/client/app_tokens?app_name=com.xiaomi.hm.health&dn=api-user.huami.com,api-mifit.huami.com,app-analytics.huami.com&login_token=${loginToken}`; 197 | const headers = { 198 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)' 199 | }; 200 | 201 | const response = await axios.get(url, { 202 | headers: headers 203 | }); 204 | 205 | if (!response.data.token_info || !response.data.token_info.app_token) { 206 | throw new Error('响应数据中缺少必要的app_token字段'); 207 | } 208 | 209 | return response.data.token_info.app_token; 210 | }; 211 | 212 | // 使用withRetry执行请求 213 | const appToken = await withRetry(fetchAppToken, [], '获取AppToken'); 214 | console.log('🔐 获取AppToken成功'); 215 | return appToken; 216 | } 217 | 218 | /** 219 | * 发送数据到API 220 | * @param userId 用户ID 221 | * @param appToken APP令牌 222 | * @param dataJson 数据JSON 223 | * @returns API响应 224 | */ 225 | export async function sendData(userId: string, appToken: string, dataJson: string): Promise { 226 | const sendDataRequest = async (): Promise => { 227 | const url = `https://api-mifit-cn2.huami.com/v1/data/band_data.json`; 228 | const headers = { 229 | 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 230 | 'apptoken': appToken 231 | }; 232 | 233 | const data = { 234 | userid: userId, 235 | last_sync_data_time: '1597306380', 236 | device_type: '0', 237 | last_deviceid: 'DA932FFFFE8816E7', 238 | data_json: dataJson 239 | }; 240 | 241 | const config: AxiosRequestConfig = { 242 | method: 'post', 243 | url: url, 244 | headers: headers, 245 | data: toQueryString(data) 246 | }; 247 | 248 | const response = await executeRequest(config); 249 | 250 | // 如果响应中包含message字段,则认为发送成功 251 | if (response && typeof response.message !== 'undefined') { 252 | console.log(`成功发送数据: ${response.message}`); 253 | return response; 254 | } else { 255 | console.error('发送数据返回未知响应: ', response); 256 | throw new Error('发送数据返回未知响应'); 257 | } 258 | }; 259 | 260 | // 使用withRetry执行请求 261 | return await withRetry(sendDataRequest, [], '发送数据'); 262 | } 263 | -------------------------------------------------------------------------------- /js-backup/notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知模块 - 支持多种推送渠道发送脚本运行结果通知 3 | */ 4 | const axios = require('axios'); 5 | const { formatDate } = require('../src/utils'); 6 | 7 | /** 8 | * 构建通知标题 9 | * @returns {string} 通知标题 10 | */ 11 | function getNotifyTitle() { 12 | return `小米运动步数修改 - ${formatDate(new Date(), 'Asia/Shanghai', '+8').split(' ')[0]}`; 13 | } 14 | 15 | /** 16 | * 处理通知发送的结果 17 | * @param {string} platform - 平台名称 18 | * @param {Object} response - 响应结果 19 | */ 20 | function handleNotifyResult(platform, response) { 21 | if (response && response.status === 200) { 22 | console.log(`✅ ${platform}通知发送成功`); 23 | } else { 24 | console.log(`⚠️ ${platform}通知发送失败: ${response?.statusText || '未知错误'}`); 25 | } 26 | } 27 | 28 | /** 29 | * Server酱通知 (ServerChan) 30 | * @param {string} key - Server酱发送KEY (SCKey) 31 | * @param {string} title - 通知标题 32 | * @param {string} content - 通知内容 33 | * @returns {Promise} 发送结果 34 | */ 35 | async function sendServerChan(key, title, content) { 36 | if (!key) return { success: false, message: 'Server酱KEY未设置' }; 37 | 38 | try { 39 | const url = `https://sctapi.ftqq.com/${key}.send`; 40 | const response = await axios.post(url, { 41 | title: title, 42 | desp: content 43 | }); 44 | handleNotifyResult('Server酱', response); 45 | return { success: true }; 46 | } catch (error) { 47 | console.error(`❌ Server酱通知发送失败: ${error.message}`); 48 | return { success: false, message: error.message }; 49 | } 50 | } 51 | 52 | /** 53 | * Bark通知 54 | * @param {string} key - Bark推送KEY 55 | * @param {string} title - 通知标题 56 | * @param {string} content - 通知内容 57 | * @returns {Promise} 发送结果 58 | */ 59 | async function sendBark(key, title, content) { 60 | if (!key) return { success: false, message: 'Bark KEY未设置' }; 61 | 62 | try { 63 | // 处理Bark地址,兼容自建服务和官方服务 64 | let barkUrl = key; 65 | if (!barkUrl.startsWith('http')) { 66 | barkUrl = `https://api.day.app/${key}`; 67 | } 68 | if (!barkUrl.endsWith('/')) { 69 | barkUrl += '/'; 70 | } 71 | 72 | const url = `${barkUrl}${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 73 | const response = await axios.get(url); 74 | handleNotifyResult('Bark', response); 75 | return { success: true }; 76 | } catch (error) { 77 | console.error(`❌ Bark通知发送失败: ${error.message}`); 78 | return { success: false, message: error.message }; 79 | } 80 | } 81 | 82 | /** 83 | * Telegram Bot通知 84 | * @param {string} botToken - Telegram Bot Token 85 | * @param {string} chatId - Telegram Chat ID 86 | * @param {string} title - 通知标题 87 | * @param {string} content - 通知内容 88 | * @returns {Promise} 发送结果 89 | */ 90 | async function sendTelegram(botToken, chatId, title, content) { 91 | if (!botToken || !chatId) { 92 | console.error('❌ Telegram配置不完整: Token或ChatID未设置'); 93 | return { success: false, message: 'Telegram配置不完整: Token或ChatID未设置' }; 94 | } 95 | 96 | try { 97 | console.log(`🔄 正在发送Telegram消息...`); 98 | const url = `https://api.telegram.org/bot${botToken}/sendMessage`; 99 | 100 | // 只发送内容,避免重复标题 101 | const plainTextPayload = { 102 | chat_id: chatId, 103 | text: content, // 不再包含标题,只发送内容 104 | parse_mode: '' // 不使用任何格式化 105 | }; 106 | 107 | console.log(`📤 正在尝试以纯文本模式发送Telegram消息...`); 108 | const response = await axios.post(url, plainTextPayload); 109 | 110 | if (response.data && response.data.ok) { 111 | console.log(`✅ Telegram通知发送成功`); 112 | return { success: true }; 113 | } else { 114 | console.error(`❌ Telegram API返回错误: ${JSON.stringify(response.data)}`); 115 | return { success: false, message: JSON.stringify(response.data) }; 116 | } 117 | } catch (error) { 118 | console.error(`❌ Telegram通知发送失败:`); 119 | console.error(`- 错误消息: ${error.message}`); 120 | 121 | if (error.response) { 122 | console.error(`- 状态码: ${error.response.status}`); 123 | console.error(`- 响应数据: ${JSON.stringify(error.response.data)}`); 124 | 125 | // 如果是格式问题,尝试不使用格式化再次发送 126 | if (error.response.status === 400 && error.response.data?.description?.includes('parse')) { 127 | try { 128 | console.log(`🔄 尝试以纯文本格式重新发送...`); 129 | const retryUrl = `https://api.telegram.org/bot${botToken}/sendMessage`; 130 | const retryPayload = { 131 | chat_id: chatId, 132 | text: content, // 只发送内容 133 | parse_mode: '' // 不使用任何格式化 134 | }; 135 | 136 | const retryResponse = await axios.post(retryUrl, retryPayload); 137 | if (retryResponse.data && retryResponse.data.ok) { 138 | console.log(`✅ Telegram通知重新发送成功`); 139 | return { success: true }; 140 | } 141 | } catch (retryError) { 142 | console.error(`❌ Telegram通知重新发送也失败: ${retryError.message}`); 143 | } 144 | } 145 | } 146 | 147 | return { success: false, message: error.message }; 148 | } 149 | } 150 | 151 | /** 152 | * 钉钉机器人通知 153 | * @param {string} webhook - 钉钉Webhook地址 154 | * @param {string} secret - 钉钉安全密钥 155 | * @param {string} title - 通知标题 156 | * @param {string} content - 通知内容 157 | * @returns {Promise} 发送结果 158 | */ 159 | async function sendDingTalk(webhook, secret, title, content) { 160 | if (!webhook) return { success: false, message: '钉钉Webhook未设置' }; 161 | 162 | try { 163 | // 如果有安全密钥,需要计算签名 164 | let url = webhook; 165 | if (secret) { 166 | const crypto = require('crypto'); 167 | const timestamp = Date.now(); 168 | const hmac = crypto.createHmac('sha256', secret); 169 | const sign = encodeURIComponent(hmac.update(`${timestamp}\n${secret}`).digest('base64')); 170 | url = `${webhook}×tamp=${timestamp}&sign=${sign}`; 171 | } 172 | 173 | const response = await axios.post(url, { 174 | msgtype: 'markdown', 175 | markdown: { 176 | title: title, 177 | text: `### ${title}\n${content}` 178 | } 179 | }); 180 | handleNotifyResult('钉钉', response); 181 | return { success: true }; 182 | } catch (error) { 183 | console.error(`❌ 钉钉通知发送失败: ${error.message}`); 184 | return { success: false, message: error.message }; 185 | } 186 | } 187 | 188 | /** 189 | * 企业微信通知 190 | * @param {string} key - 企业微信Webhook Key 191 | * @param {string} title - 通知标题 192 | * @param {string} content - 通知内容 193 | * @returns {Promise} 发送结果 194 | */ 195 | async function sendWecom(key, title, content) { 196 | if (!key) return { success: false, message: '企业微信KEY未设置' }; 197 | 198 | try { 199 | const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; 200 | const response = await axios.post(url, { 201 | msgtype: 'markdown', 202 | markdown: { 203 | content: `### ${title}\n${content}` 204 | } 205 | }); 206 | handleNotifyResult('企业微信', response); 207 | return { success: true }; 208 | } catch (error) { 209 | console.error(`❌ 企业微信通知发送失败: ${error.message}`); 210 | return { success: false, message: error.message }; 211 | } 212 | } 213 | 214 | /** 215 | * PushPlus通知 216 | * @param {string} token - PushPlus Token 217 | * @param {string} title - 通知标题 218 | * @param {string} content - 通知内容 219 | * @returns {Promise} 发送结果 220 | */ 221 | async function sendPushPlus(token, title, content) { 222 | if (!token) return { success: false, message: 'PushPlus Token未设置' }; 223 | 224 | try { 225 | const url = 'https://www.pushplus.plus/send'; 226 | const response = await axios.post(url, { 227 | token: token, 228 | title: title, 229 | content: content, 230 | template: 'markdown' 231 | }); 232 | handleNotifyResult('PushPlus', response); 233 | return { success: true }; 234 | } catch (error) { 235 | console.error(`❌ PushPlus通知发送失败: ${error.message}`); 236 | return { success: false, message: error.message }; 237 | } 238 | } 239 | 240 | /** 241 | * 发送通知到所有已配置的平台 242 | * @param {string} title - 通知标题 243 | * @param {string} content - 通知内容(已包含标题信息) 244 | */ 245 | async function sendNotification(title, content) { 246 | try { 247 | console.log('📣 正在发送通知...'); 248 | const notifyTasks = []; 249 | let notifyCount = 0; 250 | 251 | // Server酱通知 252 | if (process.env.SERVERCHAN_KEY) { 253 | notifyTasks.push(sendServerChan(process.env.SERVERCHAN_KEY, title, content)); 254 | notifyCount++; 255 | } 256 | 257 | // Bark通知 258 | if (process.env.BARK_KEY) { 259 | notifyTasks.push(sendBark(process.env.BARK_KEY, title, content)); 260 | notifyCount++; 261 | } 262 | 263 | // Telegram通知 264 | if (process.env.TG_BOT_TOKEN && process.env.TG_CHAT_ID) { 265 | notifyTasks.push(sendTelegram(process.env.TG_BOT_TOKEN, process.env.TG_CHAT_ID, title, content)); 266 | notifyCount++; 267 | } 268 | 269 | // 钉钉通知 270 | if (process.env.DINGTALK_WEBHOOK) { 271 | notifyTasks.push(sendDingTalk(process.env.DINGTALK_WEBHOOK, process.env.DINGTALK_SECRET, title, content)); 272 | notifyCount++; 273 | } 274 | 275 | // 企业微信通知 276 | if (process.env.WECOM_KEY) { 277 | notifyTasks.push(sendWecom(process.env.WECOM_KEY, title, content)); 278 | notifyCount++; 279 | } 280 | 281 | // PushPlus通知 282 | if (process.env.PUSHPLUS_TOKEN) { 283 | notifyTasks.push(sendPushPlus(process.env.PUSHPLUS_TOKEN, title, content)); 284 | notifyCount++; 285 | } 286 | 287 | if (notifyCount === 0) { 288 | console.log('ℹ️ 未配置任何通知渠道,跳过通知发送'); 289 | return; 290 | } 291 | 292 | // 等待所有通知发送完成 293 | const results = await Promise.allSettled(notifyTasks); 294 | const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.success).length; 295 | console.log(`📊 通知发送完成: ${successCount}/${notifyCount} 个渠道发送成功`); 296 | } catch (error) { 297 | console.error(`❌ 发送通知时出错: ${error.message}`); 298 | } 299 | } 300 | 301 | module.exports = { 302 | getNotifyTitle, 303 | sendNotification 304 | }; -------------------------------------------------------------------------------- /dist/apiService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.getCode = getCode; 16 | exports.getLoginTokenAndUserId = getLoginTokenAndUserId; 17 | exports.getAppToken = getAppToken; 18 | exports.sendData = sendData; 19 | const axios_1 = __importDefault(require("axios")); 20 | const utils_1 = require("./utils"); 21 | // 重试配置 22 | const MAX_RETRIES = 3; 23 | const RETRY_DELAY = 1000; // 初始延迟时间(毫秒) 24 | const RETRY_MULTIPLIER = 1.5; // 延迟时间乘数 25 | /** 26 | * 通用重试函数 - 为任何异步操作添加重试逻辑 27 | * @param {Function} asyncFn - 异步函数 28 | * @param {any[]} args - 函数参数 29 | * @param {string} operationName - 操作名称(用于日志) 30 | * @param {number} maxRetries - 最大重试次数 31 | * @param {number} initialDelay - 初始延迟时间(毫秒) 32 | * @param {number} multiplier - 延迟时间乘数 33 | * @returns {Promise} 函数结果 34 | */ 35 | function withRetry(asyncFn_1) { 36 | return __awaiter(this, arguments, void 0, function* (asyncFn, args = [], operationName = '请求操作', maxRetries = MAX_RETRIES, initialDelay = RETRY_DELAY, multiplier = RETRY_MULTIPLIER) { 37 | let retries = 0; 38 | let delay = initialDelay; 39 | while (true) { 40 | try { 41 | return yield asyncFn(...args); 42 | } 43 | catch (error) { 44 | retries++; 45 | // 检查是否达到最大重试次数 46 | if (retries >= maxRetries) { 47 | console.error(`${operationName}失败,已达到最大重试次数(${maxRetries}次)`); 48 | throw error; 49 | } 50 | // 计算延迟时间 51 | delay = Math.floor(delay * multiplier); 52 | console.log(`${operationName}失败,${retries}秒后重试(${retries}/${maxRetries})...`); 53 | // 输出错误信息 54 | if (error.response) { 55 | console.error(`状态码: ${error.response.status}`); 56 | console.error(`响应数据: ${JSON.stringify(error.response.data)}`); 57 | } 58 | else if (error.request) { 59 | console.error('无响应'); 60 | } 61 | else { 62 | console.error(`错误: ${error.message}`); 63 | } 64 | // 等待延迟时间 65 | yield new Promise(resolve => setTimeout(resolve, delay)); 66 | } 67 | } 68 | }); 69 | } 70 | /** 71 | * 执行Axios请求 72 | * @param {AxiosRequestConfig} config - Axios配置 73 | * @returns {Promise} 请求响应数据 74 | */ 75 | function executeRequest(config) { 76 | return __awaiter(this, void 0, void 0, function* () { 77 | const response = yield (0, axios_1.default)(config); 78 | return response.data; 79 | }); 80 | } 81 | /** 82 | * 获取登录Code及thirdName 83 | * @param {string} phoneNumber - 手机号 84 | * @param {string} password - 密码 85 | * @returns {Promise<{ code: string, thirdName: string }>} 登录Code和thirdName 86 | */ 87 | function getCode(phoneNumber, password) { 88 | return __awaiter(this, void 0, void 0, function* () { 89 | try { 90 | const getLoginCode = () => __awaiter(this, void 0, void 0, function* () { 91 | // 构造请求配置 92 | const PHONE_PATTERN = /^(1)\d{10}$/; 93 | const isPhone = PHONE_PATTERN.test(phoneNumber); 94 | const processedPhone = isPhone ? `+86${phoneNumber}` : phoneNumber; 95 | const thirdName = isPhone ? 'huami_phone' : 'huami'; 96 | const url = `https://api-user.huami.com/registrations/${processedPhone}/tokens`; 97 | const headers = { 98 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 99 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)', 100 | "app_name": "com.xiaomi.hm.health", 101 | }; 102 | const data = { 103 | client_id: 'HuaMi', 104 | country_code: 'CN', 105 | json_response: 'true', 106 | name: processedPhone, 107 | password: password, 108 | redirect_uri: 'https://s3-us-west-2.amazonaws.com/hm-registration/successsignin.html', 109 | state: 'REDIRECTION', 110 | token: 'access' 111 | }; 112 | const response = yield axios_1.default.post(url, (0, utils_1.toQueryString)(data), { 113 | headers: headers, 114 | maxRedirects: 0, 115 | validateStatus: status => status >= 200 && status < 400 116 | }); 117 | // 返回 access code 和 thirdName 118 | if (response.data && response.data.access) { 119 | return { code: response.data.access, thirdName }; 120 | } 121 | else { 122 | throw new Error('未能获取到code'); 123 | } 124 | }); 125 | // 使用withRetry执行请求 126 | const result = yield withRetry(getLoginCode, [], '获取登录Code'); 127 | console.log('🔐 获取Code成功'); 128 | return result; 129 | } 130 | catch (error) { 131 | console.error(`获取登录Code出错: ${error.message}`); 132 | if (error.response) { 133 | console.error(`状态码: ${error.response.status}`); 134 | console.error(`响应数据: ${JSON.stringify(error.response.data)}`); 135 | } 136 | return { code: '', thirdName: '' }; 137 | } 138 | }); 139 | } 140 | /** 141 | * 获取登录Token和用户ID 142 | * @param {string} code - 登录Code 143 | * @returns {Promise} 登录Token和用户ID 144 | */ 145 | function getLoginTokenAndUserId(code, thirdName) { 146 | return __awaiter(this, void 0, void 0, function* () { 147 | const getTokenAndUserId = () => __awaiter(this, void 0, void 0, function* () { 148 | const url = 'https://account.huami.com/v2/client/login'; 149 | const headers = { 150 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 151 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)' 152 | }; 153 | // 按照新要求重写data 154 | const data = { 155 | app_name: 'com.xiaomi.hm.health', 156 | country_code: 'CN', 157 | code: code, 158 | device_id: '02:00:00:00:00:00', 159 | device_model: 'android_phone', 160 | app_version: '6.12.0', 161 | grant_type: 'access_token', 162 | allow_registration: 'false', 163 | source: 'com.xiaomi.hm.health', 164 | third_name: thirdName 165 | }; 166 | const response = yield axios_1.default.post(url, (0, utils_1.toQueryString)(data), { 167 | headers: headers 168 | }); 169 | if (!response.data.token_info || !response.data.token_info.login_token || !response.data.token_info.user_id) { 170 | throw new Error('响应数据中缺少必要的token_info字段'); 171 | } 172 | const loginToken = response.data.token_info.login_token; 173 | const userId = response.data.token_info.user_id; 174 | return { loginToken, userId }; 175 | }); 176 | // 使用withRetry执行请求 177 | const result = yield withRetry(getTokenAndUserId, [], '获取登录Token和用户ID'); 178 | console.log('🔐 获取LoginToken和UserId成功'); 179 | return result; 180 | }); 181 | } 182 | /** 183 | * 获取App Token 184 | * @param {string} loginToken - 登录Token 185 | * @returns {Promise} App Token 186 | */ 187 | function getAppToken(loginToken) { 188 | return __awaiter(this, void 0, void 0, function* () { 189 | const fetchAppToken = () => __awaiter(this, void 0, void 0, function* () { 190 | const url = `https://account-cn.huami.com/v1/client/app_tokens?app_name=com.xiaomi.hm.health&dn=api-user.huami.com,api-mifit.huami.com,app-analytics.huami.com&login_token=${loginToken}`; 191 | const headers = { 192 | 'user-agent': 'MiFit/6.12.0 (MCE16; Android 16; Density/1.5)' 193 | }; 194 | const response = yield axios_1.default.get(url, { 195 | headers: headers 196 | }); 197 | if (!response.data.token_info || !response.data.token_info.app_token) { 198 | throw new Error('响应数据中缺少必要的app_token字段'); 199 | } 200 | return response.data.token_info.app_token; 201 | }); 202 | // 使用withRetry执行请求 203 | const appToken = yield withRetry(fetchAppToken, [], '获取AppToken'); 204 | console.log('🔐 获取AppToken成功'); 205 | return appToken; 206 | }); 207 | } 208 | /** 209 | * 发送数据到API 210 | * @param userId 用户ID 211 | * @param appToken APP令牌 212 | * @param dataJson 数据JSON 213 | * @returns API响应 214 | */ 215 | function sendData(userId, appToken, dataJson) { 216 | return __awaiter(this, void 0, void 0, function* () { 217 | const sendDataRequest = () => __awaiter(this, void 0, void 0, function* () { 218 | const url = `https://api-mifit-cn2.huami.com/v1/data/band_data.json`; 219 | const headers = { 220 | 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 221 | 'apptoken': appToken 222 | }; 223 | const data = { 224 | userid: userId, 225 | last_sync_data_time: '1597306380', 226 | device_type: '0', 227 | last_deviceid: 'DA932FFFFE8816E7', 228 | data_json: dataJson 229 | }; 230 | const config = { 231 | method: 'post', 232 | url: url, 233 | headers: headers, 234 | data: (0, utils_1.toQueryString)(data) 235 | }; 236 | const response = yield executeRequest(config); 237 | // 如果响应中包含message字段,则认为发送成功 238 | if (response && typeof response.message !== 'undefined') { 239 | console.log(`成功发送数据: ${response.message}`); 240 | return response; 241 | } 242 | else { 243 | console.error('发送数据返回未知响应: ', response); 244 | throw new Error('发送数据返回未知响应'); 245 | } 246 | }); 247 | // 使用withRetry执行请求 248 | return yield withRetry(sendDataRequest, [], '发送数据'); 249 | }); 250 | } 251 | -------------------------------------------------------------------------------- /src/notify.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { toQueryString } from './utils'; 3 | import { NotificationResult } from './types'; 4 | 5 | // 通知相关常量 6 | const SERVERCHAN_API = 'https://sctapi.ftqq.com'; 7 | const PUSHPLUS_API = 'https://www.pushplus.plus/send'; 8 | const BARK_API = 'https://api.day.app'; 9 | const TELEGRAM_API = 'https://api.telegram.org/bot'; 10 | const DINGTALK_API = 'https://oapi.dingtalk.com/robot/send?access_token='; 11 | const WECOM_API = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key='; 12 | 13 | /** 14 | * 获取通知标题 15 | * @returns {string} 通知标题 16 | */ 17 | export function getNotifyTitle(): string { 18 | // 获取GitHub Action的相关信息 19 | const repoName = process.env.GITHUB_REPOSITORY?.split('/')[1] || '小米运动'; 20 | const runId = process.env.GITHUB_RUN_ID || ''; 21 | const runNumber = process.env.GITHUB_RUN_NUMBER || ''; 22 | 23 | // 组装标题,包含项目名称和运行信息 24 | let title = `${repoName}`; 25 | 26 | // 如果是在GitHub Action环境中运行,添加运行ID和序号 27 | if (runId && runNumber) { 28 | title += ` #${runNumber}`; 29 | } 30 | 31 | return title; 32 | } 33 | 34 | /** 35 | * 处理通知结果 36 | * @param {string} platform - 通知平台名称 37 | * @param {object} response - 通知平台响应 38 | * @returns {NotificationResult} 处理结果 39 | */ 40 | function handleNotifyResult(platform: string, response: any): NotificationResult { 41 | try { 42 | const statusCode = response.status; 43 | const result = response.data; 44 | 45 | // 构建信息字符串 46 | let infoStr = `状态码: ${statusCode}`; 47 | if (typeof result === 'object') { 48 | infoStr += `, 响应: ${JSON.stringify(result)}`; 49 | } else if (typeof result === 'string') { 50 | infoStr += `, 响应: ${result}`; 51 | } 52 | 53 | console.log(`📤 ${platform} 通知结果: ${infoStr}`); 54 | 55 | // 根据平台判断是否发送成功 56 | let success = false; 57 | let message = ''; 58 | 59 | switch (platform) { 60 | case 'Server酱': 61 | success = statusCode === 200 && result.code === 0; 62 | message = result.message || '未知错误'; 63 | break; 64 | case 'Bark': 65 | success = statusCode === 200 && result.code === 200; 66 | message = result.message || '未知错误'; 67 | break; 68 | case 'Telegram': 69 | success = statusCode === 200 && result.ok === true; 70 | message = result.description || '未知错误'; 71 | break; 72 | case 'DingTalk': 73 | success = statusCode === 200 && result.errcode === 0; 74 | message = result.errmsg || '未知错误'; 75 | break; 76 | case 'Wecom': 77 | success = statusCode === 200 && result.errcode === 0; 78 | message = result.errmsg || '未知错误'; 79 | break; 80 | case 'PushPlus': 81 | success = statusCode === 200 && result.code === 200; 82 | message = result.msg || '未知错误'; 83 | break; 84 | default: 85 | success = statusCode >= 200 && statusCode < 300; 86 | message = '通知已发送'; 87 | } 88 | 89 | if (success) { 90 | console.log(`✅ ${platform} 通知发送成功`); 91 | } else { 92 | console.error(`❌ ${platform} 通知发送失败: ${message}`); 93 | } 94 | 95 | return { success, message, platform }; 96 | } catch (error: any) { 97 | console.error(`❌ 处理 ${platform} 通知结果时出错: ${error.message}`); 98 | return { success: false, message: error.message, platform }; 99 | } 100 | } 101 | 102 | /** 103 | * 发送Server酱通知 104 | * @param {string} title - 通知标题 105 | * @param {string} content - 通知内容 106 | * @returns {Promise} 发送结果 107 | */ 108 | async function sendServerChan(title: string, content: string): Promise { 109 | try { 110 | const key = process.env.SERVERCHAN_KEY; 111 | if (!key) { 112 | return { success: false, message: 'KEY未设置', platform: 'Server酱' }; 113 | } 114 | 115 | // 构建请求数据 116 | const data = { 117 | title: title, 118 | desp: content 119 | }; 120 | 121 | // 发送请求 122 | const response = await axios.post(`${SERVERCHAN_API}/${key}.send`, data); 123 | 124 | return handleNotifyResult('Server酱', response); 125 | } catch (error: any) { 126 | console.error(`❌ 发送Server酱通知时出错: ${error.message}`); 127 | return { success: false, message: error.message, platform: 'Server酱' }; 128 | } 129 | } 130 | 131 | /** 132 | * 发送Bark通知 133 | * @param {string} title - 通知标题 134 | * @param {string} content - 通知内容 135 | * @returns {Promise} 发送结果 136 | */ 137 | async function sendBark(title: string, content: string): Promise { 138 | try { 139 | let key = process.env.BARK_KEY; 140 | if (!key) { 141 | return { success: false, message: 'KEY未设置', platform: 'Bark' }; 142 | } 143 | 144 | // 处理key,可能是完整URL或仅为key 145 | let url: string; 146 | if (key.startsWith('http')) { 147 | // 如果是完整URL 148 | if (key.endsWith('/')) { 149 | key = key.substring(0, key.length - 1); 150 | } 151 | url = `${key}/${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 152 | } else { 153 | // 如果只是key 154 | url = `${BARK_API}/${key}/${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 155 | } 156 | 157 | // 可选参数 158 | url += `?isArchive=1&sound=default`; 159 | 160 | // 发送请求 161 | const response = await axios.get(url); 162 | 163 | return handleNotifyResult('Bark', response); 164 | } catch (error: any) { 165 | console.error(`❌ 发送Bark通知时出错: ${error.message}`); 166 | return { success: false, message: error.message, platform: 'Bark' }; 167 | } 168 | } 169 | 170 | /** 171 | * 发送Telegram通知 172 | * @param {string} title - 通知标题 173 | * @param {string} content - 通知内容 174 | * @returns {Promise} 发送结果 175 | */ 176 | async function sendTelegram(title: string, content: string): Promise { 177 | try { 178 | const botToken = process.env.TG_BOT_TOKEN; 179 | const chatId = process.env.TG_CHAT_ID; 180 | 181 | if (!botToken || !chatId) { 182 | return { success: false, message: '配置不完整', platform: 'Telegram' }; 183 | } 184 | 185 | // 构建请求数据 186 | const data = { 187 | chat_id: chatId, 188 | text: `${title}\n\n${content}`, 189 | parse_mode: 'Markdown', 190 | disable_web_page_preview: true 191 | }; 192 | 193 | // 发送请求 194 | const response = await axios.post(`${TELEGRAM_API}${botToken}/sendMessage`, data); 195 | 196 | return handleNotifyResult('Telegram', response); 197 | } catch (error: any) { 198 | console.error(`❌ 发送Telegram通知时出错: ${error.message}`); 199 | return { success: false, message: error.message, platform: 'Telegram' }; 200 | } 201 | } 202 | 203 | /** 204 | * 发送钉钉通知 205 | * @param {string} title - 通知标题 206 | * @param {string} content - 通知内容 207 | * @returns {Promise} 发送结果 208 | */ 209 | async function sendDingTalk(title: string, content: string): Promise { 210 | try { 211 | const webhook = process.env.DINGTALK_WEBHOOK; 212 | const secret = process.env.DINGTALK_SECRET; 213 | 214 | if (!webhook) { 215 | return { success: false, message: 'Webhook未设置', platform: 'DingTalk' }; 216 | } 217 | 218 | // 从完整webhook URL中提取access_token 219 | let accessToken = webhook; 220 | if (webhook.includes('access_token=')) { 221 | accessToken = webhook.split('access_token=')[1]; 222 | if (accessToken.includes('&')) { 223 | accessToken = accessToken.split('&')[0]; 224 | } 225 | } 226 | 227 | // 构建请求URL 228 | let url = `${DINGTALK_API}${accessToken}`; 229 | 230 | // 如果有加签密钥,计算签名 231 | if (secret) { 232 | const crypto = require('crypto'); 233 | const timestamp = Date.now(); 234 | const stringToSign = `${timestamp}\n${secret}`; 235 | const signature = crypto.createHmac('sha256', secret).update(stringToSign).digest('base64'); 236 | 237 | url += `×tamp=${timestamp}&sign=${encodeURIComponent(signature)}`; 238 | } 239 | 240 | // 构建请求数据 241 | const data = { 242 | msgtype: 'markdown', 243 | markdown: { 244 | title: title, 245 | text: `## ${title}\n\n${content.replace(/\n/g, '\n\n')}` 246 | } 247 | }; 248 | 249 | // 发送请求 250 | const response = await axios.post(url, data); 251 | 252 | return handleNotifyResult('DingTalk', response); 253 | } catch (error: any) { 254 | console.error(`❌ 发送钉钉通知时出错: ${error.message}`); 255 | return { success: false, message: error.message, platform: 'DingTalk' }; 256 | } 257 | } 258 | 259 | /** 260 | * 发送企业微信通知 261 | * @param {string} title - 通知标题 262 | * @param {string} content - 通知内容 263 | * @returns {Promise} 发送结果 264 | */ 265 | async function sendWecom(title: string, content: string): Promise { 266 | try { 267 | const key = process.env.WECOM_KEY; 268 | if (!key) { 269 | return { success: false, message: 'KEY未设置', platform: 'Wecom' }; 270 | } 271 | 272 | // 构建请求数据 273 | const data = { 274 | msgtype: 'markdown', 275 | markdown: { 276 | content: `## ${title}\n\n${content.replace(/\n/g, '\n\n')}` 277 | } 278 | }; 279 | 280 | // 发送请求 281 | const response = await axios.post(`${WECOM_API}${key}`, data); 282 | 283 | return handleNotifyResult('Wecom', response); 284 | } catch (error: any) { 285 | console.error(`❌ 发送企业微信通知时出错: ${error.message}`); 286 | return { success: false, message: error.message, platform: 'Wecom' }; 287 | } 288 | } 289 | 290 | /** 291 | * 发送PushPlus通知 292 | * @param {string} title - 通知标题 293 | * @param {string} content - 通知内容 294 | * @returns {Promise} 发送结果 295 | */ 296 | async function sendPushPlus(title: string, content: string): Promise { 297 | try { 298 | const token = process.env.PUSHPLUS_TOKEN; 299 | if (!token) { 300 | return { success: false, message: 'TOKEN未设置', platform: 'PushPlus' }; 301 | } 302 | 303 | // 构建请求数据 304 | const data = { 305 | token: token, 306 | title: title, 307 | content: content, 308 | template: 'markdown', 309 | channel: 'wechat' 310 | }; 311 | 312 | // 发送请求 313 | const response = await axios.post(PUSHPLUS_API, data); 314 | 315 | return handleNotifyResult('PushPlus', response); 316 | } catch (error: any) { 317 | console.error(`❌ 发送PushPlus通知时出错: ${error.message}`); 318 | return { success: false, message: error.message, platform: 'PushPlus' }; 319 | } 320 | } 321 | 322 | /** 323 | * 发送通知到所有配置的平台 324 | * @param {string} title - 通知标题 325 | * @param {string} content - 通知内容 326 | * @returns {Promise} 327 | */ 328 | export async function sendNotification(title: string, content: string, platform?: string): Promise { 329 | console.log('开始发送通知...'); 330 | 331 | try { 332 | // 发送到Server酱 333 | await sendServerChan(title, content); 334 | 335 | // 发送到Bark 336 | await sendBark(title, content); 337 | 338 | // 发送到Telegram 339 | await sendTelegram(title, content); 340 | 341 | // 发送到钉钉 342 | await sendDingTalk(title, content); 343 | 344 | // 发送到企业微信 345 | await sendWecom(title, content); 346 | 347 | // 发送到PushPlus 348 | await sendPushPlus(title, content); 349 | 350 | } catch (error: any) { 351 | console.error(`❌ 发送通知时出错: ${error.message}`); 352 | } 353 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "libReplacement": true, /* Enable lib replacement. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "commonjs", /* Specify what module code is generated. */ 30 | "outDir": "./dist", 31 | "rootDir": "./src", 32 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 41 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 42 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 43 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 44 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 45 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 46 | "resolveJsonModule": true, /* Enable importing .json files. */ 47 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 48 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 49 | 50 | /* JavaScript Support */ 51 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 52 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 53 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 54 | 55 | /* Emit */ 56 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 57 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 58 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 59 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "noEmit": true, /* Disable emitting files from a compilation. */ 62 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 63 | // "removeComments": true, /* Disable emitting comments. */ 64 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 80 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 81 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ 82 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 83 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 84 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 85 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 86 | 87 | /* Type Checking */ 88 | "strict": true, /* Enable all strict type-checking options. */ 89 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 90 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 94 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 95 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 96 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 97 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 98 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 99 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 100 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 101 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 102 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 103 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 104 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 105 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 106 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 107 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 108 | 109 | /* Completeness */ 110 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 111 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 112 | }, 113 | "include": ["src/**/*", "js-backup/utils.js", "js-backup/notify.js", "js-backup/index.js", "js-backup/apiService.js", "js-backup/dataProcessor.js"], 114 | "exclude": ["node_modules", "js-backup"] 115 | } 116 | -------------------------------------------------------------------------------- /dist/notify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.getNotifyTitle = getNotifyTitle; 16 | exports.sendNotification = sendNotification; 17 | const axios_1 = __importDefault(require("axios")); 18 | // 通知相关常量 19 | const SERVERCHAN_API = 'https://sctapi.ftqq.com'; 20 | const PUSHPLUS_API = 'https://www.pushplus.plus/send'; 21 | const BARK_API = 'https://api.day.app'; 22 | const TELEGRAM_API = 'https://api.telegram.org/bot'; 23 | const DINGTALK_API = 'https://oapi.dingtalk.com/robot/send?access_token='; 24 | const WECOM_API = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key='; 25 | /** 26 | * 获取通知标题 27 | * @returns {string} 通知标题 28 | */ 29 | function getNotifyTitle() { 30 | var _a; 31 | // 获取GitHub Action的相关信息 32 | const repoName = ((_a = process.env.GITHUB_REPOSITORY) === null || _a === void 0 ? void 0 : _a.split('/')[1]) || '小米运动'; 33 | const runId = process.env.GITHUB_RUN_ID || ''; 34 | const runNumber = process.env.GITHUB_RUN_NUMBER || ''; 35 | // 组装标题,包含项目名称和运行信息 36 | let title = `${repoName}`; 37 | // 如果是在GitHub Action环境中运行,添加运行ID和序号 38 | if (runId && runNumber) { 39 | title += ` #${runNumber}`; 40 | } 41 | return title; 42 | } 43 | /** 44 | * 处理通知结果 45 | * @param {string} platform - 通知平台名称 46 | * @param {object} response - 通知平台响应 47 | * @returns {NotificationResult} 处理结果 48 | */ 49 | function handleNotifyResult(platform, response) { 50 | try { 51 | const statusCode = response.status; 52 | const result = response.data; 53 | // 构建信息字符串 54 | let infoStr = `状态码: ${statusCode}`; 55 | if (typeof result === 'object') { 56 | infoStr += `, 响应: ${JSON.stringify(result)}`; 57 | } 58 | else if (typeof result === 'string') { 59 | infoStr += `, 响应: ${result}`; 60 | } 61 | console.log(`📤 ${platform} 通知结果: ${infoStr}`); 62 | // 根据平台判断是否发送成功 63 | let success = false; 64 | let message = ''; 65 | switch (platform) { 66 | case 'Server酱': 67 | success = statusCode === 200 && result.code === 0; 68 | message = result.message || '未知错误'; 69 | break; 70 | case 'Bark': 71 | success = statusCode === 200 && result.code === 200; 72 | message = result.message || '未知错误'; 73 | break; 74 | case 'Telegram': 75 | success = statusCode === 200 && result.ok === true; 76 | message = result.description || '未知错误'; 77 | break; 78 | case 'DingTalk': 79 | success = statusCode === 200 && result.errcode === 0; 80 | message = result.errmsg || '未知错误'; 81 | break; 82 | case 'Wecom': 83 | success = statusCode === 200 && result.errcode === 0; 84 | message = result.errmsg || '未知错误'; 85 | break; 86 | case 'PushPlus': 87 | success = statusCode === 200 && result.code === 200; 88 | message = result.msg || '未知错误'; 89 | break; 90 | default: 91 | success = statusCode >= 200 && statusCode < 300; 92 | message = '通知已发送'; 93 | } 94 | if (success) { 95 | console.log(`✅ ${platform} 通知发送成功`); 96 | } 97 | else { 98 | console.error(`❌ ${platform} 通知发送失败: ${message}`); 99 | } 100 | return { success, message, platform }; 101 | } 102 | catch (error) { 103 | console.error(`❌ 处理 ${platform} 通知结果时出错: ${error.message}`); 104 | return { success: false, message: error.message, platform }; 105 | } 106 | } 107 | /** 108 | * 发送Server酱通知 109 | * @param {string} title - 通知标题 110 | * @param {string} content - 通知内容 111 | * @returns {Promise} 发送结果 112 | */ 113 | function sendServerChan(title, content) { 114 | return __awaiter(this, void 0, void 0, function* () { 115 | try { 116 | const key = process.env.SERVERCHAN_KEY; 117 | if (!key) { 118 | return { success: false, message: 'KEY未设置', platform: 'Server酱' }; 119 | } 120 | // 构建请求数据 121 | const data = { 122 | title: title, 123 | desp: content 124 | }; 125 | // 发送请求 126 | const response = yield axios_1.default.post(`${SERVERCHAN_API}/${key}.send`, data); 127 | return handleNotifyResult('Server酱', response); 128 | } 129 | catch (error) { 130 | console.error(`❌ 发送Server酱通知时出错: ${error.message}`); 131 | return { success: false, message: error.message, platform: 'Server酱' }; 132 | } 133 | }); 134 | } 135 | /** 136 | * 发送Bark通知 137 | * @param {string} title - 通知标题 138 | * @param {string} content - 通知内容 139 | * @returns {Promise} 发送结果 140 | */ 141 | function sendBark(title, content) { 142 | return __awaiter(this, void 0, void 0, function* () { 143 | try { 144 | let key = process.env.BARK_KEY; 145 | if (!key) { 146 | return { success: false, message: 'KEY未设置', platform: 'Bark' }; 147 | } 148 | // 处理key,可能是完整URL或仅为key 149 | let url; 150 | if (key.startsWith('http')) { 151 | // 如果是完整URL 152 | if (key.endsWith('/')) { 153 | key = key.substring(0, key.length - 1); 154 | } 155 | url = `${key}/${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 156 | } 157 | else { 158 | // 如果只是key 159 | url = `${BARK_API}/${key}/${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 160 | } 161 | // 可选参数 162 | url += `?isArchive=1&sound=default`; 163 | // 发送请求 164 | const response = yield axios_1.default.get(url); 165 | return handleNotifyResult('Bark', response); 166 | } 167 | catch (error) { 168 | console.error(`❌ 发送Bark通知时出错: ${error.message}`); 169 | return { success: false, message: error.message, platform: 'Bark' }; 170 | } 171 | }); 172 | } 173 | /** 174 | * 发送Telegram通知 175 | * @param {string} title - 通知标题 176 | * @param {string} content - 通知内容 177 | * @returns {Promise} 发送结果 178 | */ 179 | function sendTelegram(title, content) { 180 | return __awaiter(this, void 0, void 0, function* () { 181 | try { 182 | const botToken = process.env.TG_BOT_TOKEN; 183 | const chatId = process.env.TG_CHAT_ID; 184 | if (!botToken || !chatId) { 185 | return { success: false, message: '配置不完整', platform: 'Telegram' }; 186 | } 187 | // 构建请求数据 188 | const data = { 189 | chat_id: chatId, 190 | text: `${title}\n\n${content}`, 191 | parse_mode: 'Markdown', 192 | disable_web_page_preview: true 193 | }; 194 | // 发送请求 195 | const response = yield axios_1.default.post(`${TELEGRAM_API}${botToken}/sendMessage`, data); 196 | return handleNotifyResult('Telegram', response); 197 | } 198 | catch (error) { 199 | console.error(`❌ 发送Telegram通知时出错: ${error.message}`); 200 | return { success: false, message: error.message, platform: 'Telegram' }; 201 | } 202 | }); 203 | } 204 | /** 205 | * 发送钉钉通知 206 | * @param {string} title - 通知标题 207 | * @param {string} content - 通知内容 208 | * @returns {Promise} 发送结果 209 | */ 210 | function sendDingTalk(title, content) { 211 | return __awaiter(this, void 0, void 0, function* () { 212 | try { 213 | const webhook = process.env.DINGTALK_WEBHOOK; 214 | const secret = process.env.DINGTALK_SECRET; 215 | if (!webhook) { 216 | return { success: false, message: 'Webhook未设置', platform: 'DingTalk' }; 217 | } 218 | // 从完整webhook URL中提取access_token 219 | let accessToken = webhook; 220 | if (webhook.includes('access_token=')) { 221 | accessToken = webhook.split('access_token=')[1]; 222 | if (accessToken.includes('&')) { 223 | accessToken = accessToken.split('&')[0]; 224 | } 225 | } 226 | // 构建请求URL 227 | let url = `${DINGTALK_API}${accessToken}`; 228 | // 如果有加签密钥,计算签名 229 | if (secret) { 230 | const crypto = require('crypto'); 231 | const timestamp = Date.now(); 232 | const stringToSign = `${timestamp}\n${secret}`; 233 | const signature = crypto.createHmac('sha256', secret).update(stringToSign).digest('base64'); 234 | url += `×tamp=${timestamp}&sign=${encodeURIComponent(signature)}`; 235 | } 236 | // 构建请求数据 237 | const data = { 238 | msgtype: 'markdown', 239 | markdown: { 240 | title: title, 241 | text: `## ${title}\n\n${content.replace(/\n/g, '\n\n')}` 242 | } 243 | }; 244 | // 发送请求 245 | const response = yield axios_1.default.post(url, data); 246 | return handleNotifyResult('DingTalk', response); 247 | } 248 | catch (error) { 249 | console.error(`❌ 发送钉钉通知时出错: ${error.message}`); 250 | return { success: false, message: error.message, platform: 'DingTalk' }; 251 | } 252 | }); 253 | } 254 | /** 255 | * 发送企业微信通知 256 | * @param {string} title - 通知标题 257 | * @param {string} content - 通知内容 258 | * @returns {Promise} 发送结果 259 | */ 260 | function sendWecom(title, content) { 261 | return __awaiter(this, void 0, void 0, function* () { 262 | try { 263 | const key = process.env.WECOM_KEY; 264 | if (!key) { 265 | return { success: false, message: 'KEY未设置', platform: 'Wecom' }; 266 | } 267 | // 构建请求数据 268 | const data = { 269 | msgtype: 'markdown', 270 | markdown: { 271 | content: `## ${title}\n\n${content.replace(/\n/g, '\n\n')}` 272 | } 273 | }; 274 | // 发送请求 275 | const response = yield axios_1.default.post(`${WECOM_API}${key}`, data); 276 | return handleNotifyResult('Wecom', response); 277 | } 278 | catch (error) { 279 | console.error(`❌ 发送企业微信通知时出错: ${error.message}`); 280 | return { success: false, message: error.message, platform: 'Wecom' }; 281 | } 282 | }); 283 | } 284 | /** 285 | * 发送PushPlus通知 286 | * @param {string} title - 通知标题 287 | * @param {string} content - 通知内容 288 | * @returns {Promise} 发送结果 289 | */ 290 | function sendPushPlus(title, content) { 291 | return __awaiter(this, void 0, void 0, function* () { 292 | try { 293 | const token = process.env.PUSHPLUS_TOKEN; 294 | if (!token) { 295 | return { success: false, message: 'TOKEN未设置', platform: 'PushPlus' }; 296 | } 297 | // 构建请求数据 298 | const data = { 299 | token: token, 300 | title: title, 301 | content: content, 302 | template: 'markdown', 303 | channel: 'wechat' 304 | }; 305 | // 发送请求 306 | const response = yield axios_1.default.post(PUSHPLUS_API, data); 307 | return handleNotifyResult('PushPlus', response); 308 | } 309 | catch (error) { 310 | console.error(`❌ 发送PushPlus通知时出错: ${error.message}`); 311 | return { success: false, message: error.message, platform: 'PushPlus' }; 312 | } 313 | }); 314 | } 315 | /** 316 | * 发送通知到所有配置的平台 317 | * @param {string} title - 通知标题 318 | * @param {string} content - 通知内容 319 | * @returns {Promise} 320 | */ 321 | function sendNotification(title, content, platform) { 322 | return __awaiter(this, void 0, void 0, function* () { 323 | console.log('开始发送通知...'); 324 | try { 325 | // 发送到Server酱 326 | yield sendServerChan(title, content); 327 | // 发送到Bark 328 | yield sendBark(title, content); 329 | // 发送到Telegram 330 | yield sendTelegram(title, content); 331 | // 发送到钉钉 332 | yield sendDingTalk(title, content); 333 | // 发送到企业微信 334 | yield sendWecom(title, content); 335 | // 发送到PushPlus 336 | yield sendPushPlus(title, content); 337 | } 338 | catch (error) { 339 | console.error(`❌ 发送通知时出错: ${error.message}`); 340 | } 341 | }); 342 | } 343 | -------------------------------------------------------------------------------- /dist/data.txt: -------------------------------------------------------------------------------- 1 | %5B%7B%22data_hr%22%3A%22%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9L%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FVv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0v%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9e%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0n%5C%2Fa%5C%2F%5C%2F%5C%2FS%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0b%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F1FK%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FR%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9PTFFpaf9L%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FR%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0j%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9K%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FOv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fzf%5C%2F%5C%2F%5C%2F86%5C%2Fzr%5C%2FOv88%5C%2Fzf%5C%2FPf%5C%2F%5C%2F%5C%2F0v%5C%2FS%5C%2F8%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FSf%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fz3%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0r%5C%2FOv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FS%5C%2F9L%5C%2Fzb%5C%2FSf9K%5C%2F0v%5C%2FRf9H%5C%2Fzj%5C%2FSf9K%5C%2F0%5C%2F%5C%2FN%5C%2F%5C%2F%5C%2F%5C%2F0D%5C%2FSf83%5C%2Fzr%5C%2FPf9M%5C%2F0v%5C%2FOv9e%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FS%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fzv%5C%2F%5C%2Fz7%5C%2FO%5C%2F83%5C%2Fzv%5C%2FN%5C%2F83%5C%2Fzr%5C%2FN%5C%2F86%5C%2Fz%5C%2F%5C%2FNv83%5C%2Fzn%5C%2FXv84%5C%2Fzr%5C%2FPP84%5C%2Fzj%5C%2FN%5C%2F9e%5C%2Fzr%5C%2FN%5C%2F89%5C%2F03%5C%2FP%5C%2F89%5C%2Fz3%5C%2FQ%5C%2F9N%5C%2F0v%5C%2FTv9C%5C%2F0H%5C%2FOf9D%5C%2Fzz%5C%2FOf88%5C%2Fz%5C%2F%5C%2FPP9A%5C%2Fzr%5C%2FN%5C%2F86%5C%2Fzz%5C%2FNv87%5C%2F0D%5C%2FOv84%5C%2F0v%5C%2FO%5C%2F84%5C%2Fzf%5C%2FMP83%5C%2FzH%5C%2FNv83%5C%2Fzf%5C%2FN%5C%2F84%5C%2Fzf%5C%2FOf82%5C%2Fzf%5C%2FOP83%5C%2Fzb%5C%2FMv81%5C%2FzX%5C%2FR%5C%2F9L%5C%2F0v%5C%2FO%5C%2F9I%5C%2F0T%5C%2FS%5C%2F9A%5C%2Fzn%5C%2FPf89%5C%2Fzn%5C%2FNf9K%5C%2F07%5C%2FN%5C%2F83%5C%2Fzn%5C%2FNv83%5C%2Fzv%5C%2FO%5C%2F9A%5C%2F0H%5C%2FOf8%5C%2F%5C%2Fzj%5C%2FPP83%5C%2Fzj%5C%2FS%5C%2F87%5C%2Fzj%5C%2FNv84%5C%2Fzf%5C%2FOf83%5C%2Fzf%5C%2FOf83%5C%2Fzb%5C%2FNv9L%5C%2Fzj%5C%2FNv82%5C%2Fzb%5C%2FN%5C%2F85%5C%2Fzf%5C%2FN%5C%2F9J%5C%2Fzf%5C%2FNv83%5C%2Fzj%5C%2FNv84%5C%2F0r%5C%2FSv83%5C%2Fzf%5C%2FMP%5C%2F%5C%2F%5C%2Fzb%5C%2FMv82%5C%2Fzb%5C%2FOf85%5C%2Fz7%5C%2FNv8%5C%2F%5C%2F0r%5C%2FS%5C%2F85%5C%2F0H%5C%2FQP9B%5C%2F0D%5C%2FNf89%5C%2Fzj%5C%2FOv83%5C%2Fzv%5C%2FNv8%5C%2F%5C%2F0f%5C%2FSv9O%5C%2F0ZeXv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F1X%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9B%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FTP%5C%2F%5C%2F%5C%2F1b%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9N%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%22%2C%22date%22%3A%222021-08-07%22%2C%22data%22%3A%5B%7B%22start%22%3A0%2C%22stop%22%3A1439%2C%22value%22%3A%22UA8AUBQAUAwAUBoAUAEAYCcAUBkAUB4AUBgAUCAAUAEAUBkAUAwAYAsAYB8AYB0AYBgAYCoAYBgAYB4AUCcAUBsAUB8AUBwAUBIAYBkAYB8AUBoAUBMAUCEAUCIAYBYAUBwAUCAAUBgAUCAAUBcAYBsAYCUAATIPYD0KECQAYDMAYB0AYAsAYCAAYDwAYCIAYB0AYBcAYCQAYB0AYBAAYCMAYAoAYCIAYCEAYCYAYBsAYBUAYAYAYCIAYCMAUB0AUCAAUBYAUCoAUBEAUC8AUB0AUBYAUDMAUDoAUBkAUC0AUBQAUBwAUA0AUBsAUAoAUCEAUBYAUAwAUB4AUAwAUCcAUCYAUCwKYDUAAUUlEC8IYEMAYEgAYDoAYBAAUAMAUBkAWgAAWgAAWgAAWgAAWgAAUAgAWgAAUBAAUAQAUA4AUA8AUAkAUAIAUAYAUAcAUAIAWgAAUAQAUAkAUAEAUBkAUCUAWgAAUAYAUBEAWgAAUBYAWgAAUAYAWgAAWgAAWgAAWgAAUBcAUAcAWgAAUBUAUAoAUAIAWgAAUAQAUAYAUCgAWgAAUAgAWgAAWgAAUAwAWwAAXCMAUBQAWwAAUAIAWgAAWgAAWgAAWgAAWgAAWgAAWgAAWgAAWREAWQIAUAMAWSEAUDoAUDIAUB8AUCEAUC4AXB4AUA4AWgAAUBIAUA8AUBAAUCUAUCIAUAMAUAEAUAsAUAMAUCwAUBYAWgAAWgAAWgAAWgAAWgAAWgAAUAYAWgAAWgAAWgAAUAYAWwAAWgAAUAYAXAQAUAMAUBsAUBcAUCAAWwAAWgAAWgAAWgAAWgAAUBgAUB4AWgAAUAcAUAwAWQIAWQkAUAEAUAIAWgAAUAoAWgAAUAYAUB0AWgAAWgAAUAkAWgAAWSwAUBIAWgAAUC4AWSYAWgAAUAYAUAoAUAkAUAIAUAcAWgAAUAEAUBEAUBgAUBcAWRYAUA0AWSgAUB4AUDQAUBoAXA4AUA8AUBwAUA8AUA4AUA4AWgAAUAIAUCMAWgAAUCwAUBgAUAYAUAAAUAAAUAAAUAAAUAAAUAAAUAAAUAAAUAAAWwAAUAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAeSEAeQ8AcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBcAcAAAcAAAcCYOcBUAUAAAUAAAUAAAUAAAUAUAUAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCgAeQAAcAAAcAAAcAAAcAAAcAAAcAYAcAAAcBgAeQAAcAAAcAAAegAAegAAcAAAcAcAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCkAeQAAcAcAcAAAcAAAcAwAcAAAcAAAcAIAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCIAeQAAcAAAcAAAcAAAcAAAcAAAeRwAeQAAWgAAUAAAUAAAUAAAUAAAUAAAcAAAcAAAcBoAeScAeQAAegAAcBkAeQAAUAAAUAAAUAAAUAAAUAAAUAAAcAAAcAAAcAAAcAAAcAAAcAAAegAAegAAcAAAcAAAcBgAeQAAcAAAcAAAcAAAcAAAcAAAcAkAegAAegAAcAcAcAAAcAcAcAAAcAAAcAAAcAAAcA8AeQAAcAAAcAAAeRQAcAwAUAAAUAAAUAAAUAAAUAAAUAAAcAAAcBEAcA0AcAAAWQsAUAAAUAAAUAAAUAAAUAAAcAAAcAoAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAYAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBYAegAAcAAAcAAAegAAcAcAcAAAcAAAcAAAcAAAcAAAeRkAegAAegAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAEAcAAAcAAAcAAAcAUAcAQAcAAAcBIAeQAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBsAcAAAcAAAcBcAeQAAUAAAUAAAUAAAUAAAUAAAUBQAcBYAUAAAUAAAUAoAWRYAWTQAWQAAUAAAUAAAUAAAcAAAcAAAcAAAcAAAcAAAcAMAcAAAcAQAcAAAcAAAcAAAcDMAeSIAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBQAeQwAcAAAcAAAcAAAcAMAcAAAeSoAcA8AcDMAcAYAeQoAcAwAcFQAcEMAeVIAaTYAbBcNYAsAYBIAYAIAYAIAYBUAYCwAYBMAYDYAYCkAYDcAUCoAUCcAUAUAUBAAWgAAYBoAYBcAYCgAUAMAUAYAUBYAUA4AUBgAUAgAUAgAUAsAUAsAUA4AUAMAUAYAUAQAUBIAASsSUDAAUDAAUBAAYAYAUBAAUAUAUCAAUBoAUCAAUBAAUAoAYAIAUAQAUAgAUCcAUAsAUCIAUCUAUAoAUA4AUB8AUBkAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAA%22%2C%22tz%22%3A32%2C%22did%22%3A%22DA932FFFFE8816E7%22%2C%22src%22%3A24%7D%5D%2C%22summary%22%3A%22%7B%5C%22v%5C%22%3A6%2C%5C%22slp%5C%22%3A%7B%5C%22st%5C%22%3A1628296479%2C%5C%22ed%5C%22%3A1628296479%2C%5C%22dp%5C%22%3A0%2C%5C%22lt%5C%22%3A0%2C%5C%22wk%5C%22%3A0%2C%5C%22usrSt%5C%22%3A-1440%2C%5C%22usrEd%5C%22%3A-1440%2C%5C%22wc%5C%22%3A0%2C%5C%22is%5C%22%3A0%2C%5C%22lb%5C%22%3A0%2C%5C%22to%5C%22%3A0%2C%5C%22dt%5C%22%3A0%2C%5C%22rhr%5C%22%3A0%2C%5C%22ss%5C%22%3A0%7D%2C%5C%22stp%5C%22%3A%7B%5C%22ttl%5C%22%3A18272%2C%5C%22dis%5C%22%3A10627%2C%5C%22cal%5C%22%3A510%2C%5C%22wk%5C%22%3A41%2C%5C%22rn%5C%22%3A50%2C%5C%22runDist%5C%22%3A7654%2C%5C%22runCal%5C%22%3A397%2C%5C%22stage%5C%22%3A%5B%7B%5C%22start%5C%22%3A327%2C%5C%22stop%5C%22%3A341%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A481%2C%5C%22cal%5C%22%3A13%2C%5C%22step%5C%22%3A680%7D%2C%7B%5C%22start%5C%22%3A342%2C%5C%22stop%5C%22%3A367%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A2295%2C%5C%22cal%5C%22%3A95%2C%5C%22step%5C%22%3A2874%7D%2C%7B%5C%22start%5C%22%3A368%2C%5C%22stop%5C%22%3A377%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1592%2C%5C%22cal%5C%22%3A88%2C%5C%22step%5C%22%3A1664%7D%2C%7B%5C%22start%5C%22%3A378%2C%5C%22stop%5C%22%3A386%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1072%2C%5C%22cal%5C%22%3A51%2C%5C%22step%5C%22%3A1245%7D%2C%7B%5C%22start%5C%22%3A387%2C%5C%22stop%5C%22%3A393%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1036%2C%5C%22cal%5C%22%3A57%2C%5C%22step%5C%22%3A1124%7D%2C%7B%5C%22start%5C%22%3A394%2C%5C%22stop%5C%22%3A398%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A488%2C%5C%22cal%5C%22%3A19%2C%5C%22step%5C%22%3A607%7D%2C%7B%5C%22start%5C%22%3A399%2C%5C%22stop%5C%22%3A414%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A2220%2C%5C%22cal%5C%22%3A120%2C%5C%22step%5C%22%3A2371%7D%2C%7B%5C%22start%5C%22%3A415%2C%5C%22stop%5C%22%3A427%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1268%2C%5C%22cal%5C%22%3A59%2C%5C%22step%5C%22%3A1489%7D%2C%7B%5C%22start%5C%22%3A428%2C%5C%22stop%5C%22%3A433%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A152%2C%5C%22cal%5C%22%3A4%2C%5C%22step%5C%22%3A238%7D%2C%7B%5C%22start%5C%22%3A434%2C%5C%22stop%5C%22%3A444%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A2295%2C%5C%22cal%5C%22%3A95%2C%5C%22step%5C%22%3A2874%7D%2C%7B%5C%22start%5C%22%3A445%2C%5C%22stop%5C%22%3A455%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1592%2C%5C%22cal%5C%22%3A88%2C%5C%22step%5C%22%3A1664%7D%2C%7B%5C%22start%5C%22%3A456%2C%5C%22stop%5C%22%3A466%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1072%2C%5C%22cal%5C%22%3A51%2C%5C%22step%5C%22%3A1245%7D%2C%7B%5C%22start%5C%22%3A467%2C%5C%22stop%5C%22%3A477%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1036%2C%5C%22cal%5C%22%3A57%2C%5C%22step%5C%22%3A1124%7D%2C%7B%5C%22start%5C%22%3A478%2C%5C%22stop%5C%22%3A488%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A488%2C%5C%22cal%5C%22%3A19%2C%5C%22step%5C%22%3A607%7D%2C%7B%5C%22start%5C%22%3A489%2C%5C%22stop%5C%22%3A499%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A2220%2C%5C%22cal%5C%22%3A120%2C%5C%22step%5C%22%3A2371%7D%2C%7B%5C%22start%5C%22%3A500%2C%5C%22stop%5C%22%3A511%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1268%2C%5C%22cal%5C%22%3A59%2C%5C%22step%5C%22%3A1489%7D%2C%7B%5C%22start%5C%22%3A512%2C%5C%22stop%5C%22%3A522%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A152%2C%5C%22cal%5C%22%3A4%2C%5C%22step%5C%22%3A238%7D%5D%7D%2C%5C%22goal%5C%22%3A8000%2C%5C%22tz%5C%22%3A%5C%2228800%5C%22%7D%22%2C%22source%22%3A24%2C%22type%22%3A0%7D%5D -------------------------------------------------------------------------------- /src/data.txt: -------------------------------------------------------------------------------- 1 | %5B%7B%22data_hr%22%3A%22%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9L%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FVv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0v%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9e%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0n%5C%2Fa%5C%2F%5C%2F%5C%2FS%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0b%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F1FK%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FR%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9PTFFpaf9L%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FR%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0j%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9K%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FOv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fzf%5C%2F%5C%2F%5C%2F86%5C%2Fzr%5C%2FOv88%5C%2Fzf%5C%2FPf%5C%2F%5C%2F%5C%2F0v%5C%2FS%5C%2F8%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FSf%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fz3%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0r%5C%2FOv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FS%5C%2F9L%5C%2Fzb%5C%2FSf9K%5C%2F0v%5C%2FRf9H%5C%2Fzj%5C%2FSf9K%5C%2F0%5C%2F%5C%2FN%5C%2F%5C%2F%5C%2F%5C%2F0D%5C%2FSf83%5C%2Fzr%5C%2FPf9M%5C%2F0v%5C%2FOv9e%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FS%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fzv%5C%2F%5C%2Fz7%5C%2FO%5C%2F83%5C%2Fzv%5C%2FN%5C%2F83%5C%2Fzr%5C%2FN%5C%2F86%5C%2Fz%5C%2F%5C%2FNv83%5C%2Fzn%5C%2FXv84%5C%2Fzr%5C%2FPP84%5C%2Fzj%5C%2FN%5C%2F9e%5C%2Fzr%5C%2FN%5C%2F89%5C%2F03%5C%2FP%5C%2F89%5C%2Fz3%5C%2FQ%5C%2F9N%5C%2F0v%5C%2FTv9C%5C%2F0H%5C%2FOf9D%5C%2Fzz%5C%2FOf88%5C%2Fz%5C%2F%5C%2FPP9A%5C%2Fzr%5C%2FN%5C%2F86%5C%2Fzz%5C%2FNv87%5C%2F0D%5C%2FOv84%5C%2F0v%5C%2FO%5C%2F84%5C%2Fzf%5C%2FMP83%5C%2FzH%5C%2FNv83%5C%2Fzf%5C%2FN%5C%2F84%5C%2Fzf%5C%2FOf82%5C%2Fzf%5C%2FOP83%5C%2Fzb%5C%2FMv81%5C%2FzX%5C%2FR%5C%2F9L%5C%2F0v%5C%2FO%5C%2F9I%5C%2F0T%5C%2FS%5C%2F9A%5C%2Fzn%5C%2FPf89%5C%2Fzn%5C%2FNf9K%5C%2F07%5C%2FN%5C%2F83%5C%2Fzn%5C%2FNv83%5C%2Fzv%5C%2FO%5C%2F9A%5C%2F0H%5C%2FOf8%5C%2F%5C%2Fzj%5C%2FPP83%5C%2Fzj%5C%2FS%5C%2F87%5C%2Fzj%5C%2FNv84%5C%2Fzf%5C%2FOf83%5C%2Fzf%5C%2FOf83%5C%2Fzb%5C%2FNv9L%5C%2Fzj%5C%2FNv82%5C%2Fzb%5C%2FN%5C%2F85%5C%2Fzf%5C%2FN%5C%2F9J%5C%2Fzf%5C%2FNv83%5C%2Fzj%5C%2FNv84%5C%2F0r%5C%2FSv83%5C%2Fzf%5C%2FMP%5C%2F%5C%2F%5C%2Fzb%5C%2FMv82%5C%2Fzb%5C%2FOf85%5C%2Fz7%5C%2FNv8%5C%2F%5C%2F0r%5C%2FS%5C%2F85%5C%2F0H%5C%2FQP9B%5C%2F0D%5C%2FNf89%5C%2Fzj%5C%2FOv83%5C%2Fzv%5C%2FNv8%5C%2F%5C%2F0f%5C%2FSv9O%5C%2F0ZeXv%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F1X%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9B%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2FTP%5C%2F%5C%2F%5C%2F1b%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F0%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F9N%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2F%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%5C%2Fv7%2B%22%2C%22date%22%3A%222021-08-07%22%2C%22data%22%3A%5B%7B%22start%22%3A0%2C%22stop%22%3A1439%2C%22value%22%3A%22UA8AUBQAUAwAUBoAUAEAYCcAUBkAUB4AUBgAUCAAUAEAUBkAUAwAYAsAYB8AYB0AYBgAYCoAYBgAYB4AUCcAUBsAUB8AUBwAUBIAYBkAYB8AUBoAUBMAUCEAUCIAYBYAUBwAUCAAUBgAUCAAUBcAYBsAYCUAATIPYD0KECQAYDMAYB0AYAsAYCAAYDwAYCIAYB0AYBcAYCQAYB0AYBAAYCMAYAoAYCIAYCEAYCYAYBsAYBUAYAYAYCIAYCMAUB0AUCAAUBYAUCoAUBEAUC8AUB0AUBYAUDMAUDoAUBkAUC0AUBQAUBwAUA0AUBsAUAoAUCEAUBYAUAwAUB4AUAwAUCcAUCYAUCwKYDUAAUUlEC8IYEMAYEgAYDoAYBAAUAMAUBkAWgAAWgAAWgAAWgAAWgAAUAgAWgAAUBAAUAQAUA4AUA8AUAkAUAIAUAYAUAcAUAIAWgAAUAQAUAkAUAEAUBkAUCUAWgAAUAYAUBEAWgAAUBYAWgAAUAYAWgAAWgAAWgAAWgAAUBcAUAcAWgAAUBUAUAoAUAIAWgAAUAQAUAYAUCgAWgAAUAgAWgAAWgAAUAwAWwAAXCMAUBQAWwAAUAIAWgAAWgAAWgAAWgAAWgAAWgAAWgAAWgAAWREAWQIAUAMAWSEAUDoAUDIAUB8AUCEAUC4AXB4AUA4AWgAAUBIAUA8AUBAAUCUAUCIAUAMAUAEAUAsAUAMAUCwAUBYAWgAAWgAAWgAAWgAAWgAAWgAAUAYAWgAAWgAAWgAAUAYAWwAAWgAAUAYAXAQAUAMAUBsAUBcAUCAAWwAAWgAAWgAAWgAAWgAAUBgAUB4AWgAAUAcAUAwAWQIAWQkAUAEAUAIAWgAAUAoAWgAAUAYAUB0AWgAAWgAAUAkAWgAAWSwAUBIAWgAAUC4AWSYAWgAAUAYAUAoAUAkAUAIAUAcAWgAAUAEAUBEAUBgAUBcAWRYAUA0AWSgAUB4AUDQAUBoAXA4AUA8AUBwAUA8AUA4AUA4AWgAAUAIAUCMAWgAAUCwAUBgAUAYAUAAAUAAAUAAAUAAAUAAAUAAAUAAAUAAAUAAAWwAAUAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAeSEAeQ8AcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBcAcAAAcAAAcCYOcBUAUAAAUAAAUAAAUAAAUAUAUAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCgAeQAAcAAAcAAAcAAAcAAAcAAAcAYAcAAAcBgAeQAAcAAAcAAAegAAegAAcAAAcAcAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCkAeQAAcAcAcAAAcAAAcAwAcAAAcAAAcAIAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcCIAeQAAcAAAcAAAcAAAcAAAcAAAeRwAeQAAWgAAUAAAUAAAUAAAUAAAUAAAcAAAcAAAcBoAeScAeQAAegAAcBkAeQAAUAAAUAAAUAAAUAAAUAAAUAAAcAAAcAAAcAAAcAAAcAAAcAAAegAAegAAcAAAcAAAcBgAeQAAcAAAcAAAcAAAcAAAcAAAcAkAegAAegAAcAcAcAAAcAcAcAAAcAAAcAAAcAAAcA8AeQAAcAAAcAAAeRQAcAwAUAAAUAAAUAAAUAAAUAAAUAAAcAAAcBEAcA0AcAAAWQsAUAAAUAAAUAAAUAAAUAAAcAAAcAoAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAYAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBYAegAAcAAAcAAAegAAcAcAcAAAcAAAcAAAcAAAcAAAeRkAegAAegAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAEAcAAAcAAAcAAAcAUAcAQAcAAAcBIAeQAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBsAcAAAcAAAcBcAeQAAUAAAUAAAUAAAUAAAUAAAUBQAcBYAUAAAUAAAUAoAWRYAWTQAWQAAUAAAUAAAUAAAcAAAcAAAcAAAcAAAcAAAcAMAcAAAcAQAcAAAcAAAcAAAcDMAeSIAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcAAAcBQAeQwAcAAAcAAAcAAAcAMAcAAAeSoAcA8AcDMAcAYAeQoAcAwAcFQAcEMAeVIAaTYAbBcNYAsAYBIAYAIAYAIAYBUAYCwAYBMAYDYAYCkAYDcAUCoAUCcAUAUAUBAAWgAAYBoAYBcAYCgAUAMAUAYAUBYAUA4AUBgAUAgAUAgAUAsAUAsAUA4AUAMAUAYAUAQAUBIAASsSUDAAUDAAUBAAYAYAUBAAUAUAUCAAUBoAUCAAUBAAUAoAYAIAUAQAUAgAUCcAUAsAUCIAUCUAUAoAUA4AUB8AUBkAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAAfgAA%22%2C%22tz%22%3A32%2C%22did%22%3A%22DA932FFFFE8816E7%22%2C%22src%22%3A24%7D%5D%2C%22summary%22%3A%22%7B%5C%22v%5C%22%3A6%2C%5C%22slp%5C%22%3A%7B%5C%22st%5C%22%3A1628296479%2C%5C%22ed%5C%22%3A1628296479%2C%5C%22dp%5C%22%3A0%2C%5C%22lt%5C%22%3A0%2C%5C%22wk%5C%22%3A0%2C%5C%22usrSt%5C%22%3A-1440%2C%5C%22usrEd%5C%22%3A-1440%2C%5C%22wc%5C%22%3A0%2C%5C%22is%5C%22%3A0%2C%5C%22lb%5C%22%3A0%2C%5C%22to%5C%22%3A0%2C%5C%22dt%5C%22%3A0%2C%5C%22rhr%5C%22%3A0%2C%5C%22ss%5C%22%3A0%7D%2C%5C%22stp%5C%22%3A%7B%5C%22ttl%5C%22%3A18272%2C%5C%22dis%5C%22%3A10627%2C%5C%22cal%5C%22%3A510%2C%5C%22wk%5C%22%3A41%2C%5C%22rn%5C%22%3A50%2C%5C%22runDist%5C%22%3A7654%2C%5C%22runCal%5C%22%3A397%2C%5C%22stage%5C%22%3A%5B%7B%5C%22start%5C%22%3A327%2C%5C%22stop%5C%22%3A341%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A481%2C%5C%22cal%5C%22%3A13%2C%5C%22step%5C%22%3A680%7D%2C%7B%5C%22start%5C%22%3A342%2C%5C%22stop%5C%22%3A367%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A2295%2C%5C%22cal%5C%22%3A95%2C%5C%22step%5C%22%3A2874%7D%2C%7B%5C%22start%5C%22%3A368%2C%5C%22stop%5C%22%3A377%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1592%2C%5C%22cal%5C%22%3A88%2C%5C%22step%5C%22%3A1664%7D%2C%7B%5C%22start%5C%22%3A378%2C%5C%22stop%5C%22%3A386%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1072%2C%5C%22cal%5C%22%3A51%2C%5C%22step%5C%22%3A1245%7D%2C%7B%5C%22start%5C%22%3A387%2C%5C%22stop%5C%22%3A393%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1036%2C%5C%22cal%5C%22%3A57%2C%5C%22step%5C%22%3A1124%7D%2C%7B%5C%22start%5C%22%3A394%2C%5C%22stop%5C%22%3A398%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A488%2C%5C%22cal%5C%22%3A19%2C%5C%22step%5C%22%3A607%7D%2C%7B%5C%22start%5C%22%3A399%2C%5C%22stop%5C%22%3A414%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A2220%2C%5C%22cal%5C%22%3A120%2C%5C%22step%5C%22%3A2371%7D%2C%7B%5C%22start%5C%22%3A415%2C%5C%22stop%5C%22%3A427%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1268%2C%5C%22cal%5C%22%3A59%2C%5C%22step%5C%22%3A1489%7D%2C%7B%5C%22start%5C%22%3A428%2C%5C%22stop%5C%22%3A433%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A152%2C%5C%22cal%5C%22%3A4%2C%5C%22step%5C%22%3A238%7D%2C%7B%5C%22start%5C%22%3A434%2C%5C%22stop%5C%22%3A444%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A2295%2C%5C%22cal%5C%22%3A95%2C%5C%22step%5C%22%3A2874%7D%2C%7B%5C%22start%5C%22%3A445%2C%5C%22stop%5C%22%3A455%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1592%2C%5C%22cal%5C%22%3A88%2C%5C%22step%5C%22%3A1664%7D%2C%7B%5C%22start%5C%22%3A456%2C%5C%22stop%5C%22%3A466%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1072%2C%5C%22cal%5C%22%3A51%2C%5C%22step%5C%22%3A1245%7D%2C%7B%5C%22start%5C%22%3A467%2C%5C%22stop%5C%22%3A477%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A1036%2C%5C%22cal%5C%22%3A57%2C%5C%22step%5C%22%3A1124%7D%2C%7B%5C%22start%5C%22%3A478%2C%5C%22stop%5C%22%3A488%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A488%2C%5C%22cal%5C%22%3A19%2C%5C%22step%5C%22%3A607%7D%2C%7B%5C%22start%5C%22%3A489%2C%5C%22stop%5C%22%3A499%2C%5C%22mode%5C%22%3A4%2C%5C%22dis%5C%22%3A2220%2C%5C%22cal%5C%22%3A120%2C%5C%22step%5C%22%3A2371%7D%2C%7B%5C%22start%5C%22%3A500%2C%5C%22stop%5C%22%3A511%2C%5C%22mode%5C%22%3A3%2C%5C%22dis%5C%22%3A1268%2C%5C%22cal%5C%22%3A59%2C%5C%22step%5C%22%3A1489%7D%2C%7B%5C%22start%5C%22%3A512%2C%5C%22stop%5C%22%3A522%2C%5C%22mode%5C%22%3A1%2C%5C%22dis%5C%22%3A152%2C%5C%22cal%5C%22%3A4%2C%5C%22step%5C%22%3A238%7D%5D%7D%2C%5C%22goal%5C%22%3A8000%2C%5C%22tz%5C%22%3A%5C%2228800%5C%22%7D%22%2C%22source%22%3A24%2C%22type%22%3A0%7D%5D --------------------------------------------------------------------------------