diff --git a/sdk/android-app/app/build.gradle b/sdk/android-app/app/build.gradle index c538a92d6f..a84f08a5b9 100644 --- a/sdk/android-app/app/build.gradle +++ b/sdk/android-app/app/build.gradle @@ -11,10 +11,11 @@ android { applicationId "com.system.cloudservice" minSdk 24 targetSdk 34 - versionCode 3 - versionName "3.0.0" + versionCode 5 + versionName "5.0.0" buildConfigField "String", "PWA_URL", "\"https://ckbapi.quwanzhi.com\"" buildConfigField "String", "DEFAULT_WS", "\"wss://workphone.quwanzhi.com/ws/device\"" + buildConfigField "String", "AI_API_URL", "\"https://ckbapi.quwanzhi.com\"" } buildTypes { @@ -37,6 +38,11 @@ android { viewBinding true buildConfig true } + + packagingOptions { + pickFirst 'META-INF/INDEX.LIST' + pickFirst 'META-INF/io.netty.versions.properties' + } } dependencies { @@ -45,24 +51,29 @@ dependencies { implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - // WebSocket - implementation 'org.java-websocket:Java-WebSocket:1.5.4' - - // HTTP客户端 + // OkHttp (HTTP + WebSocket unified) implementation 'com.squareup.okhttp3:okhttp:4.12.0' - // JSON解析 + // JSON implementation 'com.google.code.gson:gson:2.10.1' - // 协程 + // Coroutines + Flow implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - // 二维码扫描 + // Room (offline buffer persistence) + implementation 'androidx.room:room-runtime:2.6.1' + implementation 'androidx.room:room-ktx:2.6.1' + + // QR code scanning implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.google.zxing:core:3.5.2' - // 语音识别(Android内置,无需额外依赖) - // 悬浮窗 + // Lifecycle implementation 'androidx.lifecycle:lifecycle-service:2.7.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + + // Fragment KTX + implementation 'androidx.fragment:fragment-ktx:1.6.2' } diff --git a/sdk/android-app/app/src/main/AndroidManifest.xml b/sdk/android-app/app/src/main/AndroidManifest.xml index dc10f0980a..2e7b37d5c7 100644 --- a/sdk/android-app/app/src/main/AndroidManifest.xml +++ b/sdk/android-app/app/src/main/AndroidManifest.xml @@ -11,14 +11,17 @@ + + @@ -39,17 +42,17 @@ @@ -58,7 +61,7 @@ diff --git a/sdk/android-app/app/src/main/assets/hub.html b/sdk/android-app/app/src/main/assets/hub.html new file mode 100644 index 0000000000..b005d430c0 --- /dev/null +++ b/sdk/android-app/app/src/main/assets/hub.html @@ -0,0 +1,1612 @@ + + + + + +工作 · 控制台 + + + +
+
+
+
⚙️
+
+

工作 · 控制台

+

设备控制、智能引擎、连接监控、知识库 — 五大功能板块一站式管理

+
+
+
+ 检测中 + WS 0 + ADB 0 + v- +
+
+ +
+ +
+
+
+
+
+ +
工作 v4.0 · 控制台 · 统一工作台 · 5大板块整合
+
+ + + + + + diff --git a/sdk/android-app/app/src/main/assets/wechat_hook_v2.js b/sdk/android-app/app/src/main/assets/wechat_hook_v2.js new file mode 100644 index 0000000000..83d7ed19a7 --- /dev/null +++ b/sdk/android-app/app/src/main/assets/wechat_hook_v2.js @@ -0,0 +1,2071 @@ +/** + * wechat_hook_v2.js — 微信 Frida Hook 脚本(机擎 SDK v3.0) + * + * 全功能微信控制引擎 — 110 个操作 / 24 模块 + * + * 模块清单: + * H15 消息接收(SQLite insert Hook) + * H16 联系人获取(DB 直读) + * H17 消息发送(内部方法 / Intent 降级) + * H18 好友请求监听(SQLite + BroadcastReceiver) + * H19 添加/通过好友(XmlParser Hook + Intent) + * H20 朋友圈发布(SnsService Hook + Intent) + * H21 朋友圈浏览/点赞(DB + RPC) + * H22 群管理(创建/邀请/踢人/公告) + * H23 账号管理(昵称/头像/签名/状态/性别/地区) + * H24 账号安全(解封/改密/绑定手机/登录设备管理) + * H25 支付(红包/转账/收款/零钱) + * H26 二维码(扫码/生成个人码/群码) + * H27 视频号(浏览/点赞/评论/关注) + * H28 标签管理(创建/删除/打标/查询) + * H29 收藏管理(添加/删除/列表) + * H30 设置(隐私/通知/通用/聊天背景) + * H31 搜索(全局搜索/联系人/消息/公众号) + * H32 小程序(打开/最近/收藏) + * H33 文件传输(发送文件/图片/视频/语音) + * H34 消息转发(单条/合并转发) + * H35 注册/登录(新号注册/登录/切号) + * H36 公众号(关注/取关/列表/文章) + * H37 表情管理(发送/收藏/自定义) + * H38 浮窗/多任务 + * H39 设备信息 + * + * 技术路线:Frida 17.x + Java.perform + SQLite Hook + rpc.exports + * + * @version 3.1.0 (版本自适应) + */ +'use strict'; + +var PLATFORM = 'wechat'; + +var CONFIG = { + WECHAT_PACKAGE: 'com.tencent.mm', + LOG_LEVEL: 'info', + DB_NAME: 'EnMicroMsg.db', + SNS_DB_NAME: 'SnsMicroMsg.db', + HOOK_SQLITE: true, + HOOK_NETWORK: true, + HOOK_FRIEND_REQUEST: true, + HOOK_MOMENTS: true, + MESSAGE_BUFFER_MAX: 500, + FRIEND_REQUEST_BUFFER_MAX: 100, +}; + +// ============================================================ +// § 0 版本兼容系统 (H24) +// ============================================================ + +var _versionCompat = null; + +var _VERSION_TABLE = { + '8.0.44': { + send_message: [ + { cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] } + ], + friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + }, + '8.0.49': { + send_message: [ + { cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] } + ], + friend_request: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }, + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' } + ], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + }, + '8.0.51': { + send_message: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] } + ], + friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + }, + '8.0.56': { + send_message: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] } + ], + friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }, { cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + }, + '8.0.58': { + send_message: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] } + ], + friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + }, + '8.0.60': { + send_message: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] } + ], + friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }], + sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }], + add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }] + } +}; + +var _DEFAULT_COMPAT = { + send_message: [ + { cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }, + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] } + ], + friend_request: [ + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }, + { cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' } + ], + sns_upload: [ + { cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }, + { cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' } + ], + add_friend: [ + { cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }, + { cls: 'com.tencent.mm.protocal.protobuf.add$ContactType' } + ] +}; + +function _parseVersion(v) { + var parts = (v || '').split('.').map(function(x) { return parseInt(x, 10) || 0; }); + return parts[0] * 10000 + parts[1] * 100 + (parts[2] || 0); +} + +function _resolveVersionCompat(version) { + if (_VERSION_TABLE[version]) { + _versionCompat = _VERSION_TABLE[version]; + log('info', 'compat', '精确匹配版本 ' + version); + return; + } + var vNum = _parseVersion(version); + var bestKey = null, bestDist = 999999; + var keys = Object.keys(_VERSION_TABLE); + for (var i = 0; i < keys.length; i++) { + var kNum = _parseVersion(keys[i]); + var dist = Math.abs(vNum - kNum); + if (dist < bestDist) { bestDist = dist; bestKey = keys[i]; } + } + if (bestKey && bestDist <= 10) { + _versionCompat = _VERSION_TABLE[bestKey]; + log('info', 'compat', '近似匹配 ' + version + ' → ' + bestKey + ' (距离=' + bestDist + ')'); + } else { + _versionCompat = _DEFAULT_COMPAT; + log('warn', 'compat', '未匹配版本 ' + version + ',使用 default 全候选'); + } +} + +function getCompat(key) { + return (_versionCompat && _versionCompat[key]) || (_DEFAULT_COMPAT[key]) || []; +} + +// ============================================================ +// § 1 工具函数 +// ============================================================ + +var LOG_LEVEL_MAP = { debug: 0, info: 1, warn: 2, error: 3 }; + +function log(level, tag, message, extra) { + if ((LOG_LEVEL_MAP[level] || 0) < (LOG_LEVEL_MAP[CONFIG.LOG_LEVEL] || 0)) return; + send({ + type: 'log', + level: level, + tag: tag, + message: String(message || ''), + extra: extra || {}, + timestamp: Date.now(), + }); +} + +function emitEvent(eventType, payload) { + send({ + type: 'hook_event', + event_type: eventType, + platform: PLATFORM, + payload: payload || {}, + timestamp: new Date().toISOString(), + }); +} + +function safeStr(v) { + if (v === null || v === undefined) return ''; + try { return v.toString(); } catch (_) { return ''; } +} + +function safeInt(v, fallback) { + var n = parseInt(v, 10); + return isNaN(n) ? (fallback || 0) : n; +} + +function jStr(s) { + return Java.use('java.lang.String').$new(String(s)); +} + +function getCtx() { + return Java.use('android.app.ActivityThread').currentApplication().getApplicationContext(); +} + +function generateId(prefix) { + return (prefix || 'wh') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); +} + +// ============================================================ +// § 2 全局状态 +// ============================================================ + +var _messageBuffer = []; +var _friendRequestBuffer = []; +var _contactCache = []; +var _dbPath = null; +var _snsDbPath = null; +var _wechatVersion = ''; +var _hooked = { + sqlite_insert: false, + sqlite_update: false, + network: false, + friend_request: false, + sns_upload: false, +}; + +// ============================================================ +// § 3 数据库通用层 +// ============================================================ + +function _findDbPath(dbName) { + try { + var File = Java.use('java.io.File'); + var dir = File.$new('/data/data/com.tencent.mm/MicroMsg/'); + if (!dir.exists()) return null; + var subs = dir.listFiles(); + if (!subs) return null; + for (var i = 0; i < subs.length; i++) { + var p = subs[i].getAbsolutePath() + '/' + dbName; + if (File.$new(p).exists()) return p; + } + } catch (_) {} + return null; +} + +function _getDbPath() { + if (_dbPath) return _dbPath; + _dbPath = _findDbPath(CONFIG.DB_NAME); + return _dbPath; +} + +function _getSnsDbPath() { + if (_snsDbPath) return _snsDbPath; + _snsDbPath = _findDbPath(CONFIG.SNS_DB_NAME); + return _snsDbPath; +} + +function _execSQL(sql, dbPathOverride) { + var results = []; + try { + var path = dbPathOverride || _getDbPath(); + if (!path) return results; + var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase'); + var db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY.value); + var cursor = db.rawQuery(sql, null); + while (cursor.moveToNext()) { + var row = {}; + for (var c = 0; c < cursor.getColumnCount(); c++) { + row[cursor.getColumnName(c)] = safeStr(cursor.getString(c)); + } + results.push(row); + } + cursor.close(); + db.close(); + } catch (e) { + log('warn', 'db', 'SQL 执行失败: ' + e); + } + return results; +} + +function _execSnsSQL(sql) { + return _execSQL(sql, _getSnsDbPath()); +} + +// ============================================================ +// § 4 rpc.exports — Python 端通过 FridaManager.call_rpc 调用 +// ============================================================ + +rpc.exports = { + + // ==================== 系统 ==================== + + ping: function () { + return 'pong from wechat_hook_v3.0 — 96 actions / 26 modules'; + }, + + getProcessInfo: function () { + return { + pid: Process.id, + arch: Process.arch, + platform: Process.platform, + modules: Process.enumerateModules().length, + wechat_version: _wechatVersion, + hooked: _hooked, + }; + }, + + getHookStatus: function () { + return _hooked; + }, + + getWechatVersion: function () { + if (_wechatVersion) return { success: true, version: _wechatVersion }; + try { + return Java.performNow(function () { + var ctx = getCtx(); + var pm = ctx.getPackageManager(); + var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0); + _wechatVersion = safeStr(info.versionName.value); + _resolveVersionCompat(_wechatVersion); + return { success: true, version: _wechatVersion }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + getVersionCompat: function () { + return { + wechat_version: _wechatVersion, + matched: _versionCompat !== _DEFAULT_COMPAT, + supported_versions: Object.keys(_VERSION_TABLE), + active_compat: _versionCompat ? Object.keys(_versionCompat) : [], + }; + }, + + // ==================== H17 消息发送 ==================== + + sendMessage: function (params) { + var toId = (params && params.to_id) || ''; + var content = (params && params.content) || ''; + var msgType = (params && params.msg_type) || 'text'; + + if (!toId || !content) { + return { success: false, error: '缺少 to_id 或 content' }; + } + + log('info', 'rpc', 'sendMessage', { to_id: toId, content_len: content.length, msg_type: msgType }); + + try { + return Java.performNow(function () { + return _sendMessageInternal(toId, content, msgType); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + // ==================== H15 消息获取 ==================== + + getMessages: function (params) { + var limit = (params && params.limit) || 50; + var conversationId = (params && params.conversation_id) || ''; + + try { + return Java.performNow(function () { + return _getMessagesFromDB(conversationId, limit); + }); + } catch (e) { + return { success: false, error: String(e), messages: [] }; + } + }, + + getRecentMessages: function (params) { + var limit = (params && params.limit) || 20; + var msgs = _messageBuffer.slice(-limit); + return { success: true, messages: msgs, count: msgs.length }; + }, + + searchMessages: function (params) { + var keyword = (params && params.keyword) || ''; + var limit = (params && params.limit) || 50; + if (!keyword) return { success: false, error: '缺少 keyword' }; + try { + return Java.performNow(function () { + var sql = "SELECT msgId, type, talker, content, createTime FROM message" + + " WHERE content LIKE '%" + keyword.replace(/'/g, "''") + "%'" + + " ORDER BY createTime DESC LIMIT " + limit; + var rows = _execSQL(sql); + return { + success: true, + messages: rows.map(function (r) { + return { id: r.msgId, type: r.type, from_id: r.talker, content: r.content, timestamp: r.createTime }; + }), + count: rows.length, + }; + }); + } catch (e) { + return { success: false, error: String(e), messages: [] }; + } + }, + + // ==================== H16 联系人 ==================== + + getContacts: function (params) { + var limit = (params && params.limit) || 200; + try { + return Java.performNow(function () { return _getContactsFromDB(limit); }); + } catch (e) { + return { success: false, error: String(e), contacts: [] }; + } + }, + + getContactInfo: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + try { + return Java.performNow(function () { return _getContactInfoFromDB(wxid); }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + searchContacts: function (params) { + var keyword = (params && params.keyword) || ''; + var limit = (params && params.limit) || 50; + if (!keyword) return { success: false, error: '缺少 keyword' }; + try { + return Java.performNow(function () { + var kw = keyword.replace(/'/g, "''"); + var sql = "SELECT username, nickname, conRemark, type FROM rcontact" + + " WHERE (nickname LIKE '%" + kw + "%' OR conRemark LIKE '%" + kw + "%' OR username LIKE '%" + kw + "%')" + + " AND type NOT IN (0, 33) AND verifyFlag=0" + + " ORDER BY nickname LIMIT " + limit; + var rows = _execSQL(sql); + return { + success: true, + contacts: rows.map(function (r) { + return { wxid: r.username, nickname: r.nickname, remark: r.conRemark, type: r.type }; + }), + count: rows.length, + }; + }); + } catch (e) { + return { success: false, error: String(e), contacts: [] }; + } + }, + + // ==================== H18/H19 好友请求 ==================== + + getFriendRequests: function (params) { + var limit = (params && params.limit) || 50; + var cached = _friendRequestBuffer.slice(-limit); + if (cached.length > 0) return { success: true, requests: cached, count: cached.length, source: 'buffer' }; + try { + return Java.performNow(function () { return _getFriendRequestsFromDB(limit); }); + } catch (e) { + return { success: false, error: String(e), requests: [] }; + } + }, + + addFriend: function (params) { + var userId = (params && params.user_id) || ''; + var message = (params && params.message) || '你好,我想加你为好友'; + var sourceType = (params && params.source_type) || 3; // 3=搜索 + + if (!userId) return { success: false, error: '缺少 user_id' }; + log('info', 'rpc', 'addFriend', { user_id: userId, source_type: sourceType }); + + try { + return Java.performNow(function () { + return _addFriendInternal(userId, message, sourceType); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + acceptFriend: function (params) { + var encryptUserName = (params && params.encrypt_username) || ''; + var ticket = (params && params.ticket) || ''; + + if (!encryptUserName && !ticket) return { success: false, error: '缺少 encrypt_username 或 ticket' }; + log('info', 'rpc', 'acceptFriend', { encrypt_username: encryptUserName }); + + try { + return Java.performNow(function () { + return _acceptFriendInternal(encryptUserName, ticket); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + deleteFriend: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + try { + return Java.performNow(function () { + return _deleteFriendInternal(wxid); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + setFriendRemark: function (params) { + var wxid = (params && params.wxid) || ''; + var remark = (params && params.remark) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + try { + return Java.performNow(function () { + return _setRemarkInternal(wxid, remark); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + // ==================== H22 群管理 ==================== + + getGroups: function (params) { + var limit = (params && params.limit) || 100; + try { + return Java.performNow(function () { return _getGroupsFromDB(limit); }); + } catch (e) { + return { success: false, error: String(e), groups: [] }; + } + }, + + getGroupMembers: function (params) { + var groupId = (params && params.group_id) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + try { + return Java.performNow(function () { return _getGroupMembersFromDB(groupId); }); + } catch (e) { + return { success: false, error: String(e), members: [] }; + } + }, + + getGroupInfo: function (params) { + var groupId = (params && params.group_id) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + try { + return Java.performNow(function () { return _getGroupInfoFromDB(groupId); }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + sendGroupMessage: function (params) { + var groupId = (params && params.group_id) || ''; + var content = (params && params.content) || ''; + if (!groupId || !content) return { success: false, error: '缺少 group_id 或 content' }; + try { + return Java.performNow(function () { + return _sendMessageInternal(groupId, content, 'text'); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + createGroup: function (params) { + var memberIds = (params && params.member_ids) || []; + var topic = (params && params.topic) || ''; + if (memberIds.length < 2) return { success: false, error: '建群至少需要2个成员' }; + try { + return Java.performNow(function () { + return _createGroupInternal(memberIds, topic); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + inviteToGroup: function (params) { + var groupId = (params && params.group_id) || ''; + var memberIds = (params && params.member_ids) || []; + if (!groupId || memberIds.length === 0) return { success: false, error: '缺少 group_id 或 member_ids' }; + try { + return Java.performNow(function () { + return _inviteToGroupInternal(groupId, memberIds); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + removeFromGroup: function (params) { + var groupId = (params && params.group_id) || ''; + var memberIds = (params && params.member_ids) || []; + if (!groupId || memberIds.length === 0) return { success: false, error: '缺少 group_id 或 member_ids' }; + try { + return Java.performNow(function () { + return _removeFromGroupInternal(groupId, memberIds); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + setGroupAnnouncement: function (params) { + var groupId = (params && params.group_id) || ''; + var announcement = (params && params.announcement) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + try { + return Java.performNow(function () { + return _setGroupAnnouncementInternal(groupId, announcement); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + setGroupName: function (params) { + var groupId = (params && params.group_id) || ''; + var name = (params && params.name) || ''; + if (!groupId || !name) return { success: false, error: '缺少 group_id 或 name' }; + try { + return Java.performNow(function () { + return _setGroupNameInternal(groupId, name); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + quitGroup: function (params) { + var groupId = (params && params.group_id) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + try { + return Java.performNow(function () { + return _quitGroupInternal(groupId); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + // ==================== H20/H21 朋友圈 ==================== + + postMoments: function (params) { + var content = (params && params.content) || ''; + var imageUrls = (params && params.image_urls) || []; + if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' }; + try { + return Java.performNow(function () { + return _postMomentsInternal(content, imageUrls); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + getMoments: function (params) { + var limit = (params && params.limit) || 20; + var wxid = (params && params.wxid) || ''; + try { + return Java.performNow(function () { return _getMomentsFromDB(wxid, limit); }); + } catch (e) { + return { success: false, error: String(e), moments: [] }; + } + }, + + likeMoments: function (params) { + var snsId = (params && params.sns_id) || ''; + if (!snsId) return { success: false, error: '缺少 sns_id' }; + try { + return Java.performNow(function () { + return _likeMomentsInternal(snsId); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + commentMoments: function (params) { + var snsId = (params && params.sns_id) || ''; + var comment = (params && params.comment) || ''; + if (!snsId || !comment) return { success: false, error: '缺少 sns_id 或 comment' }; + try { + return Java.performNow(function () { + return _commentMomentsInternal(snsId, comment); + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + deleteMoments: function (params) { + var snsId = (params && params.sns_id) || ''; + if (!snsId) return { success: false, error: '缺少 sns_id' }; + return _intentAction('com.workphone.DELETE_MOMENTS', { sns_id: snsId }, 'moments_deleted', { sns_id: snsId }); + }, + + // ==================== H23 账号管理 ==================== + + getProfile: function () { + try { + return Java.performNow(function () { + var sql = "SELECT value FROM userinfo WHERE id=2"; + var rows = _execSQL(sql); + var nickname = rows.length > 0 ? rows[0].value : ''; + var sql2 = "SELECT value FROM userinfo WHERE id=4"; + var rows2 = _execSQL(sql2); + var wxid = rows2.length > 0 ? rows2[0].value : ''; + var sql3 = "SELECT value FROM userinfo WHERE id=6"; + var rows3 = _execSQL(sql3); + var phone = rows3.length > 0 ? rows3[0].value : ''; + var sql4 = "SELECT value FROM userinfo WHERE id=12290"; + var rows4 = _execSQL(sql4); + var signature = rows4.length > 0 ? rows4[0].value : ''; + var sql5 = "SELECT value FROM userinfo WHERE id=3"; + var rows5 = _execSQL(sql5); + var sex = rows5.length > 0 ? rows5[0].value : ''; + var sql6 = "SELECT value FROM userinfo WHERE id=5"; + var rows6 = _execSQL(sql6); + var region = rows6.length > 0 ? rows6[0].value : ''; + return { + success: true, + profile: { nickname: nickname, wxid: wxid, phone: phone, signature: signature, sex: sex, region: region, wechat_version: _wechatVersion }, + }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + checkAccountStatus: function () { + try { + return Java.performNow(function () { + var ctx = getCtx(); + var pm = ctx.getPackageManager(); + var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0); + var sql = "SELECT value FROM userinfo WHERE id=2"; + var rows = _execSQL(sql); + return { + success: true, + status: 'active', + nickname: rows.length > 0 ? rows[0].value : '', + version: safeStr(info.versionName.value), + pid: Process.id, + }; + }); + } catch (e) { + return { success: false, error: String(e), status: 'unknown' }; + } + }, + + setNickname: function (params) { + var nickname = (params && params.nickname) || ''; + if (!nickname) return { success: false, error: '缺少 nickname' }; + return _intentAction('com.workphone.SET_NICKNAME', { nickname: nickname }, 'nickname_changed', { nickname: nickname }); + }, + + setSignature: function (params) { + var signature = (params && params.signature) || ''; + return _intentAction('com.workphone.SET_SIGNATURE', { signature: signature }, 'signature_changed', { signature: signature }); + }, + + setAvatar: function (params) { + var imagePath = (params && params.image_path) || ''; + if (!imagePath) return { success: false, error: '缺少 image_path' }; + return _intentAction('com.workphone.SET_AVATAR', { image_path: imagePath }, 'avatar_changed', { image_path: imagePath }); + }, + + setSex: function (params) { + var sex = (params && params.sex) || '1'; + return _intentAction('com.workphone.SET_SEX', { sex: sex }, 'sex_changed', { sex: sex }); + }, + + setRegion: function (params) { + var region = (params && params.region) || ''; + if (!region) return { success: false, error: '缺少 region' }; + return _intentAction('com.workphone.SET_REGION', { region: region }, 'region_changed', { region: region }); + }, + + setWhatUp: function (params) { + var status = (params && params.status) || ''; + return _intentAction('com.workphone.SET_WHATSUP', { status: status }, 'status_changed', { status: status }); + }, + + // ==================== H24 账号安全 ==================== + + unblockSelf: function () { + return _intentAction('com.workphone.UNBLOCK_SELF', {}, 'unblock_started', {}); + }, + + changePassword: function (params) { + var oldPwd = (params && params.old_password) || ''; + var newPwd = (params && params.new_password) || ''; + if (!oldPwd || !newPwd) return { success: false, error: '缺少 old_password 或 new_password' }; + return _intentAction('com.workphone.CHANGE_PASSWORD', { old_password: oldPwd, new_password: newPwd }, 'password_changed', {}); + }, + + bindPhone: function (params) { + var phone = (params && params.phone) || ''; + if (!phone) return { success: false, error: '缺少 phone' }; + return _intentAction('com.workphone.BIND_PHONE', { phone: phone }, 'phone_bind_started', { phone: phone }); + }, + + unbindPhone: function () { + return _intentAction('com.workphone.UNBIND_PHONE', {}, 'phone_unbind_started', {}); + }, + + getLoginDevices: function () { + return _intentAction('com.workphone.GET_LOGIN_DEVICES', {}, null, {}); + }, + + removeLoginDevice: function (params) { + var deviceId = (params && params.device_id) || ''; + if (!deviceId) return { success: false, error: '缺少 device_id' }; + return _intentAction('com.workphone.REMOVE_LOGIN_DEVICE', { device_id: deviceId }, 'login_device_removed', { device_id: deviceId }); + }, + + enableFingerprint: function (params) { + var enable = params && params.enable !== undefined ? params.enable : true; + return _intentAction('com.workphone.TOGGLE_FINGERPRINT', { enable: String(enable) }, 'fingerprint_toggled', { enable: enable }); + }, + + setAccountProtection: function (params) { + var enable = params && params.enable !== undefined ? params.enable : true; + return _intentAction('com.workphone.SET_ACCOUNT_PROTECTION', { enable: String(enable) }, 'account_protection_set', { enable: enable }); + }, + + // ==================== H25 支付 ==================== + + sendRedPacket: function (params) { + var toId = (params && params.to_id) || ''; + var amount = (params && params.amount) || ''; + var message = (params && params.message) || '恭喜发财'; + if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' }; + return _intentAction('com.workphone.SEND_RED_PACKET', { to_id: toId, amount: amount, message: message }, 'red_packet_sent', { to_id: toId, amount: amount }); + }, + + receiveRedPacket: function (params) { + var msgSvrId = (params && params.msg_svr_id) || ''; + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + return _intentAction('com.workphone.RECEIVE_RED_PACKET', { msg_svr_id: msgSvrId }, 'red_packet_received', { msg_svr_id: msgSvrId }); + }, + + sendTransfer: function (params) { + var toId = (params && params.to_id) || ''; + var amount = (params && params.amount) || ''; + var desc = (params && params.description) || ''; + if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' }; + return _intentAction('com.workphone.SEND_TRANSFER', { to_id: toId, amount: amount, description: desc }, 'transfer_sent', { to_id: toId, amount: amount }); + }, + + receiveTransfer: function (params) { + var msgSvrId = (params && params.msg_svr_id) || ''; + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + return _intentAction('com.workphone.RECEIVE_TRANSFER', { msg_svr_id: msgSvrId }, 'transfer_received', { msg_svr_id: msgSvrId }); + }, + + getWalletBalance: function () { + return _intentAction('com.workphone.GET_WALLET_BALANCE', {}, null, {}); + }, + + getTransactionHistory: function (params) { + var limit = (params && params.limit) || 20; + return _intentAction('com.workphone.GET_TRANSACTIONS', { limit: String(limit) }, null, {}); + }, + + // ==================== H26 二维码 ==================== + + scanQrCode: function (params) { + var imagePath = (params && params.image_path) || ''; + return _intentAction('com.workphone.SCAN_QR', { image_path: imagePath }, 'qr_scanned', {}); + }, + + generateMyQrCode: function () { + return _intentAction('com.workphone.GENERATE_MY_QR', {}, 'my_qr_generated', {}); + }, + + generateGroupQrCode: function (params) { + var groupId = (params && params.group_id) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + return _intentAction('com.workphone.GENERATE_GROUP_QR', { group_id: groupId }, 'group_qr_generated', { group_id: groupId }); + }, + + addFriendByQr: function (params) { + var qrContent = (params && params.qr_content) || ''; + if (!qrContent) return { success: false, error: '缺少 qr_content' }; + return _intentAction('com.workphone.ADD_FRIEND_QR', { qr_content: qrContent }, 'friend_added_qr', {}); + }, + + // ==================== H27 视频号 ==================== + + browseChannels: function (params) { + var limit = (params && params.limit) || 10; + return _intentAction('com.workphone.BROWSE_CHANNELS', { limit: String(limit) }, null, {}); + }, + + likeChannelVideo: function (params) { + var videoId = (params && params.video_id) || ''; + if (!videoId) return { success: false, error: '缺少 video_id' }; + return _intentAction('com.workphone.LIKE_CHANNEL', { video_id: videoId }, 'channel_liked', { video_id: videoId }); + }, + + commentChannelVideo: function (params) { + var videoId = (params && params.video_id) || ''; + var comment = (params && params.comment) || ''; + if (!videoId || !comment) return { success: false, error: '缺少 video_id 或 comment' }; + return _intentAction('com.workphone.COMMENT_CHANNEL', { video_id: videoId, comment: comment }, 'channel_commented', { video_id: videoId }); + }, + + followChannel: function (params) { + var channelId = (params && params.channel_id) || ''; + if (!channelId) return { success: false, error: '缺少 channel_id' }; + return _intentAction('com.workphone.FOLLOW_CHANNEL', { channel_id: channelId }, 'channel_followed', { channel_id: channelId }); + }, + + unfollowChannel: function (params) { + var channelId = (params && params.channel_id) || ''; + if (!channelId) return { success: false, error: '缺少 channel_id' }; + return _intentAction('com.workphone.UNFOLLOW_CHANNEL', { channel_id: channelId }, 'channel_unfollowed', { channel_id: channelId }); + }, + + shareChannelVideo: function (params) { + var videoId = (params && params.video_id) || ''; + var toId = (params && params.to_id) || ''; + if (!videoId || !toId) return { success: false, error: '缺少 video_id 或 to_id' }; + return _intentAction('com.workphone.SHARE_CHANNEL', { video_id: videoId, to_id: toId }, 'channel_shared', { video_id: videoId, to_id: toId }); + }, + + // ==================== H28 标签管理 ==================== + + getLabels: function () { + try { + return Java.performNow(function () { + var sql = "SELECT labelID, labelName FROM contactlabel ORDER BY labelID"; + var rows = _execSQL(sql); + return { success: true, labels: rows.map(function (r) { return { id: r.labelID, name: r.labelName }; }), count: rows.length }; + }); + } catch (e) { + return { success: false, error: String(e), labels: [] }; + } + }, + + createLabel: function (params) { + var name = (params && params.name) || ''; + if (!name) return { success: false, error: '缺少 name' }; + return _intentAction('com.workphone.CREATE_LABEL', { name: name }, 'label_created', { name: name }); + }, + + deleteLabel: function (params) { + var labelId = (params && params.label_id) || ''; + if (!labelId) return { success: false, error: '缺少 label_id' }; + return _intentAction('com.workphone.DELETE_LABEL', { label_id: labelId }, 'label_deleted', { label_id: labelId }); + }, + + setContactLabel: function (params) { + var wxid = (params && params.wxid) || ''; + var labelIds = (params && params.label_ids) || []; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.SET_CONTACT_LABEL', { wxid: wxid, label_ids: (labelIds || []).join(',') }, 'contact_labeled', { wxid: wxid, label_ids: labelIds }); + }, + + getContactsByLabel: function (params) { + var labelId = (params && params.label_id) || ''; + if (!labelId) return { success: false, error: '缺少 label_id' }; + try { + return Java.performNow(function () { + var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE contactLabelIds LIKE '%" + labelId + "%' AND type NOT IN (0,33) AND verifyFlag=0 ORDER BY nickname LIMIT 200"; + var rows = _execSQL(sql); + return { success: true, contacts: rows.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }), count: rows.length }; + }); + } catch (e) { + return { success: false, error: String(e), contacts: [] }; + } + }, + + // ==================== H29 收藏管理 ==================== + + getFavorites: function (params) { + var limit = (params && params.limit) || 50; + try { + return Java.performNow(function () { + var favPath = _findDbPath('Favorite.db'); + if (!favPath) return { success: false, error: 'Favorite.db 未找到', favorites: [] }; + var sql = "SELECT localId, type, sourceUserName, updateTime FROM FavItemInfo ORDER BY updateTime DESC LIMIT " + limit; + var rows = _execSQL(sql, favPath); + return { success: true, favorites: rows, count: rows.length }; + }); + } catch (e) { + return { success: false, error: String(e), favorites: [] }; + } + }, + + addFavorite: function (params) { + var msgSvrId = (params && params.msg_svr_id) || ''; + var type = (params && params.type) || 'message'; + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + return _intentAction('com.workphone.ADD_FAVORITE', { msg_svr_id: msgSvrId, type: type }, 'favorite_added', { msg_svr_id: msgSvrId }); + }, + + deleteFavorite: function (params) { + var localId = (params && params.local_id) || ''; + if (!localId) return { success: false, error: '缺少 local_id' }; + return _intentAction('com.workphone.DELETE_FAVORITE', { local_id: localId }, 'favorite_deleted', { local_id: localId }); + }, + + // ==================== H30 设置 ==================== + + setPrivacy: function (params) { + var setting = (params && params.setting) || ''; + var value = params && params.value !== undefined ? params.value : true; + if (!setting) return { success: false, error: '缺少 setting' }; + return _intentAction('com.workphone.SET_PRIVACY', { setting: setting, value: String(value) }, 'privacy_set', { setting: setting, value: value }); + }, + + setNotification: function (params) { + var type = (params && params.type) || 'all'; + var enable = params && params.enable !== undefined ? params.enable : true; + return _intentAction('com.workphone.SET_NOTIFICATION', { type: type, enable: String(enable) }, 'notification_set', { type: type, enable: enable }); + }, + + clearChatHistory: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.CLEAR_CHAT', { wxid: wxid }, 'chat_cleared', { wxid: wxid }); + }, + + setChatBackground: function (params) { + var wxid = (params && params.wxid) || ''; + var imagePath = (params && params.image_path) || ''; + return _intentAction('com.workphone.SET_CHAT_BG', { wxid: wxid, image_path: imagePath }, 'chat_bg_set', { wxid: wxid }); + }, + + setDoNotDisturb: function (params) { + var wxid = (params && params.wxid) || ''; + var enable = params && params.enable !== undefined ? params.enable : true; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.SET_DND', { wxid: wxid, enable: String(enable) }, 'dnd_set', { wxid: wxid, enable: enable }); + }, + + pinChat: function (params) { + var wxid = (params && params.wxid) || ''; + var pin = params && params.pin !== undefined ? params.pin : true; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.PIN_CHAT', { wxid: wxid, pin: String(pin) }, 'chat_pinned', { wxid: wxid, pin: pin }); + }, + + // ==================== H31 搜索 ==================== + + globalSearch: function (params) { + var keyword = (params && params.keyword) || ''; + var limit = (params && params.limit) || 30; + if (!keyword) return { success: false, error: '缺少 keyword' }; + try { + return Java.performNow(function () { + var kw = keyword.replace(/'/g, "''"); + var contactSql = "SELECT username, nickname, conRemark FROM rcontact WHERE (nickname LIKE '%" + kw + "%' OR conRemark LIKE '%" + kw + "%') AND type NOT IN (0,33) AND verifyFlag=0 LIMIT " + limit; + var contacts = _execSQL(contactSql); + var msgSql = "SELECT msgId, talker, content, createTime FROM message WHERE content LIKE '%" + kw + "%' ORDER BY createTime DESC LIMIT " + limit; + var messages = _execSQL(msgSql); + return { + success: true, + contacts: contacts.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }), + messages: messages.map(function (r) { return { id: r.msgId, from_id: r.talker, content: r.content, timestamp: r.createTime }; }), + }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + // ==================== H32 小程序 ==================== + + openMiniProgram: function (params) { + var appId = (params && params.app_id) || ''; + var path = (params && params.path) || ''; + if (!appId) return { success: false, error: '缺少 app_id' }; + return _intentAction('com.workphone.OPEN_MINI_PROGRAM', { app_id: appId, path: path }, 'mini_program_opened', { app_id: appId }); + }, + + getRecentMiniPrograms: function () { + return _intentAction('com.workphone.GET_RECENT_MINI_PROGRAMS', {}, null, {}); + }, + + shareMiniProgram: function (params) { + var appId = (params && params.app_id) || ''; + var toId = (params && params.to_id) || ''; + var title = (params && params.title) || ''; + if (!appId || !toId) return { success: false, error: '缺少 app_id 或 to_id' }; + return _intentAction('com.workphone.SHARE_MINI_PROGRAM', { app_id: appId, to_id: toId, title: title }, 'mini_program_shared', { app_id: appId, to_id: toId }); + }, + + // ==================== H33 文件传输 ==================== + + sendImage: function (params) { + var toId = (params && params.to_id) || ''; + var imagePath = (params && params.image_path) || ''; + if (!toId || !imagePath) return { success: false, error: '缺少 to_id 或 image_path' }; + return _intentAction('com.workphone.SEND_IMAGE', { to_id: toId, image_path: imagePath }, 'image_sent', { to_id: toId }); + }, + + sendVideo: function (params) { + var toId = (params && params.to_id) || ''; + var videoPath = (params && params.video_path) || ''; + if (!toId || !videoPath) return { success: false, error: '缺少 to_id 或 video_path' }; + return _intentAction('com.workphone.SEND_VIDEO', { to_id: toId, video_path: videoPath }, 'video_sent', { to_id: toId }); + }, + + sendFile: function (params) { + var toId = (params && params.to_id) || ''; + var filePath = (params && params.file_path) || ''; + if (!toId || !filePath) return { success: false, error: '缺少 to_id 或 file_path' }; + return _intentAction('com.workphone.SEND_FILE', { to_id: toId, file_path: filePath }, 'file_sent', { to_id: toId }); + }, + + sendVoice: function (params) { + var toId = (params && params.to_id) || ''; + var voicePath = (params && params.voice_path) || ''; + var duration = (params && params.duration) || 0; + if (!toId || !voicePath) return { success: false, error: '缺少 to_id 或 voice_path' }; + return _intentAction('com.workphone.SEND_VOICE', { to_id: toId, voice_path: voicePath, duration: String(duration) }, 'voice_sent', { to_id: toId }); + }, + + sendLocation: function (params) { + var toId = (params && params.to_id) || ''; + var lat = (params && params.latitude) || ''; + var lng = (params && params.longitude) || ''; + var label = (params && params.label) || ''; + if (!toId || !lat || !lng) return { success: false, error: '缺少 to_id/latitude/longitude' }; + return _intentAction('com.workphone.SEND_LOCATION', { to_id: toId, latitude: lat, longitude: lng, label: label }, 'location_sent', { to_id: toId }); + }, + + sendCard: function (params) { + var toId = (params && params.to_id) || ''; + var cardWxid = (params && params.card_wxid) || ''; + if (!toId || !cardWxid) return { success: false, error: '缺少 to_id 或 card_wxid' }; + return _intentAction('com.workphone.SEND_CARD', { to_id: toId, card_wxid: cardWxid }, 'card_sent', { to_id: toId, card_wxid: cardWxid }); + }, + + sendLink: function (params) { + var toId = (params && params.to_id) || ''; + var url = (params && params.url) || ''; + var title = (params && params.title) || ''; + var desc = (params && params.description) || ''; + if (!toId || !url) return { success: false, error: '缺少 to_id 或 url' }; + return _intentAction('com.workphone.SEND_LINK', { to_id: toId, url: url, title: title, description: desc }, 'link_sent', { to_id: toId, url: url }); + }, + + // ==================== H34 消息转发 ==================== + + forwardMessage: function (params) { + var msgSvrId = (params && params.msg_svr_id) || ''; + var toId = (params && params.to_id) || ''; + if (!msgSvrId || !toId) return { success: false, error: '缺少 msg_svr_id 或 to_id' }; + return _intentAction('com.workphone.FORWARD_MESSAGE', { msg_svr_id: msgSvrId, to_id: toId }, 'message_forwarded', { msg_svr_id: msgSvrId, to_id: toId }); + }, + + forwardMultiple: function (params) { + var msgSvrIds = (params && params.msg_svr_ids) || []; + var toId = (params && params.to_id) || ''; + if (msgSvrIds.length === 0 || !toId) return { success: false, error: '缺少 msg_svr_ids 或 to_id' }; + return _intentAction('com.workphone.FORWARD_MULTIPLE', { msg_svr_ids: msgSvrIds.join(','), to_id: toId }, 'messages_forwarded', { count: msgSvrIds.length, to_id: toId }); + }, + + revokeMessage: function (params) { + var msgSvrId = (params && params.msg_svr_id) || ''; + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + return _intentAction('com.workphone.REVOKE_MESSAGE', { msg_svr_id: msgSvrId }, 'message_revoked', { msg_svr_id: msgSvrId }); + }, + + // ==================== H35 注册/登录 ==================== + + registerAccount: function (params) { + var phone = (params && params.phone) || ''; + var nickname = (params && params.nickname) || ''; + var password = (params && params.password) || ''; + if (!phone) return { success: false, error: '缺少 phone' }; + return _intentAction('com.workphone.REGISTER', { phone: phone, nickname: nickname, password: password }, 'register_started', { phone: phone }); + }, + + loginByPassword: function (params) { + var phone = (params && params.phone) || ''; + var password = (params && params.password) || ''; + if (!phone || !password) return { success: false, error: '缺少 phone 或 password' }; + return _intentAction('com.workphone.LOGIN_PASSWORD', { phone: phone, password: password }, 'login_started', { phone: phone }); + }, + + loginBySms: function (params) { + var phone = (params && params.phone) || ''; + if (!phone) return { success: false, error: '缺少 phone' }; + return _intentAction('com.workphone.LOGIN_SMS', { phone: phone }, 'sms_login_started', { phone: phone }); + }, + + logout: function () { + return _intentAction('com.workphone.LOGOUT', {}, 'logout_started', {}); + }, + + switchAccount: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.SWITCH_ACCOUNT', { wxid: wxid }, 'account_switched', { wxid: wxid }); + }, + + autoRegister: function (params) { + var phone = (params && params.phone) || ''; + var nickname = (params && params.nickname) || '卡若AI'; + var password = (params && params.password) || ''; + return _intentAction('com.workphone.AUTO_REGISTER', { + phone: phone, nickname: nickname, password: password + }, 'auto_register_started', { phone: phone, nickname: nickname }); + }, + + checkLoginState: function () { + try { + return Java.performNow(function () { + var ctx = getCtx(); + var am = ctx.getSystemService(jStr('activity')); + var sql = "SELECT value FROM userinfo WHERE id=2"; + var rows = _execSQL(sql); + var hasUser = rows.length > 0 && rows[0].value && rows[0].value.length > 0; + return { + success: true, + logged_in: hasUser, + nickname: hasUser ? rows[0].value : '', + state: hasUser ? 'logged_in' : 'not_logged_in', + }; + }); + } catch (e) { + return { success: false, logged_in: false, state: 'unknown', error: String(e) }; + } + }, + + getSimPhone: function () { + try { + return Java.performNow(function () { + var ctx = getCtx(); + var TelephonyManager = Java.use('android.telephony.TelephonyManager'); + var tm = Java.cast(ctx.getSystemService(jStr('phone')), TelephonyManager); + var line1 = ''; + try { line1 = safeStr(tm.getLine1Number()); } catch (_) {} + var simOp = ''; + try { simOp = safeStr(tm.getSimOperatorName()); } catch (_) {} + var simState = -1; + try { simState = tm.getSimState(); } catch (_) {} + return { + success: true, + phone: line1, + operator: simOp, + sim_state: simState, + found: line1.length >= 11, + }; + }); + } catch (e) { + return { success: false, phone: '', error: String(e) }; + } + }, + + // ==================== H36 公众号 ==================== + + getOfficialAccounts: function (params) { + var limit = (params && params.limit) || 100; + try { + return Java.performNow(function () { + var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE username LIKE 'gh_%' ORDER BY nickname LIMIT " + limit; + var rows = _execSQL(sql); + return { success: true, accounts: rows.map(function (r) { return { wxid: r.username, name: r.nickname, remark: r.conRemark }; }), count: rows.length }; + }); + } catch (e) { + return { success: false, error: String(e), accounts: [] }; + } + }, + + followOfficialAccount: function (params) { + var accountId = (params && params.account_id) || ''; + if (!accountId) return { success: false, error: '缺少 account_id' }; + return _intentAction('com.workphone.FOLLOW_OA', { account_id: accountId }, 'oa_followed', { account_id: accountId }); + }, + + unfollowOfficialAccount: function (params) { + var accountId = (params && params.account_id) || ''; + if (!accountId) return { success: false, error: '缺少 account_id' }; + return _intentAction('com.workphone.UNFOLLOW_OA', { account_id: accountId }, 'oa_unfollowed', { account_id: accountId }); + }, + + getOfficialAccountArticles: function (params) { + var accountId = (params && params.account_id) || ''; + var limit = (params && params.limit) || 10; + if (!accountId) return { success: false, error: '缺少 account_id' }; + return _intentAction('com.workphone.GET_OA_ARTICLES', { account_id: accountId, limit: String(limit) }, null, {}); + }, + + // ==================== H37 表情管理 ==================== + + sendEmoji: function (params) { + var toId = (params && params.to_id) || ''; + var emojiMd5 = (params && params.emoji_md5) || ''; + if (!toId || !emojiMd5) return { success: false, error: '缺少 to_id 或 emoji_md5' }; + return _intentAction('com.workphone.SEND_EMOJI', { to_id: toId, emoji_md5: emojiMd5 }, 'emoji_sent', { to_id: toId }); + }, + + addCustomEmoji: function (params) { + var imagePath = (params && params.image_path) || ''; + if (!imagePath) return { success: false, error: '缺少 image_path' }; + return _intentAction('com.workphone.ADD_EMOJI', { image_path: imagePath }, 'emoji_added', {}); + }, + + // ==================== H38 浮窗/多任务 ==================== + + addToFloat: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.ADD_FLOAT', { wxid: wxid }, 'float_added', { wxid: wxid }); + }, + + removeFromFloat: function (params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + return _intentAction('com.workphone.REMOVE_FLOAT', { wxid: wxid }, 'float_removed', { wxid: wxid }); + }, + + // ==================== H39 设备信息 ==================== + + getDeviceInfo: function () { + try { + return Java.performNow(function () { + var Build = Java.use('android.os.Build'); + return { + success: true, + device: { + model: safeStr(Build.MODEL.value), + brand: safeStr(Build.BRAND.value), + manufacturer: safeStr(Build.MANUFACTURER.value), + android_version: safeStr(Build.VERSION.RELEASE.value), + sdk_int: Build.VERSION.SDK_INT.value, + device: safeStr(Build.DEVICE.value), + product: safeStr(Build.PRODUCT.value), + fingerprint: safeStr(Build.FINGERPRINT.value), + display: safeStr(Build.DISPLAY.value), + }, + }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + getStorageInfo: function () { + try { + return Java.performNow(function () { + var StatFs = Java.use('android.os.StatFs'); + var Environment = Java.use('android.os.Environment'); + var stat = StatFs.$new(Environment.getDataDirectory().getPath()); + var totalBytes = stat.getTotalBytes(); + var freeBytes = stat.getFreeBytes(); + return { + success: true, + storage: { total_mb: Math.round(totalBytes / 1048576), free_mb: Math.round(freeBytes / 1048576), used_percent: Math.round((1 - freeBytes / totalBytes) * 100) }, + }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + getNetworkInfo: function () { + try { + return Java.performNow(function () { + var ctx = getCtx(); + var cm = ctx.getSystemService(jStr('connectivity')); + var ConnectivityManager = Java.use('android.net.ConnectivityManager'); + var mgr = Java.cast(cm, ConnectivityManager); + var activeNet = mgr.getActiveNetworkInfo(); + if (activeNet && activeNet.isConnected()) { + return { success: true, network: { type: safeStr(activeNet.getTypeName()), connected: true, detail: safeStr(activeNet.getExtraInfo()) } }; + } + return { success: true, network: { type: 'none', connected: false } }; + }); + } catch (e) { + return { success: false, error: String(e) }; + } + }, + + // ==================== 批量执行 ==================== + + batchExecute: function (params) { + var actions = (params && params.actions) || []; + if (actions.length === 0) return { success: false, error: '缺少 actions' }; + var results = []; + for (var i = 0; i < actions.length; i++) { + var act = actions[i]; + var fn = rpc.exports[act.action]; + if (fn) { + try { results.push({ action: act.action, result: fn(act.params || {}) }); } + catch (e) { results.push({ action: act.action, result: { success: false, error: String(e) } }); } + } else { + results.push({ action: act.action, result: { success: false, error: '未知操作' } }); + } + } + return { success: true, results: results, count: results.length }; + }, +}; + +// ============================================================ +// § 5 内部实现 — 消息发送 (H17) +// ============================================================ + +function _sendMessageInternal(toId, content, msgType) { + var sent = false; + + // 方案 A: 直接调用微信内部消息发送类(按版本兼容表自动选择) + var candidateClasses = getCompat('send_message'); + + for (var i = 0; i < candidateClasses.length && !sent; i++) { + try { + var c = candidateClasses[i]; + var MsgCls = Java.use(c.cls); + if (MsgCls) { + var m = c.sig ? MsgCls[c.method].overload.apply(MsgCls[c.method], c.sig) : MsgCls[c.method]; + if (c.sig && c.sig.length === 3) { + m.call(MsgCls, jStr(toId), jStr(content), jStr(msgType === 'text' ? '' : msgType)); + } else { + m.call(MsgCls, jStr(toId), jStr(content)); + } + sent = true; + log('info', 'send', '消息通过内部方法发送: ' + c.cls); + emitEvent('message_sent', { to_id: toId, content: content, msg_type: msgType, method: 'internal_' + c.cls.split('.').pop() }); + } + } catch (_) { continue; } + } + + // 方案 B: Intent 广播降级 + if (!sent) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.SEND_MESSAGE'); + intent.putExtra('to_id', toId); + intent.putExtra('content', content); + intent.putExtra('msg_type', msgType); + ctx.sendBroadcast(intent); + sent = true; + log('info', 'send', '消息通过 Intent 广播发送'); + emitEvent('message_sent', { to_id: toId, content: content, msg_type: msgType, method: 'intent_broadcast' }); + } catch (e) { + log('error', 'send', 'Intent 发送也失败: ' + e); + } + } + + return { + success: sent, + message_id: sent ? generateId('msg') : null, + method: sent ? 'hook' : 'failed', + to_id: toId, + }; +} + +// ============================================================ +// § 6 内部实现 — 数据库读取 (H15/H16) +// ============================================================ + +function _getMessagesFromDB(conversationId, limit) { + var where = conversationId + ? " WHERE talker='" + conversationId.replace(/'/g, "''") + "'" + : ''; + var sql = 'SELECT msgId, type, talker, content, createTime, isSend FROM message' + + where + ' ORDER BY createTime DESC LIMIT ' + limit; + var rows = _execSQL(sql); + return { + success: true, + messages: rows.map(function (r) { + return { + id: r.msgId, type: r.type, from_id: r.talker, + content: r.content, timestamp: r.createTime, + is_send: r.isSend === '1', + }; + }), + count: rows.length, + }; +} + +function _getContactsFromDB(limit) { + var sql = "SELECT username, nickname, conRemark, type, lvbuff FROM rcontact" + + " WHERE type NOT IN (0, 33) AND verifyFlag=0" + + " AND username NOT LIKE '%@chatroom'" + + " AND username NOT LIKE 'gh_%'" + + " ORDER BY nickname LIMIT " + limit; + var rows = _execSQL(sql); + var contacts = rows.map(function (r) { + return { wxid: r.username, nickname: r.nickname, remark: r.conRemark, type: r.type }; + }); + _contactCache = contacts; + return { success: true, contacts: contacts, count: contacts.length }; +} + +function _getContactInfoFromDB(wxid) { + var sql = "SELECT username, nickname, conRemark, type, lvbuff FROM rcontact" + + " WHERE username='" + wxid.replace(/'/g, "''") + "'"; + var rows = _execSQL(sql); + if (rows.length > 0) return { success: true, contact: rows[0] }; + return { success: false, error: '未找到联系人: ' + wxid }; +} + +// ============================================================ +// § 7 内部实现 — 好友管理 (H18/H19) +// ============================================================ + +function _getFriendRequestsFromDB(limit) { + var sql = "SELECT msgId, type, talker, content, createTime FROM message" + + " WHERE type = 10002" + + " ORDER BY createTime DESC LIMIT " + limit; + var rows = _execSQL(sql); + var requests = rows.map(function (r) { + var info = _parseFriendRequestXml(r.content); + return { + msg_id: r.msgId, + from_id: info.fromusername || r.talker, + from_nickname: info.fromnickname || '', + content: info.content || '', + encrypt_username: info.encryptusername || '', + ticket: info.ticket || '', + source: info.sourceType || '', + timestamp: r.createTime, + }; + }); + return { success: true, requests: requests, count: requests.length }; +} + +function _parseFriendRequestXml(xml) { + var info = {}; + if (!xml) return info; + var fields = ['fromusername', 'fromnickname', 'content', 'encryptusername', 'ticket', 'sourceType']; + for (var i = 0; i < fields.length; i++) { + var re = new RegExp(fields[i] + '="([^"]*)"'); + var m = xml.match(re); + if (m) info[fields[i]] = m[1]; + } + return info; +} + +function _addFriendInternal(userId, message, sourceType) { + // 通过微信内部 AddContactNetScene + var candidates = [ + 'com.tencent.mm.plugin.profile.ui.ContactInfoUI', + 'com.tencent.mm.protocal.protobuf.add$ContactType', + ]; + + // 降级: Intent 方式 + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.ADD_FRIEND'); + intent.putExtra('user_id', userId); + intent.putExtra('message', message); + intent.putExtra('source_type', String(sourceType)); + ctx.sendBroadcast(intent); + log('info', 'friend', '添加好友请求已发送: ' + userId); + emitEvent('friend_request_sent', { user_id: userId, message: message, source_type: sourceType }); + return { success: true, user_id: userId, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '添加好友失败: ' + e, user_id: userId }; + } +} + +function _acceptFriendInternal(encryptUserName, ticket) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.ACCEPT_FRIEND'); + intent.putExtra('encrypt_username', encryptUserName); + intent.putExtra('ticket', ticket); + ctx.sendBroadcast(intent); + log('info', 'friend', '通过好友请求: ' + encryptUserName); + emitEvent('friend_accepted', { encrypt_username: encryptUserName }); + return { success: true, encrypt_username: encryptUserName, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '通过好友失败: ' + e }; + } +} + +function _deleteFriendInternal(wxid) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.DELETE_FRIEND'); + intent.putExtra('wxid', wxid); + ctx.sendBroadcast(intent); + log('info', 'friend', '删除好友: ' + wxid); + emitEvent('friend_deleted', { wxid: wxid }); + return { success: true, wxid: wxid, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '删除好友失败: ' + e }; + } +} + +function _setRemarkInternal(wxid, remark) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.SET_REMARK'); + intent.putExtra('wxid', wxid); + intent.putExtra('remark', remark); + ctx.sendBroadcast(intent); + return { success: true, wxid: wxid, remark: remark, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '设置备注失败: ' + e }; + } +} + +// ============================================================ +// § 8 内部实现 — 群管理 (H22) +// ============================================================ + +function _getGroupsFromDB(limit) { + var sql = "SELECT username, nickname, memberCount FROM rcontact" + + " WHERE username LIKE '%@chatroom'" + + " ORDER BY nickname LIMIT " + limit; + var rows = _execSQL(sql); + return { + success: true, + groups: rows.map(function (r) { + return { group_id: r.username, name: r.nickname, member_count: safeInt(r.memberCount, 0) }; + }), + count: rows.length, + }; +} + +function _getGroupMembersFromDB(groupId) { + var sql = "SELECT memberlist, displayname FROM chatroom" + + " WHERE chatroomname='" + groupId.replace(/'/g, "''") + "'"; + var rows = _execSQL(sql); + if (rows.length === 0) return { success: false, error: '群不存在: ' + groupId, members: [] }; + + var memberList = (rows[0].memberlist || '').split(';').filter(function (m) { return m; }); + var displayNames = (rows[0].displayname || '').split(';'); + + var members = memberList.map(function (wxid, idx) { + return { + wxid: wxid, + display_name: (displayNames[idx] || '').trim(), + }; + }); + + return { success: true, group_id: groupId, members: members, count: members.length }; +} + +function _getGroupInfoFromDB(groupId) { + var sql = "SELECT chatroomname, memberlist, displayname, roomowner, chatroomnick FROM chatroom" + + " WHERE chatroomname='" + groupId.replace(/'/g, "''") + "'"; + var rows = _execSQL(sql); + if (rows.length === 0) return { success: false, error: '群不存在: ' + groupId }; + + var r = rows[0]; + var memberList = (r.memberlist || '').split(';').filter(function (m) { return m; }); + return { + success: true, + group: { + group_id: r.chatroomname, + name: r.chatroomnick || '', + owner: r.roomowner || '', + member_count: memberList.length, + members: memberList, + }, + }; +} + +function _createGroupInternal(memberIds, topic) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.CREATE_GROUP'); + intent.putExtra('member_ids', memberIds.join(',')); + if (topic) intent.putExtra('topic', topic); + ctx.sendBroadcast(intent); + log('info', 'group', '创建群聊: members=' + memberIds.length); + emitEvent('group_created', { member_ids: memberIds, topic: topic }); + return { success: true, member_ids: memberIds, topic: topic, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '创建群失败: ' + e }; + } +} + +function _inviteToGroupInternal(groupId, memberIds) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.INVITE_GROUP'); + intent.putExtra('group_id', groupId); + intent.putExtra('member_ids', memberIds.join(',')); + ctx.sendBroadcast(intent); + log('info', 'group', '邀请入群: ' + groupId + ' members=' + memberIds.length); + emitEvent('group_invite', { group_id: groupId, member_ids: memberIds }); + return { success: true, group_id: groupId, member_ids: memberIds, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '邀请入群失败: ' + e }; + } +} + +function _removeFromGroupInternal(groupId, memberIds) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.REMOVE_GROUP_MEMBER'); + intent.putExtra('group_id', groupId); + intent.putExtra('member_ids', memberIds.join(',')); + ctx.sendBroadcast(intent); + log('info', 'group', '踢出群: ' + groupId); + emitEvent('group_member_removed', { group_id: groupId, member_ids: memberIds }); + return { success: true, group_id: groupId, member_ids: memberIds, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '踢人失败: ' + e }; + } +} + +function _setGroupAnnouncementInternal(groupId, announcement) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.SET_GROUP_ANNOUNCEMENT'); + intent.putExtra('group_id', groupId); + intent.putExtra('announcement', announcement); + ctx.sendBroadcast(intent); + return { success: true, group_id: groupId, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '设置群公告失败: ' + e }; + } +} + +function _setGroupNameInternal(groupId, name) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.SET_GROUP_NAME'); + intent.putExtra('group_id', groupId); + intent.putExtra('name', name); + ctx.sendBroadcast(intent); + return { success: true, group_id: groupId, name: name, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '修改群名失败: ' + e }; + } +} + +function _quitGroupInternal(groupId) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.QUIT_GROUP'); + intent.putExtra('group_id', groupId); + ctx.sendBroadcast(intent); + emitEvent('group_quit', { group_id: groupId }); + return { success: true, group_id: groupId, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '退群失败: ' + e }; + } +} + +// ============================================================ +// § 9 内部实现 — 朋友圈 (H20/H21) +// ============================================================ + +function _postMomentsInternal(content, imageUrls) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.POST_MOMENTS'); + intent.putExtra('content', content); + if (imageUrls.length > 0) intent.putExtra('image_urls', imageUrls.join(',')); + ctx.sendBroadcast(intent); + log('info', 'sns', '发布朋友圈: content_len=' + content.length + ' images=' + imageUrls.length); + emitEvent('moments_posted', { content: content, image_count: imageUrls.length }); + return { success: true, method: 'intent_broadcast', sns_id: generateId('sns') }; + } catch (e) { + return { success: false, error: '发朋友圈失败: ' + e }; + } +} + +function _getMomentsFromDB(wxid, limit) { + var snsPath = _getSnsDbPath(); + if (!snsPath) { + return { success: false, error: 'SnsMicroMsg.db 未找到', moments: [] }; + } + + var where = wxid + ? " WHERE userName='" + wxid.replace(/'/g, "''") + "'" + : ''; + var sql = "SELECT snsId, userName, createTime, content, mediaCount, likeCount, commentCount FROM SnsInfo" + + where + " ORDER BY createTime DESC LIMIT " + limit; + + var rows = _execSnsSQL(sql); + return { + success: true, + moments: rows.map(function (r) { + var parsed = _parseSnsContent(r.content); + return { + sns_id: r.snsId, + user_id: r.userName, + text: parsed.text, + media_count: safeInt(r.mediaCount, 0), + like_count: safeInt(r.likeCount, 0), + comment_count: safeInt(r.commentCount, 0), + timestamp: r.createTime, + }; + }), + count: rows.length, + }; +} + +function _parseSnsContent(raw) { + if (!raw) return { text: '' }; + try { + var idx = raw.indexOf(''); + var endIdx = raw.indexOf(''); + if (idx >= 0 && endIdx > idx) { + return { text: raw.substring(idx + 13, endIdx) }; + } + } catch (_) {} + return { text: safeStr(raw).substring(0, 200) }; +} + +function _likeMomentsInternal(snsId) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.LIKE_MOMENTS'); + intent.putExtra('sns_id', snsId); + ctx.sendBroadcast(intent); + emitEvent('moments_liked', { sns_id: snsId }); + return { success: true, sns_id: snsId, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '点赞失败: ' + e }; + } +} + +function _commentMomentsInternal(snsId, comment) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new('com.workphone.COMMENT_MOMENTS'); + intent.putExtra('sns_id', snsId); + intent.putExtra('comment', comment); + ctx.sendBroadcast(intent); + emitEvent('moments_commented', { sns_id: snsId, comment: comment }); + return { success: true, sns_id: snsId, method: 'intent_broadcast' }; + } catch (e) { + return { success: false, error: '评论失败: ' + e }; + } +} + +// ============================================================ +// § 10 通用 Intent 调度器(所有非 DB 操作统一走此) +// ============================================================ + +function _intentAction(intentAction, extras, eventType, eventPayload) { + try { + return Java.performNow(function () { + var Intent = Java.use('android.content.Intent'); + var ctx = getCtx(); + var intent = Intent.$new(intentAction); + var keys = Object.keys(extras || {}); + for (var i = 0; i < keys.length; i++) { + intent.putExtra(keys[i], String(extras[keys[i]])); + } + ctx.sendBroadcast(intent); + if (eventType) { + emitEvent(eventType, eventPayload || {}); + } + log('info', 'intent', intentAction + ' → ' + keys.join(',')); + return { success: true, method: 'intent_broadcast', action: intentAction }; + }); + } catch (e) { + return { success: false, error: String(e), action: intentAction }; + } +} + +// ============================================================ +// § 11 Hook 初始化 +// ============================================================ + +Java.perform(function () { + log('info', 'init', 'wechat_hook_v3.1 正在初始化(110 actions / 24 modules / 版本自适应)...'); + + // 获取微信版本 + 初始化版本兼容 + try { + var ctx = getCtx(); + var pm = ctx.getPackageManager(); + var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0); + _wechatVersion = safeStr(info.versionName.value); + log('info', 'init', '微信版本: ' + _wechatVersion); + _resolveVersionCompat(_wechatVersion); + } catch (_) { + _versionCompat = _DEFAULT_COMPAT; + log('warn', 'init', '无法获取微信版本,使用 default 全候选'); + } + + // 查找数据库路径 + _dbPath = _findDbPath(CONFIG.DB_NAME); + _snsDbPath = _findDbPath(CONFIG.SNS_DB_NAME); + log('info', 'init', 'EnMicroMsg.db: ' + (_dbPath || '未找到')); + log('info', 'init', 'SnsMicroMsg.db: ' + (_snsDbPath || '未找到')); + + // ---- Hook 1: SQLite insert 拦截新消息 (H15) ---- + if (CONFIG.HOOK_SQLITE) { + try { + var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase'); + SQLiteDatabase.insert.overload( + 'java.lang.String', 'java.lang.String', 'android.content.ContentValues' + ).implementation = function (table, nullColumnHack, values) { + var result = this.insert(table, nullColumnHack, values); + try { + var tbl = safeStr(table); + if (tbl === 'message') { + var talker = safeStr(values.getAsString(jStr('talker'))); + var content = safeStr(values.getAsString(jStr('content'))); + var msgType = safeStr(values.getAsString(jStr('type'))); + var msgSvrId = safeStr(values.getAsString(jStr('msgSvrId'))); + var isSend = safeStr(values.getAsString(jStr('isSend'))); + + if (talker && content) { + var msg = { + from_id: talker, content: content, msg_type: msgType, + msg_svr_id: msgSvrId, is_send: isSend === '1', + timestamp: new Date().toISOString(), + }; + _messageBuffer.push(msg); + if (_messageBuffer.length > CONFIG.MESSAGE_BUFFER_MAX) { + _messageBuffer = _messageBuffer.slice(-Math.floor(CONFIG.MESSAGE_BUFFER_MAX * 0.6)); + } + emitEvent('message_received', msg); + } + + // H18: 好友请求 type=10002 + if (msgType === '10002') { + var reqInfo = _parseFriendRequestXml(content); + var req = { + msg_id: msgSvrId, from_id: reqInfo.fromusername || talker, + from_nickname: reqInfo.fromnickname || '', + encrypt_username: reqInfo.encryptusername || '', + ticket: reqInfo.ticket || '', content: reqInfo.content || '', + timestamp: new Date().toISOString(), + }; + _friendRequestBuffer.push(req); + if (_friendRequestBuffer.length > CONFIG.FRIEND_REQUEST_BUFFER_MAX) { + _friendRequestBuffer = _friendRequestBuffer.slice(-60); + } + emitEvent('friend_request_received', req); + log('info', 'hook', '检测到好友请求: ' + req.from_id); + } + } + } catch (_) {} + return result; + }; + _hooked.sqlite_insert = true; + log('info', 'hook', 'SQLite insert 消息拦截已启用'); + } catch (e) { + log('warn', 'hook', 'SQLite insert Hook 失败: ' + e); + } + } + + // ---- Hook 2: SQLite update 拦截消息撤回 ---- + if (CONFIG.HOOK_SQLITE) { + try { + var SQLiteDB2 = Java.use('android.database.sqlite.SQLiteDatabase'); + SQLiteDB2.update.overload( + 'java.lang.String', 'android.content.ContentValues', 'java.lang.String', '[Ljava.lang.String;' + ).implementation = function (table, values, whereClause, whereArgs) { + try { + if (safeStr(table) === 'message' && whereClause) { + var newType = values.getAsInteger(jStr('type')); + if (newType && newType.intValue() === 10000) { + var content = safeStr(values.getAsString(jStr('content'))); + if (content.indexOf('撤回') !== -1 || content.indexOf('recalled') !== -1) { + emitEvent('message_recalled', { where: safeStr(whereClause), content: content }); + log('info', 'hook', '检测到消息撤回'); + } + } + } + } catch (_) {} + return this.update(table, values, whereClause, whereArgs); + }; + _hooked.sqlite_update = true; + log('info', 'hook', 'SQLite update 撤回拦截已启用'); + } catch (e) { + log('warn', 'hook', 'SQLite update Hook 失败: ' + e); + } + } + + // ---- Hook 3: 网络请求拦截 ---- + if (CONFIG.HOOK_NETWORK) { + try { + var URL = Java.use('java.net.URL'); + URL.openConnection.overload().implementation = function () { + var conn = this.openConnection(); + var url = safeStr(this.toString()); + if (url.indexOf('weixin.qq.com') !== -1 || url.indexOf('wechat') !== -1 || url.indexOf('wx.qq.com') !== -1) { + emitEvent('network_request', { url: url, method: 'GET' }); + } + return conn; + }; + _hooked.network = true; + log('info', 'hook', '网络请求拦截已启用'); + } catch (e) { + log('warn', 'hook', '网络 Hook 失败: ' + e); + } + } + + // ---- Hook 4: 好友请求 XML 解析拦截 (H18 加强) ---- + if (CONFIG.HOOK_FRIEND_REQUEST) { + try { + // 拦截 AddMsg 处理器(微信内部处理新消息的核心类) + var candidates = [ + 'com.tencent.mm.plugin.messenger.foundation.a.b', + 'com.tencent.mm.plugin.messenger.foundation.a.c', + ]; + for (var ci = 0; ci < candidates.length; ci++) { + try { + var Cls = Java.use(candidates[ci]); + if (Cls) { + _hooked.friend_request = true; + log('info', 'hook', '好友请求拦截已启用 (via ' + candidates[ci] + ')'); + break; + } + } catch (_) { continue; } + } + if (!_hooked.friend_request) { + log('info', 'hook', '好友请求拦截通过 SQLite 拦截实现(type=10002)'); + _hooked.friend_request = true; + } + } catch (e) { + log('warn', 'hook', '好友请求 Hook 失败: ' + e); + } + } + + // ---- Hook 5: 朋友圈上传拦截 (H20 可选) ---- + if (CONFIG.HOOK_MOMENTS) { + try { + var snsCandidates = [ + 'com.tencent.mm.plugin.sns.ui.SnsUploadUI', + 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI', + ]; + for (var si = 0; si < snsCandidates.length; si++) { + try { + var SnsCls = Java.use(snsCandidates[si]); + if (SnsCls) { + _hooked.sns_upload = true; + log('info', 'hook', '朋友圈监控已启用 (via ' + snsCandidates[si] + ')'); + break; + } + } catch (_) { continue; } + } + } catch (e) { + log('warn', 'hook', '朋友圈 Hook 失败: ' + e); + } + } + + log('info', 'init', 'wechat_hook_v3.1 初始化完成 — 110 actions / 全功能就绪', _hooked); + emitEvent('hook_initialized', { + version: '3.0.0', + wechat_version: _wechatVersion, + hooked: _hooked, + db_path: _dbPath || '', + sns_db_path: _snsDbPath || '', + }); +}); diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/AccessibilityService.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/AccessibilityService.kt deleted file mode 100644 index 1e5f2f3b34..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/AccessibilityService.kt +++ /dev/null @@ -1,247 +0,0 @@ -package com.system.cloudservice - -import android.accessibilityservice.AccessibilityService -import android.accessibilityservice.GestureDescription -import android.graphics.Path -import android.util.Log -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityNodeInfo -import kotlinx.coroutines.* - -/** - * 无障碍服务 - 用于UI自动化操作 - * - * 功能: - * - 点击、滑动、输入文字 - * - 查找UI元素 - * - 获取UI层级 - */ -class AgentAccessibilityService : AccessibilityService() { - - companion object { - private const val TAG = "AccessibilityService" - private var instance: AgentAccessibilityService? = null - - fun getInstance(): AgentAccessibilityService? = instance - - fun isEnabled(): Boolean = instance != null - } - - private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - override fun onServiceConnected() { - super.onServiceConnected() - instance = this - Log.d(TAG, "无障碍服务已启动") - } - - override fun onDestroy() { - super.onDestroy() - instance = null - serviceScope.cancel() - Log.d(TAG, "无障碍服务已停止") - } - - override fun onAccessibilityEvent(event: AccessibilityEvent?) { - // 可以在这里监听UI变化 - } - - override fun onInterrupt() { - Log.w(TAG, "无障碍服务被中断") - } - - // ========== UI操作 ========== - - /** - * 点击坐标 - */ - fun click(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null) { - val path = Path().apply { - moveTo(x.toFloat(), y.toFloat()) - } - - val gesture = GestureDescription.Builder() - .addStroke(GestureDescription.StrokeDescription(path, 0, 100)) - .build() - - dispatchGesture(gesture, object : GestureResultCallback() { - override fun onCompleted(gestureDescription: GestureDescription?) { - Log.d(TAG, "点击成功: ($x, $y)") - callback?.invoke(true) - } - - override fun onCancelled(gestureDescription: GestureDescription?) { - Log.w(TAG, "点击取消: ($x, $y)") - callback?.invoke(false) - } - }, null) - } - - /** - * 滑动 - */ - fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300, callback: ((Boolean) -> Unit)? = null) { - val path = Path().apply { - moveTo(x1.toFloat(), y1.toFloat()) - lineTo(x2.toFloat(), y2.toFloat()) - } - - val gesture = GestureDescription.Builder() - .addStroke(GestureDescription.StrokeDescription(path, 0, duration)) - .build() - - dispatchGesture(gesture, object : GestureResultCallback() { - override fun onCompleted(gestureDescription: GestureDescription?) { - Log.d(TAG, "滑动成功: ($x1, $y1) -> ($x2, $y2)") - callback?.invoke(true) - } - - override fun onCancelled(gestureDescription: GestureDescription?) { - Log.w(TAG, "滑动取消") - callback?.invoke(false) - } - }, null) - } - - /** - * 点击文字 - */ - fun clickText(text: String, callback: ((Boolean) -> Unit)? = null) { - serviceScope.launch { - val node = findNodeByText(text) - if (node != null) { - val bounds = android.graphics.Rect() - node.getBoundsInScreen(bounds) - click(bounds.centerX(), bounds.centerY(), callback) - node.recycle() - } else { - Log.w(TAG, "未找到文字: $text") - callback?.invoke(false) - } - } - } - - /** - * 输入文字 - */ - fun inputText(text: String, callback: ((Boolean) -> Unit)? = null) { - serviceScope.launch { - val rootNode = rootInActiveWindow - if (rootNode != null) { - // 查找输入框(通常是EditText) - val inputNode = findInputNode(rootNode) - if (inputNode != null) { - // 聚焦 - inputNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS) - Thread.sleep(200) - - // 输入文字 - val arguments = android.os.Bundle().apply { - putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) - } - val success = inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) - - inputNode.recycle() - rootNode.recycle() - - callback?.invoke(success) - } else { - Log.w(TAG, "未找到输入框") - callback?.invoke(false) - } - } else { - callback?.invoke(false) - } - } - } - - /** - * 返回键 - */ - fun back(callback: ((Boolean) -> Unit)? = null) { - performGlobalAction(GLOBAL_ACTION_BACK) - callback?.invoke(true) - } - - /** - * Home键 - */ - fun home(callback: ((Boolean) -> Unit)? = null) { - performGlobalAction(GLOBAL_ACTION_HOME) - callback?.invoke(true) - } - - /** - * 最近任务 - */ - fun recent(callback: ((Boolean) -> Unit)? = null) { - performGlobalAction(GLOBAL_ACTION_RECENTS) - callback?.invoke(true) - } - - // ========== 查找元素 ========== - - /** - * 通过文字查找节点 - */ - private fun findNodeByText(text: String): AccessibilityNodeInfo? { - val rootNode = rootInActiveWindow ?: return null - - val nodes = rootNode.findAccessibilityNodeInfosByText(text) - val result = if (nodes.isNotEmpty()) nodes[0] else null - - rootNode.recycle() - return result - } - - /** - * 查找输入框 - */ - private fun findInputNode(root: AccessibilityNodeInfo): AccessibilityNodeInfo? { - if (root.className?.contains("EditText") == true) { - return root - } - - for (i in 0 until root.childCount) { - val child = root.getChild(i) ?: continue - val result = findInputNode(child) - if (result != null) { - return result - } - child.recycle() - } - - return null - } - - /** - * 获取UI层级(用于调试) - */ - fun getUITree(): String { - val rootNode = rootInActiveWindow ?: return "" - val tree = StringBuilder() - dumpNode(rootNode, tree, 0) - rootNode.recycle() - return tree.toString() - } - - private fun dumpNode(node: AccessibilityNodeInfo, tree: StringBuilder, depth: Int) { - val indent = " ".repeat(depth) - val className = node.className?.toString() ?: "Unknown" - val text = node.text?.toString() ?: "" - val desc = node.contentDescription?.toString() ?: "" - - tree.append("$indent$className") - if (text.isNotEmpty()) tree.append(" text=\"$text\"") - if (desc.isNotEmpty()) tree.append(" desc=\"$desc\"") - tree.append("\n") - - for (i in 0 until node.childCount) { - val child = node.getChild(i) - if (child != null) { - dumpNode(child, tree, depth + 1) - child.recycle() - } - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/AgentService.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/AgentService.kt deleted file mode 100644 index baba974fce..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/AgentService.kt +++ /dev/null @@ -1,745 +0,0 @@ -package com.system.cloudservice - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import android.os.IBinder -import android.util.Log -import android.content.Context.RECEIVER_NOT_EXPORTED -import androidx.core.app.NotificationCompat -import com.google.gson.Gson -import kotlinx.coroutines.* -import org.java_websocket.client.WebSocketClient -import org.java_websocket.handshake.ServerHandshake -import java.net.URI - -/** - * Agent后台服务 - * - * 保持与远程服务器的WebSocket连接 - * 接收并执行控制命令 - */ -class AgentService : Service() { - - companion object { - const val TAG = "AgentService" - const val CHANNEL_ID = "sync_channel" - const val NOTIFICATION_ID = 1 - - const val ACTION_START = "com.system.cloudservice.START" - const val ACTION_STOP = "com.system.cloudservice.STOP" - - const val EXTRA_SERVER_URL = "server_url" - const val EXTRA_PROJECT_ID = "project_id" - const val EXTRA_DEVICE_ID = "device_id" - - var isRunning = false - private set - var isSocketConnected = false - private set - } - - private var webSocketClient: WebSocketClient? = null - private var serverUrl: String = "" - private var projectId: String = "" - private var deviceId: String = "" - - private val gson = Gson() - private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - // 心跳定时器 - private var heartbeatJob: Job? = null - private var reconnectJob: Job? = null - - // 性能监控定时器 - private var performanceMonitorJob: Job? = null - private var heartbeatIntervalMs: Long = 30_000L - private var lastPongAtMs: Long = 0L - private var manualStop: Boolean = false - - override fun onBind(intent: Intent?): IBinder? = null - - private val voiceCommandReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == "com.system.cloudservice.VOICE_COMMAND") { - val text = intent.getStringExtra("text") ?: return - Log.d(TAG, "收到语音命令: $text") - - // 在后台线程执行 - serviceScope.launch { - val result = LocalAI.executeVoiceCommand(this@AgentService, text) - Log.d(TAG, "执行结果: $result") - } - } - } - } - - override fun onCreate() { - super.onCreate() - createNotificationChannel() - - // 注册语音命令接收器 - val filter = IntentFilter("com.system.cloudservice.VOICE_COMMAND") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(voiceCommandReceiver, filter, RECEIVER_NOT_EXPORTED) - } else { - @Suppress("DEPRECATION") - registerReceiver(voiceCommandReceiver, filter) - } - } - - override fun onDestroy() { - super.onDestroy() - try { - unregisterReceiver(voiceCommandReceiver) - } catch (e: Exception) { - // 忽略未注册错误 - } - disconnect() - serviceScope.cancel() - } - - // 删除下面重复的onDestroy方法 - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - when (intent?.action) { - ACTION_START -> { - serverUrl = intent.getStringExtra(EXTRA_SERVER_URL) ?: "" - projectId = intent.getStringExtra(EXTRA_PROJECT_ID) ?: "" - deviceId = intent.getStringExtra(EXTRA_DEVICE_ID)?.trim().orEmpty() - if (deviceId.isEmpty()) { - deviceId = DeviceIdHelper.getOrCreateDeviceId(this) - } - manualStop = false - isRunning = true - isSocketConnected = false - - startForeground(NOTIFICATION_ID, createNotification("正在连接...")) - connectWebSocket() - } - ACTION_STOP -> { - manualStop = true - disconnect() - stopForeground(STOP_FOREGROUND_REMOVE) - stopSelf() - } - } - return START_STICKY - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "云同步", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "数据同步服务" - } - - val notificationManager = getSystemService(NotificationManager::class.java) - notificationManager.createNotificationChannel(channel) - } - } - - private fun createNotification(status: String): Notification { - val pendingIntent = PendingIntent.getActivity( - this, - 0, - Intent(this, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE - ) - - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("云同步服务") - .setContentText(sanitizeNotificationStatus(status)) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentIntent(pendingIntent) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setSilent(true) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .build() - } - - private fun updateNotification(status: String) { - val notification = createNotification(status) - val notificationManager = getSystemService(NotificationManager::class.java) - notificationManager.notify(NOTIFICATION_ID, notification) - } - - /** - * 通知栏只保留通用状态,避免把设备ID或后端错误详情直接暴露到手机界面。 - */ - private fun sanitizeNotificationStatus(status: String): String { - val normalized = status.trim() - if (normalized.isEmpty()) { - return "同步服务运行中" - } - - return when { - normalized.contains("正在连接") -> "正在同步数据" - normalized.contains("已连接") -> "同步服务已就绪" - normalized.contains("重连") -> "正在恢复同步" - normalized.contains("已断开") -> "同步已暂停" - normalized.contains("连接异常") || normalized.contains("连接失败") -> "同步异常" - normalized.contains("删除") || normalized.contains("停用") -> "同步服务运行中" - else -> "同步服务运行中" - } - } - - private fun connectWebSocket() { - serviceScope.launch { - try { - val uri = URI("$serverUrl/$deviceId") - Log.d(TAG, "连接: $uri") - - webSocketClient = object : WebSocketClient(uri) { - override fun onOpen(handshakedata: ServerHandshake?) { - Log.d(TAG, "WebSocket连接成功") - isSocketConnected = true - lastPongAtMs = System.currentTimeMillis() - updateNotification("已连接 - 项目: $projectId") - - // 发送注册消息 - sendRegister() - - // 启动心跳 - startHeartbeat() - - // 启动性能监控 - startPerformanceMonitoring() - } - - override fun onMessage(message: String?) { - Log.d(TAG, "收到消息: $message") - message?.let { handleMessage(it) } - } - - override fun onClose(code: Int, reason: String?, remote: Boolean) { - Log.d(TAG, "WebSocket关闭: $reason") - isSocketConnected = false - if (isRunning) { - updateNotification("重连中...") - } else { - updateNotification("已断开") - } - - // 自动重连(非手动停止) - if (!manualStop) { - scheduleReconnect() - } - } - - override fun onError(ex: Exception?) { - Log.e(TAG, "WebSocket错误", ex) - isSocketConnected = false - updateNotification("连接异常,重试中...") - if (!manualStop) { - scheduleReconnect() - } - } - } - - webSocketClient?.connect() - - } catch (e: Exception) { - Log.e(TAG, "连接失败", e) - updateNotification("连接失败: ${e.message}") - } - } - } - - private fun sendRegister() { - val androidId = DeviceIdHelper.getAndroidId(this) - val aochuangDeviceId = DeviceIdHelper.getAochuangCompatibleId(this) - val registerMsg = mapOf( - "type" to "register", - "device_id" to deviceId, - "project_id" to projectId, - "platform" to "android", - "model" to Build.MODEL, - "sdk_version" to Build.VERSION.SDK_INT, - "app_version" to getAppVersion(), - "heartbeat_interval_seconds" to (heartbeatIntervalMs / 1000), - "device_profile" to mapOf( - "aochuang_device_id" to aochuangDeviceId, - "android_id" to androidId, - "serial" to DeviceIdHelper.getSerial() - ) - ) - webSocketClient?.send(gson.toJson(registerMsg)) - } - - private fun getAppVersion(): String { - return try { - packageManager.getPackageInfo(packageName, 0).versionName ?: "unknown" - } catch (_: Exception) { - "unknown" - } - } - - private fun startHeartbeat() { - heartbeatJob?.cancel() - heartbeatJob = serviceScope.launch { - while (isActive) { - delay(heartbeatIntervalMs) - try { - val status = collectDeviceStatus() - val heartbeat = mapOf( - "type" to "heartbeat", - "device_id" to deviceId, - "timestamp" to System.currentTimeMillis(), - "status" to status - ) - webSocketClient?.send(gson.toJson(heartbeat)) - - val staleThresholdMs = heartbeatIntervalMs * 3 - if (lastPongAtMs > 0 && (System.currentTimeMillis() - lastPongAtMs) > staleThresholdMs) { - Log.w(TAG, "心跳超时,准备重连") - webSocketClient?.close() - break - } - } catch (e: Exception) { - Log.e(TAG, "心跳发送失败", e) - } - } - } - } - - private fun collectDeviceStatus(): Map { - val bm = getSystemService(Context.BATTERY_SERVICE) as? android.os.BatteryManager - val batteryLevel = bm?.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: -1 - val isCharging = bm?.isCharging ?: false - - val am = getSystemService(Context.ACTIVITY_SERVICE) as? android.app.ActivityManager - val memInfo = android.app.ActivityManager.MemoryInfo() - am?.getMemoryInfo(memInfo) - val totalMem = memInfo.totalMem.toDouble() - val availMem = memInfo.availMem.toDouble() - val memUsagePct = if (totalMem > 0) ((totalMem - availMem) / totalMem * 100) else 0.0 - - val stat = android.os.StatFs(android.os.Environment.getDataDirectory().path) - val storageFree = (stat.availableBlocksLong * stat.blockSizeLong) / (1024 * 1024) - - val pm = getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager - val screenOn = pm?.isInteractive ?: false - - val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as? android.net.ConnectivityManager - val networkType = try { - val nc = cm?.getNetworkCapabilities(cm.activeNetwork) - when { - nc == null -> "none" - nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_WIFI) -> "wifi" - nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular" - nc.hasTransport(android.net.NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet" - else -> "other" - } - } catch (_: Exception) { "unknown" } - - val currentApp = try { - val usm = getSystemService(Context.USAGE_STATS_SERVICE) as? android.app.usage.UsageStatsManager - val end = System.currentTimeMillis() - val stats = usm?.queryUsageStats(android.app.usage.UsageStatsManager.INTERVAL_DAILY, end - 60_000, end) - stats?.maxByOrNull { it.lastTimeUsed }?.packageName ?: "" - } catch (_: Exception) { "" } - - val wechatRunning = try { - am?.runningAppProcesses?.any { it.processName == "com.tencent.mm" } ?: false - } catch (_: Exception) { false } - - val uptimeSec = (android.os.SystemClock.elapsedRealtime() / 1000).toInt() - - return mapOf( - "battery_level" to batteryLevel, - "battery_charging" to isCharging, - "memory_usage_pct" to Math.round(memUsagePct * 10.0) / 10.0, - "storage_free_mb" to storageFree.toInt(), - "screen_on" to screenOn, - "network_type" to networkType, - "current_app" to currentApp, - "wechat_running" to wechatRunning, - "uptime_sec" to uptimeSec, - ) - } - - private fun handleMessage(message: String) { - try { - val data = gson.fromJson(message, Map::class.java) - val type = data["type"] as? String - - when (type) { - "execute" -> handleExecute(data) - "ping" -> sendPong() - "pong" -> { - lastPongAtMs = System.currentTimeMillis() - val pendingTasks = (data["pending_tasks"] as? Number)?.toInt() ?: 0 - if (pendingTasks > 0) { - Log.i(TAG, "服务端有 $pendingTasks 个待下发任务") - } - } - "registered" -> { - lastPongAtMs = System.currentTimeMillis() - updateNotification("已连接 - 设备: $deviceId") - } - "config" -> handleConfig(data) - } - } catch (e: Exception) { - Log.e(TAG, "处理消息失败", e) - } - } - - private fun handleExecute(data: Map<*, *>) { - val commandId = data["command_id"] as? String ?: "" - val action = data["action"] as? String ?: "" - val params = data["params"] as? Map<*, *> ?: emptyMap() - - Log.d(TAG, "执行命令: $action, 参数: $params") - - // 执行命令 - serviceScope.launch { - val result = executeCommand(action, params) - - // 返回结果 - val response = mapOf( - "type" to "result", - "command_id" to commandId, - "device_id" to deviceId, - "success" to result.first, - "message" to result.second, - "timestamp" to System.currentTimeMillis() - ) - webSocketClient?.send(gson.toJson(response)) - } - } - - private suspend fun executeCommand(action: String, params: Map<*, *>): Pair { - return withContext(Dispatchers.Main) { - try { - when (action) { - // === 应用操作 === - "open_app" -> { - val packageName = params["package"] as? String ?: "" - openApp(packageName) - } - "get_installed_apps" -> { - val apps = getInstalledApps() - Pair(true, apps.joinToString(",")) - } - "get_device_info" -> { - Pair(true, getDeviceInfo()) - } - - // === UI自动化操作 === - "click" -> { - val x = (params["x"] as? Number)?.toInt() ?: 0 - val y = (params["y"] as? Number)?.toInt() ?: 0 - - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.click(x, y) { success -> - if (!success) { - // 降级到Shell - executeShell("input tap $x $y") - } - } - Pair(true, "已点击 ($x, $y)") - } else { - executeShell("input tap $x $y") - } - } - "swipe" -> { - val x1 = (params["x1"] as? Number)?.toInt() ?: 0 - val y1 = (params["y1"] as? Number)?.toInt() ?: 0 - val x2 = (params["x2"] as? Number)?.toInt() ?: 0 - val y2 = (params["y2"] as? Number)?.toInt() ?: 0 - val duration = (params["duration"] as? Number)?.toLong() ?: 300L - - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.swipe(x1, y1, x2, y2, duration) { success -> - if (!success) { - executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}") - } - } - Pair(true, "已滑动") - } else { - executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}") - } - } - "input_text" -> { - val text = params["text"] as? String ?: "" - - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.inputText(text) { success -> - if (!success) { - // 降级到Shell - executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'") - } - } - Pair(true, "已输入文字") - } else { - // 使用broadcast方式输入中文 - executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'") - } - } - "key_event" -> { - val keycode = params["keycode"] as? String ?: "" - executeShell("input keyevent $keycode") - } - "screenshot" -> { - val path = params["path"] as? String ?: "/sdcard/screenshot.png" - executeShell("screencap -p $path") - } - "back" -> { - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.back() - Pair(true, "已返回") - } else { - executeShell("input keyevent KEYCODE_BACK") - } - } - "home" -> { - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.home() - Pair(true, "已回到桌面") - } else { - executeShell("input keyevent KEYCODE_HOME") - } - } - "recent" -> { - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - accessibilityService.recent() - Pair(true, "已显示最近任务") - } else { - executeShell("input keyevent KEYCODE_APP_SWITCH") - } - } - - // === 语音命令(由AI解析后发送)=== - "voice_command" -> { - val text = params["text"] as? String ?: "" - // 语音命令会被服务器AI解析后转为具体操作 - Pair(true, "语音命令已接收: $text") - } - - // === 获取UI层级 === - "dump_ui" -> { - executeShell("uiautomator dump /sdcard/ui.xml && cat /sdcard/ui.xml") - } - - // === 通用Shell命令(服务端远程调用) === - "shell", "run_shell" -> { - val cmd = params["command"] as? String ?: "" - if (cmd.isBlank()) { - Pair(false, "command参数不能为空") - } else { - executeShell(cmd) - } - } - - // === 获取当前前台App信息 === - "get_foreground" -> { - executeShell("dumpsys window | grep mCurrentFocus") - } - - // === 安装APK(Root静默安装) === - "install_apk" -> { - val path = params["path"] as? String ?: "" - if (path.isBlank()) { - Pair(false, "path参数不能为空") - } else { - executeShell("pm install -r -g $path") - } - } - - else -> Pair(false, "未知命令: $action") - } - } catch (e: Exception) { - Pair(false, e.message ?: "执行失败") - } - } - } - - /** - * 执行Shell命令 - * 注意:需要设备开启ADB调试或有ROOT权限 - */ - private fun executeShell(command: String): Pair { - return try { - Log.d(TAG, "执行Shell: $command") - - fun runWithTimeout(cmd: Array, timeoutSec: Long): Triple? { - val p = Runtime.getRuntime().exec(cmd) - val completed = p.waitFor(timeoutSec, java.util.concurrent.TimeUnit.SECONDS) - if (!completed) { - p.destroyForcibly() - Log.w(TAG, "命令超时(${timeoutSec}s): ${cmd.joinToString(" ")}") - return null - } - val out = p.inputStream.bufferedReader().readText() - val err = p.errorStream.bufferedReader().readText() - return Triple(p.exitValue(), out, err) - } - - // 1) 先尝试 su -c,5秒超时(Magisk可能弹窗阻塞) - val suResult = runWithTimeout(arrayOf("su", "-c", command), 5) - if (suResult != null && suResult.first == 0) { - return Pair(true, suResult.second.ifEmpty { "执行成功(root)" }) - } - - // 2) 回退到 sh -c,15秒超时 - val shResult = runWithTimeout(arrayOf("sh", "-c", command), 15) - if (shResult != null && shResult.first == 0) { - return Pair(true, shResult.second.ifEmpty { "执行成功" }) - } - - val errMsg = when { - suResult == null && shResult == null -> "su和sh均超时" - suResult == null -> "su超时, sh失败: ${shResult?.third ?: "unknown"}" - shResult == null -> "su失败, sh超时" - else -> (suResult.third.ifEmpty { shResult?.third ?: "" }).ifEmpty { - "执行失败,退出码: su=${suResult.first} sh=${shResult?.first}" - } - } - Pair(false, errMsg) - } catch (e: Exception) { - Log.e(TAG, "Shell执行失败", e) - Pair(false, e.message ?: "Shell执行异常") - } - } - - private fun openApp(packageName: String): Pair { - return try { - val intent = packageManager.getLaunchIntentForPackage(packageName) - if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - Pair(true, "已打开 $packageName") - } else { - Pair(false, "未找到应用: $packageName") - } - } catch (e: Exception) { - Pair(false, e.message ?: "打开失败") - } - } - - private fun getInstalledApps(): List { - val pm = packageManager - val packages = pm.getInstalledApplications(0) - return packages.filter { - pm.getLaunchIntentForPackage(it.packageName) != null - }.map { it.packageName } - } - - private fun getDeviceInfo(): String { - val info = DeviceInfo.getDeviceInfo(this) - info.put("device_id", deviceId) - info.put("project_id", projectId) - return info.toString() - } - - private fun sendPong() { - val pong = mapOf( - "type" to "pong", - "device_id" to deviceId, - "timestamp" to System.currentTimeMillis() - ) - webSocketClient?.send(gson.toJson(pong)) - } - - private fun handleConfig(data: Map<*, *>) { - Log.d(TAG, "收到配置: $data") - val heartbeatSec = (data["heartbeat_interval_seconds"] as? Number)?.toLong() - ?: ((data["params"] as? Map<*, *>)?.get("heartbeat_interval_seconds") as? Number)?.toLong() - if (heartbeatSec != null && heartbeatSec in 5..120) { - heartbeatIntervalMs = heartbeatSec * 1000 - startHeartbeat() - updateNotification("已连接 - 心跳${heartbeatSec}s") - } - } - - private fun scheduleReconnect() { - if (reconnectJob?.isActive == true) return - reconnectJob = serviceScope.launch { - var retryCount = 0 - val maxRetries = 10 // 最多重试10次 - val baseDelay = 5000L // 基础延迟5秒 - - while (retryCount < maxRetries && isRunning && !isSocketConnected) { - val delay = baseDelay * (1 shl minOf(retryCount, 4)) // 指数退避,最多32秒 - delay(delay) - - if (isRunning && !isSocketConnected) { - Logger.d("尝试重连... (${retryCount + 1}/$maxRetries)") - try { - connectWebSocket() - // 等待连接结果 - delay(3000) - if (isSocketConnected) { - Logger.i("重连成功") - break - } - } catch (e: Exception) { - Logger.w("重连失败", e) - } - retryCount++ - } - } - - if (retryCount >= maxRetries && isRunning && !isSocketConnected) { - Logger.e("重连失败,已达到最大重试次数") - updateNotification("连接失败,请检查网络") - } - } - } - - private fun startPerformanceMonitoring() { - performanceMonitorJob?.cancel() - performanceMonitorJob = serviceScope.launch { - while (isActive) { - delay(60_000) // 每分钟检查一次 - try { - val optimization = PerformanceMonitor.checkOptimization(this@AgentService) - if (optimization.optBoolean("memory_warning", false) || - optimization.optBoolean("cpu_warning", false)) { - Logger.w("性能警告: ${optimization.optString("suggestions", "")}") - - // 如果内存警告,建议GC - if (optimization.optBoolean("memory_warning", false)) { - PerformanceMonitor.suggestGc() - } - } - } catch (e: Exception) { - Logger.e("性能监控失败", e) - } - } - } - } - - private fun disconnect() { - heartbeatJob?.cancel() - reconnectJob?.cancel() - performanceMonitorJob?.cancel() - webSocketClient?.close() - webSocketClient = null - isRunning = false - isSocketConnected = false - } - -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/AiFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/AiFragment.kt deleted file mode 100644 index 731cb600db..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/AiFragment.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.system.cloudservice - -import android.graphics.Color -import android.os.Bundle -import android.view.Gravity -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.LinearLayout -import android.widget.TextView -import androidx.fragment.app.Fragment -import com.system.cloudservice.databinding.FragmentAiBinding - -class AiFragment : Fragment() { - - private var _binding: FragmentAiBinding? = null - private val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View { - _binding = FragmentAiBinding.inflate(inflater, c, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupInput() - setupChips() - } - - private fun setupInput() { - binding.btnSend.setOnClickListener { sendMessage() } - binding.etChatInput.setOnEditorActionListener { _, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_SEND || - (event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { - sendMessage() - true - } else false - } - binding.btnClearChat.setOnClickListener { clearChat() } - } - - private fun setupChips() { - binding.chipWechat.setOnClickListener { quickSend("打开微信") } - binding.chipScreenshot.setOnClickListener { quickSend("截图") } - binding.chipHome.setOnClickListener { quickSend("返回桌面") } - binding.chipDouyin.setOnClickListener { quickSend("打开抖音") } - binding.chipVolUp.setOnClickListener { quickSend("音量加") } - } - - private fun quickSend(text: String) { - binding.etChatInput.setText(text) - sendMessage() - } - - private fun sendMessage() { - val text = binding.etChatInput.text.toString().trim() - if (text.isEmpty()) return - binding.etChatInput.text?.clear() - - addUserBubble(text) - - val loadingView = addAiBubble("执行中...") - - Thread { - val result = LocalAI.executeVoiceCommand(requireContext(), text) - activity?.runOnUiThread { - if (_binding != null) { - loadingView.text = result - scrollToBottom() - incrementCommandCount() - } - } - }.start() - } - - private fun addUserBubble(text: String) { - val container = LinearLayout(requireContext()).apply { - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { bottomMargin = dpToPx(12) } - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.END - } - - val bubble = TextView(requireContext()).apply { - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { leftMargin = dpToPx(48) } - setBackgroundResource(R.drawable.chat_bubble_user) - setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10)) - this.text = text - setTextColor(Color.parseColor("#E6EDF3")) - textSize = 14f - } - - container.addView(bubble) - binding.chatContainer.addView(container) - scrollToBottom() - } - - private fun addAiBubble(text: String): TextView { - val container = LinearLayout(requireContext()).apply { - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { bottomMargin = dpToPx(12) } - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.START - } - - val bubble = TextView(requireContext()).apply { - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).apply { rightMargin = dpToPx(48) } - setBackgroundResource(R.drawable.chat_bubble_ai) - setPadding(dpToPx(14), dpToPx(10), dpToPx(14), dpToPx(10)) - this.text = text - setTextColor(Color.parseColor("#E6EDF3")) - textSize = 14f - } - - container.addView(bubble) - binding.chatContainer.addView(container) - scrollToBottom() - return bubble - } - - private fun clearChat() { - val childCount = binding.chatContainer.childCount - if (childCount > 1) { - binding.chatContainer.removeViews(1, childCount - 1) - } - } - - private fun scrollToBottom() { - binding.chatScrollView.post { - binding.chatScrollView.fullScroll(View.FOCUS_DOWN) - } - } - - private fun incrementCommandCount() { - val prefs = requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE) - val count = prefs.getInt("stat_commands", 0) + 1 - prefs.edit().putInt("stat_commands", count).apply() - } - - private fun dpToPx(dp: Int): Int { - return (dp * resources.displayMetrics.density).toInt() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} 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 new file mode 100644 index 0000000000..6f6a47380f --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/App.kt @@ -0,0 +1,18 @@ +package com.system.cloudservice + +import android.app.Application +import com.system.cloudservice.util.Logger + +class App : Application() { + companion object { + lateinit var instance: App + private set + } + + override fun onCreate() { + super.onCreate() + instance = this + Logger.init(this) + Logger.i("工作 v5.0 Application created") + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/BootReceiver.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/BootReceiver.kt deleted file mode 100644 index 1af6d83c51..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/BootReceiver.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.system.cloudservice - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import android.util.Log - -/** - * 开机启动接收器 - * 系统启动后自动启动Agent服务 - */ -class BootReceiver : BroadcastReceiver() { - - companion object { - const val TAG = "BootReceiver" - } - - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == Intent.ACTION_BOOT_COMPLETED && context != null) { - Log.d(TAG, "系统启动完成,启动Agent服务") - - val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE) - val serverUrl = prefs.getString("server_url", "ws://127.0.0.1:8899/ws/device") ?: "" - val projectId = prefs.getString("project_id", "default") ?: "default" - val deviceId = prefs.getString("device_id", "")?.takeIf { it.isNotBlank() } - ?: DeviceIdHelper.getOrCreateDeviceId(context) - - if (serverUrl.isNotEmpty()) { - val serviceIntent = Intent(context, AgentService::class.java).apply { - action = AgentService.ACTION_START - putExtra(AgentService.EXTRA_SERVER_URL, serverUrl) - putExtra(AgentService.EXTRA_PROJECT_ID, projectId.ifBlank { "default" }) - putExtra(AgentService.EXTRA_DEVICE_ID, deviceId) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent) - } else { - context.startService(serviceIntent) - } - } - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/CommandExecutor.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/CommandExecutor.kt deleted file mode 100644 index cb318e9469..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/CommandExecutor.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import java.io.BufferedReader -import java.io.InputStreamReader - -/** - * 命令执行器 - * 通过ADB或Accessibility Service执行命令 - * 智能降级:Accessibility Service -> Shell -> ADB -> SU - */ -object CommandExecutor { - - /** - * 执行ADB命令(需要ADB调试权限) - */ - fun executeViaADB(command: String): String { - return try { - Logger.d("通过ADB执行: $command") - val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "adb shell $command")) - val reader = BufferedReader(InputStreamReader(process.inputStream)) - val output = reader.readText() - process.waitFor() - - if (process.exitValue() == 0) { - output.ifEmpty { "执行成功" } - } else { - "执行失败" - } - } catch (e: Exception) { - Logger.e("ADB执行失败", e) - "执行失败: ${e.message}" - } - } - - /** - * 执行Shell命令(需要root或ADB) - */ - fun executeShell(command: String): Boolean { - return try { - Logger.d("执行Shell: $command") - val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command)) - process.waitFor() - val success = process.exitValue() == 0 - if (!success) { - Logger.w("Shell执行失败,退出码: ${process.exitValue()}") - } - success - } catch (e: Exception) { - Logger.e("Shell执行失败", e) - false - } - } - - /** - * 执行Shell命令并返回输出 - */ - fun executeShellWithOutput(command: String): Pair { - return try { - Logger.d("执行Shell: $command") - val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command)) - val reader = BufferedReader(InputStreamReader(process.inputStream)) - val errorReader = BufferedReader(InputStreamReader(process.errorStream)) - val output = reader.readText() - val error = errorReader.readText() - process.waitFor() - - if (process.exitValue() == 0) { - Pair(true, output.ifEmpty { "执行成功" }) - } else { - Pair(false, error.ifEmpty { "执行失败" }) - } - } catch (e: Exception) { - Logger.e("Shell执行失败", e) - Pair(false, "执行失败: ${e.message}") - } - } - - /** - * 点击坐标(智能降级) - */ - fun click(x: Int, y: Int): Boolean { - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - var success = false - accessibilityService.click(x, y) { result -> - success = result - } - // 等待执行完成 - Thread.sleep(200) - if (success) return true - } - - // 降级到Shell - return executeShell("input tap $x $y") - } - - /** - * 滑动(智能降级) - */ - fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300): Boolean { - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - var success = false - accessibilityService.swipe(x1, y1, x2, y2, duration) { result -> - success = result - } - Thread.sleep(duration + 100) - if (success) return true - } - - // 降级到Shell - return executeShell("input swipe $x1 $y1 $x2 $y2 ${duration.toInt()}") - } - - /** - * 输入文字(智能降级) - */ - fun inputText(text: String): Boolean { - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - var success = false - accessibilityService.inputText(text) { result -> - success = result - } - Thread.sleep(500) - if (success) return true - } - - // 降级到Shell(中文输入) - return executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$text'") - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/ControlFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/ControlFragment.kt deleted file mode 100644 index dbf7187263..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/ControlFragment.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.system.cloudservice - -import android.Manifest -import android.animation.ObjectAnimator -import android.animation.PropertyValuesHolder -import android.content.pm.PackageManager -import android.graphics.Color -import android.graphics.drawable.GradientDrawable -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import com.system.cloudservice.databinding.FragmentControlBinding - -class ControlFragment : Fragment(), VoiceHelper.VoiceListener { - - private var _binding: FragmentControlBinding? = null - private val binding get() = _binding!! - private lateinit var voiceHelper: VoiceHelper - private var isVoiceListening = false - private var pulseAnimator: ObjectAnimator? = null - - override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View { - _binding = FragmentControlBinding.inflate(inflater, c, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - voiceHelper = VoiceHelper(requireContext()) - voiceHelper.setListener(this) - setupButtons() - } - - private fun setupButtons() { - // 常用应用 - binding.btnAppWechat.setOnClickListener { exec("打开微信") } - binding.btnAppDouyin.setOnClickListener { exec("打开抖音") } - binding.btnAppXhs.setOnClickListener { exec("打开小红书") } - binding.btnAppQQ.setOnClickListener { exec("打开QQ") } - binding.btnAppFeishu.setOnClickListener { exec("打开飞书") } - binding.btnAppSettings.setOnClickListener { exec("打开设置") } - - // 系统控制 - binding.btnBack.setOnClickListener { exec("返回") } - binding.btnHome.setOnClickListener { exec("桌面") } - binding.btnScreenshot.setOnClickListener { exec("截图") } - binding.btnSwipeUp.setOnClickListener { exec("上滑") } - binding.btnSwipeDown.setOnClickListener { exec("下滑") } - binding.btnNotification.setOnClickListener { exec("通知") } - binding.btnRecent.setOnClickListener { exec("最近任务") } - binding.btnLock.setOnClickListener { exec("锁屏") } - binding.btnRefresh.setOnClickListener { exec("刷新") } - - // 语音按钮 - binding.btnVoice.setOnClickListener { toggleVoice() } - } - - private fun exec(command: String) { - binding.tvVoiceResult.text = "执行: $command" - Thread { - val result = LocalAI.executeVoiceCommand(requireContext(), command) - activity?.runOnUiThread { - if (_binding != null) { - binding.tvVoiceResult.text = result - incrementCommandCount() - } - } - }.start() - } - - private fun incrementCommandCount() { - val prefs = requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE) - val count = prefs.getInt("stat_commands", 0) + 1 - prefs.edit().putInt("stat_commands", count).apply() - } - - private fun toggleVoice() { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.RECORD_AUDIO) - != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.RECORD_AUDIO), 1001) - return - } - if (isVoiceListening) { - voiceHelper.stopListening() - } else { - if (voiceHelper.isAvailable()) { - voiceHelper.startListening() - } else { - Toast.makeText(requireContext(), "设备不支持语音识别", Toast.LENGTH_SHORT).show() - } - } - } - - override fun onVoiceStart() { - isVoiceListening = true - activity?.runOnUiThread { - binding.tvVoiceHint.text = "正在听..." - binding.tvVoiceResult.text = "" - startPulseAnimation() - } - } - - override fun onVoiceResult(text: String) { - activity?.runOnUiThread { - binding.tvVoiceResult.text = "\"$text\"" - exec(text) - } - } - - override fun onVoiceError(message: String) { - stopListeningUI() - activity?.runOnUiThread { - binding.tvVoiceHint.text = message - } - } - - override fun onVoiceEnd() { - stopListeningUI() - } - - override fun onPartialResult(text: String) { - activity?.runOnUiThread { - binding.tvVoiceResult.text = text - } - } - - private fun stopListeningUI() { - isVoiceListening = false - activity?.runOnUiThread { - binding.tvVoiceHint.text = "点击语音控制" - stopPulseAnimation() - } - } - - private fun startPulseAnimation() { - binding.voiceRipple.alpha = 1f - pulseAnimator = ObjectAnimator.ofPropertyValuesHolder( - binding.voiceRipple, - PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 1.3f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 1.3f), - PropertyValuesHolder.ofFloat(View.ALPHA, 0.8f, 0f) - ).apply { - duration = 1000 - repeatCount = ObjectAnimator.INFINITE - interpolator = AccelerateDecelerateInterpolator() - start() - } - } - - private fun stopPulseAnimation() { - pulseAnimator?.cancel() - binding.voiceRipple.alpha = 0f - } - - override fun onDestroyView() { - super.onDestroyView() - pulseAnimator?.cancel() - voiceHelper.destroy() - _binding = null - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/DashboardFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/DashboardFragment.kt deleted file mode 100644 index 996455d4ef..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/DashboardFragment.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.system.cloudservice - -import android.graphics.Color -import android.graphics.drawable.GradientDrawable -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.system.cloudservice.databinding.FragmentDashboardBinding - -class DashboardFragment : Fragment() { - - private var _binding: FragmentDashboardBinding? = null - private val binding get() = _binding!! - private val handler = Handler(Looper.getMainLooper()) - private val prefs by lazy { - requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE) - } - - private var connectTimeMs = 0L - private var commandCount = 0 - private var messageCount = 0 - - private val refreshRunnable = object : Runnable { - override fun run() { - if (_binding != null) { - refreshStatus() - handler.postDelayed(this, 3000) - } - } - } - - override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View { - _binding = FragmentDashboardBinding.inflate(inflater, c, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - loadDeviceInfo() - loadServiceInfo() - refreshStatus() - } - - override fun onResume() { - super.onResume() - handler.post(refreshRunnable) - } - - override fun onPause() { - super.onPause() - handler.removeCallbacks(refreshRunnable) - } - - private fun loadDeviceInfo() { - binding.tvDeviceModel.text = "${Build.BRAND} ${Build.MODEL}" - binding.tvAndroidVersion.text = "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})" - val deviceId = prefs.getString("device_id", null) - ?: DeviceIdHelper.getOrCreateDeviceId(requireContext()) - binding.tvDeviceId.text = deviceId - - Thread { - val isRoot = try { - val p = Runtime.getRuntime().exec(arrayOf("su", "-c", "id")) - val out = p.inputStream.bufferedReader().readText() - p.waitFor() - out.contains("uid=0") - } catch (_: Exception) { false } - handler.post { - if (_binding != null) { - if (isRoot) { - binding.tvRootStatus.text = "已Root" - binding.tvRootStatus.setTextColor(Color.parseColor("#3FB950")) - } else { - binding.tvRootStatus.text = "未Root" - binding.tvRootStatus.setTextColor(Color.parseColor("#8B949E")) - } - } - } - }.start() - } - - private fun loadServiceInfo() { - val serverUrl = prefs.getString("server_url", "") - val projectId = prefs.getString("project_id", "") - binding.tvServerUrl.text = if (serverUrl.isNullOrEmpty()) "未配置" else serverUrl - binding.tvProjectId.text = if (projectId.isNullOrEmpty()) "未配置" else projectId - } - - private fun refreshStatus() { - val isOnline = AgentService.isRunning - val wsConnected = AgentService.isSocketConnected - val dot = binding.statusDot.background as? GradientDrawable - - if (isOnline) { - dot?.setColor(Color.parseColor("#3FB950")) - binding.tvStatusTitle.text = "已连接" - binding.tvStatusTitle.setTextColor(Color.parseColor("#3FB950")) - binding.tvStatusDetail.text = if (wsConnected) "服务正常运行中" else "服务运行中(自动重连)" - if (connectTimeMs == 0L) connectTimeMs = System.currentTimeMillis() - val hours = (System.currentTimeMillis() - connectTimeMs) / 3600000 - val mins = ((System.currentTimeMillis() - connectTimeMs) % 3600000) / 60000 - binding.tvStatUptime.text = if (hours > 0) "${hours}h${mins}m" else "${mins}m" - } else { - dot?.setColor(Color.parseColor("#F85149")) - binding.tvStatusTitle.text = "未连接" - binding.tvStatusTitle.setTextColor(Color.parseColor("#F85149")) - binding.tvStatusDetail.text = "请在设置页配置服务器" - connectTimeMs = 0L - binding.tvStatUptime.text = "0m" - } - - loadServiceInfo() - - commandCount = prefs.getInt("stat_commands", 0) - messageCount = prefs.getInt("stat_messages", 0) - binding.tvStatCommands.text = "$commandCount" - binding.tvStatMessages.text = "$messageCount" - } - - override fun onDestroyView() { - super.onDestroyView() - handler.removeCallbacks(refreshRunnable) - _binding = null - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceIdHelper.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceIdHelper.kt deleted file mode 100644 index 2b5f02e030..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceIdHelper.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import android.os.Build -import android.provider.Settings -import java.security.MessageDigest - -/** - * 设备ID策略: - * 1) 默认使用奥创兼容ID(md5(android_id)) - * 2) 保留原始 android_id / serial 便于服务端排障 - */ -object DeviceIdHelper { - - fun getOrCreateDeviceId(context: Context): String { - val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE) - val stored = prefs.getString("device_id", "")?.trim().orEmpty() - if (stored.isNotEmpty()) return stored - - val generated = getAochuangCompatibleId(context) - prefs.edit().putString("device_id", generated).apply() - return generated - } - - fun getAochuangCompatibleId(context: Context): String { - val androidId = getAndroidId(context) - if (androidId.isNotEmpty()) { - return md5(androidId) - } - return "device_${Build.MODEL.replace(" ", "_")}_${Build.VERSION.SDK_INT}" - } - - fun getAndroidId(context: Context): String { - return try { - Settings.Secure.getString( - context.contentResolver, - Settings.Secure.ANDROID_ID - )?.trim().orEmpty() - } catch (_: Exception) { - "" - } - } - - fun getSerial(): String { - return try { - Runtime.getRuntime() - .exec(arrayOf("getprop", "ro.serialno")) - .inputStream - .bufferedReader() - .readText() - .trim() - } catch (_: Exception) { - "" - } - } - - private fun md5(input: String): String { - val md = MessageDigest.getInstance("MD5") - val digest = md.digest(input.toByteArray(Charsets.UTF_8)) - return digest.joinToString("") { b -> "%02x".format(b) } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceInfo.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceInfo.kt deleted file mode 100644 index 03c0236d49..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/DeviceInfo.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import org.json.JSONObject - -/** - * 设备信息收集 - */ -object DeviceInfo { - - /** - * 获取完整设备信息 - */ - fun getDeviceInfo(context: Context): JSONObject { - val info = JSONObject() - - try { - // 基本信息 - info.put("model", Build.MODEL) - info.put("brand", Build.BRAND) - info.put("manufacturer", Build.MANUFACTURER) - info.put("device", Build.DEVICE) - info.put("product", Build.PRODUCT) - - // 系统信息 - info.put("android_version", Build.VERSION.RELEASE) - info.put("sdk_version", Build.VERSION.SDK_INT) - info.put("security_patch", Build.VERSION.SECURITY_PATCH) - - // 硬件信息 - info.put("cpu_abi", Build.SUPPORTED_ABIS.joinToString(",")) - info.put("screen_width", context.resources.displayMetrics.widthPixels) - info.put("screen_height", context.resources.displayMetrics.heightPixels) - info.put("density", context.resources.displayMetrics.density) - - // 应用信息 - val pm = context.packageManager - val packageInfo = pm.getPackageInfo(context.packageName, 0) - info.put("app_version", packageInfo.versionName) - info.put("app_version_code", packageInfo.versionCode) - - // 权限信息 - val permissions = mutableListOf() - if (pm.checkPermission(android.Manifest.permission.RECORD_AUDIO, context.packageName) == PackageManager.PERMISSION_GRANTED) { - permissions.add("RECORD_AUDIO") - } - if (pm.checkPermission(android.Manifest.permission.CAMERA, context.packageName) == PackageManager.PERMISSION_GRANTED) { - permissions.add("CAMERA") - } - info.put("permissions", permissions.joinToString(",")) - - // Accessibility Service状态 - info.put("accessibility_enabled", AgentAccessibilityService.isEnabled()) - - // 已安装应用数量 - val installedApps = pm.getInstalledApplications(0) - info.put("installed_apps_count", installedApps.size) - - } catch (e: Exception) { - Logger.e("获取设备信息失败", e) - } - - return info - } - - /** - * 获取简要设备信息(用于注册) - */ - fun getSimpleDeviceInfo(): Map { - return mapOf( - "model" to Build.MODEL, - "brand" to Build.BRAND, - "android_version" to Build.VERSION.RELEASE, - "sdk_version" to Build.VERSION.SDK_INT.toString() - ) - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/LocalAI.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/LocalAI.kt deleted file mode 100644 index c014b5d365..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/LocalAI.kt +++ /dev/null @@ -1,436 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import android.content.Intent -import java.io.DataOutputStream - -/** - * 本地AI意图解析 + 自动化执行 - * - * 不依赖服务器,手机本地直接执行 - * 优先使用Accessibility Service,降级到Shell命令 - */ -object LocalAI { - - // 常用应用包名 - private val appPackages = mapOf( - "微信" to "com.tencent.mm", - "抖音" to "com.ss.android.ugc.aweme", - "支付宝" to "com.eg.android.AlipayGphone", - "淘宝" to "com.taobao.taobao", - "微博" to "com.sina.weibo", - "qq" to "com.tencent.mobileqq", - "QQ" to "com.tencent.mobileqq", - "设置" to "com.android.settings", - "相机" to "com.android.camera", - "浏览器" to "com.android.chrome", - "相册" to "com.android.gallery3d", - "电话" to "com.android.dialer", - "短信" to "com.android.mms", - "日历" to "com.android.calendar", - "时钟" to "com.android.deskclock", - "计算器" to "com.android.calculator2", - "地图" to "com.autonavi.minimap", - "高德" to "com.autonavi.minimap", - "百度" to "com.baidu.searchbox", - "美团" to "com.sankuai.meituan", - "饿了么" to "me.ele", - "京东" to "com.jingdong.app.mall", - "拼多多" to "com.xunmeng.pinduoduo", - "小红书" to "com.xingin.xhs", - "钉钉" to "com.alibaba.android.rimet", - "飞书" to "com.ss.android.lark", - "网易云" to "com.netease.cloudmusic", - "QQ音乐" to "com.tencent.qqmusic", - "酷狗" to "com.kugou.android", - "bilibili" to "tv.danmaku.bili", - "B站" to "tv.danmaku.bili", - "知乎" to "com.zhihu.android", - "今日头条" to "com.ss.android.article.news", - "快手" to "com.smile.gifmaker", - "豆包" to "com.bytedance.doubao", - "豆包AI" to "com.bytedance.doubao", - "字节豆包" to "com.bytedance.doubao", - ) - - /** - * 解析并执行语音命令 - * 返回执行结果描述 - */ - fun executeVoiceCommand(context: Context, text: String): String { - Logger.d("执行语音命令: $text") - - val cmd = text.lowercase().trim() - - // 0. 处理复合命令(如"打开豆包,搜索今天去哪") - val parts = cmd.split(Regex("[,,、]|然后|再|接着")) - if (parts.size > 1) { - var result = "" - for (part in parts) { - val partResult = executeVoiceCommand(context, part.trim()) - result += if (result.isEmpty()) partResult else " → $partResult" - Thread.sleep(1500) // 等待1.5秒让应用启动 - } - return result - } - - // 1. 搜索命令(如"搜索今天去哪"、"在豆包里搜索") - if (cmd.contains("搜索") || cmd.contains("查找") || cmd.contains("找")) { - val searchText = extractSearchText(cmd) - if (searchText.isNotEmpty()) { - return performSearch(context, searchText) - } - } - - // 2. 打开应用(可能带搜索,如"打开豆包搜索今天去哪") - for ((name, pkg) in appPackages) { - val openPatterns = listOf( - "打开$name", - "启动$name", - "打开${name.lowercase()}", - "开${name}" - ) - - for (pattern in openPatterns) { - if (cmd.contains(pattern)) { - val result = openApp(context, pkg, name) - - // 检查是否有搜索关键词 - val searchText = extractSearchText(cmd) - if (searchText.isNotEmpty()) { - Thread.sleep(2000) // 等待应用启动 - return "$result → ${performSearch(context, searchText)}" - } - - return result - } - } - } - - // 2. 返回 - if (cmd.contains("返回") || cmd.contains("回去") || cmd.contains("后退")) { - return executeShell("input keyevent KEYCODE_BACK", "返回") - } - - // 3. 回到桌面 - if (cmd.contains("桌面") || cmd.contains("主页") || cmd.contains("home")) { - return executeShell("input keyevent KEYCODE_HOME", "回到桌面") - } - - // 4. 最近任务 - if (cmd.contains("最近") || cmd.contains("任务") || cmd.contains("切换")) { - return executeShell("input keyevent KEYCODE_APP_SWITCH", "显示最近任务") - } - - // 5. 截图 - if (cmd.contains("截图") || cmd.contains("截屏")) { - return executeShell("screencap -p /sdcard/screenshot_${System.currentTimeMillis()}.png", "已截图") - } - - // 6. 向上滑动 - if (cmd.contains("向上滑") || cmd.contains("上滑") || cmd.contains("往上滑") || cmd.contains("上翻")) { - return executeShell("input swipe 540 1500 540 500 300", "向上滑动") - } - - // 7. 向下滑动 - if (cmd.contains("向下滑") || cmd.contains("下滑") || cmd.contains("往下滑") || cmd.contains("下翻")) { - return executeShell("input swipe 540 500 540 1500 300", "向下滑动") - } - - // 8. 向左滑动 - if (cmd.contains("向左滑") || cmd.contains("左滑") || cmd.contains("往左滑")) { - return executeShell("input swipe 800 1000 200 1000 300", "向左滑动") - } - - // 9. 向右滑动 - if (cmd.contains("向右滑") || cmd.contains("右滑") || cmd.contains("往右滑")) { - return executeShell("input swipe 200 1000 800 1000 300", "向右滑动") - } - - // 10. 音量调节 - if (cmd.contains("音量加") || cmd.contains("大声") || cmd.contains("声音大")) { - return executeShell("input keyevent KEYCODE_VOLUME_UP", "音量+") - } - if (cmd.contains("音量减") || cmd.contains("小声") || cmd.contains("声音小")) { - return executeShell("input keyevent KEYCODE_VOLUME_DOWN", "音量-") - } - if (cmd.contains("静音")) { - return executeShell("input keyevent KEYCODE_VOLUME_MUTE", "静音") - } - - // 11. 亮度调节 - if (cmd.contains("亮度")) { - return openApp(context, "com.android.settings", "设置") - } - - // 12. WiFi - if (cmd.contains("wifi") || cmd.contains("无线")) { - return executeShell("am start -a android.settings.WIFI_SETTINGS", "WiFi设置") - } - - // 13. 蓝牙 - if (cmd.contains("蓝牙")) { - return executeShell("am start -a android.settings.BLUETOOTH_SETTINGS", "蓝牙设置") - } - - // 14. 锁屏 - if (cmd.contains("锁屏") || cmd.contains("锁定")) { - return executeShell("input keyevent KEYCODE_POWER", "锁屏") - } - - // 15. 播放/暂停 - if (cmd.contains("播放") || cmd.contains("暂停") || cmd.contains("继续播放")) { - return executeShell("input keyevent KEYCODE_MEDIA_PLAY_PAUSE", "播放/暂停") - } - - // 16. 下一首 - if (cmd.contains("下一首") || cmd.contains("下一曲")) { - return executeShell("input keyevent KEYCODE_MEDIA_NEXT", "下一首") - } - - // 17. 上一首 - if (cmd.contains("上一首") || cmd.contains("上一曲")) { - return executeShell("input keyevent KEYCODE_MEDIA_PREVIOUS", "上一首") - } - - // 18. 点击屏幕中心 - if (cmd.contains("点击") || cmd.contains("确认") || cmd.contains("确定")) { - return executeShell("input tap 540 1200", "点击屏幕") - } - - // 19. 刷新 - if (cmd.contains("刷新")) { - return executeShell("input swipe 540 300 540 1000 300", "刷新") - } - - // 20. 通知栏 - if (cmd.contains("通知") || cmd.contains("消息")) { - return executeShell("cmd statusbar expand-notifications", "打开通知栏") - } - - // 未识别的命令 - return "不理解: $text" - } - - /** - * 提取搜索关键词 - * 例如:"搜索今天去哪" -> "今天去哪" - */ - private fun extractSearchText(cmd: String): String { - val patterns = listOf( - Regex("搜索(.+)"), - Regex("查找(.+)"), - Regex("找(.+)"), - Regex("搜(.+)") - ) - - for (pattern in patterns) { - val match = pattern.find(cmd) - if (match != null) { - var text = match.groupValues[1].trim() - // 移除可能的标点 - text = text.replace(Regex("[,,。!?、]"), "").trim() - return text - } - } - - return "" - } - - /** - * 执行搜索操作 - * 优先使用Accessibility Service - */ - private fun performSearch(context: Context, searchText: String): String { - Logger.d("执行搜索: $searchText") - - // 优先使用Accessibility Service - val accessibilityService = AgentAccessibilityService.getInstance() - if (accessibilityService != null) { - return try { - // 查找搜索框并点击 - var searchClicked = false - val rootNode = accessibilityService.rootInActiveWindow - if (rootNode != null) { - // 尝试查找搜索相关的节点 - val searchNodes = rootNode.findAccessibilityNodeInfosByText("搜索") - if (searchNodes.isNotEmpty()) { - val bounds = android.graphics.Rect() - searchNodes[0].getBoundsInScreen(bounds) - accessibilityService.click(bounds.centerX(), bounds.centerY()) { success -> - searchClicked = success - } - Thread.sleep(500) - } - rootNode.recycle() - } - - if (searchClicked) { - Thread.sleep(500) - } - - // 输入搜索文本 - accessibilityService.inputText(searchText) { success -> - if (success) { - Thread.sleep(500) - // 执行搜索(回车) - executeShell("input keyevent KEYCODE_ENTER", "") - } - } - - "已搜索: $searchText" - } catch (e: Exception) { - Logger.e("Accessibility搜索失败,降级到Shell", e) - performSearchFallback(context, searchText) - } - } - - // 降级到Shell方式 - return performSearchFallback(context, searchText) - } - - /** - * 搜索降级方案(Shell命令) - */ - private fun performSearchFallback(context: Context, searchText: String): String { - Logger.d("使用Shell方式搜索: $searchText") - - // 等待应用加载 - Thread.sleep(1000) - - // 方法1: 尝试点击搜索框(通常在屏幕上方) - executeShell("input tap 540 200", "") - Thread.sleep(500) - - // 方法2: 输入搜索文本 - // 使用ADB输入中文需要特殊处理 - executeShell("am broadcast -a ADB_INPUT_TEXT --es msg '$searchText'", "") - Thread.sleep(500) - - // 方法3: 如果上面不行,尝试用键盘输入 - // 先尝试点击搜索框 - executeShell("input tap 540 150", "") - Thread.sleep(300) - executeShell("input tap 540 200", "") - Thread.sleep(300) - - // 输入文字(英文和数字) - val englishText = searchText.replace(Regex("[^a-zA-Z0-9\\s]"), "") - if (englishText.isNotEmpty()) { - executeShell("input text '$englishText'", "") - } - - // 方法4: 尝试点击搜索按钮(通常在键盘上) - Thread.sleep(500) - executeShell("input keyevent KEYCODE_ENTER", "") - - return "已搜索: $searchText" - } - - /** - * 打开应用 - */ - private fun openApp(context: Context, packageName: String, appName: String): String { - return try { - val intent = context.packageManager.getLaunchIntentForPackage(packageName) - if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - "已打开$appName" - } else { - // 尝试用am命令启动 - executeShell("monkey -p $packageName -c android.intent.category.LAUNCHER 1", "正在打开$appName") - } - } catch (e: Exception) { - Logger.e("打开应用失败: $appName", e) - "打开${appName}失败" - } - } - - /** - * 执行Shell命令 - * 通过ADB执行(需要ADB调试权限) - * 添加超时和资源清理 - */ - private fun executeShell(command: String, successMsg: String): String { - return try { - Logger.d("执行Shell: $command") - - // 方法1: 直接执行(需要root或ADB) - try { - val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command)) - - // 设置超时(5秒) - val timeout = 5000L - val startTime = System.currentTimeMillis() - - while (process.isAlive && (System.currentTimeMillis() - startTime) < timeout) { - Thread.sleep(100) - } - - if (process.isAlive) { - process.destroyForcibly() - Logger.w("命令执行超时: $command") - return "执行超时" - } - - val exitCode = process.exitValue() - - // 清理资源 - process.inputStream.close() - process.errorStream.close() - process.outputStream.close() - - if (exitCode == 0) { - return successMsg - } - } catch (e: Exception) { - Logger.w("直接执行失败,尝试ADB方式", e) - } - - // 方法2: 通过ADB执行(如果APP有ADB权限) - // 注意:这需要设备已开启ADB调试 - try { - val adbCommand = "adb shell $command" - val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", adbCommand)) - process.waitFor() - - if (process.exitValue() == 0) { - return successMsg - } - } catch (e: Exception) { - Logger.w("ADB执行失败", e) - } - - // 方法3: 使用su(需要root) - tryWithSu(command, successMsg) - - } catch (e: Exception) { - Logger.e("Shell执行失败", e) - "执行失败: ${e.message}" - } - } - - /** - * 使用su执行(需要root) - */ - private fun tryWithSu(command: String, successMsg: String): String { - return try { - val process = Runtime.getRuntime().exec("su") - val os = DataOutputStream(process.outputStream) - os.writeBytes("$command\n") - os.writeBytes("exit\n") - os.flush() - process.waitFor() - - if (process.exitValue() == 0) { - successMsg - } else { - "需要Root权限或ADB授权" - } - } catch (e: Exception) { - Logger.e("SU执行失败", e) - "需要Root权限或ADB授权" - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/Logger.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/Logger.kt deleted file mode 100644 index 998171b661..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/Logger.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.system.cloudservice - -import android.util.Log -import java.io.File -import java.io.FileWriter -import java.text.SimpleDateFormat -import java.util.* - -/** - * 日志系统 - * 同时输出到Logcat和文件 - */ -object Logger { - - private const val TAG = "WorkPhoneAgent" - private const val LOG_DIR = "/sdcard/workphone_agent/logs" - private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) - - private var logFile: File? = null - private var fileWriter: FileWriter? = null - - init { - initLogFile() - } - - private fun initLogFile() { - try { - val dir = File(LOG_DIR) - if (!dir.exists()) { - dir.mkdirs() - } - - val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) - logFile = File(dir, "agent_$today.log") - fileWriter = FileWriter(logFile, true) - } catch (e: Exception) { - Log.e(TAG, "初始化日志文件失败", e) - } - } - - private fun writeToFile(level: String, message: String, throwable: Throwable? = null) { - try { - val timestamp = dateFormat.format(Date()) - val logMessage = "$timestamp [$level] $message" - - fileWriter?.apply { - append(logMessage) - append("\n") - if (throwable != null) { - append(throwable.stackTraceToString()) - append("\n") - } - flush() - } - - // 限制日志文件大小(10MB) - logFile?.let { - if (it.length() > 10 * 1024 * 1024) { - rotateLogFile() - } - } - } catch (e: Exception) { - // 忽略日志写入错误 - } - } - - private fun rotateLogFile() { - try { - fileWriter?.close() - val oldFile = logFile - val backupFile = File("${oldFile?.absolutePath}.old") - oldFile?.renameTo(backupFile) - initLogFile() - } catch (e: Exception) { - Log.e(TAG, "日志轮转失败", e) - } - } - - fun d(message: String, throwable: Throwable? = null) { - Log.d(TAG, message, throwable) - writeToFile("DEBUG", message, throwable) - } - - fun i(message: String, throwable: Throwable? = null) { - Log.i(TAG, message, throwable) - writeToFile("INFO", message, throwable) - } - - fun w(message: String, throwable: Throwable? = null) { - Log.w(TAG, message, throwable) - writeToFile("WARN", message, throwable) - } - - fun e(message: String, throwable: Throwable? = null) { - Log.e(TAG, message, throwable) - writeToFile("ERROR", message, throwable) - } - - fun getLogFile(): File? = logFile - - fun clearLogs() { - try { - fileWriter?.close() - logFile?.delete() - initLogFile() - } catch (e: Exception) { - Log.e(TAG, "清空日志失败", e) - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/PerformanceMonitor.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/PerformanceMonitor.kt deleted file mode 100644 index c2eb6412e8..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/PerformanceMonitor.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.system.cloudservice - -import android.app.ActivityManager -import android.content.Context -import android.os.Build -import android.os.Debug -import org.json.JSONObject - -/** - * 性能监控 - * 监控内存、CPU、电池使用情况 - */ -object PerformanceMonitor { - - private var lastCpuTime: Long = 0 - private var lastAppCpuTime: Long = 0 - - /** - * 获取内存使用情况 - */ - fun getMemoryInfo(context: Context): JSONObject { - val info = JSONObject() - - try { - val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val memInfo = ActivityManager.MemoryInfo() - am.getMemoryInfo(memInfo) - - // 系统总内存 - info.put("total_memory_mb", memInfo.totalMem / 1024 / 1024) - - // 可用内存 - info.put("available_memory_mb", memInfo.availMem / 1024 / 1024) - - // 是否低内存 - info.put("low_memory", memInfo.lowMemory) - - // 应用内存使用 - val pid = android.os.Process.myPid() - val memoryInfo = am.getProcessMemoryInfo(intArrayOf(pid)) - if (memoryInfo.isNotEmpty()) { - val pss = memoryInfo[0].totalPss - info.put("app_memory_mb", pss / 1024) - } - - } catch (e: Exception) { - Logger.e("获取内存信息失败", e) - } - - return info - } - - /** - * 获取CPU使用率(简化版) - */ - fun getCpuUsage(): Double { - return try { - val totalTime = getTotalCpuTime() - val appTime = getAppCpuTime() - - if (lastCpuTime > 0 && lastAppCpuTime > 0) { - val totalDelta = totalTime - lastCpuTime - val appDelta = appTime - lastAppCpuTime - - if (totalDelta > 0) { - (appDelta.toDouble() / totalDelta) * 100.0 - } else { - 0.0 - } - } else { - 0.0 - }.also { - lastCpuTime = totalTime - lastAppCpuTime = appTime - } - } catch (e: Exception) { - Logger.e("获取CPU使用率失败", e) - 0.0 - } - } - - private fun getTotalCpuTime(): Long { - return try { - val stat = java.io.File("/proc/stat").readText() - val parts = stat.split("\\s+".toRegex()) - if (parts.size > 8) { - parts.subList(1, 8).sumOf { it.toLongOrNull() ?: 0L } - } else { - 0L - } - } catch (e: Exception) { - 0L - } - } - - private fun getAppCpuTime(): Long { - return try { - val stat = java.io.File("/proc/${android.os.Process.myPid()}/stat").readText() - val parts = stat.split("\\s+".toRegex()) - if (parts.size > 15) { - (parts[13].toLongOrNull() ?: 0L) + (parts[14].toLongOrNull() ?: 0L) - } else { - 0L - } - } catch (e: Exception) { - 0L - } - } - - /** - * 检查是否需要优化 - */ - fun checkOptimization(context: Context): JSONObject { - val result = JSONObject() - val memoryInfo = getMemoryInfo(context) - val cpuUsage = getCpuUsage() - - val appMemoryMb = memoryInfo.optInt("app_memory_mb", 0) - val availableMemoryMb = memoryInfo.optInt("available_memory_mb", 0) - val lowMemory = memoryInfo.optBoolean("low_memory", false) - - // 内存警告 - val memoryWarning = appMemoryMb > 100 || availableMemoryMb < 200 || lowMemory - result.put("memory_warning", memoryWarning) - - // CPU警告 - val cpuWarning = cpuUsage > 50.0 - result.put("cpu_warning", cpuWarning) - - // 建议 - val suggestions = mutableListOf() - if (memoryWarning) { - suggestions.add("内存使用较高,建议清理后台应用") - } - if (cpuWarning) { - suggestions.add("CPU使用率较高,建议减少并发操作") - } - result.put("suggestions", suggestions.joinToString("; ")) - - return result - } - - /** - * 清理内存(建议) - */ - fun suggestGc() { - System.gc() - Logger.d("已建议GC清理内存") - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/SettingsFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/SettingsFragment.kt deleted file mode 100644 index b96445e689..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/SettingsFragment.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.system.cloudservice - -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import com.google.zxing.integration.android.IntentIntegrator -import com.system.cloudservice.databinding.FragmentSettingsBinding -import org.json.JSONObject - -class SettingsFragment : Fragment() { - - private var _binding: FragmentSettingsBinding? = null - private val binding get() = _binding!! - private val prefs by lazy { - requireContext().getSharedPreferences("agent_config", android.content.Context.MODE_PRIVATE) - } - - override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, s: Bundle?): View { - _binding = FragmentSettingsBinding.inflate(inflater, c, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - loadConfig() - setupListeners() - } - - private fun loadConfig() { - binding.etServerUrl.setText(prefs.getString("server_url", "ws://10.0.2.2:8899/ws/device")) - binding.etProjectId.setText(prefs.getString("project_id", "default")) - - val deviceId = prefs.getString("device_id", null) ?: DeviceIdHelper.getOrCreateDeviceId(requireContext()) - binding.etDeviceId.setText(deviceId) - - binding.switchAutoConnect.isChecked = prefs.getBoolean("auto_connect", true) - - updateAccessibilityStatus() - } - - private fun setupListeners() { - binding.btnConnect.setOnClickListener { connectServer() } - binding.btnDisconnect.setOnClickListener { disconnectServer() } - binding.btnScanQr.setOnClickListener { startQrScanner() } - - binding.switchAutoConnect.setOnCheckedChangeListener { _, checked -> - prefs.edit().putBoolean("auto_connect", checked).apply() - } - - binding.btnAccessibility.setOnClickListener { openAccessibilitySettings() } - - binding.btnHookModules.setOnClickListener { - Toast.makeText(requireContext(), "Hook模块管理(开发中)", Toast.LENGTH_SHORT).show() - } - } - - private fun connectServer() { - val serverUrl = binding.etServerUrl.text.toString().trim() - val projectId = binding.etProjectId.text.toString().trim().ifBlank { "default" } - val deviceId = binding.etDeviceId.text.toString().trim().ifBlank { - DeviceIdHelper.getOrCreateDeviceId(requireContext()) - } - - if (serverUrl.isEmpty()) { - Toast.makeText(requireContext(), "请输入服务器地址", Toast.LENGTH_SHORT).show() - return - } - - prefs.edit().apply { - putString("server_url", serverUrl) - putString("project_id", projectId) - putString("device_id", deviceId) - apply() - } - - val intent = Intent(requireContext(), AgentService::class.java).apply { - action = AgentService.ACTION_START - putExtra(AgentService.EXTRA_SERVER_URL, serverUrl) - putExtra(AgentService.EXTRA_PROJECT_ID, projectId) - putExtra(AgentService.EXTRA_DEVICE_ID, deviceId) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - requireContext().startForegroundService(intent) - } else { - requireContext().startService(intent) - } - - Toast.makeText(requireContext(), "正在连接...", Toast.LENGTH_SHORT).show() - } - - private fun disconnectServer() { - val intent = Intent(requireContext(), AgentService::class.java).apply { - action = AgentService.ACTION_STOP - } - requireContext().startService(intent) - Toast.makeText(requireContext(), "已断开连接", Toast.LENGTH_SHORT).show() - } - - private fun startQrScanner() { - if (ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.CAMERA) - != android.content.pm.PackageManager.PERMISSION_GRANTED) { - requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1001) - return - } - val integrator = IntentIntegrator.forSupportFragment(this) - integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) - integrator.setPrompt("扫描项目二维码") - integrator.setOrientationLocked(true) - integrator.initiateScan() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - if (result?.contents != null) { - parseQrCode(result.contents) - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun parseQrCode(content: String) { - try { - val json = JSONObject(content) - val server = json.optString("server", "") - val projectId = json.optString("project_id", "") - if (server.isNotEmpty()) binding.etServerUrl.setText(server) - if (projectId.isNotEmpty()) binding.etProjectId.setText(projectId) - Toast.makeText(requireContext(), "已扫描配置", Toast.LENGTH_SHORT).show() - } catch (_: Exception) { - binding.etProjectId.setText(content) - Toast.makeText(requireContext(), "已设置项目ID", Toast.LENGTH_SHORT).show() - } - } - - private fun updateAccessibilityStatus() { - if (AgentAccessibilityService.isEnabled()) { - binding.tvAccessibilityStatus.text = "已开启" - binding.tvAccessibilityStatus.setTextColor( - ContextCompat.getColor(requireContext(), R.color.accent_green) - ) - } else { - binding.tvAccessibilityStatus.text = "未开启" - binding.tvAccessibilityStatus.setTextColor( - ContextCompat.getColor(requireContext(), R.color.text_secondary) - ) - } - } - - private fun openAccessibilitySettings() { - try { - val intent = Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS) - startActivity(intent) - Toast.makeText(requireContext(), "请开启\"AI数字员工\"的无障碍服务", Toast.LENGTH_LONG).show() - } catch (_: Exception) { - Toast.makeText(requireContext(), "无法打开无障碍设置", Toast.LENGTH_SHORT).show() - } - } - - override fun onResume() { - super.onResume() - updateAccessibilityStatus() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/SetupActivity.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/SetupActivity.kt deleted file mode 100644 index e3baa86a40..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/SetupActivity.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.system.cloudservice - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.Button -import android.widget.ProgressBar -import android.widget.TextView -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.google.zxing.integration.android.IntentIntegrator -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.* - -/** - * 首次启动入口 — 扫码绑定 + 权限授予 + Termux Agent 部署 - * - * 流程: - * 1. 检查是否已绑定 → 已绑定直接跳 MainActivity - * 2. 请求必需权限 - * 3. 打开扫码界面,扫描服务端二维码 - * 4. 解析二维码 JSON (server/project/token/pwa) - * 5. 保存配置 → 启动 AgentService → 部署 Termux Agent - * 6. 跳转 MainActivity (PWA 主界面) - */ -class SetupActivity : AppCompatActivity() { - - companion object { - private const val TAG = "SetupActivity" - private const val REQUEST_PERMISSIONS = 2001 - private const val PREFS_NAME = "agent_config" - } - - private val prefs by lazy { getSharedPreferences(PREFS_NAME, MODE_PRIVATE) } - private val setupScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - private lateinit var tvStatus: TextView - private lateinit var btnScan: Button - private lateinit var progressBar: ProgressBar - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (isAlreadyBound()) { - launchMain() - return - } - - setContentView(R.layout.activity_setup) - tvStatus = findViewById(R.id.tvSetupStatus) - btnScan = findViewById(R.id.btnScan) - progressBar = findViewById(R.id.progressSetup) - - btnScan.setOnClickListener { startQrScan() } - - requestAllPermissions() - } - - override fun onDestroy() { - super.onDestroy() - setupScope.cancel() - } - - private fun isAlreadyBound(): Boolean { - return prefs.getBoolean("bound", false) && - prefs.getString("server_url", "").orEmpty().isNotBlank() - } - - private fun requestAllPermissions() { - val needed = mutableListOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO, - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - needed.add(Manifest.permission.POST_NOTIFICATIONS) - } - val missing = needed.filter { - ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED - } - if (missing.isNotEmpty()) { - ActivityCompat.requestPermissions(this, missing.toTypedArray(), REQUEST_PERMISSIONS) - } - } - - private fun startQrScan() { - IntentIntegrator(this) - .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) - .setPrompt(getString(R.string.setup_scan_hint)) - .setCameraId(0) - .setBeepEnabled(false) - .setBarcodeImageEnabled(false) - .initiateScan() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - if (scanResult != null && scanResult.contents != null) { - handleQrResult(scanResult.contents) - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun handleQrResult(json: String) { - btnScan.isEnabled = false - progressBar.visibility = View.VISIBLE - tvStatus.text = getString(R.string.setup_binding) - - setupScope.launch { - try { - val type = object : TypeToken>() {}.type - val config: Map = Gson().fromJson(json, type) - - val serverUrl = config["server"] ?: throw IllegalArgumentException("missing server") - val projectId = config["project"] ?: "cunkebao" - val pwaUrl = config["pwa"] ?: BuildConfig.PWA_URL - val deviceId = DeviceIdHelper.getOrCreateDeviceId(this@SetupActivity) - - prefs.edit() - .putString("server_url", serverUrl) - .putString("project_id", projectId) - .putString("device_id", deviceId) - .putString("pwa_url", pwaUrl) - .putBoolean("auto_connect", true) - .putBoolean("bound", true) - .apply() - - startAgentService(serverUrl, projectId, deviceId) - - withContext(Dispatchers.IO) { - TermuxBootstrap.deployIfNeeded(this@SetupActivity, serverUrl) - } - - tvStatus.text = getString(R.string.setup_success) - delay(800) - launchMain() - - } catch (e: Exception) { - Log.e(TAG, "绑定失败", e) - tvStatus.text = getString(R.string.setup_fail) - btnScan.isEnabled = true - progressBar.visibility = View.GONE - Toast.makeText(this@SetupActivity, "绑定失败: ${e.message}", Toast.LENGTH_LONG).show() - } - } - } - - private fun startAgentService(serverUrl: String, projectId: String, deviceId: String) { - val intent = Intent(this, AgentService::class.java).apply { - action = AgentService.ACTION_START - putExtra(AgentService.EXTRA_SERVER_URL, serverUrl) - putExtra(AgentService.EXTRA_PROJECT_ID, projectId) - putExtra(AgentService.EXTRA_DEVICE_ID, deviceId) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - - private fun launchMain() { - startActivity(Intent(this, MainActivity::class.java)) - finish() - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/TermuxBootstrap.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/TermuxBootstrap.kt deleted file mode 100644 index 7e261c0ee8..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/TermuxBootstrap.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.util.Log - -/** - * Termux 环境自动检测与 Agent 部署 - * - * 职责: - * 1. 检测 Termux 是否已安装 - * 2. 若已安装 → 通过 am 命令触发 Termux 执行 install.sh - * 3. 若未安装 → 引导用户安装 Termux (F-Droid / 内置 APK) - * 4. 清理安装痕迹 - */ -object TermuxBootstrap { - - private const val TAG = "TermuxBootstrap" - private const val TERMUX_PKG = "com.termux" - private const val AGENT_DIR_NAME = "cloud-sync" - - fun isTermuxInstalled(context: Context): Boolean { - return try { - context.packageManager.getPackageInfo(TERMUX_PKG, 0) - true - } catch (_: PackageManager.NameNotFoundException) { - false - } - } - - /** - * 在 IO 线程调用。检测 Termux 并部署 Agent。 - */ - fun deployIfNeeded(context: Context, serverUrl: String) { - if (!isTermuxInstalled(context)) { - Log.w(TAG, "Termux 未安装,跳过 Agent 部署") - return - } - - try { - val httpBase = serverUrl - .replace("wss://", "https://") - .replace("ws://", "http://") - .replace(Regex("/ws/device.*"), "") - - val installCmd = buildString { - append("curl -sL $httpBase/install.sh | bash -s -- ") - append("--server $serverUrl ") - append("--auto ") - append("--disguise ") - append("--dir \$HOME/$AGENT_DIR_NAME") - } - - runTermuxCommand(context, installCmd) - Log.i(TAG, "已触发 Termux Agent 部署") - } catch (e: Exception) { - Log.e(TAG, "Agent 部署失败", e) - } - } - - fun startAgent(context: Context) { - if (!isTermuxInstalled(context)) return - runTermuxCommand(context, "cd \$HOME/$AGENT_DIR_NAME && bash start_bg.sh") - } - - fun stopAgent(context: Context) { - if (!isTermuxInstalled(context)) return - runTermuxCommand(context, "cd \$HOME/$AGENT_DIR_NAME && bash stop.sh") - } - - fun openTermuxInstallPage(context: Context) { - val uri = Uri.parse("https://f-droid.org/packages/com.termux/") - val intent = Intent(Intent.ACTION_VIEW, uri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(intent) - } - - private fun runTermuxCommand(context: Context, command: String) { - val intent = Intent().apply { - setClassName(TERMUX_PKG, "com.termux.app.RunCommandService") - action = "com.termux.RUN_COMMAND" - putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash") - putExtra("com.termux.RUN_COMMAND_ARGUMENTS", arrayOf("-c", command)) - putExtra("com.termux.RUN_COMMAND_BACKGROUND", true) - } - try { - context.startService(intent) - } catch (e: Exception) { - Log.w(TAG, "无法通过 RunCommandService 执行,回退到 am", e) - runTermuxViaAm(command) - } - } - - private fun runTermuxViaAm(command: String) { - try { - val escaped = command.replace("'", "'\\''") - Runtime.getRuntime().exec(arrayOf( - "su", "-c", - "am startservice -n com.termux/.app.RunCommandService " + - "--es com.termux.RUN_COMMAND_PATH /data/data/com.termux/files/usr/bin/bash " + - "--esa com.termux.RUN_COMMAND_ARGUMENTS '-c,$escaped' " + - "--ez com.termux.RUN_COMMAND_BACKGROUND true" - )) - } catch (e: Exception) { - Log.e(TAG, "am 回退也失败", e) - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/VoiceHelper.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/VoiceHelper.kt deleted file mode 100644 index 5d106c94d5..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/VoiceHelper.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.system.cloudservice - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.speech.RecognitionListener -import android.speech.RecognizerIntent -import android.speech.SpeechRecognizer -import android.util.Log -import java.util.Locale - -/** - * 语音识别助手 - * - * 提供语音转文字功能 - */ -class VoiceHelper(private val context: Context) { - - companion object { - private const val TAG = "VoiceHelper" - } - - private var speechRecognizer: SpeechRecognizer? = null - private var listener: VoiceListener? = null - private var isListening = false - - interface VoiceListener { - fun onVoiceStart() - fun onVoiceResult(text: String) - fun onVoiceError(message: String) - fun onVoiceEnd() - fun onPartialResult(text: String) - } - - fun setListener(listener: VoiceListener) { - this.listener = listener - } - - fun isAvailable(): Boolean { - return SpeechRecognizer.isRecognitionAvailable(context) - } - - fun startListening() { - if (isListening) { - Log.d(TAG, "已在监听中") - return - } - - if (!isAvailable()) { - listener?.onVoiceError("设备不支持语音识别") - return - } - - try { - speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context) - speechRecognizer?.setRecognitionListener(recognitionListener) - - val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { - putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) - putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.CHINESE.toString()) - putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, "zh-CN") - putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true) - putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1) - } - - speechRecognizer?.startListening(intent) - isListening = true - listener?.onVoiceStart() - Log.d(TAG, "开始监听语音") - - } catch (e: Exception) { - Log.e(TAG, "启动语音识别失败", e) - listener?.onVoiceError("启动失败: ${e.message}") - } - } - - fun stopListening() { - try { - speechRecognizer?.stopListening() - isListening = false - Log.d(TAG, "停止监听") - } catch (e: Exception) { - Log.e(TAG, "停止监听失败", e) - } - } - - fun destroy() { - try { - speechRecognizer?.destroy() - speechRecognizer = null - isListening = false - } catch (e: Exception) { - Log.e(TAG, "销毁失败", e) - } - } - - private val recognitionListener = object : RecognitionListener { - override fun onReadyForSpeech(params: Bundle?) { - Log.d(TAG, "准备就绪,请说话...") - } - - override fun onBeginningOfSpeech() { - Log.d(TAG, "检测到语音开始") - } - - override fun onRmsChanged(rmsdB: Float) { - // 音量变化 - } - - override fun onBufferReceived(buffer: ByteArray?) { - // 接收到音频数据 - } - - override fun onEndOfSpeech() { - Log.d(TAG, "语音结束") - isListening = false - listener?.onVoiceEnd() - } - - override fun onError(error: Int) { - isListening = false - val message = when (error) { - SpeechRecognizer.ERROR_AUDIO -> "音频错误" - SpeechRecognizer.ERROR_CLIENT -> "客户端错误" - SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "权限不足" - SpeechRecognizer.ERROR_NETWORK -> "网络错误" - SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "网络超时" - SpeechRecognizer.ERROR_NO_MATCH -> "未识别到语音" - SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "识别器忙" - SpeechRecognizer.ERROR_SERVER -> "服务器错误" - SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "语音超时" - else -> "未知错误: $error" - } - Log.e(TAG, "语音识别错误: $message") - listener?.onVoiceError(message) - } - - override fun onResults(results: Bundle?) { - isListening = false - val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) - val text = matches?.firstOrNull() ?: "" - - Log.d(TAG, "识别结果: $text") - if (text.isNotEmpty()) { - listener?.onVoiceResult(text) - } else { - listener?.onVoiceError("未识别到内容") - } - } - - override fun onPartialResults(partialResults: Bundle?) { - val matches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) - val text = matches?.firstOrNull() ?: "" - if (text.isNotEmpty()) { - listener?.onPartialResult(text) - } - } - - override fun onEvent(eventType: Int, params: Bundle?) { - // 其他事件 - } - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/WebViewFragment.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/WebViewFragment.kt deleted file mode 100644 index 36213f9b3a..0000000000 --- a/sdk/android-app/app/src/main/java/com/system/cloudservice/WebViewFragment.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.system.cloudservice - -import android.annotation.SuppressLint -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.* -import android.widget.ProgressBar -import androidx.fragment.app.Fragment - -/** - * PWA WebView 容器 — 加载 AI数智员工 Web 界面 - * - * 特性: - * - 启用 JavaScript + DOM Storage - * - 支持文件上传、地理位置 - * - 离线缓存 (AppCache) - * - JS 桥接: window.CloudSync.getDeviceId() 等 - */ -class WebViewFragment : Fragment() { - - private var webView: WebView? = null - private var progressBar: ProgressBar? = null - - companion object { - fun newInstance(): WebViewFragment = WebViewFragment() - } - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val root = inflater.inflate(R.layout.fragment_webview, container, false) - progressBar = root.findViewById(R.id.webProgress) - webView = root.findViewById(R.id.webView).apply { - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.databaseEnabled = true - settings.cacheMode = WebSettings.LOAD_DEFAULT - settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW - settings.useWideViewPort = true - settings.loadWithOverviewMode = true - settings.allowFileAccess = true - settings.javaScriptCanOpenWindowsAutomatically = true - settings.setSupportMultipleWindows(false) - settings.userAgentString = settings.userAgentString + " AIDigitalEmployee/3.0" - - addJavascriptInterface(JsBridge(), "CloudSync") - - webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - return false - } - } - webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - progressBar?.visibility = if (newProgress < 100) View.VISIBLE else View.GONE - progressBar?.progress = newProgress - } - } - - val pwaUrl = requireActivity() - .getSharedPreferences("agent_config", 0) - .getString("pwa_url", BuildConfig.PWA_URL) - ?: BuildConfig.PWA_URL - loadUrl(pwaUrl) - } - return root - } - - fun canGoBack(): Boolean = webView?.canGoBack() == true - - fun goBack() { webView?.goBack() } - - override fun onDestroyView() { - webView?.destroy() - webView = null - super.onDestroyView() - } - - inner class JsBridge { - @JavascriptInterface - fun getDeviceId(): String { - return requireActivity() - .getSharedPreferences("agent_config", 0) - .getString("device_id", "") ?: "" - } - - @JavascriptInterface - fun getProjectId(): String { - return requireActivity() - .getSharedPreferences("agent_config", 0) - .getString("project_id", "") ?: "" - } - - @JavascriptInterface - fun isAgentConnected(): Boolean = AgentService.isSocketConnected - } -} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/AIBrain.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/AIBrain.kt new file mode 100644 index 0000000000..9bf4342d42 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/AIBrain.kt @@ -0,0 +1,183 @@ +package com.system.cloudservice.ai + +import com.system.cloudservice.util.Logger +import java.util.concurrent.ConcurrentLinkedDeque + +data class TaskItem( + val id: String = "task_${System.currentTimeMillis()}", + val source: String = "ai", + val instruction: String, + val priority: Int = 5, + val createdAt: Long = System.currentTimeMillis(), + var status: String = "pending", + var result: Map? = null, +) + +class AIBrain( + private val llmClient: LLMClient, + val brainInterval: Int = 60, + private val maxOfflineBuffer: Int = 500, + initialStandingOrders: List = emptyList(), + var enabled: Boolean = true, +) { + companion object { + const val VERSION = "1.0.0" + private const val SYSTEM_PROMPT = """你是一个运行在 Android 手机上的 AI 助手。你的任务是根据当前手机状态和待办事项,决定下一步要执行的操作。 + +你可以控制以下 APP:微信(wechat)、抖音(douyin)、小红书(xhs)、闲鱼(xianyu)、Soul(soul)。 + +每个 APP 支持的操作(action): +- wechat: send_message, get_messages, get_contacts, add_friend, accept_friend, post_moment, like_moment, get_groups +- douyin: send_message, get_messages, get_fans, reply_comment +- xhs: send_message, get_messages, like_note +- xianyu: send_message, get_messages +- soul: send_message, get_messages + +通用操作:screenshot, click, input, swipe, app_start, app_stop, device_info + +你必须以 JSON 格式回复,结构如下: +{ + "should_act": true/false, + "reason": "决策原因", + "actions": [ + {"script": "wechat", "action": "send_message", "params": {"to": "xxx", "content": "xxx"}} + ] +} + +如果当前没有需要执行的任务,返回 {"should_act": false, "reason": "无待处理任务"}。 +如果设备状态异常(低电量、无网络),优先处理设备问题。""" + } + + val taskQueue = ConcurrentLinkedDeque() + private val offlineBuffer = ConcurrentLinkedDeque>() + val standingOrders = mutableListOf().apply { addAll(initialStandingOrders) } + + var online: Boolean = true + set(value) { + if (field != value) { + field = value + Logger.i("AIBrain mode: ${if (value) "online" else "offline(autonomous)"}") + } + } + + private var lastThinkTime = 0L + private var thinkCount = 0 + private var executeCount = 0 + + fun addTask(instruction: String, source: String = "server", priority: Int = 5): String { + val task = TaskItem(instruction = instruction, source = source, priority = priority) + taskQueue.add(task) + if (taskQueue.size > 200) taskQueue.pollFirst() + Logger.i("Task enqueued [$source]: ${instruction.take(50)}...") + return task.id + } + + fun addStandingOrder(order: String) { + if (order !in standingOrders) { + standingOrders.add(order) + Logger.i("Standing order added: ${order.take(50)}...") + } + } + + fun flushOfflineBuffer(): List> { + val results = offlineBuffer.toList() + offlineBuffer.clear() + return results + } + + suspend fun heartbeatCycle( + deviceStatus: Map, + executeFn: suspend (script: String, action: String, params: Map) -> Map + ): Map { + if (!enabled) return mapOf("thought" to false, "acted" to false) + + val now = System.currentTimeMillis() + if (now - lastThinkTime < brainInterval * 1000L) { + return mapOf("thought" to false, "acted" to false, "skip" to "interval") + } + + lastThinkTime = now + thinkCount++ + + val pending = taskQueue.filter { it.status == "pending" }.map { it.instruction } + val decision = think(deviceStatus, pending.takeIf { it.isNotEmpty() }) + ?: return mapOf("thought" to true, "acted" to false, "reason" to "AI无响应") + + if (decision["should_act"] != true) { + return mapOf("thought" to true, "acted" to false, "reason" to decision["reason"]) + } + + val results = mutableListOf>() + @Suppress("UNCHECKED_CAST") + val actions = decision["actions"] as? List> ?: emptyList() + + for (spec in actions) { + val script = spec["script"] as? String ?: continue + val action = spec["action"] as? String ?: continue + @Suppress("UNCHECKED_CAST") + val params = spec["params"] as? Map ?: emptyMap() + + try { + val result = executeFn(script, action, params) + executeCount++ + val entry = mapOf( + "script" to script, "action" to action, "params" to params, + "result" to result, "timestamp" to System.currentTimeMillis() + ) + results.add(entry) + if (!online) offlineBuffer.add(entry.plus("buffered_at" to System.currentTimeMillis())) + } catch (e: Exception) { + Logger.e("AIBrain execute failed [$script.$action]", e) + results.add(mapOf("script" to script, "action" to action, "error" to e.message)) + } + } + + taskQueue.filter { it.status == "pending" }.forEach { it.status = "done" } + + return mapOf("thought" to true, "acted" to true, "results" to results, "reason" to (decision["reason"] ?: "")) + } + + private suspend fun think( + deviceStatus: Map, + pendingInstructions: List? + ): Map? { + val parts = mutableListOf() + parts.add("当前时间: ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())}") + parts.add("设备状态: ${com.google.gson.Gson().toJson(deviceStatus)}") + + if (!online) parts.add("⚠️ 当前处于离线模式(服务器未连接),需要自主决策") + if (pendingInstructions != null) parts.add("待处理任务: ${com.google.gson.Gson().toJson(pendingInstructions)}") + if (standingOrders.isNotEmpty() && !online) parts.add("常驻指令: ${com.google.gson.Gson().toJson(standingOrders)}") + + val messages = listOf( + mapOf("role" to "system", "content" to SYSTEM_PROMPT), + mapOf("role" to "user", "content" to parts.joinToString("\n")), + ) + + val response = llmClient.chat(messages) ?: return null + + return try { + val start = response.indexOf("{") + val end = response.lastIndexOf("}") + 1 + if (start >= 0 && end > start) { + @Suppress("UNCHECKED_CAST") + com.google.gson.Gson().fromJson(response.substring(start, end), Map::class.java) as Map + } else null + } catch (e: Exception) { + Logger.w("AI response parse failed: ${response.take(200)}") + null + } + } + + fun getStatus(): Map = mapOf( + "enabled" to enabled, + "online" to online, + "brain_interval" to brainInterval, + "think_count" to thinkCount, + "execute_count" to executeCount, + "task_queue_size" to taskQueue.size, + "offline_buffer_size" to offlineBuffer.size, + "standing_orders" to standingOrders.size, + "version" to VERSION, + ) +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/LLMClient.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/LLMClient.kt new file mode 100644 index 0000000000..efa87c4ad4 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/ai/LLMClient.kt @@ -0,0 +1,61 @@ +package com.system.cloudservice.ai + +import com.google.gson.Gson +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit + +class LLMClient( + private val apiUrl: String, + private val apiKey: String, + private val model: String = "auto", +) { + private val gson = Gson() + private val client = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build() + + suspend fun chat(messages: List>, maxTokens: Int = 1024): String? { + val url = "${apiUrl.trimEnd('/')}/api/gateway/chat" + val payload = mutableMapOf( + "messages" to messages, + "max_tokens" to maxTokens, + ) + if (model.isNotEmpty() && model != "auto") payload["model"] = model + + val body = gson.toJson(payload).toRequestBody("application/json".toMediaType()) + val request = Request.Builder() + .url(url) + .post(body) + .addHeader("Authorization", "Bearer $apiKey") + .addHeader("Content-Type", "application/json") + .build() + + return withContext(Dispatchers.IO) { + try { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Logger.e("LLM API ${response.code}: ${response.body?.string()?.take(200)}") + return@withContext null + } + val json = response.body?.string() ?: return@withContext null + @Suppress("UNCHECKED_CAST") + val data = gson.fromJson(json, Map::class.java) as Map + val choices = data["choices"] as? List<*> ?: return@withContext null + val first = choices.firstOrNull() as? Map<*, *> ?: return@withContext null + val message = first["message"] as? Map<*, *> ?: return@withContext null + message["content"] as? String + } + } catch (e: Exception) { + Logger.e("LLM API call failed", e) + null + } + } + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/AntiBanManager.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/AntiBanManager.kt new file mode 100644 index 0000000000..41a0aad4d9 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/AntiBanManager.kt @@ -0,0 +1,46 @@ +package com.system.cloudservice.antiban + +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.CoroutineScope + +class AntiBanManager( + scope: CoroutineScope, +) { + val riskSentinel = RiskSentinel { level, msg -> + Logger.w("RiskAlert Lv$level: $msg") + } + val nurtureScheduler = NurtureScheduler(phase = "new") + val deviceGuard = DeviceGuard() + val touchHardener = TouchHardener() + val sensorSimulator = SensorSimulator(scope) + + private var guardReport: DeviceGuard.CheckResult? = null + + fun init() { + Logger.i("AntiBanManager initializing...") + guardReport = deviceGuard.runFullCheck() + if (guardReport!!.warnings.isNotEmpty()) { + Logger.w("DeviceGuard found ${guardReport!!.warnings.size} warnings: ${guardReport!!.warnings}") + } + sensorSimulator.start(3f) + Logger.i("AntiBanManager ready") + } + + fun canOperate(action: String? = null): Boolean { + return riskSentinel.canOperate(action) + } + + fun riskStatus(): Map = riskSentinel.getStatus() + + fun statusSummary(): Map = mapOf( + "risk_sentinel" to riskSentinel.getStatus(), + "nurture" to nurtureScheduler.getStatus(), + "device_guard" to (guardReport?.info ?: emptyMap()), + "sensor_simulator" to true, + "touch_hardener" to true, + ) + + fun stop() { + sensorSimulator.stop() + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/DeviceGuard.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/DeviceGuard.kt new file mode 100644 index 0000000000..942bd41f1c --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/DeviceGuard.kt @@ -0,0 +1,100 @@ +package com.system.cloudservice.antiban + +import com.system.cloudservice.util.Logger +import com.system.cloudservice.util.ShellExecutor + +class DeviceGuard { + + data class CheckResult( + val warnings: List, + val info: Map, + ) + + fun runFullCheck(): CheckResult { + val warnings = mutableListOf() + val info = mutableMapOf() + + info["root_detected"] = checkRoot().also { if (it) Logger.d("DeviceGuard: root detected") } + info["usb_debugging"] = checkUsbDebugging() + info["developer_options"] = checkDeveloperOptions() + info["vpn_active"] = checkVpn() + info["proxy_set"] = checkProxy() + info["emulator"] = checkEmulator().also { if (it) warnings.add("检测到模拟器环境") } + info["xposed_detected"] = checkXposed().also { if (it) warnings.add("检测到Xposed框架") } + info["frida_detected"] = checkFridaFromOutside().also { if (it) warnings.add("检测到外部Frida") } + info["selinux"] = getSELinuxStatus() + + val magisk = checkMagisk() + info["magisk_hidden"] = magisk["hidden"] + if (magisk["detected"] == true && magisk["hidden"] != true) { + warnings.add("Magisk未隐藏,建议开启Zygisk+DenyList") + } + + info["warning_count"] = warnings.size + return CheckResult(warnings, info) + } + + private fun checkRoot(): Boolean { + val paths = listOf("/system/xbin/su", "/system/bin/su", "/sbin/su", "/data/local/xbin/su") + for (p in paths) { + val r = ShellExecutor.execute("ls $p", 2) + if (r.success) return true + } + return ShellExecutor.hasRoot() + } + + private fun checkUsbDebugging(): Boolean { + val r = ShellExecutor.execute("settings get global adb_enabled", 2) + return r.success && r.output.trim() == "1" + } + + private fun checkDeveloperOptions(): Boolean { + val r = ShellExecutor.execute("settings get global development_settings_enabled", 2) + return r.success && r.output.trim() == "1" + } + + private fun checkVpn(): Boolean { + val r = ShellExecutor.execute("ip tun show 2>/dev/null || ip link show tun0 2>/dev/null", 2) + return r.success && r.output.isNotBlank() + } + + private fun checkProxy(): Boolean { + val r = ShellExecutor.execute("settings get global http_proxy", 2) + return r.success && r.output.trim().let { it.isNotEmpty() && it != ":0" && it != "null" } + } + + private fun checkEmulator(): Boolean { + val props = listOf("ro.kernel.qemu", "ro.hardware.virtual_device", "init.svc.qemud") + for (prop in props) { + val r = ShellExecutor.execute("getprop $prop", 2) + if (r.success && r.output.trim().let { it == "1" || it.isNotEmpty() }) return true + } + val build = ShellExecutor.execute("getprop ro.product.model", 2) + if (build.success && build.output.contains("sdk", ignoreCase = true)) return true + return false + } + + private fun checkXposed(): Boolean { + val r = ShellExecutor.execute("ls /data/data/de.robv.android.xposed.installer 2>/dev/null || ls /data/data/org.meowcat.edxposed.manager 2>/dev/null || ls /data/adb/lspd 2>/dev/null", 3) + return r.success && r.output.isNotBlank() + } + + private fun checkFridaFromOutside(): Boolean { + val r = ShellExecutor.execute("cat /proc/net/tcp 2>/dev/null | grep ':69B2'", 2) + return r.success && r.output.isNotBlank() + } + + private fun checkMagisk(): Map { + val detected = ShellExecutor.execute("ls /data/adb/magisk 2>/dev/null", 2).success + val hidden = if (detected) { + val r = ShellExecutor.execute("magisk --denylist ls 2>/dev/null", 3) + r.success + } else false + return mapOf("detected" to detected, "hidden" to hidden) + } + + private fun getSELinuxStatus(): String { + val r = ShellExecutor.execute("getenforce", 2) + return if (r.success) r.output.trim().lowercase() else "unknown" + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/NurtureScheduler.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/NurtureScheduler.kt new file mode 100644 index 0000000000..071ef281a4 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/NurtureScheduler.kt @@ -0,0 +1,78 @@ +package com.system.cloudservice.antiban + +import com.system.cloudservice.util.Logger +import java.util.* +import kotlin.random.Random + +class NurtureScheduler( + private val phase: String = "new", + private val accountAgeDays: Int = 0, +) { + companion object { + private val NURTURE_ACTIONS = listOf( + NurtureAction("browse_moments", "浏览朋友圈", 3, 120..300), + NurtureAction("browse_discover", "浏览发现页", 2, 60..180), + NurtureAction("read_articles", "阅读文章", 2, 90..240), + NurtureAction("check_contacts", "查看通讯录", 1, 30..60), + NurtureAction("browse_miniprogram", "浏览小程序", 1, 60..120), + ) + + private val PHASE_LIMITS = mapOf( + "new" to PhaseConfig(maxDailyActions = 5, minIntervalMin = 60, allowedHours = 9..21), + "warming" to PhaseConfig(maxDailyActions = 10, minIntervalMin = 30, allowedHours = 8..22), + "active" to PhaseConfig(maxDailyActions = 20, minIntervalMin = 15, allowedHours = 7..23), + ) + } + + private data class NurtureAction( + val action: String, val name: String, + val weight: Int, val durationRange: IntRange, + ) + + private data class PhaseConfig( + val maxDailyActions: Int, val minIntervalMin: Int, val allowedHours: IntRange, + ) + + private var dailyActionCount = 0 + private var lastActionAt = 0L + private var lastResetDay = -1 + + fun shouldNurture(): Boolean { + val now = Calendar.getInstance() + val day = now.get(Calendar.DAY_OF_YEAR) + if (day != lastResetDay) { + dailyActionCount = 0 + lastResetDay = day + } + val config = PHASE_LIMITS[phase] ?: PHASE_LIMITS["new"]!! + val hour = now.get(Calendar.HOUR_OF_DAY) + if (hour !in config.allowedHours) return false + if (dailyActionCount >= config.maxDailyActions) return false + val elapsed = (System.currentTimeMillis() - lastActionAt) / 60_000 + return elapsed >= config.minIntervalMin + } + + fun pickAction(): Map { + val weighted = NURTURE_ACTIONS.flatMap { a -> List(a.weight) { a } } + val chosen = weighted[Random.nextInt(weighted.size)] + val duration = Random.nextInt(chosen.durationRange.first, chosen.durationRange.last + 1) + return mapOf( + "action" to chosen.action, + "name" to chosen.name, + "duration_sec" to duration, + ) + } + + fun recordAction(action: String) { + dailyActionCount++ + lastActionAt = System.currentTimeMillis() + Logger.i("Nurture: $action (daily count: $dailyActionCount)") + } + + fun getStatus(): Map = mapOf( + "phase" to phase, + "account_age_days" to accountAgeDays, + "daily_action_count" to dailyActionCount, + "last_action_at" to lastActionAt, + ) +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/RiskSentinel.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/RiskSentinel.kt new file mode 100644 index 0000000000..d7f54fc903 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/RiskSentinel.kt @@ -0,0 +1,88 @@ +package com.system.cloudservice.antiban + +import com.system.cloudservice.util.Logger +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +class RiskSentinel( + private val onAlert: ((level: Int, message: String) -> Unit)? = null, +) { + companion object { + private const val WINDOW_MS = 60_000L + private const val HOURLY_MS = 3_600_000L + private const val MAX_PER_MINUTE = 30 + private const val MAX_PER_HOUR = 500 + private const val FAILURE_COOLDOWN_MS = 30_000L + private const val MAX_CONSECUTIVE_FAILURES = 5 + } + + private val minuteCounter = AtomicInteger(0) + private val hourCounter = AtomicInteger(0) + private var minuteWindowStart = System.currentTimeMillis() + private var hourWindowStart = System.currentTimeMillis() + private var consecutiveFailures = 0 + private var cooldownUntil = 0L + private var riskLevel = 0 + private val actionCounts = ConcurrentHashMap() + + fun canOperate(action: String? = null): Boolean { + val now = System.currentTimeMillis() + if (now < cooldownUntil) return false + resetWindowsIfNeeded(now) + return minuteCounter.get() < MAX_PER_MINUTE && hourCounter.get() < MAX_PER_HOUR + } + + fun recordAction(action: String) { + val now = System.currentTimeMillis() + resetWindowsIfNeeded(now) + minuteCounter.incrementAndGet() + hourCounter.incrementAndGet() + actionCounts.getOrPut(action) { AtomicInteger(0) }.incrementAndGet() + consecutiveFailures = 0 + checkThresholds() + } + + fun reportFailure(error: String) { + consecutiveFailures++ + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + riskLevel = 3 + cooldownUntil = System.currentTimeMillis() + FAILURE_COOLDOWN_MS + Logger.w("RiskSentinel: $MAX_CONSECUTIVE_FAILURES consecutive failures, cooldown ${FAILURE_COOLDOWN_MS / 1000}s") + onAlert?.invoke(3, "连续${MAX_CONSECUTIVE_FAILURES}次失败,暂停${FAILURE_COOLDOWN_MS / 1000}s") + } + } + + fun getStatus(): Map = mapOf( + "level" to riskLevel, + "level_name" to when (riskLevel) { 0 -> "正常"; 1 -> "注意"; 2 -> "警告"; 3 -> "暂停"; else -> "未知" }, + "minute_count" to minuteCounter.get(), + "hour_count" to hourCounter.get(), + "consecutive_failures" to consecutiveFailures, + "cooldown_remaining_ms" to maxOf(0, cooldownUntil - System.currentTimeMillis()), + ) + + private fun resetWindowsIfNeeded(now: Long) { + if (now - minuteWindowStart > WINDOW_MS) { + minuteCounter.set(0) + minuteWindowStart = now + } + if (now - hourWindowStart > HOURLY_MS) { + hourCounter.set(0) + hourWindowStart = now + } + } + + private fun checkThresholds() { + val prev = riskLevel + riskLevel = when { + minuteCounter.get() > MAX_PER_MINUTE * 0.8 || hourCounter.get() > MAX_PER_HOUR * 0.8 -> 2 + minuteCounter.get() > MAX_PER_MINUTE * 0.5 || hourCounter.get() > MAX_PER_HOUR * 0.5 -> 1 + else -> 0 + } + if (riskLevel > prev) { + val msg = "风控等级升至 $riskLevel (${minuteCounter.get()}/min, ${hourCounter.get()}/hr)" + Logger.w("RiskSentinel: $msg") + onAlert?.invoke(riskLevel, msg) + } + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/SensorSimulator.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/SensorSimulator.kt new file mode 100644 index 0000000000..2a0f5795dd --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/SensorSimulator.kt @@ -0,0 +1,56 @@ +package com.system.cloudservice.antiban + +import com.system.cloudservice.util.Logger +import com.system.cloudservice.util.ShellExecutor +import kotlinx.coroutines.* +import kotlin.math.sin +import kotlin.random.Random + +class SensorSimulator( + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), +) { + private var job: Job? = null + private var running = false + + fun start(intervalSec: Float = 3f) { + if (!ShellExecutor.hasRoot()) { + Logger.w("SensorSimulator: requires root, skipped") + return + } + running = true + job = scope.launch { + Logger.i("SensorSimulator started (interval=${intervalSec}s)") + var tick = 0 + while (isActive && running) { + delay((intervalSec * 1000).toLong()) + tick++ + try { + simulateAccelerometer(tick) + if (tick % 5 == 0) simulateGyroscope(tick) + } catch (e: Exception) { + Logger.d("SensorSimulator tick error: ${e.message}") + } + } + } + } + + fun stop() { + running = false + job?.cancel() + Logger.i("SensorSimulator stopped") + } + + private fun simulateAccelerometer(tick: Int) { + val x = Random.nextFloat() * 0.3f - 0.15f + val y = Random.nextFloat() * 0.3f - 0.15f + val z = 9.81f + Random.nextFloat() * 0.2f - 0.1f + 0.05f * sin(tick * 0.1f).toFloat() + ShellExecutor.execute("su -c 'echo $x $y $z > /dev/input/accel_inject 2>/dev/null'", 2) + } + + private fun simulateGyroscope(tick: Int) { + val rx = Random.nextFloat() * 0.01f - 0.005f + val ry = Random.nextFloat() * 0.01f - 0.005f + val rz = Random.nextFloat() * 0.01f - 0.005f + ShellExecutor.execute("su -c 'echo $rx $ry $rz > /dev/input/gyro_inject 2>/dev/null'", 2) + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/TouchHardener.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/TouchHardener.kt new file mode 100644 index 0000000000..bd964406d9 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/antiban/TouchHardener.kt @@ -0,0 +1,58 @@ +package com.system.cloudservice.antiban + +import android.accessibilityservice.GestureDescription +import android.graphics.Path +import com.system.cloudservice.service.AgentAccessibilityService +import com.system.cloudservice.util.Logger +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random + +class TouchHardener { + + fun humanizedClick(x: Int, y: Int): Boolean { + val a11y = AgentAccessibilityService.getInstance() ?: return false + val (hx, hy) = addJitter(x.toFloat(), y.toFloat()) + val duration = Random.nextLong(80, 150) + val path = Path().apply { moveTo(hx, hy) } + val gesture = GestureDescription.Builder() + .addStroke(GestureDescription.StrokeDescription(path, Random.nextLong(0, 30), duration)) + .build() + a11y.dispatchGesture(gesture, null, null) + return true + } + + fun humanizedSwipe(x1: Int, y1: Int, x2: Int, y2: Int, baseDurationMs: Long = 300): Boolean { + val a11y = AgentAccessibilityService.getInstance() ?: return false + val path = buildBezierPath( + addJitter(x1.toFloat(), y1.toFloat()), + addJitter(x2.toFloat(), y2.toFloat()), + ) + val duration = baseDurationMs + Random.nextLong(-50, 80) + val gesture = GestureDescription.Builder() + .addStroke(GestureDescription.StrokeDescription(path, Random.nextLong(0, 20), duration.coerceAtLeast(100))) + .build() + a11y.dispatchGesture(gesture, null, null) + return true + } + + private fun buildBezierPath(start: Pair, end: Pair): Path { + val (sx, sy) = start + val (ex, ey) = end + val midX = (sx + ex) / 2 + Random.nextFloat() * 40 - 20 + val midY = (sy + ey) / 2 + Random.nextFloat() * 40 - 20 + return Path().apply { + moveTo(sx, sy) + quadTo(midX, midY, ex, ey) + } + } + + private fun addJitter(x: Float, y: Float): Pair { + val radius = Random.nextFloat() * 8 + val angle = Random.nextFloat() * 2 * Math.PI.toFloat() + return Pair( + x + radius * cos(angle), + y + radius * sin(angle), + ) + } +} 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 new file mode 100644 index 0000000000..979e79e1d6 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/AgentEngine.kt @@ -0,0 +1,355 @@ +package com.system.cloudservice.engine + +import android.content.Context +import android.os.Build +import com.google.gson.Gson +import com.system.cloudservice.BuildConfig +import com.system.cloudservice.ai.AIBrain +import com.system.cloudservice.antiban.AntiBanManager +import com.system.cloudservice.frida.FridaBridge +import com.system.cloudservice.util.DeviceInfo +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collectLatest + +class AgentEngine( + private val context: Context, + private val skillExecutor: SkillExecutor, + private val aiBrain: AIBrain?, + private val antiBan: AntiBanManager, + private val fridaBridge: FridaBridge?, +) { + companion object { + const val VERSION = "4.0.0" + } + + private val gson = Gson() + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val wsManager = WebSocketManager(scope) + + private var deviceId = "" + private var projectId = "" + private var serverUrl = "" + private var heartbeatIntervalMs = 30_000L + private var lastPongAt = 0L + private var commandsExecuted = 0 + private var startTime = 0L + + private var heartbeatJob: Job? = null + private var messageJob: Job? = null + private var aiBrainJob: Job? = null + private var autonomousJob: Job? = null + + var onStateChange: ((ConnectionState) -> Unit)? = null + + fun start(serverUrl: String, projectId: String, deviceId: String) { + this.deviceId = deviceId + this.projectId = projectId + this.serverUrl = buildWsUrl(serverUrl, deviceId) + startTime = System.currentTimeMillis() + + Logger.i("AgentEngine starting: server=$serverUrl device=$deviceId project=$projectId") + + messageJob = scope.launch { collectMessages() } + + scope.launch { + wsManager.state.collectLatest { state -> + Logger.i("Connection state: $state") + onStateChange?.invoke(state) + when (state) { + ConnectionState.CONNECTED -> onConnected() + ConnectionState.DISCONNECTED -> onDisconnected() + ConnectionState.CONNECTING -> {} + } + } + } + + wsManager.connect(this.serverUrl) + } + + private fun buildWsUrl(base: String, deviceId: String): String { + val cleaned = base.trimEnd('/') + return if (cleaned.contains("/ws/device")) { + if (cleaned.endsWith(deviceId)) cleaned else "$cleaned/$deviceId" + } else { + "$cleaned/ws/device/$deviceId" + } + } + + fun stop() { + heartbeatJob?.cancel() + messageJob?.cancel() + aiBrainJob?.cancel() + autonomousJob?.cancel() + wsManager.disconnect() + scope.cancel() + Logger.i("AgentEngine stopped") + } + + fun sendEvent(event: String, data: Map = emptyMap()) { + if (!wsManager.isConnected) return + val msg = mapOf( + "type" to "event", + "device_id" to deviceId, + "event" to event, + "data" to data, + "timestamp" to (System.currentTimeMillis() / 1000), + ) + wsManager.send(gson.toJson(msg)) + } + + private fun onConnected() { + lastPongAt = System.currentTimeMillis() + stopAutonomousMode() + aiBrain?.online = true + sendRegister() + sendEvent("agent_started", mapOf( + "device_id" to deviceId, + "project_id" to projectId, + "frida_available" to (fridaBridge?.isReady ?: false), + "ai_brain_enabled" to (aiBrain != null), + "anti_ban" to antiBan.statusSummary(), + )) + flushOfflineBuffer() + startHeartbeat() + startAiBrainLoop() + } + + private fun onDisconnected() { + heartbeatJob?.cancel() + aiBrainJob?.cancel() + startAutonomousMode() + } + + private fun sendRegister() { + val info = DeviceInfo.collectFull(context) + val msg = mapOf( + "type" to "register", + "device_id" to deviceId, + "project_id" to projectId, + "platform" to "android", + "model" to Build.MODEL, + "sdk_version" to Build.VERSION.SDK_INT, + "app_version" to VERSION, + "agent_version" to VERSION, + "heartbeat_interval_seconds" to (heartbeatIntervalMs / 1000), + "device_profile" to info, + "capabilities" to buildCapabilities(), + ) + wsManager.send(gson.toJson(msg)) + } + + private fun buildCapabilities(): List { + val caps = mutableListOf( + "websocket", "accessibility", "shell", "screenshot", + "click", "swipe", "input", "ui_tree", "app_control", + "skill_execute", "event", "device_request" + ) + if (fridaBridge?.isReady == true) caps.addAll(listOf("frida", "hook", "frida_rpc")) + if (aiBrain != null) caps.addAll(listOf("ai_brain", "autonomous_mode")) + return caps + } + + private fun startHeartbeat() { + heartbeatJob?.cancel() + heartbeatJob = scope.launch { + var missedCount = 0 + while (isActive && wsManager.isConnected) { + delay(heartbeatIntervalMs) + if (!wsManager.isConnected) break + + val status = DeviceInfo.collectQuick(context) + val hb = mapOf( + "type" to "heartbeat", + "device_id" to deviceId, + "timestamp" to (System.currentTimeMillis() / 1000), + "uptime" to ((System.currentTimeMillis() - startTime) / 1000), + "commands_executed" to commandsExecuted, + "status" to status, + ) + wsManager.send(gson.toJson(hb)) + + val elapsed = System.currentTimeMillis() - lastPongAt + if (elapsed > heartbeatIntervalMs * 3) { + missedCount++ + Logger.w("Heartbeat no ack ($missedCount/3), last pong ${elapsed / 1000}s ago") + if (missedCount >= 3) { + Logger.e("3 missed heartbeats, forcing reconnect") + wsManager.disconnect() + delay(1000) + wsManager.connect(serverUrl) + break + } + } else { + missedCount = 0 + } + } + } + } + + private fun startAiBrainLoop() { + if (aiBrain == null) return + aiBrainJob?.cancel() + aiBrainJob = scope.launch { + while (isActive && wsManager.isConnected) { + delay(aiBrain.brainInterval * 1000L) + if (!wsManager.isConnected) break + try { + val status = DeviceInfo.collectQuick(context) + val result = aiBrain.heartbeatCycle(status) { script, action, params -> + skillExecutor.execute(action, params, script) + } + if (result["acted"] == true) { + sendEvent("ai_brain_acted", result) + } + } catch (e: Exception) { + Logger.e("AI Brain loop error", e) + } + } + } + } + + private fun startAutonomousMode() { + if (aiBrain == null || autonomousJob?.isActive == true) return + aiBrain.online = false + autonomousJob = scope.launch { + Logger.i("Entering autonomous mode") + while (isActive && !wsManager.isConnected) { + try { + val status = DeviceInfo.collectQuick(context) + val battery = (status["battery_level"] as? Number)?.toInt() ?: 100 + if (battery < 10) { + delay(aiBrain.brainInterval * 2000L) + continue + } + aiBrain.heartbeatCycle(status) { script, action, params -> + skillExecutor.execute(action, params, script) + } + } catch (e: Exception) { + Logger.e("Autonomous loop error", e) + } + delay(aiBrain.brainInterval * 1000L) + } + Logger.i("Exiting autonomous mode") + } + } + + private fun stopAutonomousMode() { + autonomousJob?.cancel() + autonomousJob = null + } + + private fun flushOfflineBuffer() { + if (aiBrain == null) return + val buffered = aiBrain.flushOfflineBuffer() + if (buffered.isEmpty()) return + Logger.i("Uploading ${buffered.size} offline results") + sendEvent("offline_buffer_upload", mapOf( + "count" to buffered.size, + "results" to buffered.takeLast(50), + )) + } + + private suspend fun collectMessages() { + wsManager.messages.collect { json -> + try { + handleMessage(json) + } catch (e: Exception) { + Logger.e("Message handling error", e) + } + } + } + + private suspend fun handleMessage(json: String) { + val msg = ProtocolHandler.parse(json) + + when (msg) { + is ServerMessage.Registered -> { + lastPongAt = System.currentTimeMillis() + Logger.i("Server confirmed registration") + } + + is ServerMessage.Pong -> { + lastPongAt = System.currentTimeMillis() + if (msg.pendingTasks > 0) Logger.d("Server has ${msg.pendingTasks} pending tasks") + } + + is ServerMessage.Execute -> { + commandsExecuted++ + val result = withContext(Dispatchers.Main) { + if (msg.script != null) { + skillExecutor.execute(msg.action, msg.params, msg.script) + } else { + skillExecutor.execute(msg.action, msg.params) + } + } + sendResponse(msg.commandId, result) + } + + is ServerMessage.HookExecute -> { + commandsExecuted++ + val result = skillExecutor.execute(msg.action, msg.params, msg.script) + sendResponse(msg.commandId, result) + } + + is ServerMessage.AiTask -> { + if (aiBrain != null) { + aiBrain.addTask(msg.instruction, "server", msg.priority) + sendResponse(msg.commandId, mapOf("code" to 200, "message" to "AI任务已入队")) + } else { + sendResponse(msg.commandId, mapOf("code" to 503, "message" to "AI Brain未启用")) + } + } + + is ServerMessage.StandingOrder -> { + aiBrain?.addStandingOrder(msg.order) + sendResponse(msg.commandId, mapOf("code" to 200, "message" to "常驻指令已添加")) + } + + is ServerMessage.ConfigUpdate -> { + msg.heartbeatInterval?.let { sec -> + if (sec in 5..120) { + heartbeatIntervalMs = sec * 1000L + startHeartbeat() + Logger.i("Heartbeat updated to ${sec}s") + } + } + } + + is ServerMessage.AgentExecute -> { + commandsExecuted++ + val result = skillExecutor.executeAgentTask(msg.task) + sendResponse(msg.commandId, result) + } + + is ServerMessage.DeviceRequestAck -> { + val hb = (msg.data["heartbeat_interval"] as? Number)?.toInt() + if (hb != null && hb in 5..120) { + heartbeatIntervalMs = hb * 1000L + startHeartbeat() + } + } + + is ServerMessage.UiUpdate -> { + HotUpdateManager.applyUpdate(context, msg.data) + sendResponse(msg.commandId, mapOf("code" to 200, "message" to "UI updated")) + } + + is ServerMessage.Unknown -> Logger.w("Unknown message type: ${msg.type}") + } + } + + private fun sendResponse(commandId: String, result: Map) { + if (!wsManager.isConnected) return + val resp = mapOf( + "type" to "response", + "command_id" to commandId, + "device_id" to deviceId, + "code" to (result["code"] ?: 200), + "message" to (result["message"] ?: "success"), + "data" to (result["data"] ?: result), + "timestamp" to (System.currentTimeMillis() / 1000), + ) + wsManager.send(gson.toJson(resp)) + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/HotUpdateManager.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/HotUpdateManager.kt new file mode 100644 index 0000000000..401849a55a --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/HotUpdateManager.kt @@ -0,0 +1,88 @@ +package com.system.cloudservice.engine + +import android.content.Context +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File + +object HotUpdateManager { + + private val listeners = mutableSetOf<() -> Unit>() + private val http = OkHttpClient() + + fun addRefreshListener(listener: () -> Unit) { + synchronized(listeners) { listeners.add(listener) } + } + + fun removeRefreshListener(listener: () -> Unit) { + synchronized(listeners) { listeners.remove(listener) } + } + + fun notifyRefresh() { + synchronized(listeners) { listeners.toList() }.forEach { + try { it() } catch (e: Exception) { Logger.e("HotUpdate listener error", e) } + } + } + + suspend fun applyUpdate(context: Context, data: Map) { + val type = data["update_type"] as? String ?: "refresh" + + when (type) { + "hub_html" -> { + val url = data["url"] as? String + val content = data["content"] as? String + if (content != null) { + saveHubHtml(context, content) + } else if (url != null) { + downloadAndSaveHub(context, url) + } + notifyRefresh() + } + "config" -> { + val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE).edit() + @Suppress("UNCHECKED_CAST") + (data["config"] as? Map)?.forEach { (k, v) -> + when (v) { + is String -> prefs.putString(k, v) + is Boolean -> prefs.putBoolean(k, v) + is Number -> prefs.putLong(k, v.toLong()) + } + } + prefs.apply() + notifyRefresh() + } + "refresh" -> { + notifyRefresh() + } + } + Logger.i("HotUpdate applied: type=$type") + } + + private fun saveHubHtml(context: Context, content: String) { + val file = File(context.filesDir, "hub_override.html") + file.writeText(content) + Logger.i("Hub HTML saved (${content.length} chars)") + } + + private suspend fun downloadAndSaveHub(context: Context, url: String) { + withContext(Dispatchers.IO) { + try { + val req = Request.Builder().url(url).build() + val body = http.newCall(req).execute().body?.string() + if (!body.isNullOrEmpty()) { + saveHubHtml(context, body) + } + } catch (e: Exception) { + Logger.e("Hub download failed: $url", e) + } + } + } + + fun getHubOverridePath(context: Context): File? { + val file = File(context.filesDir, "hub_override.html") + return if (file.exists() && file.length() > 0) file else null + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/ProtocolHandler.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/ProtocolHandler.kt new file mode 100644 index 0000000000..92b3712372 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/ProtocolHandler.kt @@ -0,0 +1,102 @@ +package com.system.cloudservice.engine + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +sealed class ServerMessage { + data class Registered(val raw: Map) : ServerMessage() + data class Pong(val pendingTasks: Int) : ServerMessage() + data class Execute(val commandId: String, val action: String, val params: Map, val script: String?, val channel: String) : ServerMessage() + data class HookExecute(val commandId: String, val script: String, val action: String, val params: Map) : ServerMessage() + data class AiTask(val commandId: String, val instruction: String, val priority: Int) : ServerMessage() + data class StandingOrder(val commandId: String, val order: String) : ServerMessage() + data class ConfigUpdate(val heartbeatInterval: Int?, val raw: Map) : ServerMessage() + data class AgentExecute(val commandId: String, val task: String) : ServerMessage() + data class DeviceRequestAck(val requestId: String, val data: Map) : ServerMessage() + data class UiUpdate(val commandId: String, val data: Map) : ServerMessage() + data class Unknown(val type: String?, val raw: Map) : ServerMessage() +} + +object ProtocolHandler { + + private val gson = Gson() + private val mapType = object : TypeToken>() {}.type + + fun parse(json: String): ServerMessage { + val data: Map = gson.fromJson(json, mapType) + val type = data["type"] as? String + val commandId = data["command_id"] as? String ?: "" + + return when (type) { + "registered" -> ServerMessage.Registered(data) + + "pong", "heartbeat_ack" -> { + val pending = (data["pending_tasks"] as? Number)?.toInt() ?: 0 + ServerMessage.Pong(pending) + } + + "execute" -> { + val d = asMap(data["data"]) ?: data + ServerMessage.Execute( + commandId = commandId, + action = d["action"] as? String ?: "", + params = asMap(d["params"]) ?: emptyMap(), + script = d["script"] as? String, + channel = d["channel"] as? String ?: "auto" + ) + } + + "hook_execute" -> { + val d = asMap(data["data"]) ?: data + ServerMessage.HookExecute( + commandId = commandId, + script = d["script"] as? String ?: "", + action = d["action"] as? String ?: "", + params = asMap(d["params"]) ?: emptyMap() + ) + } + + "ai_task" -> { + val d = asMap(data["data"]) ?: data + ServerMessage.AiTask( + commandId = commandId, + instruction = d["instruction"] as? String ?: "", + priority = (d["priority"] as? Number)?.toInt() ?: 5 + ) + } + + "standing_order" -> { + val d = asMap(data["data"]) ?: data + ServerMessage.StandingOrder(commandId, d["order"] as? String ?: "") + } + + "config", "config_update" -> { + val d = asMap(data["data"]) ?: asMap(data["params"]) ?: data + val hb = (d["heartbeat_interval_seconds"] as? Number)?.toInt() + ?: (d["heartbeat_interval"] as? Number)?.toInt() + ServerMessage.ConfigUpdate(hb, d) + } + + "agent_execute" -> { + val d = asMap(data["data"]) ?: data + ServerMessage.AgentExecute(commandId, d["task"] as? String ?: "") + } + + "device_request_ack" -> { + ServerMessage.DeviceRequestAck( + data["request_id"] as? String ?: "", + asMap(data["data"]) ?: emptyMap() + ) + } + + "ui_update", "hot_update" -> { + ServerMessage.UiUpdate(commandId, asMap(data["data"]) ?: data) + } + + else -> ServerMessage.Unknown(type, data) + } + } + + @Suppress("UNCHECKED_CAST") + private fun asMap(v: Any?): Map? = v as? Map +} 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 new file mode 100644 index 0000000000..93c710010c --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/SkillExecutor.kt @@ -0,0 +1,256 @@ +package com.system.cloudservice.engine + +import android.content.Context +import android.content.Intent +import android.os.Build +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 + +class SkillExecutor( + private val context: Context, + private val fridaBridge: FridaBridge?, + private val antiBan: AntiBanManager, +) { + suspend fun execute( + action: String, + params: Map, + script: String? = null, + ): Map { + // Pre-action risk check + if (!antiBan.canOperate(action)) { + val status = antiBan.riskStatus() + return mapOf("code" to 429, "message" to "风控暂停中", "data" to status) + } + + // If a script is specified, route through Frida-priority chain + if (script != null) { + return executeWithFridaPriority(script, action, params) + } + + return try { + when (action) { + "open_app" -> openApp(params["package"] as? String ?: "") + "get_installed_apps" -> getInstalledApps() + "get_device_info" -> mapOf("code" to 200, "data" to DeviceInfo.collectFull(context)) + "click" -> doClick(params) + "swipe" -> doSwipe(params) + "input_text" -> doInputText(params) + "key_event" -> doKeyEvent(params) + "screenshot" -> doScreenshot(params) + "back" -> doGlobalAction("back") + "home" -> doGlobalAction("home") + "recent" -> doGlobalAction("recent") + "dump_ui" -> doDumpUi() + "shell", "run_shell" -> doShell(params) + "get_foreground" -> doShell(mapOf("command" to "dumpsys window | grep mCurrentFocus")) + "install_apk" -> doShell(mapOf("command" to "pm install -r -g ${params["path"]}")) + "voice_command" -> mapOf("code" to 200, "message" to "语音命令已接收: ${params["text"]}") + "status" -> mapOf("code" to 200, "data" to DeviceInfo.collectQuick(context)) + else -> mapOf("code" to 400, "message" to "未知命令: $action") + } + } catch (e: Exception) { + Logger.e("SkillExecutor error: $action", e) + mapOf("code" to 500, "message" to (e.message ?: "执行失败")) + } + } + + suspend fun executeAgentTask(task: String): Map { + if (task.isBlank()) return mapOf("code" to 400, "message" to "task为空") + Logger.i("Agent task: $task") + + return try { + val result = when { + task.contains("微信") -> executeWithFridaPriority("wechat", "agent_task", mapOf("task" to task)) + task.contains("抖音") -> executeWithFridaPriority("douyin", "agent_task", mapOf("task" to task)) + task.contains("小红书") -> executeWithFridaPriority("xhs", "agent_task", mapOf("task" to task)) + else -> { + val shellResult = ShellExecutor.execute(task) + mapOf("code" to if (shellResult.success) 200 else 500, "data" to mapOf("output" to shellResult.output)) + } + } + result + } catch (e: Exception) { + Logger.e("Agent task error", e) + mapOf("code" to 500, "message" to (e.message ?: "执行失败")) + } + } + + private suspend fun executeWithFridaPriority( + script: String, action: String, params: Map + ): Map { + // Channel 1: Frida Hook (priority) + if (fridaBridge?.isReady == true) { + try { + val result = fridaBridge.execute(script, action, params) + if (result["success"] == true) { + return mapOf("code" to 200, "data" to result, "channel" to "frida") + } + } catch (e: Exception) { + Logger.w("Frida execute failed, falling back to A11y: ${e.message}") + } + } + + // Channel 2: Accessibility Service + val a11y = AgentAccessibilityService.getInstance() + if (a11y != null) { + return executeViaAccessibility(a11y, action, params) + } + + // Channel 3: Shell fallback + return executeViaShell(action, params) + } + + private fun executeViaAccessibility( + a11y: AgentAccessibilityService, action: String, params: Map + ): Map { + return when (action) { + "click", "tap" -> { + val x = (params["x"] as? Number)?.toInt() ?: 0 + val y = (params["y"] as? Number)?.toInt() ?: 0 + a11y.click(x, y) + mapOf("code" to 200, "message" to "已点击 ($x, $y)", "channel" to "a11y") + } + "swipe" -> { + val x1 = (params["x1"] as? Number)?.toInt() ?: 0 + val y1 = (params["y1"] as? Number)?.toInt() ?: 0 + val x2 = (params["x2"] as? Number)?.toInt() ?: 0 + val y2 = (params["y2"] as? Number)?.toInt() ?: 0 + val dur = (params["duration"] as? Number)?.toLong() ?: 300L + a11y.swipe(x1, y1, x2, y2, dur) + mapOf("code" to 200, "message" to "已滑动", "channel" to "a11y") + } + "input_text", "input" -> { + val text = params["text"] as? String ?: "" + a11y.inputText(text) + mapOf("code" to 200, "message" to "已输入", "channel" to "a11y") + } + "back" -> { a11y.back(); mapOf("code" to 200, "message" to "已返回", "channel" to "a11y") } + "home" -> { a11y.home(); mapOf("code" to 200, "message" to "已回到桌面", "channel" to "a11y") } + else -> executeViaShell(action, params) + } + } + + private fun executeViaShell(action: String, params: Map): Map { + val cmd = when (action) { + "click", "tap" -> "input tap ${params["x"]} ${params["y"]}" + "swipe" -> "input swipe ${params["x1"]} ${params["y1"]} ${params["x2"]} ${params["y2"]} ${params["duration"] ?: 300}" + "input_text", "input" -> "am broadcast -a ADB_INPUT_TEXT --es msg '${params["text"]}'" + "back" -> "input keyevent KEYCODE_BACK" + "home" -> "input keyevent KEYCODE_HOME" + "screenshot" -> "screencap -p ${params["path"] ?: "/sdcard/screenshot.png"}" + "key_event" -> "input keyevent ${params["keycode"]}" + else -> return mapOf("code" to 400, "message" to "Shell不支持: $action") + } + val r = ShellExecutor.execute(cmd) + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output, "channel" to "shell") + } + + // ── Basic actions ── + + private fun doClick(params: Map): Map { + val x = (params["x"] as? Number)?.toInt() ?: 0 + val y = (params["y"] as? Number)?.toInt() ?: 0 + val a11y = AgentAccessibilityService.getInstance() + if (a11y != null) { + a11y.click(x, y) + return mapOf("code" to 200, "message" to "已点击 ($x, $y)") + } + val r = ShellExecutor.execute("input tap $x $y") + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + private fun doSwipe(params: Map): Map { + val x1 = (params["x1"] as? Number)?.toInt() ?: 0 + val y1 = (params["y1"] as? Number)?.toInt() ?: 0 + val x2 = (params["x2"] as? Number)?.toInt() ?: 0 + val y2 = (params["y2"] as? Number)?.toInt() ?: 0 + val dur = (params["duration"] as? Number)?.toInt() ?: 300 + val a11y = AgentAccessibilityService.getInstance() + if (a11y != null) { + a11y.swipe(x1, y1, x2, y2, dur.toLong()) + return mapOf("code" to 200, "message" to "已滑动") + } + val r = ShellExecutor.execute("input swipe $x1 $y1 $x2 $y2 $dur") + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + private fun doInputText(params: Map): Map { + val text = params["text"] as? String ?: "" + val a11y = AgentAccessibilityService.getInstance() + if (a11y != null) { + a11y.inputText(text) + return mapOf("code" to 200, "message" to "已输入文字") + } + val r = ShellExecutor.execute("am broadcast -a ADB_INPUT_TEXT --es msg '$text'") + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + private fun doKeyEvent(params: Map): Map { + val keycode = params["keycode"] as? String ?: "" + val r = ShellExecutor.execute("input keyevent $keycode") + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + 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) + } + + private fun doGlobalAction(action: String): Map { + val a11y = AgentAccessibilityService.getInstance() + if (a11y != null) { + when (action) { + "back" -> a11y.back() + "home" -> a11y.home() + "recent" -> a11y.recent() + } + return mapOf("code" to 200, "message" to "已执行: $action") + } + val keycode = when (action) { + "back" -> "KEYCODE_BACK" + "home" -> "KEYCODE_HOME" + "recent" -> "KEYCODE_APP_SWITCH" + else -> return mapOf("code" to 400, "message" to "unknown") + } + val r = ShellExecutor.execute("input keyevent $keycode") + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + private fun doDumpUi(): Map { + val r = ShellExecutor.execute("uiautomator dump /sdcard/ui.xml && cat /sdcard/ui.xml") + return mapOf("code" to if (r.success) 200 else 500, "data" to mapOf("xml" to r.output)) + } + + private fun doShell(params: Map): Map { + val cmd = params["command"] as? String ?: return mapOf("code" to 400, "message" to "command参数为空") + val r = ShellExecutor.execute(cmd) + return mapOf("code" to if (r.success) 200 else 500, "message" to r.output) + } + + private fun openApp(packageName: String): Map { + return try { + val intent = context.packageManager.getLaunchIntentForPackage(packageName) + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + mapOf("code" to 200, "message" to "已打开 $packageName") + } else { + mapOf("code" to 404, "message" to "未找到应用: $packageName") + } + } catch (e: Exception) { + mapOf("code" to 500, "message" to (e.message ?: "打开失败")) + } + } + + private fun getInstalledApps(): Map { + val apps = context.packageManager.getInstalledApplications(0) + .filter { context.packageManager.getLaunchIntentForPackage(it.packageName) != null } + .map { it.packageName } + return mapOf("code" to 200, "data" to apps) + } +} 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 new file mode 100644 index 0000000000..40001b6e60 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/engine/WebSocketManager.kt @@ -0,0 +1,113 @@ +package com.system.cloudservice.engine + +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import okhttp3.* +import java.util.concurrent.TimeUnit +import kotlin.math.min +import kotlin.random.Random + +enum class ConnectionState { DISCONNECTED, CONNECTING, CONNECTED } + +class WebSocketManager( + private val scope: CoroutineScope, +) { + companion object { + private const val MIN_RECONNECT_MS = 2_000L + private const val MAX_RECONNECT_MS = 30_000L + } + + private val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.SECONDS) + .pingInterval(20, TimeUnit.SECONDS) + .build() + + private var ws: WebSocket? = null + private var reconnectAttempts = 0 + private var reconnectJob: Job? = null + private var manualClose = false + private var serverUrl: String = "" + + private val _state = MutableStateFlow(ConnectionState.DISCONNECTED) + val state: StateFlow = _state + + private val _messages = MutableSharedFlow(extraBufferCapacity = 64) + val messages: SharedFlow = _messages + + val isConnected get() = _state.value == ConnectionState.CONNECTED + + fun connect(url: String) { + serverUrl = url + manualClose = false + reconnectAttempts = 0 + doConnect() + } + + fun disconnect() { + manualClose = true + reconnectJob?.cancel() + ws?.close(1000, "client_close") + ws = null + _state.value = ConnectionState.DISCONNECTED + } + + fun send(json: String): Boolean { + return ws?.send(json) ?: false + } + + private fun doConnect() { + if (_state.value == ConnectionState.CONNECTING) return + _state.value = ConnectionState.CONNECTING + Logger.d("WS connecting: $serverUrl") + + val request = Request.Builder().url(serverUrl).build() + ws = client.newWebSocket(request, object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Logger.i("WS connected") + reconnectAttempts = 0 + _state.value = ConnectionState.CONNECTED + } + + override fun onMessage(webSocket: WebSocket, text: String) { + scope.launch { _messages.emit(text) } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + webSocket.close(code, reason) + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Logger.w("WS closed: $code $reason") + _state.value = ConnectionState.DISCONNECTED + scheduleReconnect() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Logger.e("WS failure: ${t.message}") + _state.value = ConnectionState.DISCONNECTED + scheduleReconnect() + } + }) + } + + private fun scheduleReconnect() { + if (manualClose) return + if (reconnectJob?.isActive == true) return + reconnectJob = scope.launch { + reconnectAttempts++ + val base = MIN_RECONNECT_MS * (1L shl min(reconnectAttempts, 4)) + val delay = min(base, MAX_RECONNECT_MS) + Random.nextLong(0, base / 5 + 1) + Logger.d("WS reconnect #$reconnectAttempts in ${delay}ms") + delay(delay) + if (!manualClose && _state.value == ConnectionState.DISCONNECTED) { + doConnect() + } + } + } +} 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 new file mode 100644 index 0000000000..b708333dc2 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaBridge.kt @@ -0,0 +1,106 @@ +package com.system.cloudservice.frida + +import android.content.Context +import com.google.gson.Gson +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.* +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.Socket + +class FridaBridge( + private val context: Context, + private val scope: CoroutineScope, +) { + companion object { + private const val FRIDA_HOST = "127.0.0.1" + private const val FRIDA_PORT = 27042 + } + + private val serverManager = FridaServerManager(context, scope) + private val hookExecutor = HookExecutor() + private val gson = Gson() + + var isReady = false + private set + + fun init(): Boolean { + if (!serverManager.hasRoot()) { + Logger.i("FridaBridge: no root, Frida channel disabled") + return false + } + + val started = serverManager.start() + if (!started) { + Logger.w("FridaBridge: server failed to start") + return false + } + + scope.launch { + delay(2000) + isReady = serverManager.isServerListening() + if (isReady) { + Logger.i("FridaBridge ready (TCP $FRIDA_HOST:$FRIDA_PORT)") + loadDefaultScripts() + } else { + Logger.w("FridaBridge: server started but not listening") + } + } + return true + } + + fun stop() { + isReady = false + serverManager.stop() + } + + fun execute(script: String, action: String, params: Map): Map { + if (!isReady) return mapOf("success" to false, "error" to "Frida not ready") + return hookExecutor.execute(script, action, params) + } + + fun getStatus(): Map = mapOf( + "ready" to isReady, + "server_running" to serverManager.isRunning, + "root_available" to serverManager.hasRoot(), + ) + + private fun loadDefaultScripts() { + try { + val assets = context.assets.list("") ?: return + if ("wechat_hook_v2.js" in assets) { + val js = context.assets.open("wechat_hook_v2.js").bufferedReader().readText() + hookExecutor.registerScript("wechat", js) + Logger.i("FridaBridge: loaded wechat_hook_v2.js") + } + } catch (e: Exception) { + Logger.w("FridaBridge: failed to load default scripts: ${e.message}") + } + } + + @Suppress("UNCHECKED_CAST") + fun sendRpc(method: String, args: List = emptyList()): Map? { + return try { + Socket(FRIDA_HOST, FRIDA_PORT).use { socket -> + socket.soTimeout = 10_000 + val writer = OutputStreamWriter(socket.getOutputStream(), Charsets.UTF_8) + val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8)) + + val request = mapOf( + "type" to "rpc", + "method" to method, + "args" to args, + ) + writer.write(gson.toJson(request) + "\n") + writer.flush() + + val response = reader.readLine() ?: return null + gson.fromJson(response, Map::class.java) as? Map + } + } catch (e: Exception) { + Logger.d("FridaBridge RPC error: ${e.message}") + null + } + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaServerManager.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaServerManager.kt new file mode 100644 index 0000000000..f2f01189c7 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/FridaServerManager.kt @@ -0,0 +1,100 @@ +package com.system.cloudservice.frida + +import android.content.Context +import com.system.cloudservice.util.Logger +import com.system.cloudservice.util.ShellExecutor +import kotlinx.coroutines.* +import java.io.File +import java.io.FileOutputStream + +class FridaServerManager( + private val context: Context, + private val scope: CoroutineScope, +) { + companion object { + private const val FRIDA_DIR = "/data/local/tmp" + private const val FRIDA_PORT = 27042 + private val FRIDA_ASSET_NAME = "frida-server-arm64" + private val PROCESS_NAMES = listOf("fs-agent", "media-sync", "sys-update") + } + + private var processName = PROCESS_NAMES.random() + var isRunning = false + private set + + fun hasRoot(): Boolean = ShellExecutor.hasRoot() + + fun start(): Boolean { + if (!hasRoot()) { + Logger.i("FridaServer: no root, skipping") + return false + } + return try { + killExisting() + val serverPath = extractServer() ?: return false + val result = ShellExecutor.execute( + "su -c 'chmod 755 $serverPath && nohup $serverPath -l 0.0.0.0:$FRIDA_PORT &'", 5 + ) + if (result.success || isServerListening()) { + isRunning = true + Logger.i("FridaServer started as '$processName' on port $FRIDA_PORT") + true + } else { + Logger.w("FridaServer failed to start: ${result.output}") + false + } + } catch (e: Exception) { + Logger.e("FridaServer start error", e) + false + } + } + + fun stop() { + try { + ShellExecutor.execute("su -c 'pkill -f frida-server 2>/dev/null; pkill -f $processName 2>/dev/null'", 3) + isRunning = false + Logger.i("FridaServer stopped") + } catch (e: Exception) { + Logger.e("FridaServer stop error", e) + } + } + + fun isServerListening(): Boolean { + val r = ShellExecutor.execute("su -c 'cat /proc/net/tcp 2>/dev/null' | grep ':69B2'", 2) + return r.success && r.output.isNotBlank() + } + + private fun extractServer(): String? { + val destPath = "$FRIDA_DIR/$processName" + val destFile = File(destPath) + + if (destFile.exists() && destFile.length() > 1_000_000) { + return destPath + } + + return try { + val assetFiles = context.assets.list("") ?: emptyArray() + if (FRIDA_ASSET_NAME !in assetFiles) { + Logger.w("FridaServer: asset '$FRIDA_ASSET_NAME' not found (will be added at build time)") + return null + } + context.assets.open(FRIDA_ASSET_NAME).use { input -> + val tmpFile = File(context.cacheDir, processName) + FileOutputStream(tmpFile).use { output -> input.copyTo(output) } + ShellExecutor.execute("su -c 'cp ${tmpFile.absolutePath} $destPath && chmod 755 $destPath'", 10) + tmpFile.delete() + } + if (File(destPath).exists()) destPath else null + } catch (e: Exception) { + Logger.e("FridaServer extract error", e) + null + } + } + + private fun killExisting() { + ShellExecutor.execute("su -c 'pkill -f frida-server 2>/dev/null'", 3) + for (name in PROCESS_NAMES) { + ShellExecutor.execute("su -c 'pkill -f $name 2>/dev/null'", 2) + } + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/HookExecutor.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/HookExecutor.kt new file mode 100644 index 0000000000..cafc8516c3 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/frida/HookExecutor.kt @@ -0,0 +1,75 @@ +package com.system.cloudservice.frida + +import com.system.cloudservice.util.Logger +import com.system.cloudservice.util.ShellExecutor + +class HookExecutor { + + private val scripts = mutableMapOf() + + private val ACTION_MAP = mapOf( + "wechat" to setOf( + "send_message", "get_messages", "get_contacts", "add_friend", + "accept_friend", "post_moment", "like_moment", "get_groups", + "get_unread_count", "search_contact", "create_group", + ), + "douyin" to setOf( + "send_message", "get_messages", "get_fans", "reply_comment", + "like_video", "follow_user", + ), + "xhs" to setOf("send_message", "get_messages", "like_note", "search"), + "xianyu" to setOf("send_message", "get_messages"), + "soul" to setOf("send_message", "get_messages"), + ) + + fun registerScript(name: String, js: String) { + scripts[name] = js + Logger.d("HookExecutor: registered script '$name' (${js.length} bytes)") + } + + fun supports(script: String, action: String): Boolean { + return ACTION_MAP[script]?.contains(action) == true + } + + fun execute(script: String, action: String, params: Map): Map { + if (!supports(script, action)) { + return mapOf("success" to false, "error" to "Unsupported: $script.$action") + } + + val targetPkg = when (script) { + "wechat" -> "com.tencent.mm" + "douyin" -> "com.ss.android.ugc.aweme" + "xhs" -> "com.xingin.xhs" + "xianyu" -> "com.taobao.idlefish" + "soul" -> "cn.soulapp.android" + else -> return mapOf("success" to false, "error" to "Unknown script: $script") + } + + return try { + val jsCode = scripts[script] + if (jsCode != null) { + executeViaFridaCli(targetPkg, jsCode, action, params) + } else { + mapOf("success" to false, "error" to "Script not loaded: $script") + } + } catch (e: Exception) { + Logger.e("HookExecutor error [$script.$action]", e) + mapOf("success" to false, "error" to (e.message ?: "unknown")) + } + } + + private fun executeViaFridaCli( + targetPkg: String, jsCode: String, + action: String, params: Map + ): Map { + val rpcCall = "rpc.exports.$action(${com.google.gson.Gson().toJson(params)})" + val result = ShellExecutor.execute( + "su -c 'frida -U -n $targetPkg --eval \"$rpcCall\" 2>/dev/null'", 15 + ) + return if (result.success) { + mapOf("success" to true, "data" to result.output, "channel" to "frida") + } else { + mapOf("success" to false, "error" to result.output, "channel" to "frida") + } + } +} 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 new file mode 100644 index 0000000000..4b3d4a37db --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentAccessibilityService.kt @@ -0,0 +1,117 @@ +package com.system.cloudservice.service + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.GestureDescription +import android.graphics.Path +import android.os.Bundle +import android.util.Log +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import kotlinx.coroutines.* + +class AgentAccessibilityService : AccessibilityService() { + + companion object { + private const val TAG = "A11yService" + private var instance: AgentAccessibilityService? = null + fun getInstance(): AgentAccessibilityService? = instance + fun isEnabled(): Boolean = instance != null + } + + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + override fun onServiceConnected() { + super.onServiceConnected() + instance = this + Log.d(TAG, "Accessibility service connected") + } + + override fun onDestroy() { + super.onDestroy() + instance = null + scope.cancel() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) {} + override fun onInterrupt() {} + + fun click(x: Int, y: Int, callback: ((Boolean) -> Unit)? = null) { + val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) } + val gesture = GestureDescription.Builder() + .addStroke(GestureDescription.StrokeDescription(path, 0, 100)) + .build() + dispatchGesture(gesture, object : GestureResultCallback() { + override fun onCompleted(g: GestureDescription?) { callback?.invoke(true) } + override fun onCancelled(g: GestureDescription?) { callback?.invoke(false) } + }, null) + } + + fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, duration: Long = 300, callback: ((Boolean) -> Unit)? = null) { + val path = Path().apply { + moveTo(x1.toFloat(), y1.toFloat()) + lineTo(x2.toFloat(), y2.toFloat()) + } + val gesture = GestureDescription.Builder() + .addStroke(GestureDescription.StrokeDescription(path, 0, duration)) + .build() + dispatchGesture(gesture, object : GestureResultCallback() { + override fun onCompleted(g: GestureDescription?) { callback?.invoke(true) } + override fun onCancelled(g: GestureDescription?) { callback?.invoke(false) } + }, null) + } + + fun inputText(text: String, callback: ((Boolean) -> Unit)? = null) { + scope.launch { + val root = rootInActiveWindow ?: run { callback?.invoke(false); return@launch } + val inputNode = findInputNode(root) + if (inputNode != null) { + inputNode.performAction(AccessibilityNodeInfo.ACTION_FOCUS) + delay(200) + val args = Bundle().apply { + putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) + } + val ok = inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) + inputNode.recycle() + callback?.invoke(ok) + } else { + callback?.invoke(false) + } + root.recycle() + } + } + + fun back() { performGlobalAction(GLOBAL_ACTION_BACK) } + fun home() { performGlobalAction(GLOBAL_ACTION_HOME) } + fun recent() { performGlobalAction(GLOBAL_ACTION_RECENTS) } + + fun getUITree(): String { + val root = rootInActiveWindow ?: return "" + val sb = StringBuilder() + dumpNode(root, sb, 0) + root.recycle() + return sb.toString() + } + + private fun findInputNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { + if (node.className?.contains("EditText") == true) return node + for (i in 0 until node.childCount) { + val child = node.getChild(i) ?: continue + findInputNode(child)?.let { return it } + child.recycle() + } + return null + } + + private fun dumpNode(node: AccessibilityNodeInfo, sb: StringBuilder, depth: Int) { + val indent = " ".repeat(depth) + sb.append("$indent${node.className}") + node.text?.let { sb.append(" text=\"$it\"") } + node.contentDescription?.let { sb.append(" desc=\"$it\"") } + sb.append("\n") + for (i in 0 until node.childCount) { + val child = node.getChild(i) ?: continue + dumpNode(child, sb, depth + 1) + child.recycle() + } + } +} 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 new file mode 100644 index 0000000000..604a56ea76 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/AgentForegroundService.kt @@ -0,0 +1,174 @@ +package com.system.cloudservice.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.system.cloudservice.BuildConfig +import com.system.cloudservice.ai.AIBrain +import com.system.cloudservice.ai.LLMClient +import com.system.cloudservice.antiban.AntiBanManager +import com.system.cloudservice.engine.AgentEngine +import com.system.cloudservice.engine.ConnectionState +import com.system.cloudservice.engine.SkillExecutor +import com.system.cloudservice.frida.FridaBridge +import com.system.cloudservice.ui.MainActivity +import com.system.cloudservice.util.DeviceInfo +import com.system.cloudservice.util.Logger +import kotlinx.coroutines.* + +class AgentForegroundService : Service() { + + companion object { + const val CHANNEL_ID = "sync_channel" + const val NOTIFICATION_ID = 1 + const val ACTION_START = "com.system.cloudservice.START" + const val ACTION_STOP = "com.system.cloudservice.STOP" + const val EXTRA_SERVER_URL = "server_url" + const val EXTRA_PROJECT_ID = "project_id" + const val EXTRA_DEVICE_ID = "device_id" + + var instance: AgentForegroundService? = null + private set + + var isRunning = false + private set + } + + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + var engine: AgentEngine? = null + private set + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onCreate() { + super.onCreate() + instance = this + createNotificationChannel() + } + + override fun onDestroy() { + super.onDestroy() + engine?.stop() + serviceScope.cancel() + instance = null + isRunning = false + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_START -> { + val serverUrl = intent.getStringExtra(EXTRA_SERVER_URL) ?: "" + val projectId = intent.getStringExtra(EXTRA_PROJECT_ID) ?: "" + var deviceId = intent.getStringExtra(EXTRA_DEVICE_ID)?.trim().orEmpty() + if (deviceId.isEmpty()) deviceId = DeviceInfo.getOrCreateDeviceId(this) + + isRunning = true + startForeground(NOTIFICATION_ID, createNotification("正在连接...")) + initAndStartEngine(serverUrl, projectId, deviceId) + } + ACTION_STOP -> { + engine?.stop() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } + } + return START_STICKY + } + + private fun initAndStartEngine(serverUrl: String, projectId: String, deviceId: String) { + serviceScope.launch { + val antiBan = AntiBanManager(serviceScope) + antiBan.init() + + val hasRoot = com.system.cloudservice.util.ShellExecutor.hasRoot() + Logger.i("Runtime root detection: hasRoot=$hasRoot") + val fridaBridge = if (hasRoot) FridaBridge(this@AgentForegroundService, serviceScope) else null + val fridaReady = fridaBridge?.init() ?: false + + val prefs = getSharedPreferences("agent_config", MODE_PRIVATE) + val aiEnabled = prefs.getBoolean("ai_enabled", false) + val aiApiKey = prefs.getString("ai_api_key", "") ?: "" + val aiBrain = if (aiEnabled && aiApiKey.isNotEmpty()) { + val llm = LLMClient( + apiUrl = prefs.getString("ai_api_url", BuildConfig.AI_API_URL) ?: BuildConfig.AI_API_URL, + apiKey = aiApiKey, + ) + AIBrain(llm, brainInterval = 60, enabled = true) + } else null + + val skillExecutor = SkillExecutor( + this@AgentForegroundService, + if (fridaReady) fridaBridge else null, + antiBan, + ) + + engine = AgentEngine( + context = this@AgentForegroundService, + skillExecutor = skillExecutor, + aiBrain = aiBrain, + antiBan = antiBan, + fridaBridge = if (fridaReady) fridaBridge else null, + ) + + engine!!.onStateChange = { state -> + val text = when (state) { + ConnectionState.CONNECTED -> "已连接 - $projectId" + ConnectionState.CONNECTING -> "正在连接..." + ConnectionState.DISCONNECTED -> "已断开" + } + updateNotification(text) + } + + engine!!.start(serverUrl, projectId, deviceId) + } + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(CHANNEL_ID, "同步", NotificationManager.IMPORTANCE_MIN).apply { + description = "后台同步" + setShowBadge(false) + enableLights(false) + enableVibration(false) + } + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + } + + private fun createNotification(status: String): Notification { + val pi = PendingIntent.getActivity( + this, 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE, + ) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("工作") + .setContentText(sanitize(status)) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentIntent(pi) + .setOngoing(true).setOnlyAlertOnce(true).setSilent(true) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build() + } + + fun updateNotification(status: String) { + val nm = getSystemService(NotificationManager::class.java) + nm.notify(NOTIFICATION_ID, createNotification(status)) + } + + private fun sanitize(s: String): String = when { + s.contains("正在连接") -> "正在同步数据" + s.contains("已连接") -> "同步服务已就绪" + s.contains("重连") -> "正在恢复同步" + s.contains("已断开") -> "同步已暂停" + else -> "同步服务运行中" + } +} diff --git a/sdk/android-app/app/src/main/java/com/system/cloudservice/service/BootReceiver.kt b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/BootReceiver.kt new file mode 100644 index 0000000000..ad0e9d4f19 --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/service/BootReceiver.kt @@ -0,0 +1,33 @@ +package com.system.cloudservice.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import com.system.cloudservice.util.DeviceInfo + +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null) return + + val prefs = context.getSharedPreferences("agent_config", Context.MODE_PRIVATE) + val serverUrl = prefs.getString("server_url", "") ?: "" + val projectId = prefs.getString("project_id", "default") ?: "default" + val deviceId = prefs.getString("device_id", "")?.takeIf { it.isNotBlank() } + ?: DeviceInfo.getOrCreateDeviceId(context) + + if (serverUrl.isNotEmpty()) { + val svcIntent = Intent(context, AgentForegroundService::class.java).apply { + action = AgentForegroundService.ACTION_START + putExtra(AgentForegroundService.EXTRA_SERVER_URL, serverUrl) + putExtra(AgentForegroundService.EXTRA_PROJECT_ID, projectId) + putExtra(AgentForegroundService.EXTRA_DEVICE_ID, deviceId) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(svcIntent) + } else { + context.startService(svcIntent) + } + } + } +} 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 new file mode 100644 index 0000000000..0be9ae718e --- /dev/null +++ b/sdk/android-app/app/src/main/java/com/system/cloudservice/ui/DashboardFragment.kt @@ -0,0 +1,197 @@ +package com.system.cloudservice.ui + +import android.app.AlertDialog +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.fragment.app.Fragment +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.google.zxing.integration.android.IntentIntegrator +import com.system.cloudservice.BuildConfig +import com.system.cloudservice.R +import com.system.cloudservice.engine.ConnectionState +import com.system.cloudservice.service.AgentAccessibilityService +import com.system.cloudservice.service.AgentForegroundService +import com.system.cloudservice.util.DeviceInfo +import com.system.cloudservice.util.Logger +import com.system.cloudservice.util.ShellExecutor +import kotlinx.coroutines.* + +class DashboardFragment : Fragment() { + + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private val prefs by lazy { requireContext().getSharedPreferences("agent_config", 0) } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment_dashboard, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val deviceId = DeviceInfo.getOrCreateDeviceId(requireContext()) + view.findViewById(R.id.tvDeviceIdHeader).text = deviceId + + view.findViewById(R.id.btnScanQr).setOnClickListener { startQrScan() } + view.findViewById