未读消息

概述

未读消息模块基于设备本地 SQLite 数据库实现,用于记录业务消息并同步至云控后台,在投屏界面实时展示未读数量及关键状态。

适用于:

  • 任务执行状态跟踪
  • 异常 / 错误上报
  • 收益或数据统计展示
  • 多设备集中监控
未读消息
未读消息

架构说明

本地存储(SQLite)

消息数据存储在设备本地数据库:

脚本写入消息

通过脚本向 SQLite 写入或更新消息数据:

  • 添加未读消息(isRead = 0

通知云端刷新

调用接口通知云控后台更新数据

云控后台聚合

后台接收通知并处理数据:

  • 聚合设备消息
  • 统计未读数量

投屏界面展示

投屏界面根据数据字段实时渲染未读消息内容:

  • 未读消息数量
    • 点击可打开消息窗口
  • 消息详情列表

消息操作

在投屏界面点击未读消息图标,可打开消息窗口并进行管理:

  • 查看消息详情
  • 删除或批量删除消息
  • 标记为已读(isRead = 1,支持批量操作)
  • 触发关联脚本执行

脚本执行

在消息列表中点击「执行」后,将触发设备端执行对应脚本:

  • 自动获取当前消息数据(作为执行上下文)
  • 按预设逻辑处理业务(如跳转、操作、请求等)
  • 可在执行过程中更新任务状态(taskStatus

数据库结构

meta_actions 表为系统必选核心表,结构固定且不可修改,必须存在于数据库中,用于系统任务调度与执行控制。

业务表支持按需创建,可创建多个,表名完全自定义,用于承载具体业务数据。

字段结构需遵循以下规则:

  • 必选字段与推荐字段必须保留
  • 其余字段均为业务扩展字段,支持自定义(包含中文字段名)
  • 字段命名需保持语义清晰,不得与系统保留字段冲突
未读消息.db(数据库文件)
├── meta_actions(必选表 / 系统任务调度表)
│   ├── id(主键)
│   ├── activity(启动 Activity 页面)
│   ├── broadcast(执行时发送广播)
│   ├── scriptContent(脚本内容代码)
│   ├── scriptName(脚本名称)
│   ├── scriptPath(脚本文件路径)
│   ├── scriptConfig(脚本执行配置参数)
│   ├── targetTable(关联业务表名)
│   └── targetId(关联业务表数据 ID)
│
├── 设备消息(业务表)
│   ├── id
│   ├── 标题
│   ├── 内容
│   ├── 今日收益
│   ├── 错误信息
│   ├── 创建时间
│   ├── isRead
│   ├── taskStatus
│   └── taskUpdateTime
│
├── 盈利汇总(业务表)
│   ├── id
│   ├── 日期
│   ├── 总收益
│   ├── 备注
│   └── isRead
│
├── 任务汇总(业务表)
│   ├── id
│   ├── 任务名称
│   ├── 执行状态
│   ├── 更新时间
│   └── isRead
├── ...(其他业务表)

meta_actions(系统任务调度核心表)

meta_actions 用于定义任务执行行为,是系统的控制中心表,不可删除,不可修改结构。

该表负责:

  • 启动 Activity 页面
  • 发送广播通知
  • 执行脚本代码
  • 关联业务数据表(targetTable + targetId)

字段说明

字段类型描述
idINTEGER主键
activityTEXT用于定义需要启动的 Android Activity 页面信息,通常包含 packageName 与 className,用于在执行任务时跳转到指定页面
broadcastTEXT用于定义广播触发配置,在任务执行过程中发送系统或自定义广播,用于通知其他模块或应用进行联动处理
scriptContentTEXT存储可直接执行的脚本代码内容,脚本执行时优先使用该字段内容作为运行逻辑主体,详情参阅脚本引擎
scriptNameTEXT脚本名称标识,运行脚本代码内容时得名称,详情参阅脚本引擎
scriptPathTEXT脚本文件存储路径,当脚本内容不直接内嵌时,通过该路径加载外部脚本文件执行,详情参阅脚本引擎
scriptConfigTEXT脚本运行时配置参数,通常为 JSON 结构,用于控制执行频率、循环次数、延迟等运行行为,详情参阅脚本引擎
targetTableTEXT关联的业务数据表名称,用于指定当前任务操作的数据来源表,实现任务与业务数据解耦
targetIdINTEGER关联业务表中的具体数据 ID,用于定位当前任务对应的具体业务记录
可通过 SQLite 模板中的示例数据,理解各字段的实际结构与使用方式。

业务表

支持完全自定义表结构(字段名支持中文),但需满足以下约束:

必选字段

字段名类型说明
idINTEGER主键(自增)
isReadINTEGER是否已读(0=未读,1=已读)

推荐字段

字段名类型说明
taskStatusINTEGER任务状态
taskUpdateTimeINTEGER状态更新时间(时间戳)

taskStatus 用于表示任务执行状态,取值如下:

  • 0:待执行(初始状态)
  • 1:开始执行
  • 2:执行中(任务已被脚本获取)
  • 3:已完成(执行成功)
  • 4:执行失败(执行异常)

扩展字段

除上述字段外,可根据业务需求自定义扩展字段(支持中文字段名)。

建议:

  • 字段命名保持语义清晰、统一规范
  • 数据类型与实际业务含义保持一致
  • 避免与系统字段(如 idisReadtaskStatustaskUpdateTime)冲突

数据库创建

可在后台创建并编辑 SQLite 模板,详情参阅 SQLite 数据库

完成数据库设计后,数据库可通过以下两种方式在设备端实现:

数据库文件分发

适用于已设计完成的 SQLite 数据库模板。

数据库文件(.db)可通过任意方式分发至设备,例如本地传输或远程下载等,实现快速部署与同步。

存放路径要求:

/sdcard/cloud/SQLite/

将数据库文件放入上述目录后,系统即可自动识别并使用。

脚本动态创建

适用于运行时初始化或动态生成数据库结构的场景。

无需依赖预置 .db 文件,可通过脚本在设备端直接创建表结构。


// 👉 示例调用
createUnreadMessageDB("/sdcard/cloud/SQLite/未读消息.db");

/**
 * 创建【未读消息数据库模板】
 *
 * 📌 功能说明:
 * 该函数用于在指定路径生成一个完整的 SQLite 数据库文件,
 * 包含:
 * 1. 表结构(meta_actions / 设备消息)
 * 2. 初始化数据(任务数据 + 示例消息)
 *
 * 📌 使用场景:
 * - 云控设备初始化数据库
 * - 未读消息模板下发
 * - 本地测试数据构建
 * - 数据库恢复 / 克隆
 *
 * 📌 特点:
 * - 自动删除旧数据库(避免结构冲突)
 * - 自动创建表
 * - 自动插入初始化数据
 * - 支持中文表名 / 字段
 *
 * @param {string} dbPath 数据库文件路径(如:/sdcard/cloud/SQLite/未读消息.db)
 */
function createUnreadMessageDB(dbPath) {
  // =========================
  // 0️⃣ 如果已存在数据库 → 先删除
  // =========================
  var file = new java.io.File(dbPath);
  if (file.exists()) {
    file.delete();
    console.log("🗑️ 已删除旧数据库:", dbPath);
  }

  // 打开(不存在会自动创建)
  var db = sqlite.open(dbPath);

  try {
    // =========================
    // 1️⃣ 创建表结构
    // =========================

    // 👉 脚本任务配置表
    db.execSQL(
      "CREATE TABLE meta_actions (" +
        "id INTEGER NOT NULL PRIMARY KEY, " +
        "activity TEXT, " +
        "broadcast TEXT, " +
        "scriptContent TEXT, " +
        "scriptName TEXT, " +
        "scriptPath TEXT, " +
        "scriptConfig TEXT, " +
        "targetTable TEXT, " +
        "targetId INTEGER" +
        ")"
    );

    // 👉 未读消息表(核心)
    db.execSQL(
      "CREATE TABLE 设备消息 (" +
        "id INTEGER NOT NULL PRIMARY KEY, " +
        "标题 TEXT, " +
        "内容 TEXT, " +
        "今日收益 REAL, " +
        "错误信息 TEXT, " +
        "创建时间 TEXT, " +
        "isRead INTEGER, " +
        "taskStatus INTEGER, " +
        "taskUpdateTime INTEGER" +
        ")"
    );

    // =========================
    // 2️⃣ 插入初始化数据
    // =========================

    // 👉 脚本任务配置(用于联动执行)
    db.insert("meta_actions", {
      id: 1,
      activity: '{\n    "packageName": "bin.mt.plus",\n    "className": "bin.mt.plus.Main"\n}',
      broadcast:
        '{\n    "broadcastType": "global",\n    "action": "com.example.TEST_ACTION",\n    "packageName": "com.example.app",\n    "className": "com.example.app.MyReceiver",\n    "dataUri": "content://com.example.provider/test",\n    "mimeType": "text/plain",\n    "categories": [\n        "android.intent.category.DEFAULT",\n        "android.intent.category.BROWSABLE"\n    ],\n    "flags": 268435456,\n    "permission": "com.example.permission.TEST",\n    "extras": {\n        "stringKey": "字符串",\n        "intKey": 123,\n        "longKey": 123456789,\n        "doubleKey": 3.14,\n        "booleanKey": true\n    }\n}',
      scriptContent:
        '\n\nvar messageConfig = {\n  DB_PATH: "/sdcard/cloud/SQLite/未读消息.db",\n  TABLE_TASK: "设备消息",\n  TABLE_META: "meta_actions"\n};\n\nvar task = getNextTask(messageConfig);\nif (task) {\n  console.log("🎯 任务:", task);\n\n  try {\n\n    // =====================\n    // 你的业务逻辑\n    // =====================\n    // doSomething(task);\n\n    taskSuccess(messageConfig, task.id);\n\n  } catch (e) {\n    console.error(e);\n    taskFail(messageConfig, task.id);\n  }\n\n} else {\n  console.log("❌ 没任务");\n}\n\n\n\n\n\n/**\n * 获取“未读消息任务”\n * \n * 功能说明:\n * 1. 打开 SQLite 数据库\n * 2. 从“设备消息”表中获取一条可执行任务\n * 3. 支持 taskStatus / taskUpdateTime 字段动态判断\n * 4. 支持 meta_actions 兜底获取任务\n * 5. 自动抢占任务(taskStatus = 2)\n * 6. 返回完整任务行数据\n * 7. 自动关闭数据库连接\n * \n * 任务状态说明:\n * 0 = 待执行\n * 1 = 开始执行(已被抢占)\n * 2 = 执行中\n * 3 = 已完成\n * 4 = 执行失败\n * \n * @param {Object} config 配置参数\n * @param {string} config.DB_PATH 数据库路径\n * @param {string} config.TABLE_TASK 任务表名(如:设备消息)\n * @param {string} config.TABLE_META 关联表(如:meta_actions)\n * \n * @returns {Object|null} 返回一条任务数据(整行记录),没有则返回 null\n */\nfunction getNextTask(config) {\n\n  // =============================\n  // 打开数据库\n  // =============================\n  var db = sqlite.open(config.DB_PATH);\n\n  try {\n\n    var TABLE_TASK = config.TABLE_TASK; // 任务表(如:设备消息)\n    var TABLE_META = config.TABLE_META; // 关联表(如:meta_actions)\n\n    // =============================\n    // 判断某个字段是否存在\n    // 👉 因为不同数据库结构可能不一样\n    // =============================\n    function hasColumn(table, column) {\n      var cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);\n      var exists = false;\n\n      while (cursor.moveToNext()) {\n        var row = cursor.pick();\n\n        // 如果字段名匹配说明存在\n        if (row.name === column) {\n          exists = true;\n          break;\n        }\n      }\n\n      cursor.close();\n      return exists;\n    }\n\n    // 判断任务表是否有这些字段\n    var hasTaskStatus = hasColumn(TABLE_TASK, "taskStatus"); // 是否有状态字段\n    var hasTaskUpdateTime = hasColumn(TABLE_TASK, "taskUpdateTime"); // 是否有时间字段\n\n    // =============================\n    // 抢占任务(非常关键)\n    // 👉 防止多个脚本同时执行同一条任务\n    // =============================\n    function take(id) {\n\n      if (!id) return null;\n\n      var affect = 1;\n\n      // 如果有 taskStatus 字段,就先“抢占任务”\n      // 把状态从 1(开始执行) 改成 2(执行中)\n      if (hasTaskStatus) {\n        affect = db.update(\n          TABLE_TASK,\n          { taskStatus: 2 },\n          "id = ? AND taskStatus = 1", // 只有未执行的才能被抢\n          [id]\n        );\n      }\n\n      // 如果 update 失败,说明已经被别人抢走了\n      if (affect === 0) return null;\n\n      // 返回完整任务数据\n      return db.rawQuery(\n        "SELECT * FROM " + TABLE_TASK + " WHERE id = ?",\n        [id]\n      ).single();\n    }\n\n    var task = null;\n\n    // =============================\n    // 方法1:从任务表获取\n    // =============================\n    var sql = "SELECT id FROM " + TABLE_TASK + " WHERE isRead = 1";\n\n    // 如果有状态字段,只取“待执行”的任务\n    if (hasTaskStatus) {\n      sql += " AND taskStatus = 1";\n    }\n\n    // 如果有时间字段,就取最新的一条\n    if (hasTaskUpdateTime) {\n      sql += " ORDER BY taskUpdateTime DESC LIMIT 1";\n    } else {\n      sql += " LIMIT 1";\n    }\n\n    // 执行查询(只拿 id,减少数据量)\n    var row1 = db.rawQuery(sql, null).single();\n\n    // 如果查到了,就尝试抢占任务\n    if (row1 && row1.id) {\n      task = take(row1.id);\n    }\n\n    // =============================\n    // 方法2:兜底来源 meta_actions\n    // =============================\n    // 👉 如果方法1没拿到任务,再从关联表找\n    if (!task) {\n\n      var row2 = db.rawQuery(\n        "SELECT targetId FROM " + TABLE_META +\n        " WHERE targetTable = ? LIMIT 1",\n        [TABLE_TASK]\n      ).single();\n\n      // 用 targetId 再去抢占一次任务\n      if (row2 && row2.targetId) {\n        task = take(row2.targetId);\n      }\n    }\n\n    // 返回最终任务(可能为 null)\n    return task;\n\n  } catch (e) {\n\n    // 出错日志\n    console.error("getNextTask 执行失败:", e);\n    return null;\n\n  } finally {\n\n    // 一定要关闭数据库,释放资源\n    db.close();\n  }\n}\n\n/**\n * =============== 任务成功 ===============\n * taskStatus = 3\n */\nfunction taskSuccess(config, id) {\n\n  var db = sqlite.open(config.DB_PATH);\n\n  try {\n\n    db.update(\n      config.TABLE_TASK,\n      { taskStatus: 3 },\n      "id = ?",\n      [id]\n    );\n\n  } catch (e) {\n\n    console.error(\n      "【任务成功更新失败】\\n" +\n      "数据库路径: " + config.DB_PATH + "\\n" +\n      "表名: " + config.TABLE_TASK + "\\n" +\n      "任务ID: " + id + "\\n" +\n      "错误信息: " + e\n    );\n\n  } finally {\n    db.close();\n  }\n}\n\n/**\n * =============== 任务失败 ===============\n * taskStatus = 4\n */\nfunction taskFail(config, id) {\n\n  var db = sqlite.open(config.DB_PATH);\n\n  try {\n\n    db.update(\n      config.TABLE_TASK,\n      { taskStatus: 4 },\n      "id = ?",\n      [id]\n    );\n\n  } catch (e) {\n\n    console.error(\n      "【任务失败状态更新失败】\\n" +\n      "数据库路径: " + config.DB_PATH + "\\n" +\n      "表名: " + config.TABLE_TASK + "\\n" +\n      "任务ID: " + id + "\\n" +\n      "错误信息: " + e\n    );\n\n  } finally {\n    db.close();\n  }\n}\n                ',
      scriptName: "脚本名称-未读消息",
      scriptPath: "",
      scriptConfig:
        '{\n    "delay": 1000,\n    "loopTimes": 1,\n    "interval": 200,\n    "path": [\n        "/sdcard/cloud/script"\n    ]\n}',
      targetTable: "设备消息",
    });

    // 👉 未读消息模拟数据(业务示例,更易理解)
    db.insert("设备消息", {
      标题: "新任务待执行",
      内容: "任务已下发,等待设备执行",
      今日收益: 0.0,
      错误信息: "",
      创建时间: "2026/04/10 16:10:56",
      isRead: 0,
      taskStatus: 0,
      taskUpdateTime: 0,
    });

    db.insert("设备消息", {
      标题: "任务执行失败",
      内容: "请求接口超时,任务中断",
      今日收益: 0.0,
      错误信息: "网络请求超时:5000ms",
      创建时间: "2026/04/10 16:11:22",
      isRead: 0,
      taskStatus: 4,
      taskUpdateTime: 1712736682000,
    });

    db.insert("设备消息", {
      标题: "今日收益结算",
      内容: "本次任务收益 23.50 元",
      今日收益: 23.5,
      错误信息: "",
      创建时间: formatDateTime(),
      isRead: 1,
      taskStatus: 3,
      taskUpdateTime: new Date().getTime(),
    });

    db.insert("设备消息", {
      标题: "任务执行完成",
      内容: "自动化任务已正常执行完毕",
      今日收益: 0.0,
      错误信息: "",
      创建时间: formatDateTime(),
      isRead: 1,
      taskStatus: 3,
      taskUpdateTime: new Date().getTime(),
    });

    db.insert("设备消息", {
      标题: "设备运行异常",
      内容: "应用意外退出,任务未完成",
      今日收益: 0.0,
      错误信息: "进程异常终止:CODE:9",
      创建时间: formatDateTime(),
      isRead: 1,
      taskStatus: 4,
      taskUpdateTime: new Date().getTime(),
    });

    console.log("✅ 未读消息数据库创建完成:", dbPath);
  } catch (e) {
    console.error("❌ 数据库创建失败:", e);
  } finally {
    db.close();
  }
}

/**
 * 格式化时间为:yyyy/MM/dd HH:mm:ss
 *
 * @param {number} [timestamp] 可选时间戳(毫秒)
 * @returns {string} 格式化后的时间字符串
 */
function formatDateTime(timestamp) {
  var date = timestamp ? new Date(timestamp) : new Date();
  return (
    date.getFullYear() +
    "/" +
    (date.getMonth() + 1) +
    "/" +
    date.getDate() +
    " " +
    date.getHours() +
    ":" +
    date.getMinutes() +
    ":" +
    date.getSeconds()
  );
}

消息更新与同步

通过脚本向手机本地的 SQLite 数据库文件里添加数据并通知云控后台更新未读消息

添加消息

通过 Bot.js Pro sqlite 模块

在脚本中向 SQLite 数据库文件里添加消息:

// 👉 示例
insertMessage(
    "/sdcard/cloud/SQLite/未读消息_副本.db",
    "设备消息",
    {
        标题: "新任务",
        内容: "执行中...",
        今日收益: 0,
        错误信息: "",
        创建时间: formatDateTime(),
        isRead: 0,
        taskStatus: 1,
        taskUpdateTime: new Date().getTime()
    }
);

/**
 * 向指定表添加一条消息数据
 *
 * 📌 功能说明:
 * 向 SQLite 数据库中的指定表插入一条记录(消息对象)。
 *
 * 📌 使用场景:
 * - 添加未读消息
 * - 记录任务执行结果
 * - 动态写入业务数据
 *
 * 📌 特点:
 * - 自动打开 / 关闭数据库
 * - 支持任意表(不局限“设备消息”)
 * - 传入对象即插入(键值对应字段)
 *
 * ⚠️ 注意:
 * - message 对象的字段必须和表结构一致
 * - 如果包含主键 id,请确保不冲突
 *
 * @param {string} dbPath 数据库路径
 * @param {string} tableName 表名(如:设备消息)
 * @param {Object} message 要插入的数据(对象形式)
 *
 * @returns {number} 插入成功返回 rowId,失败返回 -1
 */
function insertMessage(dbPath, tableName, message) {

    var db = sqlite.open(dbPath);

    try {

        // 👉 插入数据
        var rowId = db.insert(tableName, message);

        if (rowId === -1) {
            console.error("❌ 插入失败:", message);
        } else {
            console.log("✅ 插入成功,rowId:", rowId);
        }

        return rowId;

    } catch (e) {

        console.error("❌ 插入异常:", e);
        return -1;

    } finally {
        db.close();
    }
}
/**
 * 格式化时间为:yyyy/MM/dd HH:mm:ss
 *
 * @param {number} [timestamp] 可选时间戳(毫秒)
 * @returns {string} 格式化后的时间字符串
 */
function formatDateTime(timestamp) {
    var date = timestamp ? new Date(timestamp) : new Date();
    return date.getFullYear() + "/" +
        (date.getMonth() + 1) + "/" +
        date.getDate() + " " +
        date.getHours() + ":" +
        date.getMinutes() + ":" +
        date.getSeconds();
}

未读消息同步接口

$cloud.notifyUnreadMessages() 用于在脚本向本地 SQLite 写入未读消息数据后,通知云控后台刷新数据,实现投屏界面未读消息的实时同步展示。

该接口仅用于触发同步机制,不参与具体业务数据处理。

$cloud.notifyUnreadMessages();

显示数量

全局配置 → 投屏配置 → 未读任务消息显示 中设置

  • 建议:1~2 条
  • 说明:数量过多会影响悬浮日志显示效果