 | |  |  | 豆包生成文章ADB 方式采集问答内容对接 MySQL 入库
- /**
- * 案例 12:豆包生成文章 + ADB 方式采集问答内容 + 对接 MySQL 入库
- *
- * 只教一件事:怎么用 ADB(uiautomator dump)把豆包对话页的问答文本整页抓下来,
- * 攒成列表,再通过 SDK 的 mysql 模块直连数据库逐条入库。
- *
- * 流程(每个主题一轮):
- * 打开豆包 → 输入框输入"写一篇 XX 的文章"并发送 → 轮询 dump 等生成稳定
- * → 对比发送前后的页面文本,新出现的长文本就是 AI 回答
- * → push 进内存列表 + INSERT 进 MySQL
- *
- * 采集原理:
- * adb.exec("uiautomator dump /sdcard/qa_dump.xml") 把当前界面控件树导出成 XML,
- * 再 adb.exec("cat ...") 读回来,正则抠所有 text="..." —— 一次拿到整页文字,
- * 不用逐个控件找。若无障碍服务和 uiautomator 冲突导致 dump 失败,
- * 自动降级用 auto.getXml()(AIWROK 自己的控件树,同样能抠 text)。
- *
- * 配置说明:
- * 豆包安卓包名:com.larus.nova(应用宝确认)
- * DB_MODE="sqlite"(默认):设备本地生成 /sdcard/qa_articles.db,零配置直接能跑;
- * DB_MODE="mysql":DB_URL 要填"模拟器能访问到"的 MySQL 地址(127.0.0.1 是模拟器自己,
- * 连不上宿主机),且 URL 必须带 connectTimeout/socketTimeout,否则连不上会无限卡死。
- * 数据库不可用时不中止,降级为只采集进内存列表。
- *
- * 运行:雷电模拟器 + 豆包已登录 + 无障碍开启;不用 HID、不用截图
- * (本脚本无截图对象,若日后加了 screenShot,记得最后 .recycle() 释放)。
- */
- // ==================== 配置(改这里)====================
- var DB_MODE = "sqlite"; // "sqlite"=设备本地库文件(零配置,默认) | "mysql"=远程 MySQL
- var PKG = "com.larus.nova"; // 豆包
- var DB_URL = "jdbc:mysql://192.168.1.100:3306/aiwork?useSSL=false&characterEncoding=utf8&connectTimeout=5000&socketTimeout=10000"; // 改成你的 MySQL(必须带超时,否则连不上会无限卡死)
- var DB_USER = "root";
- var DB_PASS = "请填写数据库密码";
- var SQLITE_PATH = "/sdcard/qa_articles.db"; // SQLite 模式:本地库文件路径
- var DB_TABLE = "qa_articles";
- var TOPICS = ["春天的景色", "人工智能改变生活", "健康饮食习惯"]; // 要生成文章的题目列表
- var POLL_MS = 3000; // 生成期间 dump 轮询间隔
- var MIN_GEN_MS = 35000; // 最短等待:豆包生成一篇 800 字文章实测要 35 秒左右,之前 12 秒就误判"稳定"
- var MAX_WAIT_MS = 90000; // 单篇最长等待生成时间
- var STEP_WAIT = 3000; // 每篇入库后的缓冲
- // ==================== 全局变量放顶部 ====================
- importClass(android.database.sqlite.SQLiteDatabase);
- importClass(java.io.File);
- var gStop = false;
- var gConn = null;
- var gDumpWarned = false; // adb dump 不可用只提示一次
- var gList = []; // 采集到的问答列表 [{topic, question, answer, time}]
- // ==================== 工具 ====================
- function doSleep(ms) { sleep.millisecond(ms); }
- function isStop() { return gStop; }
- event.onStopBtnEvent(function () {
- gStop = true;
- printl("[停止] 收到停止请求,收尾后退出");
- });
- // ==================== 数据库:sqlite(本地文件) / mysql(JDBC) 双模式 ====================
- function dbConnect() {
- if (DB_MODE == "mysql") {
- printl("[DB] 正在连接 MySQL(5 秒超时)...");
- try {
- gConn = mysql.getConnection(DB_URL, DB_USER, DB_PASS);
- printl("[DB] ✅ MySQL 连接成功");
- return true;
- } catch (e) {
- printl("[DB] ❌ MySQL 连接失败: " + e);
- return false;
- }
- }
- // sqlite:设备本地库文件,零配置
- try {
- gConn = SQLiteDatabase.openOrCreateDatabase(new File(SQLITE_PATH), null);
- printl("[DB] ✅ SQLite 已打开: " + SQLITE_PATH);
- return true;
- } catch (e2) {
- printl("[DB] ❌ SQLite 打开失败: " + e2);
- return false;
- }
- }
- function dbEnsureTable() {
- var sql;
- if (DB_MODE == "mysql") {
- sql = "CREATE TABLE IF NOT EXISTS " + DB_TABLE + " (" +
- "id INT AUTO_INCREMENT PRIMARY KEY," +
- "topic VARCHAR(255)," +
- "question TEXT," +
- "answer LONGTEXT," +
- "collected_at VARCHAR(32)" +
- ") DEFAULT CHARSET=utf8mb4";
- } else {
- sql = "CREATE TABLE IF NOT EXISTS " + DB_TABLE + " (" +
- "id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "topic TEXT, question TEXT, answer TEXT, collected_at TEXT)";
- }
- try {
- gConn.execSQL(sql);
- printl("[DB] 表 " + DB_TABLE + " 就绪");
- return true;
- } catch (e) {
- printl("[DB] ❌ 建表失败: " + e);
- return false;
- }
- }
- function dbInsert(item) {
- try {
- var sql = "INSERT INTO " + DB_TABLE +
- " (topic, question, answer, collected_at) VALUES (?,?,?,?)";
- if (DB_MODE == "mysql") {
- var stmt = gConn.prepareStatement(sql);
- stmt.setString(1, item.topic);
- stmt.setString(2, item.question);
- stmt.setString(3, item.answer);
- stmt.setString(4, item.time);
- var rows = stmt.executeUpdate();
- stmt.close();
- printl("[DB] ✅ 入库 " + rows + " 条:《" + item.topic + "》");
- } else {
- var stmt2 = gConn.compileStatement(sql);
- stmt2.bindString(1, item.topic);
- stmt2.bindString(2, item.question);
- stmt2.bindString(3, item.answer);
- stmt2.bindString(4, item.time);
- stmt2.executeInsert();
- stmt2.close();
- printl("[DB] ✅ 入库成功:《" + item.topic + "》");
- }
- return true;
- } catch (e) {
- printl("[DB] ❌ 入库失败: " + e);
- return false;
- }
- }
- function dbClose() {
- try {
- if (gConn != null) { gConn.close(); gConn = null; printl("[DB] 已断开连接"); }
- } catch (e) {}
- }
- // ==================== ADB 采集:整页 dump → 抠文本 ====================
- // 优先 adb uiautomator dump;失败降级 auto.getXml()
- function dumpPageXml() {
- var xml = "";
- try {
- adb.exec("uiautomator dump /sdcard/qa_dump.xml", false);
- xml = adb.exec("cat /sdcard/qa_dump.xml", false);
- } catch (e) {}
- if (xml == null || xml.indexOf("<hierarchy") < 0) {
- if (!gDumpWarned) { gDumpWarned = true; printl("[采集] adb dump 不可用(与无障碍服务冲突),后续统一用 auto.getXml()"); }
- try { xml = auto.getXml(); } catch (e2) { xml = ""; }
- }
- return xml == null ? "" : xml;
- }
- // 抠 XML 里所有 text="..." 的非空文本(顺带还原常见转义符)
- function extractTexts(xml) {
- var out = [];
- var re = /text="([^"]*)"/g;
- var m;
- while ((m = re.exec(xml)) != null) {
- var t = m[1];
- if (t != null && t.length > 1) {
- t = t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
- .replace(/"/g, '"').replace(/
- /g, "\n");
- out.push(t);
- }
- }
- return out;
- }
- // 页面文本指纹:用来判断生成有没有"写完"(文本不再变 = 完成)
- function pageSignature(texts) {
- return texts.join("#");
- }
- // 发送前后的文本做差:新出现的长文本就是 AI 的回答
- function pickAnswer(baseTexts, afterTexts) {
- var best = "";
- for (var i = 0; i < afterTexts.length; i++) {
- var t = afterTexts[i];
- if (t.length < 30) continue; // 回答正文至少 30 字,短的是按钮/标题
- var isNew = true;
- for (var j = 0; j < baseTexts.length; j++) {
- if (baseTexts[j] == t) { isNew = false; break; }
- }
- if (isNew && t.length > best.length) best = t;
- }
- return best;
- }
- // ==================== 豆包操作:输入 → 发送 → 等生成稳定 ====================
- function askDoubao(promptText) {
- // 1) 找输入框:先标准 EditText,再 editable 兜底
- var nodeObj = null;
- try {
- var ed = auto.nodeSearch(3000).className("android.widget.EditText");
- if (ed != '' && ed.count() > 0) nodeObj = ed.getNode(0);
- } catch (e) {}
- if (nodeObj == null) {
- try {
- var ed2 = auto.nodeSearch(2000).editable(true);
- if (ed2 != '' && ed2.count() > 0) nodeObj = ed2.getNode(0);
- } catch (e2) {}
- }
- if (nodeObj == null) { printl("[输入] ❌ 没找到输入框"); return false; }
- nodeObj.click();
- sleep.second(1);
- nodeObj.input(promptText); // 中文只能走 node.input,adb input text 不支持中文
- doSleep(800);
- // 2) 发送:文本"发送" → desc"发送" → 回车,三路兜底
- var sent = false;
- try {
- var sb = auto.nodeSearch(1500).text("发送");
- if (sb != '' && sb.count() > 0) { sb.getNode(0).click(); sent = true; }
- } catch (e) {}
- if (!sent) {
- try {
- var sb2 = auto.nodeSearch(1500).desc("发送");
- if (sb2 != '' && sb2.count() > 0) { sb2.getNode(0).click(); sent = true; }
- } catch (e2) {}
- }
- if (!sent) {
- try { input.enter(); sent = true; printl("[发送] 没找到发送按钮,用回车发送"); } catch (e3) {}
- }
- printl("[发送] " + (sent ? "✅ 已发送" : "❌ 发送失败"));
- return sent;
- }
- // 轮询 dump:先等满 MIN_GEN_MS,之后页面文本连续两次不变 → 判定生成完成
- function waitForAnswer(baseSig) {
- var waited = 0;
- var lastSig = "";
- var stable = 0;
- while (waited < MAX_WAIT_MS && !isStop()) {
- sleep.second(POLL_MS / 1000);
- waited += POLL_MS;
- var sig = pageSignature(extractTexts(dumpPageXml()));
- if (waited < MIN_GEN_MS) {
- // 35 秒内豆包大概率还在生成,不做稳定判断,避免刚发出去就误判完成
- if (waited % 15000 == 0) printl("[生成] 最短等待中...已 " + (waited / 1000) + " 秒");
- lastSig = sig;
- continue;
- }
- if (sig != baseSig && sig.length > baseSig.length && sig == lastSig) {
- stable++;
- if (stable >= 2) {
- printl("[生成] 文本已稳定,判定生成完成(用时 " + (waited / 1000) + " 秒)");
- return true;
- }
- } else {
- stable = 0;
- }
- lastSig = sig;
- if (waited % 15000 == 0) printl("[生成] 已等待 " + (waited / 1000) + " 秒...");
- }
- printl("[生成] ⏰ 超时,按当前页面内容采集");
- return true; // 超时也照采,有多少算多少
- }
- // ==================== 主流程 ====================
- function main() {
- printl("╔════ 案例12:豆包文章生成 + ADB采集入库 ════╗");
- printl("[配置] 主题 " + TOPICS.length + " 个 | 表 " + DB_TABLE);
- // 数据库连不上不中止:降级为只采集进内存列表
- var dbOk = dbConnect();
- if (dbOk) dbOk = dbEnsureTable();
- if (!dbOk) printl("[DB] ⚠️ 数据库不可用,本次只采集进列表,不入库");
- printl("[DB] 数据存放位置: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));
- // 重启豆包,保证从干净对话页开始
- app.closeApp(PKG);
- doSleep(1500);
- app.openApp(PKG);
- printl("[启动] 等 15 秒让豆包加载完");
- sleep.second(15);
- for (var i = 0; i < TOPICS.length; i++) {
- if (isStop()) break;
- var topic = TOPICS[i];
- var promptText = "请写一篇关于《" + topic + "》的文章,800字左右";
- printl("──── 第 " + (i + 1) + "/" + TOPICS.length + " 篇:" + topic + " ────");
- // 发送前先记基线,回答 = 之后新出现的长文本
- var baseTexts = extractTexts(dumpPageXml());
- var baseSig = pageSignature(baseTexts);
- if (!askDoubao(promptText)) continue;
- waitForAnswer(baseSig);
- if (isStop()) break;
- var afterTexts = extractTexts(dumpPageXml());
- var answer = pickAnswer(baseTexts, afterTexts);
- if (answer == "") {
- printl("[采集] ❌ 没抓到正文(可能生成失败/被折叠),跳过");
- continue;
- }
- var item = { topic: topic, question: promptText, answer: answer, time: time.now() };
- gList.push(item); // 内存列表
- if (dbOk) dbInsert(item); // 数据库
- printl("[采集] 正文 " + answer.length + " 字 | 已存: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));
- printl("──── 《" + topic + "》全文开始 ────");
- printl(answer);
- printl("──── 《" + topic + "》全文结束 ────");
- sleep.second(STEP_WAIT / 1000);
- }
- // 列表汇总
- printl("╔════ 采集列表(共 " + gList.length + " 条)════╗");
- for (var k = 0; k < gList.length; k++) {
- printl("[" + (k + 1) + "] 《" + gList[k].topic + "》 | " + gList[k].time +
- " | " + gList[k].answer.length + " 字");
- }
- printl("[DB] 全部数据存放位置: " + (DB_MODE == "mysql" ? DB_URL : SQLITE_PATH));
- if (dbOk) dbClose();
- printl("[结束] 豆包采集流程执行完毕");
- }
- main();
复制代码
| |  | |  |
|