From 8c759fad17d6d2b617c8c6d4436697590e308890 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 14 Jul 2026 18:03:20 +0800 Subject: [PATCH] feat: publish workphone public SDK deployment and API docs --- .cursorignore | 55 ++ .cursorindexingignore | 8 + .gitignore | 14 + sdk/.dockerignore | 20 + sdk/Dockerfile | 6 +- sdk/agent/config.nas-primary.example.json | 12 + sdk/agent/sdk_discovery.py | 59 +++ sdk/android-app/app/build.gradle | 2 +- .../app/src/main/AndroidManifest.xml | 6 +- .../main/java/com/system/cloudservice/App.kt | 4 +- .../system/cloudservice/engine/AgentEngine.kt | 20 +- .../cloudservice/engine/SkillExecutor.kt | 59 ++- .../cloudservice/engine/WebSocketManager.kt | 2 +- .../system/cloudservice/frida/FridaBridge.kt | 5 + .../service/AgentAccessibilityService.kt | 34 ++ .../service/AgentForegroundService.kt | 14 +- .../cloudservice/ui/DashboardFragment.kt | 13 +- .../system/cloudservice/ui/SetupActivity.kt | 14 +- .../system/cloudservice/ui/StatusFragment.kt | 4 +- .../system/cloudservice/ui/WechatFragment.kt | 2 +- .../drawable-nodpi/workphone_launcher_art.png | Bin 0 -> 229830 bytes .../res/drawable/ic_launcher_foreground.xml | 17 +- .../src/main/res/layout/activity_setup.xml | 4 +- .../main/res/layout/fragment_settings_v2.xml | 13 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 9380 -> 8164 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 34357 -> 8164 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 9380 -> 8164 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 5256 -> 4837 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 17645 -> 4837 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 5256 -> 4837 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 14963 -> 12409 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 53830 -> 12409 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 14963 -> 12409 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 27902 -> 23325 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 104196 -> 23325 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 27902 -> 23325 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 44936 -> 38306 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 167056 -> 38306 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 44936 -> 38306 bytes .../app/src/main/res/values/colors.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 +- sdk/app/agent/__init__.py | 1 + sdk/app/agent/sdk_discovery.py | 59 +++ sdk/app/agent/vision_helper.py | 2 +- sdk/app/config.py | 8 + sdk/app/data/connection_providers.json | 114 ++++ sdk/app/data/hook/device_modules.json | 2 +- sdk/app/main.py | 33 +- sdk/app/routers/adb.py | 7 +- sdk/app/routers/connection.py | 6 +- sdk/app/routers/connection_provider.py | 138 +++++ sdk/app/routers/devices.py | 4 +- sdk/app/routers/fleet.py | 224 ++++++++ sdk/app/routers/integration.py | 294 +++++++++++ sdk/app/routers/process.py | 24 + sdk/app/routers/qrcode.py | 15 +- sdk/app/services/ai_agent.py | 2 +- sdk/app/services/brain_skill_registry.py | 203 +++++++ sdk/app/services/connection_provider.py | 498 ++++++++++++++++++ sdk/app/services/device_fleet.py | 4 +- sdk/app/services/device_id_util.py | 18 + sdk/app/services/device_transport.py | 266 ++++++++++ sdk/app/services/integration_manifest.py | 417 +++++++++++++++ sdk/app/services/karuo_device_ai.py | 130 +++++ sdk/app/services/process_state.py | 77 +++ sdk/app/services/server_frida_bridge.py | 96 ++++ sdk/app/services/wechat_contact_normalizer.py | 72 +++ sdk/app/static/hub.html | 35 +- sdk/app/static/index.html | 2 +- sdk/app/static/llms.txt | 51 ++ sdk/deploy/baota/wpsdk.quwanzhi.com.conf | 36 ++ sdk/docker-compose.baota.yml | 85 +++ .../com.workphone.python-agent.plist | 50 ++ sdk/scripts/adb_bootstrap_termux_agent.sh | 60 +++ sdk/scripts/adb_connection_stabilize.sh | 82 +++ sdk/scripts/aochuang_protocol_coverage.py | 151 ++++++ sdk/scripts/auto_accept_when_ready.sh | 43 ++ sdk/scripts/auto_capture_when_frida_back.sh | 31 ++ sdk/scripts/build_agent_config.py | 150 ++++++ sdk/scripts/clean_aochuang_device.sh | 252 +++++++++ sdk/scripts/connection_keeper_daemon.sh | 210 ++++++++ sdk/scripts/deploy_baota_wpsdk.sh | 48 ++ sdk/scripts/deploy_nas_rsync.sh | 4 + sdk/scripts/ensure_ws_agent.sh | 6 + sdk/scripts/ensure_ws_agent_wireless.sh | 37 ++ sdk/scripts/extract_aochuang_to_ziliao.sh | 152 ++++++ .../install_connection_keeper_launchagent.sh | 63 +++ .../install_sdk_watcher_launchagent.sh | 57 ++ sdk/scripts/lan_connect_setup.sh | 165 ++++++ sdk/scripts/nas_primary_agent_setup.sh | 115 ++++ sdk/scripts/phantom_frida_keepalive.sh | 33 ++ sdk/scripts/run_extended_acceptance.sh | 125 +++++ sdk/scripts/run_stability_longrun.mjs | 198 +++++++ sdk/scripts/run_stability_longrun.py | 208 ++++++++ sdk/scripts/run_three_acceptance.sh | 126 +++++ sdk/scripts/run_wechat_full_acceptance.sh | 26 + .../run_wechat_private_domain_acceptance.mjs | 194 +++++++ .../run_wechat_private_domain_acceptance.py | 263 +++++++++ .../run_wechat_realdevice_acceptance.sh | 98 ++++ .../run_wechat_safe_batch_acceptance.sh | 65 +++ .../run_wechat_safe_matrix_acceptance.py | 189 +++++++ sdk/scripts/run_wireless_wechat_full_suite.sh | 90 ++++ sdk/scripts/sdk_connection_watcher.sh | 155 ++++++ sdk/scripts/skill_registry_audit.py | 78 +++ sdk/scripts/termux_agent_bootstrap.sh | 36 ++ sdk/scripts/termux_frida_up_onphone.sh | 131 +++++ sdk/scripts/termux_push_and_start_agent.sh | 141 +++++ sdk/scripts/verify_aochuang_clean.sh | 110 ++++ sdk/scripts/wait_adb_then_login.sh | 67 +++ sdk/scripts/wait_ws_agent.sh | 44 ++ sdk/scripts/watch_device_ai_chat.sh | 25 + sdk/scripts/wechat_interface_audit.py | 181 +++++++ sdk/scripts/wireless_adb_pair.sh | 39 ++ sdk/scripts/wireless_autoconnect_bootstrap.sh | 34 ++ sdk/scripts/wireless_master_oneclick.sh | 166 ++++++ sdk/tests/run_wx_hook_regression.sh | 31 ++ sdk/tests/test_agent_public_fallback.py | 125 +++++ sdk/tests/test_ai_brain_gateway_fallback.py | 33 ++ sdk/tests/test_connection_provider.py | 140 +++++ sdk/tests/test_forward_message_logic.mjs | 169 ++++++ sdk/tests/test_integration_manifest.py | 84 +++ sdk/tests/test_matrix_classifier_honesty.py | 50 ++ sdk/tests/test_rate_limiter_antiban.py | 126 +++++ sdk/tests/test_skill_registry_alignment.py | 56 ++ sdk/tests/test_wechat_acceptance_offline.py | 100 ++++ sdk/tests/test_wechat_interface_audit.py | 24 + 开发文档/10、项目管理/工作日志.md | 30 ++ 开发文档/10、项目管理/开发进度总表.md | 37 +- .../1、需求/修改/工作手机_进行中_20260613.md | 38 ++ 开发文档/5、接口/02-业务对接/README.md | 1 + .../工作手机SDK公网API对接文档_20260714.md | 173 ++++++ .../06-验收与矩阵/2026-07-14_公网SDK完整测试汇总.md | 80 +++ 开发文档/5、接口/06-验收与矩阵/README.md | 1 + 开发文档/5、接口/README.md | 4 +- .../images/公网API真机截图_20260714.png | Bin 0 -> 245296 bytes .../06-存客宝宝塔/images/工作手机新图标_20260714.png | Bin 0 -> 275936 bytes .../images/工作手机新图标真机桌面_20260714.png | Bin 0 -> 2453809 bytes .../06-存客宝宝塔/images/朋友圈重试失败_20260714.png | Bin 0 -> 915994 bytes .../images/朋友圈首次发布失败_20260714.png | Bin 0 -> 1144335 bytes .../公司NAS_WebStation与工作手机SDK配置.md | 64 +++ .../工作手机SDK_存客宝宝塔部署总方案.md | 22 +- .../06-存客宝宝塔/线上部署与真机验收_20260714.md | 94 ++++ .../06-存客宝宝塔/部署前置条件与端口清单.md | 9 +- .../8、部署/06-存客宝宝塔/部署验收记录.md | 44 +- 开发文档/8、部署/README.md | 3 +- 145 files changed, 9243 insertions(+), 111 deletions(-) create mode 100644 .cursorignore create mode 100644 .cursorindexingignore create mode 100644 sdk/.dockerignore create mode 100644 sdk/agent/config.nas-primary.example.json create mode 100644 sdk/agent/sdk_discovery.py create mode 100644 sdk/android-app/app/src/main/res/drawable-nodpi/workphone_launcher_art.png create mode 100644 sdk/app/agent/__init__.py create mode 100644 sdk/app/agent/sdk_discovery.py create mode 100644 sdk/app/data/connection_providers.json create mode 100644 sdk/app/routers/connection_provider.py create mode 100644 sdk/app/routers/fleet.py create mode 100644 sdk/app/routers/integration.py create mode 100644 sdk/app/routers/process.py create mode 100644 sdk/app/services/brain_skill_registry.py create mode 100644 sdk/app/services/connection_provider.py create mode 100644 sdk/app/services/device_transport.py create mode 100644 sdk/app/services/integration_manifest.py create mode 100644 sdk/app/services/karuo_device_ai.py create mode 100644 sdk/app/services/process_state.py create mode 100644 sdk/app/services/server_frida_bridge.py create mode 100644 sdk/app/services/wechat_contact_normalizer.py create mode 100644 sdk/app/static/llms.txt create mode 100644 sdk/deploy/baota/wpsdk.quwanzhi.com.conf create mode 100644 sdk/docker-compose.baota.yml create mode 100644 sdk/launchagents/com.workphone.python-agent.plist create mode 100755 sdk/scripts/adb_bootstrap_termux_agent.sh create mode 100755 sdk/scripts/adb_connection_stabilize.sh create mode 100644 sdk/scripts/aochuang_protocol_coverage.py create mode 100755 sdk/scripts/auto_accept_when_ready.sh create mode 100755 sdk/scripts/auto_capture_when_frida_back.sh create mode 100755 sdk/scripts/build_agent_config.py create mode 100755 sdk/scripts/clean_aochuang_device.sh create mode 100755 sdk/scripts/connection_keeper_daemon.sh create mode 100755 sdk/scripts/deploy_baota_wpsdk.sh create mode 100755 sdk/scripts/ensure_ws_agent_wireless.sh create mode 100644 sdk/scripts/extract_aochuang_to_ziliao.sh create mode 100755 sdk/scripts/install_connection_keeper_launchagent.sh create mode 100755 sdk/scripts/install_sdk_watcher_launchagent.sh create mode 100755 sdk/scripts/lan_connect_setup.sh create mode 100755 sdk/scripts/nas_primary_agent_setup.sh create mode 100644 sdk/scripts/phantom_frida_keepalive.sh create mode 100755 sdk/scripts/run_extended_acceptance.sh create mode 100755 sdk/scripts/run_stability_longrun.mjs create mode 100755 sdk/scripts/run_stability_longrun.py create mode 100755 sdk/scripts/run_three_acceptance.sh create mode 100755 sdk/scripts/run_wechat_full_acceptance.sh create mode 100644 sdk/scripts/run_wechat_private_domain_acceptance.mjs create mode 100755 sdk/scripts/run_wechat_private_domain_acceptance.py create mode 100755 sdk/scripts/run_wechat_realdevice_acceptance.sh create mode 100755 sdk/scripts/run_wechat_safe_batch_acceptance.sh create mode 100755 sdk/scripts/run_wechat_safe_matrix_acceptance.py create mode 100755 sdk/scripts/run_wireless_wechat_full_suite.sh create mode 100755 sdk/scripts/sdk_connection_watcher.sh create mode 100644 sdk/scripts/skill_registry_audit.py create mode 100644 sdk/scripts/termux_agent_bootstrap.sh create mode 100755 sdk/scripts/termux_frida_up_onphone.sh create mode 100755 sdk/scripts/termux_push_and_start_agent.sh create mode 100755 sdk/scripts/verify_aochuang_clean.sh create mode 100755 sdk/scripts/wait_adb_then_login.sh create mode 100755 sdk/scripts/wait_ws_agent.sh create mode 100755 sdk/scripts/watch_device_ai_chat.sh create mode 100644 sdk/scripts/wechat_interface_audit.py create mode 100755 sdk/scripts/wireless_adb_pair.sh create mode 100755 sdk/scripts/wireless_autoconnect_bootstrap.sh create mode 100755 sdk/scripts/wireless_master_oneclick.sh create mode 100755 sdk/tests/run_wx_hook_regression.sh create mode 100644 sdk/tests/test_agent_public_fallback.py create mode 100644 sdk/tests/test_ai_brain_gateway_fallback.py create mode 100644 sdk/tests/test_connection_provider.py create mode 100644 sdk/tests/test_forward_message_logic.mjs create mode 100644 sdk/tests/test_integration_manifest.py create mode 100644 sdk/tests/test_matrix_classifier_honesty.py create mode 100644 sdk/tests/test_rate_limiter_antiban.py create mode 100644 sdk/tests/test_skill_registry_alignment.py create mode 100644 sdk/tests/test_wechat_acceptance_offline.py create mode 100644 sdk/tests/test_wechat_interface_audit.py create mode 100644 开发文档/5、接口/02-业务对接/工作手机SDK公网API对接文档_20260714.md create mode 100644 开发文档/5、接口/06-验收与矩阵/2026-07-14_公网SDK完整测试汇总.md create mode 100644 开发文档/8、部署/06-存客宝宝塔/images/公网API真机截图_20260714.png create mode 100644 开发文档/8、部署/06-存客宝宝塔/images/工作手机新图标_20260714.png create mode 100644 开发文档/8、部署/06-存客宝宝塔/images/工作手机新图标真机桌面_20260714.png create mode 100644 开发文档/8、部署/06-存客宝宝塔/images/朋友圈重试失败_20260714.png create mode 100644 开发文档/8、部署/06-存客宝宝塔/images/朋友圈首次发布失败_20260714.png create mode 100644 开发文档/8、部署/06-存客宝宝塔/公司NAS_WebStation与工作手机SDK配置.md create mode 100644 开发文档/8、部署/06-存客宝宝塔/线上部署与真机验收_20260714.md diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000000..6974cdf24c --- /dev/null +++ b/.cursorignore @@ -0,0 +1,55 @@ +# 工作手机 · Cursor 忽略(防持续上传/索引) +# 根因:反编译产物 ~45 万文件,Agent 会反复同步到云端 + +# APK 反编译与资料(最大头) +资料/** +**/apk_analysis/** + +# 微信反编译 / jadx / smali 产物 +sdk/tmp/** +sdk/scripts/gadget_work/** +**/jadx_out/** +**/wechat_decompiled/** +**/smali/** +**/smali_classes*/** + +# 依赖与构建 +**/node_modules/** +**/.pnpm-store/** +**/.yarn/** +**/.npm/** +**/.venv/** +**/venv/** +**/__pycache__/** +**/.pytest_cache/** +**/dist/** +**/build/** +**/.next/** +**/.turbo/** +**/target/** +**/.gradle/** +**/.m2/** +**/vendor/** +**/coverage/** + +# 缓存与索引 +**/.cache/** +**/.codegraph/** +**/.cursor/** +**/.git/objects/** + +# 大文件 +**/*.apk +**/*.aab +**/*.dex +**/*.so +**/*.jar +**/*.zip +**/*.7z +**/*.dmg +**/*.mp4 +**/*.mov +**/*.MP4 +**/*.MOV +**/*.log +**/logs/** diff --git a/.cursorindexingignore b/.cursorindexingignore new file mode 100644 index 0000000000..8bda13d7f7 --- /dev/null +++ b/.cursorindexingignore @@ -0,0 +1,8 @@ +# 仅排除索引搜索,Agent 仍可 @ 读取(工作手机反编译目录) +资料/** +sdk/tmp/** +sdk/scripts/gadget_work/** +**/jadx_out/** +**/wechat_decompiled/** +**/smali/** +**/smali_classes*/** diff --git a/.gitignore b/.gitignore index d39da9a26d..ce24c6147a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,20 @@ sdk/logs/ sdk/tmp_rom/ sdk/.runtime/ +# 本机/真机运行态配置与验收日志(模板文件可提交) +sdk/config/*.env +sdk/config/agent_config_*.json +sdk/agent/config.nas-active.json +sdk/data/operation_logs/ +sdk/app/data/hook/events.jsonl + +# 本地发行包、逆向工作区与设备二进制 +sdk/releases/ +sdk/scripts/gadget_work/ +sdk/scripts/frida-server-* +sdk/scripts/frida-gadget-* +sdk/scripts/wechat_original.apk + # Obsidian 粘贴图误落 vault 根(应进 开发文档/**/images/) /Pasted image*.png /Pasted image*.jpg diff --git a/sdk/.dockerignore b/sdk/.dockerignore new file mode 100644 index 0000000000..b57887e944 --- /dev/null +++ b/sdk/.dockerignore @@ -0,0 +1,20 @@ +.git +.env +.env.* +*.log +__pycache__/ +*.pyc +.pytest_cache/ +.gradle/ +node_modules/ +android-app/ +admin/ +releases/ +tests/ +data/ +app/data/cunke_bao_config.json +app/data/hook/events.jsonl +app/data/operation_logs/ +app/agent/config.json +agent/config.json +agent/config.*.json diff --git a/sdk/Dockerfile b/sdk/Dockerfile index 09da03e1f6..a5fc061e79 100644 --- a/sdk/Dockerfile +++ b/sdk/Dockerfile @@ -1,9 +1,11 @@ # 工作手机SDK v3.1 - Dockerfile(含 AI Brain + Hook 模块管理) -FROM python:3.11-slim +ARG PYTHON_IMAGE=docker.1ms.run/library/python:3.11-slim +FROM ${PYTHON_IMAGE} WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN sed -i 's#deb.debian.org#mirrors.cloud.tencent.com#g; s#security.debian.org#mirrors.cloud.tencent.com#g' /etc/apt/sources.list.d/debian.sources \ + && apt-get update && apt-get install -y --no-install-recommends \ curl \ adb \ && rm -rf /var/lib/apt/lists/* diff --git a/sdk/agent/config.nas-primary.example.json b/sdk/agent/config.nas-primary.example.json new file mode 100644 index 0000000000..36d72c679a --- /dev/null +++ b/sdk/agent/config.nas-primary.example.json @@ -0,0 +1,12 @@ +{ + "device_id": "xgfe65eimrrofyws", + "server_url": "ws://192.168.110.101:8899/ws/device", + "public_servers": [ + "ws://192.168.1.201:8899/ws/device", + "ws://192.168.110.167:8899/ws/device", + "ws://open.quwanzhi.com:8899/ws/device" + ], + "heartbeat_interval": 10, + "project_id": "cunkebao", + "comment": "顺序固定: ①NAS局域网(server_url) ②本机Docker(局网IP,示例110.167) ③NAS外网frp。勿把frp排在Docker前。" +} diff --git a/sdk/agent/sdk_discovery.py b/sdk/agent/sdk_discovery.py new file mode 100644 index 0000000000..5692510483 --- /dev/null +++ b/sdk/agent/sdk_discovery.py @@ -0,0 +1,59 @@ +""" +设备端 SDK 自动发现 — 监听 UDP beacon(8898),无线主控免手填 IP。 + +与 sdk/app/services/discovery_service.py BEACON 协议对齐。 +""" + +from __future__ import annotations + +import json +import socket +import time +from typing import Optional + +BEACON_PORT = 8898 +BEACON_MAGIC = "WORKPHONE_SDK" + + +def _pick_best_ip(ips: list) -> str: + """优先 192.168.110.x(工作机常用网段),否则取第一个私网 IP。""" + for ip in ips: + if ip.startswith("192.168.110."): + return ip + for ip in ips: + if ip.startswith(("192.168.", "10.", "172.")): + return ip + return ips[0] + + +def discover_sdk_ws_base(timeout: float = 20.0) -> Optional[str]: + """ + 监听局域网 UDP beacon,返回 WebSocket 基础地址。 + 例: ws://192.168.110.251:8899/ws/device + """ + deadline = time.time() + timeout + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind(("", BEACON_PORT)) + except OSError: + # Termux 可能无权限绑 8898,改绑任意端口并只 recv + sock.bind(("", 0)) + sock.settimeout(1.0) + + while time.time() < deadline: + try: + data, _addr = sock.recvfrom(4096) + payload = json.loads(data.decode("utf-8")) + if payload.get("magic") != BEACON_MAGIC: + continue + port = int(payload.get("port") or 8899) + ws_path = (payload.get("ws_path") or "/ws/device").rstrip("/") + ips = payload.get("ips") or [] + host = _pick_best_ip(ips) if ips else "127.0.0.1" + return f"ws://{host}:{port}{ws_path}" + except socket.timeout: + continue + except Exception: + continue + return None diff --git a/sdk/android-app/app/build.gradle b/sdk/android-app/app/build.gradle index ed7cf78ff0..2fd7f6fd04 100644 --- a/sdk/android-app/app/build.gradle +++ b/sdk/android-app/app/build.gradle @@ -14,7 +14,7 @@ android { versionCode 6 versionName "5.0.1" buildConfigField "String", "PWA_URL", "\"https://ckbapi.quwanzhi.com\"" - buildConfigField "String", "DEFAULT_WS", "\"wss://workphone.quwanzhi.com/ws/device\"" + buildConfigField "String", "DEFAULT_WS", "\"wss://wpsdk.quwanzhi.com/ws/device\"" buildConfigField "String", "AI_API_URL", "\"https://ckbapi.quwanzhi.com\"" } diff --git a/sdk/android-app/app/src/main/AndroidManifest.xml b/sdk/android-app/app/src/main/AndroidManifest.xml index a3b0933bf4..5a1d3f8a77 100644 --- a/sdk/android-app/app/src/main/AndroidManifest.xml +++ b/sdk/android-app/app/src/main/AndroidManifest.xml @@ -19,6 +19,10 @@ + + + android:exported="true"> diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/App.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/App.kt index bd23884abe..e4b5a985bd 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/App.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/App.kt @@ -1,6 +1,7 @@ package com.system.cloudservice import android.app.Application +import com.system.cloudservice.service.AgentAccessibilityService import com.system.cloudservice.util.Logger class App : Application() { @@ -13,6 +14,7 @@ class App : Application() { super.onCreate() instance = this Logger.init(this) - Logger.i("AI数智员工 Application created (${BuildConfig.VERSION_NAME})") + AgentAccessibilityService.enableIfAuthorized(this) + Logger.i("工作手机 Application created (${BuildConfig.VERSION_NAME})") } } diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/AgentEngine.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/AgentEngine.kt index 78a7858063..4d2bad349e 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/AgentEngine.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/AgentEngine.kt @@ -35,6 +35,7 @@ class AgentEngine( private var deviceId = "" private var projectId = "" private var serverUrl = "" + private var pairingToken = "" private var heartbeatIntervalMs = 30_000L private var lastPongAt = 0L private var commandsExecuted = 0 @@ -58,6 +59,9 @@ class AgentEngine( fun isFridaReady(): Boolean = fridaBridge?.isReady == true + /** 微信进程真实注入状态;与 frida-server 端口就绪严格分离。 */ + fun isWechatHookAttached(): Boolean = fridaBridge?.isTargetHooked() == true + /** AB-02~06 runtime 信号:risk_sentinel / nurture / device_guard / sensor / touch */ fun getAntiBanStatus(): Map = antiBan.statusSummary() @@ -87,6 +91,8 @@ class AgentEngine( ) { this.deviceId = deviceId this.projectId = projectId + this.pairingToken = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE) + .getString("pairing_token", "")?.trim().orEmpty() this.publicServers = publicServers.filter { it.isNotBlank() } this.serverUrl = buildWsUrl(serverUrl, deviceId) this.candidates = buildCandidates(this.serverUrl, this.publicServers, deviceId) @@ -114,12 +120,19 @@ class AgentEngine( } private fun buildWsUrl(base: String, deviceId: String): String { - val cleaned = base.trimEnd('/') - return if (cleaned.contains("/ws/device")) { + val parts = base.trim().split("?", limit = 2) + val cleaned = parts[0].trimEnd('/') + val path = if (cleaned.contains("/ws/device")) { if (cleaned.endsWith(deviceId)) cleaned else "$cleaned/$deviceId" } else { "$cleaned/ws/device/$deviceId" } + val queryParts = mutableListOf() + if (parts.size == 2 && parts[1].isNotBlank()) queryParts.add(parts[1]) + if (pairingToken.isNotBlank() && queryParts.none { it.startsWith("token=") }) { + queryParts.add("token=${java.net.URLEncoder.encode(pairingToken, "UTF-8")}") + } + return if (queryParts.isEmpty()) path else "$path?${queryParts.joinToString("&")}" } /** BIND-03:构建有序候选 [主连接, 公网1, ...],自动补全 + 去重。 */ @@ -230,7 +243,8 @@ class AgentEngine( "device_profile" to info, "capabilities" to buildCapabilities(), "connect_stage" to connectStage, // BIND-07 寻服阶段上报 - "server_url" to serverUrl, + // 配对令牌仅用于握手,禁止上报到设备资料或管理 API。 + "server_url" to serverUrl.substringBefore('?'), "server_candidate_count" to candidates.size, ) wsManager.send(gson.toJson(msg)) diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/SkillExecutor.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/SkillExecutor.kt index 3006adc205..057b7b2772 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/SkillExecutor.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/SkillExecutor.kt @@ -3,12 +3,14 @@ package com.system.cloudservice.engine import android.content.Context import android.content.Intent import android.os.Build +import android.util.Base64 import com.system.cloudservice.antiban.AntiBanManager import com.system.cloudservice.frida.FridaBridge import com.system.cloudservice.service.AgentAccessibilityService import com.system.cloudservice.util.DeviceInfo import com.system.cloudservice.util.Logger import com.system.cloudservice.util.ShellExecutor +import java.io.File class SkillExecutor( private val context: Context, @@ -26,20 +28,25 @@ class SkillExecutor( return mapOf("code" to 429, "message" to "风控暂停中", "data" to status) } - // If a script is specified, route through Frida-priority chain - if (script != null) { + // wechat/douyin 等业务脚本走 Hook 优先链;android/system/device 是设备控制域, + // 不能误当成 Frida 脚本,否则公网 Fleet 的 app_start/ui_tree 会落到 Shell不支持。 + val deviceControlScripts = setOf("android", "system", "device") + if (script != null && script.lowercase() !in deviceControlScripts) { return executeWithFridaPriority(script, action, params) } return try { when (action) { - "open_app" -> openApp(params["package"] as? String ?: "") + "open_app", "app_start", "start_app" -> openApp(params["package"] as? String ?: "") + "app_stop", "stop_app" -> doShell( + mapOf("command" to "am force-stop ${params["package"] ?: ""}") + ) "get_installed_apps" -> getInstalledApps() "get_device_info" -> mapOf("code" to 200, "data" to DeviceInfo.collectFull(context)) - "click" -> doClick(params) + "click", "tap" -> doClick(params) "swipe" -> doSwipe(params) - "input_text" -> doInputText(params) - "key_event" -> doKeyEvent(params) + "input_text", "input" -> doInputText(params) + "key_event", "press_key" -> doKeyEvent(params) "screenshot" -> doScreenshot(params) "back" -> doGlobalAction("back") "home" -> doGlobalAction("home") @@ -139,6 +146,22 @@ class SkillExecutor( if (script != "wechat") return null when (action) { + "ping" -> { + val ver = WechatNoRootAutomation.getWechatVersion(context) + val quick = DeviceInfo.collectQuick(context) + return mapOf( + "code" to 200, + "message" to "pong", + "data" to mapOf( + "success" to true, + "installed" to (ver["success"] == true), + "wechat_version" to ver["version"], + "wechat_running" to (quick["wechat_running"] == true), + "channel" to "no_root_pkg", + ), + "channel" to "no_root_pkg", + ) + } "get_wechat_version" -> { val data = WechatNoRootAutomation.getWechatVersion(context) return mapOf( @@ -330,9 +353,27 @@ class SkillExecutor( } private fun doScreenshot(params: Map): Map { - val path = params["path"] as? String ?: "/sdcard/screenshot.png" - val r = ShellExecutor.execute("screencap -p $path") - return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + val path = params["path"] as? String + ?: File(context.cacheDir, "workphone_screenshot.png").absolutePath + val r = ShellExecutor.execute("screencap -p '$path' && chmod 644 '$path'") + if (!r.success) { + return mapOf("code" to 500, "message" to r.output) + } + return try { + val bytes = File(path).readBytes() + mapOf( + "code" to 200, + "message" to "ok", + "data" to mapOf( + "image_base64" to Base64.encodeToString(bytes, Base64.NO_WRAP), + "mime_type" to "image/png", + "size" to bytes.size, + "path" to path, + ), + ) + } catch (e: Exception) { + mapOf("code" to 500, "message" to "截图读取失败: ${e.message}") + } } private fun doGlobalAction(action: String): Map { diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/WebSocketManager.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/WebSocketManager.kt index 40001b6e60..c2da303978 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/WebSocketManager.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/WebSocketManager.kt @@ -64,7 +64,7 @@ class WebSocketManager( private fun doConnect() { if (_state.value == ConnectionState.CONNECTING) return _state.value = ConnectionState.CONNECTING - Logger.d("WS connecting: $serverUrl") + Logger.d("WS connecting: ${serverUrl.substringBefore('?')}") val request = Request.Builder().url(serverUrl).build() ws = client.newWebSocket(request, object : WebSocketListener() { diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaBridge.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaBridge.kt index 6909d8cdc8..5ebfac9ba5 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaBridge.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaBridge.kt @@ -65,10 +65,15 @@ class FridaBridge( fun getStatus(): Map = mapOf( "ready" to isReady, + // 当前 APK 只确认 frida-server 端口可达,尚未持有微信 Session/Script。 + // 必须保持 false,避免 UI 与心跳把“服务就绪”误报成“微信已注入”。 + "hooked" to false, "server_running" to serverManager.isRunning, "root_available" to serverManager.hasRoot(), ) + fun isTargetHooked(): Boolean = false + private fun loadDefaultScripts() { try { val assets = context.assets.list("") ?: return diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentAccessibilityService.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentAccessibilityService.kt index eb32b39eca..517e8d21de 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentAccessibilityService.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentAccessibilityService.kt @@ -39,6 +39,40 @@ class AgentAccessibilityService : AccessibilityService() { return flat.split(':').mapNotNull { ComponentName.unflattenFromString(it) } .any { it == expected } } + + /** + * 企业授权设备可由 ADB 一次性授予 WRITE_SECURE_SETTINGS,之后冷启动自动补齐 + * 本应用无障碍服务。保留系统中已经启用的其他服务,不覆盖用户配置。 + */ + fun enableIfAuthorized(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || + context.checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) != + android.content.pm.PackageManager.PERMISSION_GRANTED + ) return false + + return try { + val resolver = context.contentResolver + val expected = ComponentName(context, AgentAccessibilityService::class.java) + .flattenToString() + val current = Settings.Secure.getString( + resolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + ).orEmpty().split(':').filter { it.isNotBlank() }.toMutableList() + if (expected !in current) current += expected + Settings.Secure.putString( + resolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + current.distinct().joinToString(":"), + ) && Settings.Secure.putInt( + resolver, + Settings.Secure.ACCESSIBILITY_ENABLED, + 1, + ) + } catch (e: SecurityException) { + Log.w(TAG, "WRITE_SECURE_SETTINGS 未授权,保留手动授权流程", e) + false + } + } } private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentForegroundService.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentForegroundService.kt index ed9e4489e7..d192f77236 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentForegroundService.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentForegroundService.kt @@ -46,6 +46,7 @@ class AgentForegroundService : Service() { } private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var engineStartJob: Job? = null var engine: AgentEngine? = null private set @@ -98,7 +99,14 @@ class AgentForegroundService : Service() { isRunning = true startForeground(NOTIFICATION_ID, createNotification("正在连接...")) - initAndStartEngine(serverUrl, projectId, deviceId) + synchronized(this) { + if (engine != null || engineStartJob?.isActive == true) { + Logger.i("AgentEngine 已存在或正在启动,忽略重复 START,仅触发重连") + engine?.onNetworkAvailable() + } else { + initAndStartEngine(serverUrl, projectId, deviceId) + } + } } ACTION_STOP -> { engine?.stop() @@ -110,7 +118,7 @@ class AgentForegroundService : Service() { } private fun initAndStartEngine(serverUrlIn: String, projectId: String, deviceId: String) { - serviceScope.launch { + engineStartJob = serviceScope.launch { // BIND-05:未绑定 server_url 时,先走 UDP beacon 自动发现(同网零配置) var serverUrl = serverUrlIn if (serverUrl.isBlank()) { @@ -215,7 +223,7 @@ class AgentForegroundService : Service() { PendingIntent.FLAG_IMMUTABLE, ) return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("AI数智员工") + .setContentTitle("工作手机") .setContentText(sanitize(status)) .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentIntent(pi) diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/ui/DashboardFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/ui/DashboardFragment.kt index 46bedfa25e..a83fc51981 100644 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/ui/DashboardFragment.kt +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/ui/DashboardFragment.kt @@ -34,9 +34,8 @@ class DashboardFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val deviceId = DeviceInfo.getOrCreateDeviceId(requireContext()) - view.findViewById(R.id.tvDeviceIdHeader).text = deviceId - view.findViewById(R.id.tvDeviceIdMd5).text = "MD5 ${DeviceInfo.deviceIdMd5(requireContext())}" + view.findViewById(R.id.tvDeviceIdHeader).text = "设备 ID · MD5" + view.findViewById(R.id.tvDeviceIdMd5).text = DeviceInfo.deviceIdMd5(requireContext()) view.findViewById(R.id.btnScanQr).setOnClickListener { startQrScan() } view.findViewById