找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 40|回复: 0

豆包生成文章ADB 方式采集问答内容对接 MySQL 入库

[复制链接]
发表于 10 小时前 | 显示全部楼层 |阅读模式
豆包生成文章ADB 方式采集问答内容对接 MySQL 入库











  1. /**
  2. * 案例 12:豆包生成文章 + ADB 方式采集问答内容 + 对接 MySQL 入库
  3. *
  4. * 只教一件事:怎么用 ADB(uiautomator dump)把豆包对话页的问答文本整页抓下来,
  5. * 攒成列表,再通过 SDK 的 mysql 模块直连数据库逐条入库。
  6. *
  7. * 流程(每个主题一轮):
  8. *   打开豆包 → 输入框输入"写一篇 XX 的文章"并发送 → 轮询 dump 等生成稳定
  9. *   → 对比发送前后的页面文本,新出现的长文本就是 AI 回答
  10. *   → push 进内存列表 + INSERT 进 MySQL
  11. *
  12. * 采集原理:
  13. *   adb.exec("uiautomator dump /sdcard/qa_dump.xml") 把当前界面控件树导出成 XML,
  14. *   再 adb.exec("cat ...") 读回来,正则抠所有 text="..." —— 一次拿到整页文字,
  15. *   不用逐个控件找。若无障碍服务和 uiautomator 冲突导致 dump 失败,
  16. *   自动降级用 auto.getXml()(AIWROK 自己的控件树,同样能抠 text)。
  17. *
  18. * 配置说明:
  19. *   豆包安卓包名:com.larus.nova(应用宝确认)
  20. *   DB_MODE="sqlite"(默认):设备本地生成 /sdcard/qa_articles.db,零配置直接能跑;
  21. *   DB_MODE="mysql":DB_URL 要填"模拟器能访问到"的 MySQL 地址(127.0.0.1 是模拟器自己,
  22. *   连不上宿主机),且 URL 必须带 connectTimeout/socketTimeout,否则连不上会无限卡死。
  23. *   数据库不可用时不中止,降级为只采集进内存列表。
  24. *
  25. * 运行:雷电模拟器 + 豆包已登录 + 无障碍开启;不用 HID、不用截图
  26. * (本脚本无截图对象,若日后加了 screenShot,记得最后 .recycle() 释放)。
  27. */

  28. // ==================== 配置(改这里)====================
  29. var DB_MODE = "sqlite";              // "sqlite"=设备本地库文件(零配置,默认) | "mysql"=远程 MySQL
  30. var PKG = "com.larus.nova";          // 豆包
  31. var DB_URL = "jdbc:mysql://192.168.1.100:3306/aiwork?useSSL=false&characterEncoding=utf8&connectTimeout=5000&socketTimeout=10000";  // 改成你的 MySQL(必须带超时,否则连不上会无限卡死)
  32. var DB_USER = "root";
  33. var DB_PASS = "请填写数据库密码";
  34. var SQLITE_PATH = "/sdcard/qa_articles.db";  // SQLite 模式:本地库文件路径
  35. var DB_TABLE = "qa_articles";
  36. var TOPICS = ["春天的景色", "人工智能改变生活", "健康饮食习惯"];  // 要生成文章的题目列表
  37. var POLL_MS = 3000;          // 生成期间 dump 轮询间隔
  38. var MIN_GEN_MS = 35000;      // 最短等待:豆包生成一篇 800 字文章实测要 35 秒左右,之前 12 秒就误判"稳定"
  39. var MAX_WAIT_MS = 90000;     // 单篇最长等待生成时间
  40. var STEP_WAIT = 3000;        // 每篇入库后的缓冲

  41. // ==================== 全局变量放顶部 ====================
  42. importClass(android.database.sqlite.SQLiteDatabase);
  43. importClass(java.io.File);
  44. var gStop = false;
  45. var gConn = null;
  46. var gDumpWarned = false;       // adb dump 不可用只提示一次
  47. var gList = [];              // 采集到的问答列表 [{topic, question, answer, time}]

  48. // ==================== 工具 ====================
  49. function doSleep(ms) { sleep.millisecond(ms); }
  50. function isStop() { return gStop; }

  51. event.onStopBtnEvent(function () {
  52.     gStop = true;
  53.     printl("[停止] 收到停止请求,收尾后退出");
  54. });

  55. // ==================== 数据库:sqlite(本地文件) / mysql(JDBC) 双模式 ====================
  56. function dbConnect() {
  57.     if (DB_MODE == "mysql") {
  58.         printl("[DB] 正在连接 MySQL(5 秒超时)...");
  59.         try {
  60.             gConn = mysql.getConnection(DB_URL, DB_USER, DB_PASS);
  61.             printl("[DB] ✅ MySQL 连接成功");
  62.             return true;
  63.         } catch (e) {
  64.             printl("[DB] ❌ MySQL 连接失败: " + e);
  65.             return false;
  66.         }
  67.     }
  68.     // sqlite:设备本地库文件,零配置
  69.     try {
  70.         gConn = SQLiteDatabase.openOrCreateDatabase(new File(SQLITE_PATH), null);
  71.         printl("[DB] ✅ SQLite 已打开: " + SQLITE_PATH);
  72.         return true;
  73.     } catch (e2) {
  74.         printl("[DB] ❌ SQLite 打开失败: " + e2);
  75.         return false;
  76.     }
  77. }

  78. function dbEnsureTable() {
  79.     var sql;
  80.     if (DB_MODE == "mysql") {
  81.         sql = "CREATE TABLE IF NOT EXISTS " + DB_TABLE + " (" +
  82.             "id INT AUTO_INCREMENT PRIMARY KEY," +
  83.             "topic VARCHAR(255)," +
  84.             "question TEXT," +
  85.             "answer LONGTEXT," +
  86.             "collected_at VARCHAR(32)" +
  87.             ") DEFAULT CHARSET=utf8mb4";
  88.     } else {
  89.         sql = "CREATE TABLE IF NOT EXISTS " + DB_TABLE + " (" +
  90.             "id INTEGER PRIMARY KEY AUTOINCREMENT," +
  91.             "topic TEXT, question TEXT, answer TEXT, collected_at TEXT)";
  92.     }
  93.     try {
  94.         gConn.execSQL(sql);
  95.         printl("[DB] 表 " + DB_TABLE + " 就绪");
  96.         return true;
  97.     } catch (e) {
  98.         printl("[DB] ❌ 建表失败: " + e);
  99.         return false;
  100.     }
  101. }

  102. function dbInsert(item) {
  103.     try {
  104.         var sql = "INSERT INTO " + DB_TABLE +
  105.             " (topic, question, answer, collected_at) VALUES (?,?,?,?)";
  106.         if (DB_MODE == "mysql") {
  107.             var stmt = gConn.prepareStatement(sql);
  108.             stmt.setString(1, item.topic);
  109.             stmt.setString(2, item.question);
  110.             stmt.setString(3, item.answer);
  111.             stmt.setString(4, item.time);
  112.             var rows = stmt.executeUpdate();
  113.             stmt.close();
  114.             printl("[DB] ✅ 入库 " + rows + " 条:《" + item.topic + "》");
  115.         } else {
  116.             var stmt2 = gConn.compileStatement(sql);
  117.             stmt2.bindString(1, item.topic);
  118.             stmt2.bindString(2, item.question);
  119.             stmt2.bindString(3, item.answer);
  120.             stmt2.bindString(4, item.time);
  121.             stmt2.executeInsert();
  122.             stmt2.close();
  123.             printl("[DB] ✅ 入库成功:《" + item.topic + "》");
  124.         }
  125.         return true;
  126.     } catch (e) {
  127.         printl("[DB] ❌ 入库失败: " + e);
  128.         return false;
  129.     }
  130. }

  131. function dbClose() {
  132.     try {
  133.         if (gConn != null) { gConn.close(); gConn = null; printl("[DB] 已断开连接"); }
  134.     } catch (e) {}
  135. }

  136. // ==================== ADB 采集:整页 dump → 抠文本 ====================
  137. // 优先 adb uiautomator dump;失败降级 auto.getXml()
  138. function dumpPageXml() {
  139.     var xml = "";
  140.     try {
  141.         adb.exec("uiautomator dump /sdcard/qa_dump.xml", false);
  142.         xml = adb.exec("cat /sdcard/qa_dump.xml", false);
  143.     } catch (e) {}
  144.     if (xml == null || xml.indexOf("<hierarchy") < 0) {
  145.         if (!gDumpWarned) { gDumpWarned = true; printl("[采集] adb dump 不可用(与无障碍服务冲突),后续统一用 auto.getXml()"); }
  146.         try { xml = auto.getXml(); } catch (e2) { xml = ""; }
  147.     }
  148.     return xml == null ? "" : xml;
  149. }

  150. // 抠 XML 里所有 text="..." 的非空文本(顺带还原常见转义符)
  151. function extractTexts(xml) {
  152.     var out = [];
  153.     var re = /text="([^"]*)"/g;
  154.     var m;
  155.     while ((m = re.exec(xml)) != null) {
  156.         var t = m[1];
  157.         if (t != null && t.length > 1) {
  158.             t = t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
  159.                  .replace(/"/g, '"').replace(/
  160. /g, "\n");
  161.             out.push(t);
  162.         }
  163.     }
  164.     return out;
  165. }

  166. // 页面文本指纹:用来判断生成有没有"写完"(文本不再变 = 完成)
  167. function pageSignature(texts) {
  168.     return texts.join("#");
  169. }

  170. // 发送前后的文本做差:新出现的长文本就是 AI 的回答
  171. function pickAnswer(baseTexts, afterTexts) {
  172.     var best = "";
  173.     for (var i = 0; i < afterTexts.length; i++) {
  174.         var t = afterTexts[i];
  175.         if (t.length < 30) continue;          // 回答正文至少 30 字,短的是按钮/标题
  176.         var isNew = true;
  177.         for (var j = 0; j < baseTexts.length; j++) {
  178.             if (baseTexts[j] == t) { isNew = false; break; }
  179.         }
  180.         if (isNew && t.length > best.length) best = t;
  181.     }
  182.     return best;
  183. }

  184. // ==================== 豆包操作:输入 → 发送 → 等生成稳定 ====================
  185. function askDoubao(promptText) {
  186.     // 1) 找输入框:先标准 EditText,再 editable 兜底
  187.     var nodeObj = null;
  188.     try {
  189.         var ed = auto.nodeSearch(3000).className("android.widget.EditText");
  190.         if (ed != '' && ed.count() > 0) nodeObj = ed.getNode(0);
  191.     } catch (e) {}
  192.     if (nodeObj == null) {
  193.         try {
  194.             var ed2 = auto.nodeSearch(2000).editable(true);
  195.             if (ed2 != '' && ed2.count() > 0) nodeObj = ed2.getNode(0);
  196.         } catch (e2) {}
  197.     }
  198.     if (nodeObj == null) { printl("[输入] &#10060; 没找到输入框"); return false; }
  199.     nodeObj.click();
  200.     sleep.second(1);
  201.     nodeObj.input(promptText);   // 中文只能走 node.input,adb input text 不支持中文
  202.     doSleep(800);

  203.     // 2) 发送:文本"发送" → desc"发送" → 回车,三路兜底
  204.     var sent = false;
  205.     try {
  206.         var sb = auto.nodeSearch(1500).text("发送");
  207.         if (sb != '' && sb.count() > 0) { sb.getNode(0).click(); sent = true; }
  208.     } catch (e) {}
  209.     if (!sent) {
  210.         try {
  211.             var sb2 = auto.nodeSearch(1500).desc("发送");
  212.             if (sb2 != '' && sb2.count() > 0) { sb2.getNode(0).click(); sent = true; }
  213.         } catch (e2) {}
  214.     }
  215.     if (!sent) {
  216.         try { input.enter(); sent = true; printl("[发送] 没找到发送按钮,用回车发送"); } catch (e3) {}
  217.     }
  218.     printl("[发送] " + (sent ? "&#9989; 已发送" : "&#10060; 发送失败"));
  219.     return sent;
  220. }

  221. // 轮询 dump:先等满 MIN_GEN_MS,之后页面文本连续两次不变 → 判定生成完成
  222. function waitForAnswer(baseSig) {
  223.     var waited = 0;
  224.     var lastSig = "";
  225.     var stable = 0;
  226.     while (waited < MAX_WAIT_MS && !isStop()) {
  227.         sleep.second(POLL_MS / 1000);
  228.         waited += POLL_MS;
  229.         var sig = pageSignature(extractTexts(dumpPageXml()));
  230.         if (waited < MIN_GEN_MS) {
  231.             // 35 秒内豆包大概率还在生成,不做稳定判断,避免刚发出去就误判完成
  232.             if (waited % 15000 == 0) printl("[生成] 最短等待中...已 " + (waited / 1000) + " 秒");
  233.             lastSig = sig;
  234.             continue;
  235.         }
  236.         if (sig != baseSig && sig.length > baseSig.length && sig == lastSig) {
  237.             stable++;
  238.             if (stable >= 2) {
  239.                 printl("[生成] 文本已稳定,判定生成完成(用时 " + (waited / 1000) + " 秒)");
  240.                 return true;
  241.             }
  242.         } else {
  243.             stable = 0;
  244.         }
  245.         lastSig = sig;
  246.         if (waited % 15000 == 0) printl("[生成] 已等待 " + (waited / 1000) + " 秒...");
  247.     }
  248.     printl("[生成] &#9200; 超时,按当前页面内容采集");
  249.     return true;   // 超时也照采,有多少算多少
  250. }

  251. // ==================== 主流程 ====================
  252. function main() {
  253.     printl("╔════ 案例12:豆包文章生成 + ADB采集入库 ════╗");
  254.     printl("[配置] 主题 " + TOPICS.length + " 个 | 表 " + DB_TABLE);

  255.     // 数据库连不上不中止:降级为只采集进内存列表
  256.     var dbOk = dbConnect();
  257.     if (dbOk) dbOk = dbEnsureTable();
  258.     if (!dbOk) printl("[DB] &#9888;&#65039; 数据库不可用,本次只采集进列表,不入库");
  259.     printl("[DB] 数据存放位置: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));

  260.     // 重启豆包,保证从干净对话页开始
  261.     app.closeApp(PKG);
  262.     doSleep(1500);
  263.     app.openApp(PKG);
  264.     printl("[启动] 等 15 秒让豆包加载完");
  265.     sleep.second(15);

  266.     for (var i = 0; i < TOPICS.length; i++) {
  267.         if (isStop()) break;
  268.         var topic = TOPICS[i];
  269.         var promptText = "请写一篇关于《" + topic + "》的文章,800字左右";
  270.         printl("──── 第 " + (i + 1) + "/" + TOPICS.length + " 篇:" + topic + " ────");

  271.         // 发送前先记基线,回答 = 之后新出现的长文本
  272.         var baseTexts = extractTexts(dumpPageXml());
  273.         var baseSig = pageSignature(baseTexts);

  274.         if (!askDoubao(promptText)) continue;
  275.         waitForAnswer(baseSig);
  276.         if (isStop()) break;

  277.         var afterTexts = extractTexts(dumpPageXml());
  278.         var answer = pickAnswer(baseTexts, afterTexts);
  279.         if (answer == "") {
  280.             printl("[采集] &#10060; 没抓到正文(可能生成失败/被折叠),跳过");
  281.             continue;
  282.         }

  283.         var item = { topic: topic, question: promptText, answer: answer, time: time.now() };
  284.         gList.push(item);                       // 内存列表
  285.         if (dbOk) dbInsert(item);               // 数据库
  286.         printl("[采集] 正文 " + answer.length + " 字 | 已存: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));
  287.         printl("──── 《" + topic + "》全文开始 ────");
  288.         printl(answer);
  289.         printl("──── 《" + topic + "》全文结束 ────");
  290.         sleep.second(STEP_WAIT / 1000);
  291.     }

  292.     // 列表汇总
  293.     printl("╔════ 采集列表(共 " + gList.length + " 条)════╗");
  294.     for (var k = 0; k < gList.length; k++) {
  295.         printl("[" + (k + 1) + "] 《" + gList[k].topic + "》 | " + gList[k].time +
  296.             " | " + gList[k].answer.length + " 字");
  297.     }
  298.     printl("[DB] 全部数据存放位置: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));
  299.     if (dbOk) dbClose();
  300.     printl("[结束] 豆包采集流程执行完毕");
  301. }

  302. main();
复制代码



QQ截图20261015140044.png
QQ截图20261015140222.png
QQ截图20261015140152.png
QQ截图20261015140044.png
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|

GMT+8, 2026-10-16 17:05

宣传软件 · 信息发布软件 · b2b软件 · 广告发布软件

快速回复 返回顶部 返回列表