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

找图方式快手发视频全流程

[复制链接]
发表于 前天 07:43 | 显示全部楼层 |阅读模式
找图方式快手发视频全流程












  1. /**
  2. * 案例 10:快手发视频全流程(固定 .cv 五连:加号→录制→下一步→填标题→发布)
  3. *
  4. * 只教一件事:一条发视频流水线怎么用图色工程串起来——
  5. * 每步都是:findImagesEx 找 .cv → 中心坐标点击 → 等加载 → 下一步。
  6. * 录制是"点一下开始 → 等 20 秒 → 再点一下停止",不是长按。
  7. *
  8. * 用到的 .cv(放进项目 资源 文件夹):
  9. *   点击加号.cv          —— 底部中间 "+" 发布入口
  10. *   按住录制.cv          —— 录制按钮(点击开始 → 录 20 秒 → 点击停止)
  11. *   录完下一步.cv        —— 录制完成后的"下一步"
  12. *   点击OCR文字粘帖内容.cv —— 标题输入区(OCREx 定位后点击输入)
  13. *   点击发布按钮.cv      —— 发布
  14. *
  15. * 运行:真机/模拟器 + 无障碍,不用 HID;LOOP_COUNT 控制循环发几次。
  16. */

  17. // ==================== 配置(改这里)====================
  18. var PKG = "com.smile.gifmaker";   // 快手
  19. var VIDEO_TITLE = "AIWROK 自动发布测试";   // 每次发布的标题
  20. var RECORD_MS = 20000;       // 录制时长:点击开始后录 20 秒再点停止
  21. var LOOP_COUNT = 2;          // 循环发几次
  22. var STEP_WAIT = 3000;        // 每步点完等页面加载(ms)

  23. // ==================== 全局变量放顶部 ====================
  24. var gStep = 0;

  25. // ==================== 工具 ====================
  26. function doSleep(ms) { sleep.millisecond(ms); }
  27. function stepTitle(name) {
  28.     gStep++;
  29.     printl("========== " + gStep + ") " + name + " ==========");
  30. }

  31. // ==================== 找图 + 点击(核心套路,全片复用)====================
  32. // 点击用 detect.click() —— 实测这个能真正点中(auto.clickPoint 点不中)
  33. function findAndClick(cvFile, waitMs) {
  34.     var detects = opencv.findImagesEx(cvFile);
  35.     if (detects != null && detects.length > 0) {
  36.         printl("[找图] " + cvFile + " 命中 " + detects.length + " 个");
  37.         printl(detects);
  38.         detects[0].click();
  39.         doSleep(waitMs == null ? STEP_WAIT : waitMs);
  40.         return true;
  41.     }
  42.     printl("[找图] 未找到 " + cvFile);
  43.     return false;
  44. }

  45. // 带重试的找图点击:转场/处理耗时的步骤用这个,maxT 次没找到才放弃
  46. function findAndClickRetry(cvFile, maxT, intervalMs, waitMs) {
  47.     for (var i = 0; i < maxT; i++) {
  48.         var detects = opencv.findImagesEx(cvFile);
  49.         if (detects != null && detects.length > 0) {
  50.             printl("[找图] " + cvFile + " 命中 " + detects.length + " 个(第 " + (i + 1) + " 次)");
  51.             printl(detects);
  52.             detects[0].click();
  53.             doSleep(waitMs == null ? STEP_WAIT : waitMs);
  54.             return true;
  55.         }
  56.         printl("[找图] " + cvFile + " 未找到,重试 " + (i + 1) + "/" + maxT);
  57.         doSleep(intervalMs);
  58.     }
  59.     printl("[找图] &#10060; " + cvFile + " 重试 " + maxT + " 次仍未找到");
  60.     return false;
  61. }

  62. // ==================== 录制:点魔法 → 收面板 → 点击开始 → 等 20 秒 → 点击停止 ====================
  63. function holdRecord() {
  64.     // 先点一次"魔法"(拍摄页左侧,有特效才录得成)
  65.     var ocr1 = opencv.OCREx('魔法.cv');
  66.     if (ocr1 != null) {
  67.         printl("[魔法] 识别到: " + ocr1.getAllString());
  68.         var magicAll = ocr1.getAllDetect();
  69.         if (magicAll != null && magicAll.length > 0) {
  70.             magicAll[0].click();
  71.             doSleep(2000);
  72.             // 收起魔法面板:点用户实测有效的空白位置(百分比坐标,自动适配分辨率)
  73.             printl("[魔法] 点空白收起面板 (0.5283, 0.3292)");
  74.             auto.clickPercent(0.5283, 0.3292);
  75.             doSleep(1500);
  76.         }
  77.     } else {
  78.         printl("[魔法] 魔法.cv 未命中,跳过,直接录制");
  79.     }

  80.     // 再倒计时缓冲,等拍摄页加载完再去点(不然找太快,页面没出来点不到)
  81.     for (var s = 3; s >= 1; s--) {
  82.         printl("[录制] " + s + " 秒后开始录制...");
  83.         doSleep(1000);
  84.     }

  85.     // 找录制按钮,最多试 5 次(页面还在加载时可能第一下找不到)
  86.     var detects = null;
  87.     for (var t = 0; t < 5; t++) {
  88.         detects = opencv.findImagesEx("按住录制.cv");
  89.         if (detects != null && detects.length > 0) break;
  90.         printl("[录制] 未找到 按住录制.cv,重试 " + (t + 1) + "/5");
  91.         doSleep(1500);
  92.     }
  93.     if (detects == null || detects.length == 0) {
  94.         printl("[找图] 未找到 按住录制.cv");
  95.         return false;
  96.     }

  97.     // 页面刚从拍摄页切换过来可能还在动画,找到按钮后再稳 2 秒,让页面完全静止再点
  98.     printl("[录制] 找到录制按钮,等 2 秒页面静止后点击...");
  99.     doSleep(2000);

  100.     // —— 用户实测可用的点击代码,原样嵌入 ——
  101.     //找图返回目标数组
  102.     var detects = opencv.findImagesEx('按住录制.cv');
  103.     if (detects != null) {
  104.         printl(detects);
  105.         detects[0].click();
  106.     }
  107.     // 蒙层/弹窗可能吃掉第一次点击,隔 1 秒补点一次(没弹窗时录制刚开始,
  108.     // 第二次点击会被录制中的按钮忽略,无副作用)
  109.     doSleep(1000);
  110.     var d1 = opencv.findImagesEx('按住录制.cv');
  111.     if (d1 != null) {
  112.         d1[0].click();
  113.         printl("[录制] &#9989; 已点击,开始录制 " + (RECORD_MS / 1000) + " 秒");
  114.     } else {
  115.         printl("[录制] &#9989; 已点击(按钮已变录制态,第一次就生效)");
  116.     }

  117.     // 等待录制时长(期间显示剩余秒数)
  118.     var waited = 0;
  119.     while (waited < RECORD_MS) {
  120.         doSleep(1000);
  121.         waited += 1000;
  122.         var left = Math.ceil((RECORD_MS - waited) / 1000);
  123.         if (left % 5 == 0 || left <= 3) {
  124.             printl("[录制] 还剩 " + left + " 秒");
  125.         }
  126.     }

  127.     // 录完点停止:找到就点,找不到直接跳过
  128.     var d2 = opencv.findImagesEx("按住录制.cv");
  129.     if (d2 != null && d2.length > 0) {
  130.         printl("[停止] 找到停止按钮");
  131.         d2[0].click();
  132.         printl("[录制] &#9989; 已停止录制");
  133.     } else {
  134.         printl("[停止] 没找到,跳过");
  135.     }
  136.     doSleep(2500);   // 等录制结算/转场
  137.     return true;
  138. }

  139. // ==================== 填标题:OCREx 定位输入区 → 点击 → 输入 ====================
  140. function fillTitleByOcr() {
  141.     var ocr1 = opencv.OCREx("点击OCR文字粘帖内容.cv");
  142.     if (ocr1 == null) {
  143.         printl("[OCR] 点击OCR文字粘帖内容.cv 未命中");
  144.         return false;
  145.     }
  146.     printl("[OCR] 识别到: " + ocr1.getAllString());

  147.     // 拿第一个识别目标的中心坐标点击输入区(ocrResult 无 getRect,必须 getAllDetect 拿 detect[])
  148.     var all = ocr1.getAllDetect();
  149.     if (all == null || all.length == 0) {
  150.         printl("[OCR] 有结果但拿不到目标坐标");
  151.         return false;
  152.     }
  153.     var det = all[0];
  154.     det.click();   // detect.click 实测能点中
  155.     printl("[点击] 输入区");
  156.     doSleep(1500);   // 等键盘弹出

  157.     // 输入标题:auto 没有 inputText 方法,只能走节点级 node.input()(项目里已验证的写法)
  158.     // 快手标题框可能不是标准 EditText,四路尝试,全失败时打印节点树诊断
  159.     var ok = inputViaNode(VIDEO_TITLE);
  160.     if (!ok) return false;
  161.     doSleep(800);
  162.     auto.back();        // 收起键盘
  163.     doSleep(800);
  164.     return true;
  165. }

  166. // ==================== 节点输入:四路尝试(auto 没有 inputText,只有 node.input 能用)====================
  167. function inputViaNode(text) {
  168.     // 路线1:标准 EditText
  169.     if (tryInput(2000, "android.widget.EditText", text, "路线1 EditText")) return true;
  170.     // 路线2:任意可编辑控件(快手可能是自定义输入框)
  171.     if (tryInputEditable(text, "路线2 editable")) return true;
  172.     // 路线3:按占位文字找节点 → focus 激活后再输入
  173.     if (tryInputByHint(text, "路线3 按占位文字")) return true;
  174.     // 路线4:剪贴板粘贴到可编辑节点
  175.     if (tryClipboardInput(text, "路线4 剪贴板粘贴")) return true;

  176.     // 四路全失败:打印可编辑节点树,贴日志给 AI 看就知道输入框长什么样
  177.     printl("[输入] &#10060; 四路全部失败,诊断节点树:");
  178.     try {
  179.         var ed = auto.nodeSearch(1500).editable(true);
  180.         if (ed != '' && ed.length > 0) {
  181.             printl("[输入] 可编辑节点数: " + ed.length);
  182.             printl("[输入] XML: " + ed.getXmls());
  183.         } else {
  184.             printl("[输入] 页面上没有 editable 节点 → 点击没把焦点给到输入框");
  185.             printl("[输入] 页面XML片段: " + auto.getXml());
  186.         }
  187.     } catch (e) { printl("[输入] 诊断失败: " + e); }
  188.     return false;
  189. }

  190. // 按 className 找节点输入
  191. function tryInput(timeout, cls, text, tag) {
  192.     try {
  193.         var nodesObj = auto.nodeSearch(timeout).className(cls);
  194.         if (nodesObj == '' || nodesObj.length == 0) {
  195.             printl("[输入] " + tag + ": 没找到 " + cls);
  196.             return false;
  197.         }
  198.         var nodeObj = nodesObj.getNode(0);
  199.         if (nodeObj.isEditable()) {
  200.             nodeObj.input(text);
  201.             printl("[输入] &#9989; " + tag + " 成功: " + text);
  202.             doSleep(800);
  203.             return true;
  204.         }
  205.         printl("[输入] " + tag + ": 节点不可编辑");
  206.     } catch (e) {
  207.         printl("[输入] " + tag + " 异常: " + e);
  208.     }
  209.     return false;
  210. }

  211. // 按 editable 过滤找节点输入
  212. function tryInputEditable(text, tag) {
  213.     try {
  214.         var nodesObj = auto.nodeSearch(2000).editable(true);
  215.         if (nodesObj == '' || nodesObj.length == 0) {
  216.             printl("[输入] " + tag + ": 没有 editable 节点");
  217.             return false;
  218.         }
  219.         var nodeObj = nodesObj.getNode(0);
  220.         nodeObj.focus();          // 先聚焦
  221.         doSleep(300);
  222.         nodeObj.input(text);
  223.         printl("[输入] &#9989; " + tag + " 成功: " + text);
  224.         doSleep(800);
  225.         return true;
  226.     } catch (e) {
  227.         printl("[输入] " + tag + " 异常: " + e);
  228.     }
  229.     return false;
  230. }

  231. // 按占位文字找节点 → focus → input
  232. function tryInputByHint(text, tag) {
  233.     try {
  234.         var nodesObj = auto.nodeSearch(2000).textIncludes("添加合适");
  235.         if (nodesObj == '' || nodesObj.length == 0) {
  236.             printl("[输入] " + tag + ": 没找到占位文字节点");
  237.             return false;
  238.         }
  239.         var nodeObj = nodesObj.getNode(0);
  240.         nodeObj.focus();
  241.         doSleep(300);
  242.         nodeObj.input(text);
  243.         printl("[输入] &#9989; " + tag + " 成功: " + text);
  244.         doSleep(800);
  245.         return true;
  246.     } catch (e) {
  247.         printl("[输入] " + tag + " 异常: " + e);
  248.     }
  249.     return false;
  250. }

  251. // 剪贴板粘贴到可编辑节点
  252. function tryClipboardInput(text, tag) {
  253.     try {
  254.         var nodesObj = auto.nodeSearch(2000).editable(true);
  255.         if (nodesObj == '' || nodesObj.length == 0) {
  256.             printl("[输入] " + tag + ": 没有 editable 节点");
  257.             return false;
  258.         }
  259.         var nodeObj = nodesObj.getNode(0);
  260.         nodeObj.focus();
  261.         Clipboard.copy(text);
  262.         doSleep(300);
  263.         nodeObj.clipboardInput(text);
  264.         printl("[输入] &#9989; " + tag + " 成功: " + text);
  265.         doSleep(800);
  266.         return true;
  267.     } catch (e) {
  268.         printl("[输入] " + tag + " 异常: " + e);
  269.     }
  270.     return false;
  271. }

  272. // ==================== 关弹窗:打开后弹窗拦路,找到就点掉,没弹窗跳过 ====================
  273. function dismissPopup() {
  274.     var detects = opencv.findImagesEx("弹窗点取消.cv");
  275.     if (detects == null || detects.length == 0) {
  276.         printl("[弹窗] 无弹窗");
  277.         return false;
  278.     }
  279.     printl("[弹窗] 发现弹窗,点取消");
  280.     printl(detects);
  281.     detects[0].click();   // detect.click 实测能点中
  282.     doSleep(1500);
  283.     return true;
  284. }

  285. // ==================== 单次发视频流程 ====================
  286. function publishOnce(loopNo) {
  287.     printl("╔════ 第 " + loopNo + "/" + LOOP_COUNT + " 次发布开始 ════╗");

  288.     // 0. 重启打开快手(先关干净再开,避免残留上次状态)
  289.     stepTitle("重启快手");
  290.     try { app.closeApp(PKG); doSleep(1500); } catch (e) { printl("[重启] 关闭失败(可能没开): " + e); }
  291.     app.openApp(PKG);
  292.     printl("[重启] 已重新打开快手");
  293.     doSleep(5000);   // 等启动 + 开屏广告

  294.     // 0.5 关弹窗(有就关,最多试 3 次,弹窗可能一层套一层)
  295.     stepTitle("关弹窗");
  296.     for (var t = 0; t < 3; t++) {
  297.         if (!dismissPopup()) break;
  298.     }

  299.     // 1. 点加号进拍摄页
  300.     stepTitle("点 + 进拍摄页");
  301.     if (!findAndClick("点击加号.cv")) return false;
  302.     dismissPopup();   // 点加号后偶尔还会弹,顺手关一下

  303.     // 2. 点击录制 20 秒
  304.     stepTitle("点击录制 20 秒");
  305.     if (!holdRecord()) return false;

  306.     // 3. 录完点下一步(停止→编辑页有转场+视频处理耗时,必须重试,单次找不到不能中止)
  307.     stepTitle("录完点下一步");
  308.     if (!findAndClickRetry("录完下一步.cv", 8, 1500, 4000)) {
  309.         printl("[中止] 录完下一步 8 次都没找到,第 " + loopNo + " 次流程中断");
  310.         return false;
  311.     }

  312.     // 4. 填标题(OCREx 定位输入区)
  313.     stepTitle("填标题");
  314.     fillTitleByOcr();   // 填不了不拦路,继续发布

  315.     // 5. 点发布
  316.     stepTitle("点发布");
  317.     if (!findAndClick("点击发布按钮.cv", 5000)) return false;

  318.     // 6. 确认弹窗:发布后会弹"继续"确认,不点会发布失败
  319.     stepTitle("点继续");
  320.     clickContinue();

  321.     // 7. 进"我"的主页,删除刚发布的视频
  322.     stepTitle("删除刚发布的视频");
  323.     deleteLastVideo();

  324.     printl("&#9989; 第 " + loopNo + " 次发布流程完成");
  325.     try { toast.show("第 " + loopNo + " 次发布完成"); } catch (e) {}
  326.     return true;
  327. }

  328. // ==================== 删除刚发布的视频:点"我" → 上滑 → 点第一个视频 → 弹出删除 → 确认删除 ====================
  329. function deleteLastVideo() {
  330.     // 1) OCREx 点"我"
  331.     var ocr1 = opencv.OCREx("点击我.cv");
  332.     if (ocr1 == null) { printl("[删除] 点击我.cv 未命中,跳过删除"); return false; }
  333.     printl("[删除] 点我: " + ocr1.getAllString());
  334.     var all = ocr1.getAllDetect();
  335.     if (all == null || all.length == 0) { printl("[删除] 识别到但没拿到坐标,跳过"); return false; }
  336.     all[0].click();
  337.     doSleep(4000);   // 等个人主页加载

  338.     // 2) 往上滑,露出作品网格
  339.     printl("[删除] 往上滑露出作品");
  340.     swipPercent(0.5, 0.8, 0.5, 0.35, 600);
  341.     doSleep(2500);

  342.     // 3) 点第一个视频(网格里第一个就是刚发布的)
  343.     var first = findNodeWithWords(["作品", "草稿"]);
  344.     if (first == null) {
  345.         // 没有文字锚点就兜底:作品网格里的可点击格子
  346.         first = auto.nodeSearch(2000).clickable(true);
  347.         if (first == '' || first.length == 0) { printl("[删除] 没找到作品格子"); return false; }
  348.         first = first.getNode(Math.min(3, first.length - 1));
  349.     }
  350.     first.click();
  351.     doSleep(4500);   // 等视频页打开并开始播放

  352.     // 4) 点"更多"弹出删除菜单
  353.     var detects = opencv.findImagesEx('弹出删除.cv');
  354.     if (detects == null) { printl("[删除] 弹出删除.cv 未命中"); return false; }
  355.     printl(detects);
  356.     detects[0].click();
  357.     doSleep(1800);

  358.     // 5) 确认删除
  359.     var ocr2 = opencv.OCREx('删除.cv');
  360.     if (ocr2 == null) { printl("[删除] 删除.cv 未命中"); return false; }
  361.     printl("[删除] 确认: " + ocr2.getAllString());
  362.     var all2 = ocr2.getAllDetect();
  363.     if (all2 == null || all2.length == 0) { printl("[删除] 识别到但没拿到坐标"); return false; }
  364.     all2[0].click();
  365.     doSleep(1800);

  366.     // 6) 最后还会弹一次"再次确定删除"
  367.     var ocr3 = opencv.OCREx('再次确定删除.cv');
  368.     if (ocr3 != null) {
  369.         printl("[删除] 再次确定: " + ocr3.getAllString());
  370.         var all3 = ocr3.getAllDetect();
  371.         if (all3 != null && all3.length > 0) {
  372.             all3[0].click();
  373.             doSleep(2500);
  374.         }
  375.     } else {
  376.         printl("[删除] 没弹【再次确定删除】,跳过");
  377.     }
  378.     printl("&#9989; 已删除刚发布的视频");
  379.     return true;
  380. }

  381. // 找第一个匹配任一关键词的节点(用于定位"作品"入口)
  382. function findNodeWithWords(words) {
  383.     for (var w = 0; w < words.length; w++) {
  384.         try {
  385.             var nodesObj = auto.nodeSearch(1500).textIncludes(words[w]);
  386.             if (nodesObj != '' && nodesObj.length > 0) return nodesObj.getNode(0);
  387.         } catch (e) {}
  388.     }
  389.     return null;
  390. }

  391. // ==================== 发布后确认:OCREx 找"继续"并点击(弹窗可能延迟弹出,轮询多次)====================
  392. function clickContinue() {
  393.     var clicked = false;
  394.     for (var i = 0; i < 4; i++) {
  395.         var ocr1 = opencv.OCREx("点继续.cv");
  396.         if (ocr1 != null) {
  397.             printl("[继续] 识别到: " + ocr1.getAllString());
  398.             var all = ocr1.getAllDetect();
  399.             if (all != null && all.length > 0) {
  400.                 all[0].click();   // detect.click 实测能点中
  401.                 printl("[继续] &#9989; 已点击继续");
  402.                 clicked = true;
  403.                 doSleep(1500);
  404.                 break;
  405.             }
  406.         }
  407.         if (i < 3) {
  408.             printl("[继续] 暂无弹窗,等 2 秒重试 " + (i + 1) + "/3");
  409.             doSleep(2000);
  410.         }
  411.     }
  412.     if (!clicked) printl("[继续] 没有弹【继续】弹窗,跳过");
  413.     return clicked;
  414. }

  415. // ==================== 清理(顺序不能变)====================
  416. function cleanup() {
  417.     printl("[结束] 发视频流程执行完毕");
  418.     runTime.stopAllRuning();   // 永远放最后
  419.     stop();
  420. }
  421. event.onStopBtnEvent(function () { cleanup(); });

  422. // ==================== 主流程 ====================
  423. function runMain() {
  424.     printl("╔════════════════════════════════════════╗");
  425.     printl("║  案例10:快手发视频(.cv 五连流程)     ║");
  426.     printl("╚════════════════════════════════════════╝");
  427.     printl("[配置] 标题=" + VIDEO_TITLE + " 录制=" + (RECORD_MS / 1000) + "s 循环=" + LOOP_COUNT + " 次");

  428.     for (var i = 1; i <= LOOP_COUNT; i++) {
  429.         var ok = publishOnce(i);
  430.         if (!ok) {
  431.             printl("[中止] 第 " + i + " 次流程中断,停止循环");
  432.             break;
  433.         }
  434.         if (i < LOOP_COUNT) {
  435.             printl("[间隔] 等 5 秒后开始下一次...");
  436.             doSleep(5000);
  437.         }
  438.     }

  439.     cleanup();
  440. }

  441. runMain();
复制代码


QQ截图20261014144054.png
QQ截图20261014144054.png
QQ截图20261014144039.png
QQ截图20261014144031.png
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|

GMT+8, 2026-10-17 10:53

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

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