diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..c14a5ea
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,11 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(python:*)",
+ "Bash(cat:*)",
+ "Bash(find:*)",
+ "Bash(ls:*)",
+ "Bash(git stash:*)"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 78cbee6..317f9c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,7 @@ output/
running_log
gelab-zero-4b-preview/
-model_config.yaml
\ No newline at end of file
+model_config.yaml
+venv/
+user_package_map.yaml
+.claude/
\ No newline at end of file
diff --git a/README.md b/README.md
index 94eba64..a58d58e 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,6 @@
> 👋 Hi, everyone! We are proud to present the first fully open-source GUI Agent with both model and infrastructure. Our solution features plug-and-play engineering with no cloud dependencies, giving you complete privacy control.
-
@@ -16,6 +15,129 @@
简体中文
+---
+
+# 🚀 Fork Enhancements
+
+> **This project is enhanced from [stepfun-ai/gelab-zero](https://github.com/stepfun-ai/gelab-zero)**
+>
+> The following content describes new features added in this Fork.
+
+## 🖥️ Web UI Features
+
+Launch: `python start_web_ui.py`, then visit `http://localhost:8866`
+
+**Left Panel - Control**
+
+| Module | Features |
+|--------|----------|
+| **📱 Device Management** | Check device status, view device list, restart ADB service |
+| **📶 Wireless Debugging** | Connect device via IP address, enable TCP/IP mode |
+| **📊 Task Monitoring** | View task status, ⏸️ **Pause/Inject/Resume**, select historical Sessions |
+| **💬 Command/Reply** | Enter task instructions or reply to Agent, supports `Ctrl+Enter` |
+| **⚙️ Model Configuration** | Select model provider, 🔍 **Check model connection**, configure API |
+| **🛠 Utilities** | Launch scrcpy, get app list, 📄 **Export PDF trajectory**, 📦 **Scan App Mapping** |
+
+> ⚠️ **Important**: For new phones or after re-enabling developer mode, you must first connect via USB cable at least once. This initial USB connection authorizes the computer for ADB access. Once authorized, you can use wireless debugging without USB connection going forward.
+
+**Right Panel - Display**
+
+| Module | Features |
+|--------|----------|
+| **📱 Task Trajectory** | Visual replay of each step with screenshots, thought process, action details |
+| **📋 Real-time Logs** | Real-time task execution output, with clear and copy buttons |
+
+## ✨ New Features
+
+### ⏸️ Pause / Inject / Resume
+
+During task execution, you can:
+- **Instant Pause**: Click pause button to immediately terminate current execution
+- **Inject Instructions**: Enter correction instructions (e.g., "search for xxx instead")
+- **Seamless Resume**: Continue from the same Session, maintaining trajectory integrity
+
+> 💡 Solves the pain point of not being able to manually intervene during Agent execution
+
+### 🔍 Model Connection Check
+
+One-click test in configuration panel:
+- Quickly test if local/online model is available
+- Automatically distinguish local (Ollama) vs online API
+- Display connection status and model name
+
+### 📋 Multi-Provider Configuration
+
+Auto-loaded from `model_config.yaml`, each provider configures:
+
+```yaml
+local:
+ display_name: "Local Model (Ollama)"
+ api_base: "http://localhost:11434/v1"
+ api_key: "EMPTY"
+ default_model: "gelab-zero-4b-preview"
+
+stepfun:
+ display_name: "StepFun"
+ api_base: "https://api.stepfun.com/v1"
+ api_key: "YOUR_API_KEY"
+ default_model: "step-gui"
+```
+
+### 📄 PDF Trajectory Export
+
+- Export task execution trajectory to PDF file
+- Includes screenshots, thought process, action details
+- Auto-download support
+
+### 🎨 UI Improvements
+
+- **Three-line Configuration**: Base URL, API Key, Model Name on separate rows for easier input
+- **Improved Status Display**: Clearer task status feedback (Ready/Running/Waiting/Paused)
+- **Reply Interaction Fix**: Properly detects waiting for input state when Agent asks questions
+
+### 📦 App Mapping Scanner
+
+Automatically scan installed apps on the device and build a **Chinese app name → package name** mapping, enabling the AWAKE feature to recognize more apps.
+
+**File Structure:**
+
+```
+Project Root/
+├── default_package_map.yaml # Default mapping library (160+ entries)
+├── user_package_map.yaml # User mappings (scan results + custom)
+├── user_package_map.yaml.example # Template file
+└── aapt2-8.5.0-11315950-windows/ # aapt2 tool (Windows)
+```
+
+**Features:**
+
+- **Real-time Loading**: Changes to YAML files take effect immediately, no restart needed
+- **Smart Scanning**: Prioritizes mapping table (instant), auto-parses unknown apps with aapt2
+- **Priority**: `user_package_map.yaml` > `default_package_map.yaml`
+
+**Usage:**
+
+1. Click "🔍 Scan App Mapping" in Web UI
+2. Scan results auto-save to `user_package_map.yaml`
+3. Manually edit/add mappings in "📝 App Mapping Editor"
+
+**⏱️ Scan Time Reference:**
+
+| Match Type | Time per App | Description |
+|-----------|-------------|-------------|
+| Mapping Match | <1 sec | Quick lookup from 160+ mappings |
+| Deep Parse | 5-15 sec | Pull APK and parse with aapt2 |
+
+> ⚠️ **Note**: If you have many apps installed (e.g., 300+) and most are not in the default mapping, deep scanning may take **20-40 minutes**. Consider manually editing `default_package_map.yaml` first.
+
+> 💡 Project includes `aapt2` tool with auto-adaptive paths, no extra configuration needed
+
+---
+
+# 📖 Official Original Content
+
+> The following content is from [stepfun-ai/gelab-zero](https://github.com/stepfun-ai/gelab-zero) original README
+
## 📰 News
* 🎁 **[2025-12-18]** We release **Step-GUI Technical Report** on [**arXiv**](https://arxiv.org/abs/2512.15431)!
@@ -488,7 +610,7 @@ Go to Settings → Local API Server, create an API key under server configuratio
#### Step 3: Adjust GELab-Zero Agent model config
-llama.cpp’s service differs slightly from Ollama, so you must tweak the model config in GELab-Zero Agent. Two places:
+llama.cpp's service differs slightly from Ollama, so you must tweak the model config in GELab-Zero Agent. Two places:
1. In `model_config.yaml`, update the port and API key (use the key you just created):
@@ -568,10 +690,6 @@ If you find GELab-Zero useful for your research, please consider citing our work
```
-## ⭐ Star History
+## Star History
-
+[](https://www.star-history.com/#flyfox666/gelab-zero-webui&type=date&legend=top-left)
diff --git a/README_CN.md b/README_CN.md
index d466678..9471aac 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -1,11 +1,9 @@

-
> 👋 hi大家好!我们很荣幸推出首个同时包含模型和基础设施的全开源 GUI Agent。我们的解决方案主打即插即用的工程化体验,无需依赖云端,赋予您完全的隐私控制权。
-
@@ -18,6 +16,129 @@
简体中文
+---
+
+# 🚀 Fork 增强版
+
+> **本项目基于 [stepfun-ai/gelab-zero](https://github.com/stepfun-ai/gelab-zero) 进行增强开发**
+>
+> 以下内容为本 Fork 新增的功能和优化
+
+## 🖥️ Web UI 功能特性
+
+启动命令:`python start_web_ui.py`,然后访问 `http://localhost:8866`
+
+**左栏 - 控制面板**
+
+| 模块 | 功能 |
+|------|------|
+| **📱 设备管理** | 检查设备状态、查看设备列表、重启 ADB 服务 |
+| **📶 无线调试** | 通过 IP 地址无线连接设备、启用 TCP/IP 模式 |
+| **📊 任务监控** | 查看任务状态、⏸️ **暂停/注入/继续**、选择历史 Session |
+| **💬 命令/回复** | 输入任务指令或回复 Agent 询问,支持 `Ctrl+Enter` |
+| **⚙️ 参数配置** | 选择模型提供商、🔍 **检查模型连接**、配置 API 参数 |
+| **🛠 实用工具** | 启动 scrcpy、获取应用列表、📄 **导出PDF轨迹**、📦 **扫描应用映射** |
+
+> ⚠️ **重要提示**:对于新手机或重新启用开发者模式后,需要先通过 USB 数据线连接一次。这次初始 USB 连接用于授权电脑的 ADB 访问权限。授权完成后,后续可以一直使用无线连接,无需再进行有线连接。
+
+**右栏 - 任务展示**
+
+| 模块 | 功能 |
+|------|------|
+| **📱 任务轨迹** | 可视化回放每个执行步骤,包含截图、思考过程、动作详情 |
+| **📋 实时日志** | 实时显示任务执行的终端输出,支持清空和复制 |
+
+## ✨ 新增功能
+
+### ⏸️ 暂停 / 注入 / 继续
+
+在任务执行过程中,可以随时:
+- **立即暂停**:点击暂停按钮立即终止当前执行
+- **注入指令**:输入修正指令(如"改为搜索xxx")
+- **无缝继续**:从同一 Session 继续执行,保持轨迹完整性
+
+> 💡 解决了 Agent 执行过程中无法人工干预修正的痛点
+
+### 🔍 模型连接检查
+
+参数配置面板新增一键检测功能:
+- 快速测试本地/在线模型是否可用
+- 自动区分本地 (Ollama) 和在线 API
+- 显示连接状态、模型名称
+
+### 📋 多模型提供商配置
+
+从 `model_config.yaml` 自动加载,每个提供商可配置:
+
+```yaml
+local:
+ display_name: "本地模型 (Ollama)"
+ api_base: "http://localhost:11434/v1"
+ api_key: "EMPTY"
+ default_model: "gelab-zero-4b-preview"
+
+stepfun:
+ display_name: "阶跃星辰 (StepFun)"
+ api_base: "https://api.stepfun.com/v1"
+ api_key: "YOUR_API_KEY"
+ default_model: "step-gui"
+```
+
+### 📄 PDF 轨迹导出
+
+- 导出任务执行轨迹为 PDF 文件
+- 包含截图、思考过程、动作详情
+- 支持自动下载
+
+### 🎨 UI 优化
+
+- **三行独立配置**:Base URL、API Key、模型名称分行显示,更易填写
+- **改进的状态显示**:更清晰的任务状态反馈(就绪/运行中/等待输入/已暂停)
+- **回复交互修复**:Agent 询问时能正确检测等待输入状态
+
+### 📦 应用映射扫描
+
+自动扫描设备上已安装的应用,建立**中文应用名→包名**的映射,让 AWAKE 功能能识别更多应用。
+
+**文件结构:**
+
+```
+项目根目录/
+├── default_package_map.yaml # 默认映射库(160+条,项目提供)
+├── user_package_map.yaml # 用户映射(扫描结果+自定义)
+├── user_package_map.yaml.example # 模板文件
+└── aapt2-8.5.0-11315950-windows/ # aapt2 工具(Windows)
+```
+
+**功能特性:**
+
+- **实时加载**:修改 YAML 文件后立即生效,无需重启程序
+- **智能扫描**:优先从映射表匹配(秒级),未匹配的自动用 aapt2 深度解析
+- **优先级**:`user_package_map.yaml` > `default_package_map.yaml`
+
+**使用方法:**
+
+1. 在 Web UI 点击「🔍 扫描应用映射」
+2. 扫描结果自动保存到 `user_package_map.yaml`
+3. 在「📝 应用映射编辑器」中手动编辑/添加映射
+
+**⏱️ 扫描时间参考:**
+
+| 匹配方式 | 单个应用耗时 | 说明 |
+|---------|-------------|------|
+| 映射表匹配 | <1 秒 | 从 160+ 条映射中快速查找 |
+| 深度解析 | 5-15 秒 | 拉取 APK 并用 aapt2 解析 |
+
+> ⚠️ **注意**:如手机安装应用较多(如 300+ 个),且大部分未在默认映射中,深度扫描可能需要 **20-40 分钟**。建议先手动编辑 `default_package_map.yaml` 添加常用应用。
+
+> 💡 项目已内置 `aapt2` 工具,路径自动适配,无需额外配置
+
+---
+
+# 📖 官方原版内容
+
+> 以下内容来自 [stepfun-ai/gelab-zero](https://github.com/stepfun-ai/gelab-zero) 原版 README
+
## 📰 新闻
* 🎁 **[2025-12-18]** 我们在 **[arXiv](https://arxiv.org/abs/2512.15431)** 上发布了 **Step-GUI 技术报告**!
@@ -50,7 +171,7 @@
## 📖 背景
-随着 AI 体验日益深入消费级终端设备,移动 Agent 研究正处于从 **“可行性验证”** 向 **“大规模应用”** 转型的关键节点。虽然基于 GUI 的方案具有通用兼容性,但移动生态的碎片化带来了沉重的工程负担,阻碍了创新。GELab-Zero 旨在打破这些壁垒。
+随着 AI 体验日益深入消费级终端设备,移动 Agent 研究正处于从 **"可行性验证"** 向 **"大规模应用"** 转型的关键节点。虽然基于 GUI 的方案具有通用兼容性,但移动生态的碎片化带来了沉重的工程负担,阻碍了创新。GELab-Zero 旨在打破这些壁垒。
* **⚡️ 开箱即用的全栈基建**
解决移动生态碎片化痛点,提供统一的一键推理管道。自动处理多设备 ADB 连接、依赖安装及权限配置,让开发者专注于策略创新而非工程基础设施。
@@ -59,7 +180,7 @@
* **📱 灵活的任务分发与编排**
支持跨多设备分发任务并记录交互轨迹。提供 ReAct 循环、多智能体协作及定时任务三种通用模式,以处理复杂的真实业务场景。
* **🚀 加速从原型到落地**
-赋能开发者快速验证交互策略,同时允许企业直接复用底层基建实现零成本 MCP 集成,跨越从“可行性验证”到“大规模应用”的关键鸿沟。
+赋能开发者快速验证交互策略,同时允许企业直接复用底层基建实现零成本 MCP 集成,跨越从"可行性验证"到"大规模应用"的关键鸿沟。
## 🎥 应用演示
@@ -113,7 +234,7 @@
### 复杂任务 - 信息检索
-任务:在知乎上搜索“如何学习理财”,并查看第一个点赞超过 1万 的回答
+任务:在知乎上搜索"如何学习理财",并查看第一个点赞超过 1万 的回答
**[📹 点击查看演示视频](./images/video_6.mp4)**
@@ -175,7 +296,7 @@ conda init powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
-激活成功后可见“(base)” 括号显示在最新一行的开头。
+激活成功后可见"(base)" 括号显示在最新一行的开头。
3. 执行和调试代码建议使用vs code,可在官网下载安装:https://code.visualstudio.com/
@@ -201,7 +322,6 @@ conda activate gelab-zero
ollama 部署在某些 Mac 设备可能无法正常运行(表征是吐 token 特慢,原因待进一步排查),可使用 llama.cpp 部署。
#### Step 1.1: Ollama 搭建(推荐个人用户)
-
对于做本地推理的个人用户,我们强烈推荐使用 Ollama 方式进行本地部署,该方式具有安装简单、使用便捷的优势。
@@ -284,7 +404,7 @@ curl -X POST http://localhost:11434/v1/chat/completions \
通常可以按如下步骤在安卓手机上开启开发者模式和 USB 调试:
1. 打开手机上的「设置」应用。
-2. 找到「关于手机」或「系统」选项,连续点击「版本号」10 次以上,直到看到“您已处于开发者模式”或类似提示。
+2. 找到「关于手机」或「系统」选项,连续点击「版本号」10 次以上,直到看到"您已处于开发者模式"或类似提示。
3. 返回「设置」主页面,找到「开发者选项」。【重要,必须开启】
4. 在「开发者选项」中,找到并开启「USB 调试」功能,按照屏幕提示完成 USB 调试的启用。【重要,必须开启】
@@ -342,7 +462,6 @@ AN2CVB4C28000731 device
如果没有看到任何设备,请检查数据线连接是否正常,以及手机上的 USB 调试选项是否正确开启。首次连接手机时,手机上可能会弹出授权提示,只需选择「允许」即可。如下图所示:
-
diff --git a/aapt2-8.5.0-11315950-windows/META-INF/MANIFEST.MF b/aapt2-8.5.0-11315950-windows/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..59499bc
--- /dev/null
+++ b/aapt2-8.5.0-11315950-windows/META-INF/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+
diff --git a/aapt2-8.5.0-11315950-windows/NOTICE b/aapt2-8.5.0-11315950-windows/NOTICE
new file mode 100644
index 0000000..6a2000f
--- /dev/null
+++ b/aapt2-8.5.0-11315950-windows/NOTICE
@@ -0,0 +1,1943 @@
+aapt2_artifacts
+
+==============================================================================
+Android used by:
+ aapt2
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================
+art used by:
+ aapt2
+
+frameworks/base used by:
+ aapt2
+
+linux_glibc_x86_64 used by:
+ aapt2
+
+system/core used by:
+ aapt2
+
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+
+==============================================================================
+expat_v_R_2_5_0 used by:
+ aapt2
+
+Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper
+Copyright (c) 2001-2022 Expat maintainers
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+==============================================================================
+fmtlib_v_10.1.1 used by:
+ aapt2
+
+Copyright (c) 2012 - 2016, Victor Zverovich
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==============================================================================
+fmtlib_v_10.1.1 used by:
+ aapt2
+
+Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
+
+==============================================================================
+frameworks/base used by:
+ aapt2
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Android-specific code. ==
+ =========================================================================
+
+Android Code
+Copyright 2005-2008 The Android Open Source Project
+
+This product includes software developed as part of
+The Android Open Source Project (http://source.android.com).
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for Apache Commons code. ==
+ =========================================================================
+
+Apache Commons
+Copyright 1999-2006 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for Jakarta Commons Logging. ==
+ =========================================================================
+
+Jakarta Commons Logging (JCL)
+Copyright 2005,2006 The Apache Software Foundation.
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Nuance code. ==
+ =========================================================================
+
+These files are Copyright 2007 Nuance Communications, but released under
+the Apache2 License.
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Media Codecs code. ==
+ =========================================================================
+
+Media Codecs
+These files are Copyright 1998 - 2009 PacketVideo, but released under
+the Apache2 License.
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the mDnsResponder code. ==
+ =========================================================================
+
+mDnsResponder TXTRecord
+This file is Copyright 2004 Apple Computer, Inc. but released under
+the Apache2 License.
+
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the TagSoup code. ==
+ =========================================================================
+
+This file is part of TagSoup and is Copyright 2002-2008 by John Cowan.
+
+TagSoup is licensed under the Apache License,
+Version 2.0. You may obtain a copy of this license at
+http://www.apache.org/licenses/LICENSE-2.0 . You may also have
+additional legal rights not granted by this license.
+
+TagSoup is distributed in the hope that it will be useful, but
+unless required by applicable law or agreed to in writing, TagSoup
+is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+OF ANY KIND, either express or implied; not even the implied warranty
+of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for Additional Codecs code. ==
+ =========================================================================
+
+Additional Codecs
+These files are Copyright 2003-2010 VisualOn, but released under
+the Apache2 License.
+
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Audio Effects code. ==
+ =========================================================================
+
+Audio Effects
+These files are Copyright (C) 2004-2010 NXP Software and
+Copyright (C) 2010 The Android Open Source Project, but released under
+the Apache2 License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+
+
+UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
+
+Unicode Data Files include all data files under the directories
+http://www.unicode.org/Public/, http://www.unicode.org/reports/,
+and http://www.unicode.org/cldr/data/ . Unicode Software includes any
+source code published in the Unicode Standard or under the directories
+http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
+http://www.unicode.org/cldr/data/.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA
+FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY
+ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF
+THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY,
+DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 1991-2008 Unicode, Inc. All rights reserved. Distributed
+under the Terms of Use in http://www.unicode.org/copyright.html.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation (the
+"Data Files") or Unicode software and any associated documentation (the
+"Software") to deal in the Data Files or Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, and/or sell copies of the Data Files or Software,
+and to permit persons to whom the Data Files or Software are furnished to
+do so, provided that (a) the above copyright notice(s) and this permission
+notice appear with all copies of the Data Files or Software, (b) both the
+above copyright notice(s) and this permission notice appear in associated
+documentation, and (c) there is clear notice in each modified Data File
+or in the Software as well as in the documentation associated with the
+Data File(s) or Software that the data or software has been modified.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
+INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
+OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
+OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
+
+==============================================================================
+googletest_v_e47544ad31cb3ceecd04cc13e8fe556f8df9fe0b used by:
+ aapt2
+
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==============================================================================
+libcxx used by:
+ aapt2
+
+==============================================================================
+libc++ License
+==============================================================================
+
+The libc++ library is dual licensed under both the University of Illinois
+"BSD-Like" license and the MIT license. As a user of this code you may choose
+to use it under either license. As a contributor, you agree to allow your code
+to be used under both.
+
+Full text of the relevant licenses is included below.
+
+==============================================================================
+
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2009-2017 by the contributors listed in CREDITS.TXT
+
+All rights reserved.
+
+Developed by:
+
+ LLVM Team
+
+ University of Illinois at Urbana-Champaign
+
+ http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimers.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimers in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the names of the LLVM Team, University of Illinois at
+ Urbana-Champaign, nor the names of its contributors may be used to
+ endorse or promote products derived from this Software without specific
+ prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+
+Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+==============================================================================
+libcxxabi used by:
+ aapt2
+
+==============================================================================
+libc++abi License
+==============================================================================
+
+The libc++abi library is dual licensed under both the University of Illinois
+"BSD-Like" license and the MIT license. As a user of this code you may choose
+to use it under either license. As a contributor, you agree to allow your code
+to be used under both.
+
+Full text of the relevant licenses is included below.
+
+==============================================================================
+
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2009-2018 by the contributors listed in CREDITS.TXT
+
+All rights reserved.
+
+Developed by:
+
+ LLVM Team
+
+ University of Illinois at Urbana-Champaign
+
+ http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimers.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimers in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the names of the LLVM Team, University of Illinois at
+ Urbana-Champaign, nor the names of its contributors may be used to
+ endorse or promote products derived from this Software without specific
+ prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+
+Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+==============================================================================
+libcxxabi used by:
+ aapt2
+
+==============================================================================
+libc++abi License
+==============================================================================
+
+The libc++abi library is dual licensed under both the University of Illinois
+"BSD-Like" license and the MIT license. As a user of this code you may choose
+to use it under either license. As a contributor, you agree to allow your code
+to be used under both.
+
+Full text of the relevant licenses is included below.
+
+==============================================================================
+
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
+
+All rights reserved.
+
+Developed by:
+
+ LLVM Team
+
+ University of Illinois at Urbana-Champaign
+
+ http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimers.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimers in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the names of the LLVM Team, University of Illinois at
+ Urbana-Champaign, nor the names of its contributors may be used to
+ endorse or promote products derived from this Software without specific
+ prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+
+Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+==============================================================================
+libpng_v1.6.40 used by:
+ aapt2
+
+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
+=========================================
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2023 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2023 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
+
+==============================================================================
+prebuilts/runtime used by:
+ aapt2
+
+system/logging used by:
+ aapt2
+
+
+ Copyright (c) 2005-2014, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+
+==============================================================================
+protobuf_v21.12 used by:
+ aapt2
+
+Copyright 2008 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it. This code is not
+standalone and requires a support library to be linked with it. This
+support library is itself covered by the above license.
+
+==============================================================================
+system/incremental_delivery used by:
+ aapt2
+
+
+ Copyright (c) 2005-2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+
+==============================================================================
+system/libbase used by:
+ aapt2
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+Copyright 2010 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==============================================================================
+zlib_v_14dd4c4455602c9b71a1a89b5cafd1f4030d2e3f used by:
+ aapt2
+
+version 1.2.12, March 27th, 2022
+
+Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
diff --git a/aapt2-8.5.0-11315950-windows/aapt2.exe b/aapt2-8.5.0-11315950-windows/aapt2.exe
new file mode 100644
index 0000000..b0232cd
Binary files /dev/null and b/aapt2-8.5.0-11315950-windows/aapt2.exe differ
diff --git a/copilot_agent_client/__pycache__/mcp_agent_loop.cpython-312.pyc b/copilot_agent_client/__pycache__/mcp_agent_loop.cpython-312.pyc
new file mode 100644
index 0000000..a6e5dce
Binary files /dev/null and b/copilot_agent_client/__pycache__/mcp_agent_loop.cpython-312.pyc differ
diff --git a/copilot_agent_client/__pycache__/pu_client.cpython-312.pyc b/copilot_agent_client/__pycache__/pu_client.cpython-312.pyc
new file mode 100644
index 0000000..2fae9d6
Binary files /dev/null and b/copilot_agent_client/__pycache__/pu_client.cpython-312.pyc differ
diff --git a/copilot_agent_client/mcp_agent_loop.py b/copilot_agent_client/mcp_agent_loop.py
index d9f93b4..ac6569c 100644
--- a/copilot_agent_client/mcp_agent_loop.py
+++ b/copilot_agent_client/mcp_agent_loop.py
@@ -27,6 +27,42 @@
import threading
+# 暂停信号文件路径 - 使用绝对路径基于项目根目录
+def get_pause_signal_file():
+ """获取暂停信号文件的绝对路径"""
+ # 从 copilot_agent_client 目录向上一级找到项目根目录
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ return os.path.join(project_root, "tmp_screenshot", "pause_signal.txt")
+
+def check_pause_signal():
+ """
+ 检查是否有外部暂停信号。
+ 如果存在暂停信号文件,删除文件并返回 True (should_pause).
+ Returns: should_pause: bool
+ """
+ pause_file = get_pause_signal_file()
+ if os.path.exists(pause_file):
+ try:
+ # 仅检测文件存在,不需要读取内容
+ os.remove(pause_file)
+ print(f"[DEBUG] 检测到暂停信号: {pause_file}")
+ return True
+ except Exception as e:
+ print(f"[WARNING] 读取/删除暂停信号文件失败: {e}")
+ return False
+ return False
+
+def clear_pause_signal():
+ """清除暂停信号文件(如果存在)"""
+ pause_file = get_pause_signal_file()
+ if os.path.exists(pause_file):
+ try:
+ os.remove(pause_file)
+ print(f"[DEBUG] 已清除暂停信号: {pause_file}")
+ except:
+ pass
+
+
def auto_reply(current_image_url, task, info_action, model_provider, model_name):
"""
Reply with information action.
@@ -266,6 +302,14 @@ def gui_agent_loop(
# restart the steps from 0, even continuing an existing session
for step_idx in range(max_steps):
+ # >>> 检查外部暂停信号 <<<
+ # >>> 检查外部暂停信号 <<<
+ should_pause = check_pause_signal()
+ if should_pause:
+ print(f"[PAUSED] 检测到暂停信号,暂停任务...")
+ stop_reason = "USER_PAUSED"
+ break
+
if not dectect_screen_on(device_id):
print("Screen is off, turn on the screen first")
stop_reason = "MANUAL_STOP_SCREEN_OFF"
@@ -313,7 +357,15 @@ def gui_agent_loop(
# assume when reply info is provided, it must be used for current step
if reply_info is not None:
print(f"Using reply from client: {reply_info}")
- payload['observation']['query'] = reply_info
+ # 增强提示,确保LLM优先执行用户干预指令,不要继续完成原任务
+ payload['observation']['query'] = f"""【紧急用户干预 - 最高优先级】
+用户要求:{reply_info}
+
+重要提示:
+1. 立即停止当前正在执行的任务
+2. 优先执行用户的新指令
+3. 不要输出 COMPLETE,除非新指令已完成
+4. 根据当前屏幕状态,执行用户的新要求"""
reply_info = None # reset after use
server_return = agent_server.automate_step(payload)
@@ -423,9 +475,11 @@ def gui_agent_loop(
return_log['intermediate_logs'] = []
pass
- if stop_reason in ['MANUAL_STOP_SCREEN_OFF', 'INFO_ACTION_NEEDS_REPLY', "NOT_STARTED"]:
+ if stop_reason in ['MANUAL_STOP_SCREEN_OFF', 'INFO_ACTION_NEEDS_REPLY', "NOT_STARTED", "USER_PAUSED"]:
+ pass
+ elif action is None:
pass
- elif action['action_type'].upper() == 'COMPLETE':
+ elif action['action_type'].upper() == 'COMPLETE':
stop_reason = "TASK_COMPLETED_SUCCESSFULLY"
elif action['action_type'].upper() == 'ABORT':
stop_reason = "TASK_ABORTED_BY_AGENT"
diff --git a/copilot_agent_server/__pycache__/__init__.cpython-312.pyc b/copilot_agent_server/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..edfdb05
Binary files /dev/null and b/copilot_agent_server/__pycache__/__init__.cpython-312.pyc differ
diff --git a/copilot_agent_server/__pycache__/base_logger.cpython-312.pyc b/copilot_agent_server/__pycache__/base_logger.cpython-312.pyc
new file mode 100644
index 0000000..850e3ec
Binary files /dev/null and b/copilot_agent_server/__pycache__/base_logger.cpython-312.pyc differ
diff --git a/copilot_agent_server/__pycache__/base_server.cpython-312.pyc b/copilot_agent_server/__pycache__/base_server.cpython-312.pyc
new file mode 100644
index 0000000..4d741ab
Binary files /dev/null and b/copilot_agent_server/__pycache__/base_server.cpython-312.pyc differ
diff --git a/copilot_agent_server/__pycache__/local_server.cpython-312.pyc b/copilot_agent_server/__pycache__/local_server.cpython-312.pyc
new file mode 100644
index 0000000..aa6c186
Binary files /dev/null and b/copilot_agent_server/__pycache__/local_server.cpython-312.pyc differ
diff --git a/copilot_agent_server/__pycache__/local_server_logger.cpython-312.pyc b/copilot_agent_server/__pycache__/local_server_logger.cpython-312.pyc
new file mode 100644
index 0000000..4aafa6f
Binary files /dev/null and b/copilot_agent_server/__pycache__/local_server_logger.cpython-312.pyc differ
diff --git a/copilot_agent_server/__pycache__/parser_factory.cpython-312.pyc b/copilot_agent_server/__pycache__/parser_factory.cpython-312.pyc
new file mode 100644
index 0000000..297dfd5
Binary files /dev/null and b/copilot_agent_server/__pycache__/parser_factory.cpython-312.pyc differ
diff --git a/copilot_agent_server/local_server.py b/copilot_agent_server/local_server.py
index cec1da6..89fac18 100644
--- a/copilot_agent_server/local_server.py
+++ b/copilot_agent_server/local_server.py
@@ -145,6 +145,12 @@ def get_envs_acts_from_logs(logs):
"frequency_penalty": 0.0,
"max_tokens": 512,
})
+
+ # 将 base_url 和 api_key 传入 args,供 ask_llm_anything 使用
+ if 'base_url' in model_config:
+ args['base_url'] = model_config['base_url']
+ if 'api_key' in model_config:
+ args['api_key'] = model_config['api_key']
image_preprocess = model_config.get('image_preprocess', None)
diff --git a/copilot_front_end/__pycache__/mobile_action_helper.cpython-312.pyc b/copilot_front_end/__pycache__/mobile_action_helper.cpython-312.pyc
new file mode 100644
index 0000000..a059dba
Binary files /dev/null and b/copilot_front_end/__pycache__/mobile_action_helper.cpython-312.pyc differ
diff --git a/copilot_front_end/__pycache__/package_map.cpython-312.pyc b/copilot_front_end/__pycache__/package_map.cpython-312.pyc
new file mode 100644
index 0000000..93e4fbc
Binary files /dev/null and b/copilot_front_end/__pycache__/package_map.cpython-312.pyc differ
diff --git a/copilot_front_end/__pycache__/package_scanner.cpython-312.pyc b/copilot_front_end/__pycache__/package_scanner.cpython-312.pyc
new file mode 100644
index 0000000..0edd02e
Binary files /dev/null and b/copilot_front_end/__pycache__/package_scanner.cpython-312.pyc differ
diff --git a/copilot_front_end/__pycache__/pu_frontend_executor.cpython-312.pyc b/copilot_front_end/__pycache__/pu_frontend_executor.cpython-312.pyc
new file mode 100644
index 0000000..aead34e
Binary files /dev/null and b/copilot_front_end/__pycache__/pu_frontend_executor.cpython-312.pyc differ
diff --git a/copilot_front_end/mobile_action_helper.py b/copilot_front_end/mobile_action_helper.py
index a8efae6..06e8cf2 100644
--- a/copilot_front_end/mobile_action_helper.py
+++ b/copilot_front_end/mobile_action_helper.py
@@ -134,9 +134,27 @@ def dectect_screen_on(device_id, print_command = False):
command = f"{adb_command} shell dumpsys display"
if print_command:
print(f"Executing command: {command}")
- result = subprocess.run(command, shell=True, capture_output=True, text=True)
- result.stdout = result.stdout.encode('utf-8').decode('utf-8')
- screen_state = local_str_grep(result.stdout, "mScreenState").strip()
+
+ # Use text=False (or capture_output=True default) to get bytes, avoiding implicit decoding errors
+ result = subprocess.run(command, shell=True, capture_output=True, text=False)
+
+ if result.stdout:
+ # Decode carefully, ignoring errors if necessary
+ # Try utf-8 first, then gbk, or just replace errors
+ try:
+ # ADB output is usually UTF-8, but on Windows shell it might get mixed.
+ # using errors='ignore' or 'replace' is safest for logging/grepping
+ output_str = result.stdout.decode('utf-8', errors='replace')
+ except Exception:
+ output_str = result.stdout.decode('gbk', errors='replace')
+ else:
+ output_str = ""
+
+ screen_state = local_str_grep(output_str, "mScreenState")
+ if screen_state:
+ screen_state = screen_state.strip()
+ else:
+ screen_state = ""
else:
command = f"{adb_command} shell dumpsys display | grep mScreenState"
if print_command:
diff --git a/copilot_front_end/package_map.py b/copilot_front_end/package_map.py
index 99401aa..f9d62bd 100644
--- a/copilot_front_end/package_map.py
+++ b/copilot_front_end/package_map.py
@@ -1,250 +1,143 @@
+"""
+应用名称 -> 包名 映射模块
+
+映射完全从 YAML 文件加载,实时读取,无需重启程序:
+1. default_package_map.yaml - 默认通用映射(项目提供的常用应用)
+2. user_package_map.yaml - 用户自定义映射(优先级最高)
+
+修改 YAML 文件后立即生效,无需重启程序。
+"""
+
+import os
import difflib
-package_name_map = {
- "天气": "com.coloros.weather2",
- "家人守护": "com.coloros.familyguard",
- "美柚": "com.lingan.seeyou",
- "百度极速版": "com.baidu.searchbox.lite",
- "58同城": "com.wuba",
- "知乎": "com.zhihu.android",
- "滴滴出行": "com.sdu.didi.psnger",
- "计算器": "com.coloros.calculator",
- "掌上生活": "com.cmbchina.ccd.pluto.cmbActivity",
- "飞猪旅行": "com.taobao.trip",
- "网易有道词典": "com.youdao.dict",
- "百度贴吧": "com.baidu.tieba",
- "腾讯新闻": "com.tencent.news",
- "饿了么": "me.ele",
- "百度输入法": "com.baidu.input",
- "优酷视频": "com.youku.phone",
- "抖音": "com.ss.android.ugc.aweme",
- "今日头条": "com.ss.android.article.news",
- "酷我音乐": "cn.kuwo.player",
- "oppo社区": "com.oppo.community",
- "夸克": "com.quark.browser",
- "邮件": "com.android.email",
- "美团": "com.sankuai.meituan",
- "剪映": "com.lemon.lv",
- "酷狗概念版": "com.kugou.android.lite",
- "酷狗音乐": "com.kugou.android",
- "网易邮箱大师": "com.netease.mail",
- "番茄免费小说": "com.dragon.read",
- "yy": "com.duowan.mobile",
- "qq": "com.tencent.mobileqq",
- "小宇宙": "app.podcast.cosmos",
- "指南针": "com.coloros.compass2",
- "oppo视频": "com.heytap.yoli",
- "天猫": "com.tmall.wireless",
- "抖音商城": "com.ss.android.ugc.livelite",
- "点淘": "com.taobao.live",
- "录音": "com.coloros.soundrecorder",
- "哔哩哔哩": "tv.danmaku.bili",
- "B站": "tv.danmaku.bili",
- "soul": "cn.soulapp.android",
- "高德地图": "com.autonavi.minimap",
- "懂车帝": "com.ss.android.auto",
- "小红书": "com.xingin.xhs",
- "咪咕视频": "com.cmcc.cmvideo",
- "拼多多": "com.xunmeng.pinduoduo",
- "微信读书": "com.tencent.weread",
- "蘑菇街": "com.mogujie",
- "大众点评": "com.dianping.v1",
- "云闪付": "com.unionpay",
- "好看视频": "com.baidu.haokan",
- "AIAgentDemo": "com.stepfun.aiagent.demo",
- "qq浏览器": "com.tencent.mtt",
- "文件管理": "com.coloros.filemanager",
- "豆瓣": "com.douban.frodo",
- "日历": "com.coloros.calendar",
- "游戏助手": "com.oplus.games",
- "网易云音乐": "com.netease.cloudmusic",
- "中国联通": "com.sinovatech.unicom.ui",
- "喜马拉雅": "com.ximalaya.ting.android",
- # "美团外卖": "com.sankuai.meituan.takeoutnew",
- "主题商店": "com.heytap.themestore",
- "飞书": "com.ss.android.lark",
- "红袖读书": "com.hongxiu.app",
- "全民K歌": "com.tencent.karaoke",
- "抖音火山版": "com.ss.android.ugc.live",
- "美图秀秀": "com.mt.mtxx.mtxx",
- "拾程旅行": "com.hnjw.shichengtravel",
- "中国电信": "com.ct.client",
- "时钟": "com.coloros.alarmclock",
- "快对": "com.kuaiduizuoye.scan",
- "钱包": "com.finshell.wallet",
- "快手极速版": "com.kuaishou.nebula",
- "文件随心开": "andes.oplus.documentsreader",
- "微博": "com.sina.weibo",
- "墨迹天气": "com.moji.mjweather",
- "kimi 智能助手": "com.moonshot.kimichat",
- "起点读书": "com.qidian.QDReader",
- "逍遥游": "com.redteamobile.roaming",
- "豆包": "com.larus.nova",
- "平安好车主": "com.pingan.carowner",
- "去哪儿旅行": "com.Qunar",
- "银联可信服务安全组件": "com.unionpay.tsmservice",
- "腾讯微视": "com.tencent.weishi",
- "网上国网": "com.sgcc.wsgw.cn",
- "作业帮": "com.baidu.homework",
- "阅读": "com.heytap.reader",
- "keep": "com.gotokeep.keep",
- "蜻蜓FM": "fm.qingting.qtradio",
- "禅定空间": "com.oneplus.brickmode",
- "腾讯地图": "com.tencent.map",
- "虎牙直播": "com.duowan.kiwi",
- "番茄畅听音乐版": "com.xs.fm.lite",
- "今日头条极速版": "com.ss.android.article.lite",
- "转转": "com.wuba.zhuanzhuan",
- "芒果TV": "com.hunantv.imgo.activity",
- "便签": "com.coloros.note",
- "UC浏览器": "com.UCMobile",
- "百度文库": "com.baidu.wenku",
- "小猿搜题": "com.fenbi.android.solar",
- "腾讯文档": "com.tencent.docs",
- "携程旅行": "ctrip.android.view",
- "wpsoffice": "cn.wps.moffice_eng",
- "哈啰": "com.jingyao.easybike",
- "中国移动": "com.greenpoint.android.mc10086.activity",
- "唯品会": "com.achievo.vipshop",
- "手机 搬家": "com.coloros.backuprestore",
- "安逸花": "com.msxf.ayh",
- "汽水音乐": "com.luna.music",
- "音乐": "com.heytap.music",
- "小猿口算": "com.fenbi.android.leo",
- "MOMO陌陌": "com.immomo.momo",
- "支付宝": "com.eg.android.AlipayGphone",
- "爱奇艺": "com.qiyi.video",
- "DataCollection": "com.example.datacollection",
- "番茄畅听": "com.xs.fm",
- "语音翻译": "com.coloros.translate",
- "文件随心开": "cn.wps.moffice.lite",
- "无线耳机": "com.oplus.melody",
- "得物": "com.shizhuang.duapp",
- "西瓜视频": "com.ss.android.article.video",
- "网易新闻": "com.netease.newsreader.activity",
- "腾讯视频": "com.tencent.qqlive",
- "淘宝特价版": "com.taobao.litetao",
- "七猫免费小说": "com.kmxs.reader",
- "自如": "com.ziroom.ziroomcustomer",
- "爱奇艺极速版": "com.qiyi.video.lite",
- "淘宝": "com.taobao.taobao",
- "斗鱼": "air.tv.douyu.android",
- "快手": "com.smile.gifmaker",
- "扫描全能王": "com.intsig.camscanner",
- "买单吧": "com.bankcomm.maidanba",
- "飞连": "com.volcengine.corplink",
- "菜鸟": "com.cainiao.wireless",
- "盒马": "com.wudaokou.hippo",
- "阿里巴巴": "com.alibaba.wireless",
- "智能家居": "com.heytap.smarthome",
- "小布指令": "com.coloros.shortcuts",
- "闲鱼": "com.taobao.idlefish",
- "游戏中心": "com.nearme.gamecenter",
- "搜狗输入法": "com.sohu.inputmethod.sogou",
- "QQ邮箱": "com.tencent.androidqqmail",
- "百度网盘": "com.baidu.netdisk",
- "QC浏览器": "com.fjhkf.gxdsmls",
- "酷安": "com.coolapk.market",
- "QQ音乐": "com.tencent.qqmusic",
- "百度": "com.baidu.searchbox",
- "抖音极速版": "com.ss.android.ugc.aweme.lite",
- "铁路12306": "com.MobileTicket",
- "OPPO商城": "com.oppo.store",
- "自由收藏": "com.coloros.favorite",
- "我的OPPO": "com.oplus.member",
- "掌阅": "com.chaozh.iReaderFree",
- "腾讯会议": "com.tencent.wemeet.app",
- "企业微信": "com.tencent.wework",
- "健康": "com.heytap.health",
- "微信": "com.tencent.mm",
- "京东": "com.jingdong.app.mall",
- "肯德基": "com.yek.android.kfc.activitys",
- "搜狐视频": "com.sohu.sohuvideo",
- "百度地图": "com.baidu.BaiduMap",
- "山姆会员商店": "cn.samsclub.app",
- "大麦": "cn.damai",
- "醒图": "com.ss.android.picshow",
- "设置": "com.android.settings",
- "王者荣耀": "com.tencent.tmgp.sgame",
- "随手记": "com.mymoney",
- "钢琴块二": "com.cmplay.tiles2_cn",
- "麦当劳": "com.mcdonalds.gma.cn",
- "寻艺": "com.vlinkage.xunyee",
- "京东到家": "com.jingdong.pdj",
- "小象超市": "com.meituan.retail.v.android",
- "京东金融": "com.jd.jrapp",
- "猫眼": "com.sankuai.movie",
- "红果免费短剧": "com.phoenix.read",
- "三角洲行动": "com.tencent.tmgp.dfm",
- "航旅纵横": "com.umetrip.android.msky.app",
- "淘票票": "com.taobao.movie.android",
- "学习强国": "cn.xuexi.android",
- "小米商城": "com.xiaomi.shop",
- "浏览器": "com.android.browser",
- "look": "com.vision.haokan",
- "什么值得买": "com.smzdm.client.android",
- "妙兜": "com.agent.miaodou",
- "瑞幸咖啡": "com.lucky.luckyclient",
- "豆瓣阅读": "com.douban.book.reader",
- "钉钉": "com.alibaba.android.rimet",
- "达美乐披萨": "com.android.permissioncontroller",
- "同程旅行": "com.tongcheng.android",
- "opentracks": "de.dennisguse.opentracks",
- "simple sms messenger": "com.simplemobiletools.smsmessenger",
- "joplin": "net.cozic.joplin",
- "miniwob": "com.google.androidenv.miniwob",
- "simple gallery pro": "com.simplemobiletools.gallery.pro",
- "simple gallery": "com.simplemobiletools.gallery.pro",
- "gallery": "com.simplemobiletools.gallery.pro",
- "audio recorder": "com.dimowner.audiorecorder",
- "broccoli": "com.flauschcode.broccoli",
- "simple calendar pro": "com.simplemobiletools.calendar.pro",
- "simple draw pro": "com.simplemobiletools.draw.pro",
- "draw": "com.simplemobiletools.draw.pro",
- "clipper": "ca.zgrs.clipper",
- "retro music": "code.name.monkey.retromusic",
- "arduia pro expense": "com.arduia.expense",
- "markor": "net.gsantner.markor",
- "tasks": "org.tasks",
- "osmAnd": "net.osmand",
- "给到": "com.guanaitong",
- "百词斩": "com.jiongji.andriod.card",
-
-}
-
-import difflib
-
-def find_package_name(app_name):
+# 获取项目根目录
+_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_DEFAULT_MAP_FILE = os.path.join(_PROJECT_ROOT, "default_package_map.yaml")
+_USER_MAP_FILE = os.path.join(_PROJECT_ROOT, "user_package_map.yaml")
+
+
+def _load_yaml_map(file_path: str) -> dict:
+ """从 YAML 文件加载映射"""
+ if not os.path.exists(file_path):
+ return {}
+
+ try:
+ import yaml
+ with open(file_path, 'r', encoding='utf-8') as f:
+ data = yaml.safe_load(f)
+ return data if isinstance(data, dict) else {}
+ except ImportError:
+ # 没有 yaml 库,使用简单解析
+ mapping = {}
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith('#') and ':' in line:
+ parts = line.split(':', 1)
+ if len(parts) == 2:
+ key = parts[0].strip().strip('"').strip("'")
+ value = parts[1].strip().strip('"').strip("'")
+ if key and value:
+ mapping[key] = value
+ return mapping
+ except Exception:
+ return {}
+ except Exception:
+ return {}
+
+
+def get_package_name_map() -> dict:
+ """
+ 实时获取合并的映射表(每次调用都重新加载)
+
+ 加载顺序:默认映射 -> 用户映射(覆盖)
+ 修改 YAML 文件后立即生效,无需重启程序
+
+ Returns:
+ 合并后的 {应用名: 包名} 映射字典
+ """
+ # 1. 加载默认映射
+ merged_map = _load_yaml_map(_DEFAULT_MAP_FILE)
+
+ # 2. 加载用户映射并合并(用户映射优先级更高)
+ user_map = _load_yaml_map(_USER_MAP_FILE)
+ merged_map.update(user_map)
+
+ return merged_map
+
+
+# 为了向后兼容,提供一个属性访问方式
+# 注意:这个变量在模块加载时只执行一次,不会实时刷新
+# 如需实时刷新,请使用 get_package_name_map() 函数
+package_name_map = get_package_name_map()
+
+
+def reload_package_name_map():
+ """
+ 刷新全局映射表(如果需要兼容旧代码)
+ """
+ global package_name_map
+ package_name_map = get_package_name_map()
+
+
+def find_package_name(app_name: str) -> str:
+ """
+ 根据应用名查找包名(实时读取 YAML)
+
+ 查找顺序:
+ 1. 精确匹配(大小写敏感)
+ 2. 小写匹配
+ 3. 模糊匹配
+
+ Args:
+ app_name: 应用名称
+
+ Returns:
+ 包名
+
+ Raises:
+ AssertionError: 找不到匹配的包名
+ """
+ # 实时加载映射表
+ current_map = get_package_name_map()
+
app_name_lowered = app_name.lower()
- package_name = package_name_map.get(app_name_lowered, None)
+ # 1. 精确匹配
+ if app_name in current_map:
+ return current_map[app_name]
+
+ # 2. 小写匹配
+ map_lowered = {k.lower(): v for k, v in current_map.items()}
+ if app_name_lowered in map_lowered:
+ return map_lowered[app_name_lowered]
+
+ # 3. 模糊匹配
max_match = {
"name": None,
"score": 0
}
- if package_name is None:
- # to search a similar app name
- for key in package_name_map.keys():
- # Use the lowercase input for comparison
- score = difflib.SequenceMatcher(None, app_name_lowered, key.lower()).ratio()
-
- if score > max_match["score"]:
- max_match["name"] = key
- max_match["score"] = score
-
- # Check if a match was found with a score > 0 (or some threshold, though the assert below only checks if name is not None)
- assert max_match['name'] is not None, f"Cannot find package name for app {app_name}"
-
- # We retrieve the actual package name using the original (correctly cased) key from the map
- package_name = package_name_map[max_match['name']]
-
- return package_name
+ for key in current_map.keys():
+ score = difflib.SequenceMatcher(None, app_name_lowered, key.lower()).ratio()
+ if score > max_match["score"]:
+ max_match["name"] = key
+ max_match["score"] = score
+
+ assert max_match['name'] is not None, f"Cannot find package name for app {app_name}"
+
+ return current_map[max_match['name']]
-def get_list_of_package_names():
+def get_list_of_package_names() -> list:
"""
- Return a list of all package names.
+ 获取所有应用映射列表(实时读取)
+
+ Returns:
+ [{"app_name": "微信", "package_name": "com.tencent.mm"}, ...]
"""
- applications = [{"app_name": app_name, "package_name": package_name} for app_name, package_name in package_name_map.items()]
- return applications
+ current_map = get_package_name_map()
+ return [{"app_name": app_name, "package_name": package_name}
+ for app_name, package_name in current_map.items()]
\ No newline at end of file
diff --git a/copilot_front_end/package_scanner.py b/copilot_front_end/package_scanner.py
new file mode 100644
index 0000000..1530ff1
--- /dev/null
+++ b/copilot_front_end/package_scanner.py
@@ -0,0 +1,666 @@
+"""
+应用包名扫描器模块
+
+功能:
+1. 通过 ADB 获取已安装应用的包名列表
+2. 获取每个应用的中文显示名称
+3. 读写 user_package_map.yaml 文件
+
+依赖: 无需手机端安装额外工具,使用系统自带命令
+"""
+
+import os
+import sys
+import subprocess
+import re
+from typing import Optional
+
+# 添加项目根目录到路径
+if "." not in sys.path:
+ sys.path.append(".")
+
+# YAML 配置文件路径(项目根目录)
+PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+USER_PACKAGE_MAP_FILE = os.path.join(PROJECT_ROOT, "user_package_map.yaml")
+
+
+def _get_adb_command(device_id: Optional[str] = None) -> str:
+ """获取 ADB 命令前缀"""
+ if device_id is None:
+ return "adb"
+ return f"adb -s {device_id}"
+
+
+def get_installed_packages(device_id: Optional[str] = None) -> list[str]:
+ """
+ 获取第三方应用包名列表
+
+ Args:
+ device_id: 设备 ID,为 None 时使用默认设备
+
+ Returns:
+ 包名列表
+ """
+ adb_cmd = _get_adb_command(device_id)
+ cmd = f"{adb_cmd} shell pm list packages -3"
+
+ try:
+ result = subprocess.run(
+ cmd, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=30
+ )
+ if result.returncode != 0:
+ print(f"[WARNING] pm list packages failed: {result.stderr}")
+ return []
+
+ packages = []
+ for line in result.stdout.splitlines():
+ line = line.strip()
+ if line.startswith("package:"):
+ pkg = line.replace("package:", "").strip()
+ if pkg:
+ packages.append(pkg)
+ return packages
+ except subprocess.TimeoutExpired:
+ print("[ERROR] get_installed_packages timeout")
+ return []
+ except Exception as e:
+ print(f"[ERROR] get_installed_packages: {e}")
+ return []
+
+
+def get_app_label_via_dumpsys(device_id: Optional[str], package_name: str) -> Optional[str]:
+ """
+ 通过 dumpsys package 获取应用名称
+
+ 尝试解析 applicationInfo 中的 labelRes 或直接获取 label
+
+ Args:
+ device_id: 设备 ID
+ package_name: 包名
+
+ Returns:
+ 应用名称,失败返回 None
+ """
+ adb_cmd = _get_adb_command(device_id)
+
+ # 方法1: 尝试使用 cmd package 获取应用信息
+ cmd = f'{adb_cmd} shell dumpsys package {package_name}'
+
+ try:
+ result = subprocess.run(
+ cmd, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=10
+ )
+
+ if result.returncode != 0:
+ return None
+
+ output = result.stdout
+
+ # 尝试从 applicationInfo 行提取 labelRes
+ # 格式: labelRes=0x7f150001 微信 nonLocalizedLabel=null
+ # 或者: labelRes=0x7f150001 nonLocalizedLabel=微信
+
+ # 查找包含 labelRes 的行
+ for line in output.splitlines():
+ if 'labelRes=' in line and 'nonLocalizedLabel=' in line:
+ # 尝试提取 nonLocalizedLabel 的值
+ match = re.search(r'nonLocalizedLabel=(\S+)', line)
+ if match:
+ label = match.group(1)
+ if label and label != 'null':
+ return label
+
+ # 尝试提取 labelRes= 和 nonLocalizedLabel= 之间的文本
+ match = re.search(r'labelRes=0x[a-fA-F0-9]+\s+(.+?)\s+nonLocalizedLabel', line)
+ if match:
+ label = match.group(1).strip()
+ if label and label != 'null':
+ return label
+
+ return None
+
+ except subprocess.TimeoutExpired:
+ return None
+ except Exception as e:
+ print(f"[WARNING] dumpsys failed for {package_name}: {e}")
+ return None
+
+
+def get_app_label_via_aapt(device_id: Optional[str], package_name: str) -> Optional[str]:
+ """
+ 通过 aapt 获取应用名称(如果设备上有 aapt)
+
+ Args:
+ device_id: 设备 ID
+ package_name: 包名
+
+ Returns:
+ 应用名称,失败返回 None
+ """
+ adb_cmd = _get_adb_command(device_id)
+
+ try:
+ # 1. 获取 APK 路径
+ cmd_path = f'{adb_cmd} shell pm path {package_name}'
+ result = subprocess.run(
+ cmd_path, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=5
+ )
+
+ if result.returncode != 0 or not result.stdout.strip():
+ return None
+
+ # 解析 APK 路径: package:/data/app/xxx/base.apk
+ apk_path = result.stdout.strip().replace("package:", "").strip()
+ if not apk_path:
+ return None
+
+ # 2. 检查设备上是否有 aapt
+ # 优先检查 /data/local/tmp/aapt (用户 push 的)
+ # 然后检查系统自带的 /system/bin/aapt
+ aapt_paths = ['/data/local/tmp/aapt', '/system/bin/aapt', 'aapt']
+ aapt_cmd = None
+
+ for aapt_path in aapt_paths:
+ check_cmd = f'{adb_cmd} shell {aapt_path} version 2>/dev/null'
+ check_result = subprocess.run(
+ check_cmd, shell=True, capture_output=True,
+ text=True, timeout=3
+ )
+ if check_result.returncode == 0:
+ aapt_cmd = aapt_path
+ break
+
+ if aapt_cmd is None:
+ return None
+
+ # 3. 使用 aapt 获取应用名
+ cmd_aapt = f'{adb_cmd} shell {aapt_cmd} dump badging "{apk_path}" 2>/dev/null'
+ result = subprocess.run(
+ cmd_aapt, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=10
+ )
+
+ if result.returncode != 0:
+ return None
+
+ # 解析 application-label 或 application-label-zh-CN
+ output = result.stdout
+
+ # 优先中文标签
+ match = re.search(r"application-label-zh[^:]*:'([^']+)'", output)
+ if match:
+ return match.group(1)
+
+ # 然后通用标签
+ match = re.search(r"application-label:'([^']+)'", output)
+ if match:
+ return match.group(1)
+
+ return None
+
+ except subprocess.TimeoutExpired:
+ return None
+ except Exception as e:
+ print(f"[WARNING] aapt failed for {package_name}: {e}")
+ return None
+
+
+def get_app_label_via_local_apk(device_id: Optional[str], package_name: str, tmp_dir: str = None) -> Optional[str]:
+ """
+ 通过拉取 APK 到本地并解析获取应用名称(最可靠的方法)
+
+ 使用 Python zipfile 解析 APK 中的资源,提取应用名称
+
+ Args:
+ device_id: 设备 ID
+ package_name: 包名
+ tmp_dir: 临时文件目录
+
+ Returns:
+ 应用名称,失败返回 None
+ """
+ import zipfile
+ import tempfile
+ import struct
+
+ adb_cmd = _get_adb_command(device_id)
+
+ if tmp_dir is None:
+ tmp_dir = tempfile.gettempdir()
+
+ local_apk_path = os.path.join(tmp_dir, f"{package_name}_temp.apk")
+
+ try:
+ # 1. 获取 APK 路径
+ cmd_path = f'{adb_cmd} shell pm path {package_name}'
+ result = subprocess.run(
+ cmd_path, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=5
+ )
+
+ if result.returncode != 0 or not result.stdout.strip():
+ return None
+
+ # 解析 APK 路径
+ apk_path = result.stdout.strip().split('\n')[0].replace("package:", "").strip()
+ if not apk_path:
+ return None
+
+ # 2. 拉取 APK 到本地
+ cmd_pull = f'{adb_cmd} pull "{apk_path}" "{local_apk_path}"'
+ result = subprocess.run(
+ cmd_pull, shell=True, capture_output=True,
+ text=True, encoding='utf-8', errors='ignore', timeout=60
+ )
+
+ if result.returncode != 0 or not os.path.exists(local_apk_path):
+ return None
+
+ # 3. 解析 APK 获取应用名
+ label = _parse_apk_label(local_apk_path)
+
+ return label
+
+ except Exception as e:
+ print(f"[WARNING] local APK parsing failed for {package_name}: {e}")
+ return None
+ finally:
+ # 清理临时文件
+ if os.path.exists(local_apk_path):
+ try:
+ os.remove(local_apk_path)
+ except:
+ pass
+def _parse_apk_label(apk_path: str) -> Optional[str]:
+ """
+ 解析 APK 文件获取应用名称
+
+ 优先使用 aapt2(能正确获取中文名),再用 androguard 作为 fallback
+ """
+ # 方法1: 优先使用 aapt2(能正确获取中文名)
+ try:
+ import shutil
+ aapt2_path = shutil.which('aapt2')
+
+ # 如果 PATH 中没有,尝试项目目录下的 aapt2
+ if not aapt2_path:
+ project_aapt2 = os.path.join(PROJECT_ROOT, 'aapt2-8.5.0-11315950-windows', 'aapt2.exe')
+ custom_paths = [
+ project_aapt2,
+ os.path.expandvars(r'%LOCALAPPDATA%\Android\Sdk\build-tools\34.0.0\aapt2.exe'),
+ os.path.expandvars(r'%LOCALAPPDATA%\Android\Sdk\build-tools\33.0.0\aapt2.exe'),
+ ]
+ for p in custom_paths:
+ if os.path.exists(p):
+ aapt2_path = p
+ break
+
+ if aapt2_path:
+ result = subprocess.run(
+ [aapt2_path, 'dump', 'badging', apk_path],
+ capture_output=True, text=True, encoding='utf-8', errors='ignore', timeout=30
+ )
+ if result.returncode == 0:
+ # 解析 application-label (优先简体中文)
+ zh_cn_label = None # 简体中文 (zh-CN, zh-Hans)
+ zh_tw_label = None # 繁体中文 (zh-TW, zh-Hant, zh-HK)
+ zh_label = None # 通用中文 (zh)
+ default_label = None
+
+ for line in result.stdout.splitlines():
+ # 优先级1: 简体中文
+ if 'application-label-zh-CN' in line or 'application-label-zh-Hans' in line:
+ match = re.search(r"application-label-zh[^:]*:'([^']+)'", line)
+ if match:
+ zh_cn_label = match.group(1)
+ # 优先级2: 繁体中文
+ elif 'application-label-zh-TW' in line or 'application-label-zh-Hant' in line or 'application-label-zh-HK' in line:
+ match = re.search(r"application-label-zh[^:]*:'([^']+)'", line)
+ if match and not zh_tw_label:
+ zh_tw_label = match.group(1)
+ # 优先级3: 通用中文
+ elif 'application-label-zh:' in line:
+ match = re.search(r"application-label-zh:'([^']+)'", line)
+ if match:
+ zh_label = match.group(1)
+ # 优先级4: 默认标签
+ elif "application-label:'" in line and not default_label:
+ match = re.search(r"application-label:'([^']+)'", line)
+ if match:
+ default_label = match.group(1)
+
+ # 按优先级返回:简体 > 繁体 > 通用中文 > 默认
+ if zh_cn_label:
+ return zh_cn_label
+ if zh_tw_label:
+ return zh_tw_label
+ if zh_label:
+ return zh_label
+ if default_label:
+ return default_label
+ except Exception:
+ pass
+
+ # 方法2: 使用 androguard(作为 fallback)
+ try:
+ from androguard.core.apk import APK
+ import logging
+ logging.getLogger('androguard').setLevel(logging.ERROR)
+
+ apk = APK(apk_path)
+ app_name = apk.get_app_name()
+ if app_name:
+ return app_name
+
+ except ImportError:
+ pass
+ except Exception:
+ pass
+
+ return None
+
+
+
+def _extract_label_from_arsc(arsc_data: bytes) -> Optional[str]:
+ """
+ 从 resources.arsc 中提取应用名称(启发式方法,不够准确)
+ """
+ try:
+ # 将字节转为字符串进行搜索
+ text = arsc_data.decode('utf-8', errors='ignore')
+
+ # 查找中文字符串(2-10个字符,较短的更可能是应用名)
+ chinese_pattern = re.compile(r'[\u4e00-\u9fff]{2,10}')
+ matches = chinese_pattern.findall(text)
+
+ if matches:
+ # 优先返回较短的匹配(应用名通常较短)
+ valid_matches = sorted([m for m in matches if 2 <= len(m) <= 6], key=len)
+ if valid_matches:
+ return valid_matches[0]
+
+ return None
+ except Exception:
+ return None
+
+
+def get_app_label(device_id: Optional[str], package_name: str, use_local_parsing: bool = True) -> str:
+ """
+ 获取应用名称(自动 fallback)
+
+ 尝试顺序:
+ 1. 从 package_map.py 反向查找(最快最可靠)
+ 2. 从用户映射查找
+ 3. dumpsys package (系统自带)
+ 4. aapt (如果设备上有)
+ 5. 本地 APK 解析(使用 aapt2,默认启用)
+ 6. 返回包名的最后一部分
+
+ Args:
+ device_id: 设备 ID
+ package_name: 包名
+ use_local_parsing: 是否使用本地 APK 解析(默认启用)
+
+ Returns:
+ 应用名称或包名
+ """
+ # 1. 优先从 package_map.py 反向查找中文名(最快最可靠)
+ try:
+ from copilot_front_end.package_map import package_name_map
+ # 反向查找:给定包名,找到对应的中文名
+ for app_name, pkg in package_name_map.items():
+ if pkg == package_name:
+ return app_name
+ except ImportError:
+ pass
+
+ # 2. 从用户映射查找
+ user_map = load_user_package_map()
+ for app_name, pkg in user_map.items():
+ if pkg == package_name:
+ return app_name
+
+ # 3. 尝试 dumpsys (系统自带,但不一定有中文名)
+ label = get_app_label_via_dumpsys(device_id, package_name)
+ if label:
+ return label
+
+ # 4. 尝试设备上的 aapt
+ label = get_app_label_via_aapt(device_id, package_name)
+ if label:
+ return label
+
+ # 5. 尝试本地 APK 解析(使用 aapt2,拉取 APK 并解析)
+ if use_local_parsing:
+ label = get_app_label_via_local_apk(device_id, package_name)
+ if label:
+ return label
+
+ # 6. fallback: 返回包名的最后一部分作为名称
+ parts = package_name.split('.')
+ return parts[-1] if parts else package_name
+
+
+def scan_device_apps(device_id: Optional[str] = None, progress_callback=None, deep_scan: bool = True) -> dict[str, str]:
+ """
+ 智能扫描设备应用,返回 {app_name: package_name} 映射
+
+ 扫描逻辑:
+ 1. 获取设备上所有第三方应用包名
+ 2. 对每个包名:
+ a. 先尝试从【用户映射 + 内置映射】反向查找中文名
+ b. 如果找不到且启用深度扫描 → 用 aapt2 获取中文名
+ c. 都失败则用包名片段
+ 3. 结果保存到 user_package_map.yaml
+
+ Args:
+ device_id: 设备 ID
+ progress_callback: 进度回调函数 callback(current, total, package_name, status)
+ status: 'mapping' (从映射匹配) 或 'parsing' (深度扫描)
+ deep_scan: 是否对未匹配应用进行深度扫描(默认启用)
+
+ Returns:
+ 应用名称到包名的映射字典
+ """
+ packages = get_installed_packages(device_id)
+ total = len(packages)
+
+ if total == 0:
+ print("[WARNING] No packages found on device")
+ return {}
+
+ # 构建合并的反向映射表:包名 -> 中文名
+ reverse_map = {}
+
+ # 1. 加载用户映射(优先级最高)
+ user_map = load_user_package_map()
+ for app_name, pkg in user_map.items():
+ reverse_map[pkg] = app_name
+
+ # 2. 加载内置映射
+ try:
+ from copilot_front_end.package_map import package_name_map
+ for app_name, pkg in package_name_map.items():
+ if pkg not in reverse_map: # 不覆盖用户映射
+ reverse_map[pkg] = app_name
+ except ImportError:
+ pass
+
+ print(f"[INFO] 合并映射表共 {len(reverse_map)} 条记录")
+
+ mapping = {}
+ matched_count = 0
+ parsed_count = 0
+
+ for i, pkg in enumerate(packages):
+ status = 'unknown'
+
+ # 1. 优先从合并映射查找
+ if pkg in reverse_map:
+ label = reverse_map[pkg]
+ status = 'mapping'
+ matched_count += 1
+ elif deep_scan:
+ # 2. 深度扫描:使用 aapt2 解析 APK
+ if progress_callback:
+ progress_callback(i + 1, total, pkg, 'parsing')
+
+ label = get_app_label_via_local_apk(device_id, pkg)
+ if label:
+ status = 'parsed'
+ parsed_count += 1
+ else:
+ # fallback 到包名片段
+ parts = pkg.split('.')
+ label = parts[-1] if parts else pkg
+ status = 'fallback'
+ else:
+ # 3. 不启用深度扫描,使用包名片段
+ parts = pkg.split('.')
+ label = parts[-1] if parts else pkg
+ status = 'fallback'
+
+ if progress_callback:
+ progress_callback(i + 1, total, pkg, status)
+
+ # 避免重复: 如果应用名已存在,添加后缀区分
+ final_label = label
+ counter = 1
+ while final_label in mapping and mapping[final_label] != pkg:
+ final_label = f"{label}_{counter}"
+ counter += 1
+
+ mapping[final_label] = pkg
+
+ print(f"[INFO] 扫描完成: 总计 {total} 个应用, "
+ f"映射匹配 {matched_count} 个, "
+ f"深度解析 {parsed_count} 个")
+
+ return mapping
+
+
+def load_user_package_map() -> dict[str, str]:
+ """
+ 加载用户自定义映射
+
+ Returns:
+ 应用名称到包名的映射字典,文件不存在返回空字典
+ """
+ if not os.path.exists(USER_PACKAGE_MAP_FILE):
+ return {}
+
+ try:
+ import yaml
+ with open(USER_PACKAGE_MAP_FILE, 'r', encoding='utf-8') as f:
+ data = yaml.safe_load(f)
+ return data if isinstance(data, dict) else {}
+ except ImportError:
+ # 如果没有安装 yaml,使用简单的解析方式
+ mapping = {}
+ try:
+ with open(USER_PACKAGE_MAP_FILE, 'r', encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith('#') and ':' in line:
+ parts = line.split(':', 1)
+ if len(parts) == 2:
+ key = parts[0].strip().strip('"').strip("'")
+ value = parts[1].strip().strip('"').strip("'")
+ if key and value:
+ mapping[key] = value
+ return mapping
+ except Exception as e:
+ print(f"[WARNING] Failed to parse user_package_map.yaml: {e}")
+ return {}
+ except Exception as e:
+ print(f"[WARNING] Failed to load user_package_map.yaml: {e}")
+ return {}
+
+
+def save_user_package_map(mapping: dict[str, str]) -> bool:
+ """
+ 保存用户自定义映射
+
+ Args:
+ mapping: 应用名称到包名的映射字典
+
+ Returns:
+ 是否保存成功
+ """
+ try:
+ import yaml
+ with open(USER_PACKAGE_MAP_FILE, 'w', encoding='utf-8') as f:
+ f.write("# 用户自定义应用名称 -> 包名映射\n")
+ f.write("# 此文件由扫描功能自动生成,也可手动编辑\n")
+ f.write("# 优先级高于 package_map.py 中的默认映射\n\n")
+ yaml.dump(mapping, f, allow_unicode=True, sort_keys=False, default_flow_style=False)
+ return True
+ except ImportError:
+ # 如果没有安装 yaml,使用简单的格式
+ try:
+ with open(USER_PACKAGE_MAP_FILE, 'w', encoding='utf-8') as f:
+ f.write("# 用户自定义应用名称 -> 包名映射\n")
+ f.write("# 此文件由扫描功能自动生成,也可手动编辑\n")
+ f.write("# 优先级高于 package_map.py 中的默认映射\n\n")
+ for app_name, pkg_name in mapping.items():
+ # 如果名称包含特殊字符,加引号
+ if ':' in app_name or '#' in app_name:
+ f.write(f'"{app_name}": {pkg_name}\n')
+ else:
+ f.write(f'{app_name}: {pkg_name}\n')
+ return True
+ except Exception as e:
+ print(f"[ERROR] Failed to save user_package_map.yaml: {e}")
+ return False
+ except Exception as e:
+ print(f"[ERROR] Failed to save user_package_map.yaml: {e}")
+ return False
+
+
+def merge_scan_result(new_mapping: dict[str, str]) -> dict[str, str]:
+ """
+ 将扫描结果合并到用户映射(不覆盖现有条目)
+
+ Args:
+ new_mapping: 新扫描的映射
+
+ Returns:
+ 合并后的映射
+ """
+ existing = load_user_package_map()
+
+ # 新映射中不覆盖已存在的条目
+ for app_name, pkg_name in new_mapping.items():
+ if app_name not in existing:
+ existing[app_name] = pkg_name
+
+ # 保存合并后的映射
+ save_user_package_map(existing)
+
+ return existing
+
+
+def get_user_package_map_path() -> str:
+ """获取用户映射文件路径"""
+ return USER_PACKAGE_MAP_FILE
+
+
+# 测试代码
+if __name__ == "__main__":
+ print("=== 扫描设备应用 ===")
+
+ def progress(current, total, pkg):
+ print(f"[{current}/{total}] 正在扫描: {pkg}")
+
+ apps = scan_device_apps(progress_callback=progress)
+
+ print(f"\n=== 扫描结果 ({len(apps)} 个应用) ===")
+ for name, pkg in sorted(apps.items()):
+ print(f" {name}: {pkg}")
+
+ print(f"\n=== 保存到 {USER_PACKAGE_MAP_FILE} ===")
+ merged = merge_scan_result(apps)
+ print(f"合并后共 {len(merged)} 条映射")
diff --git a/copilot_tools/__pycache__/parser_0920_summary.cpython-312.pyc b/copilot_tools/__pycache__/parser_0920_summary.cpython-312.pyc
new file mode 100644
index 0000000..f302ab9
Binary files /dev/null and b/copilot_tools/__pycache__/parser_0920_summary.cpython-312.pyc differ
diff --git a/copilot_tools/parser_0920_summary.py b/copilot_tools/parser_0920_summary.py
index 47e7970..26d53f5 100644
--- a/copilot_tools/parser_0920_summary.py
+++ b/copilot_tools/parser_0920_summary.py
@@ -27,6 +27,7 @@
1. 你需要明确记录自己上一次的action,如果是滑动,不能超过5次。
2. 你需要严格遵循用户的指令,如果你和用户进行过对话,需要更遵守最后一轮的指令
+3. 如果你连续向同一个方向滑动2-3次但屏幕内容没有明显变化(可能已经到达边界),你应该尝试向相反的方向滑动。注意:不同的App和窗口中,查看历史记录或更多内容的滑动方向可能不同(有的需要向上滑动,有的需要向下滑动),请根据实际屏幕反馈灵活调整滑动方向。
# Action Space:
diff --git a/default_package_map.yaml b/default_package_map.yaml
new file mode 100644
index 0000000..fcff61e
--- /dev/null
+++ b/default_package_map.yaml
@@ -0,0 +1,210 @@
+# 默认应用名称 -> 包名映射
+#
+# 此文件包含市面上常用应用的中文名→包名映射
+# 用户可以编辑此文件添加/修改映射
+# 扫描时会优先使用此映射,匹配不到的才进行深度扫描
+#
+# 格式: 应用中文名: 包名
+#
+
+# ========== 社交通讯 ==========
+微信: com.tencent.mm
+QQ: com.tencent.mobileqq
+微博: com.sina.weibo
+钉钉: com.alibaba.android.rimet
+飞书: com.ss.android.lark
+企业微信: com.tencent.wework
+MOMO陌陌: com.immomo.momo
+soul: cn.soulapp.android
+
+# ========== 短视频/直播 ==========
+抖音: com.ss.android.ugc.aweme
+抖音极速版: com.ss.android.ugc.aweme.lite
+抖音火山版: com.ss.android.ugc.live
+抖音商城: com.ss.android.ugc.livelite
+快手: com.smile.gifmaker
+快手极速版: com.kuaishou.nebula
+小红书: com.xingin.xhs
+哔哩哔哩: tv.danmaku.bili
+B站: tv.danmaku.bili
+西瓜视频: com.ss.android.article.video
+虎牙直播: com.duowan.kiwi
+斗鱼: air.tv.douyu.android
+腾讯微视: com.tencent.weishi
+
+# ========== 视频播放 ==========
+腾讯视频: com.tencent.qqlive
+爱奇艺: com.qiyi.video
+爱奇艺极速版: com.qiyi.video.lite
+优酷视频: com.youku.phone
+芒果TV: com.hunantv.imgo.activity
+咪咕视频: com.cmcc.cmvideo
+搜狐视频: com.sohu.sohuvideo
+好看视频: com.baidu.haokan
+
+# ========== 音乐音频 ==========
+QQ音乐: com.tencent.qqmusic
+网易云音乐: com.netease.cloudmusic
+酷狗音乐: com.kugou.android
+酷狗概念版: com.kugou.android.lite
+酷我音乐: cn.kuwo.player
+汽水音乐: com.luna.music
+喜马拉雅: com.ximalaya.ting.android
+蜻蜓FM: fm.qingting.qtradio
+番茄畅听: com.xs.fm
+番茄畅听音乐版: com.xs.fm.lite
+全民K歌: com.tencent.karaoke
+小宇宙: app.podcast.cosmos
+
+# ========== 电商购物 ==========
+淘宝: com.taobao.taobao
+淘宝特价版: com.taobao.litetao
+天猫: com.tmall.wireless
+京东: com.jingdong.app.mall
+京东到家: com.jingdong.pdj
+京东金融: com.jd.jrapp
+拼多多: com.xunmeng.pinduoduo
+唯品会: com.achievo.vipshop
+闲鱼: com.taobao.idlefish
+蘑菇街: com.mogujie
+得物: com.shizhuang.duapp
+转转: com.wuba.zhuanzhuan
+什么值得买: com.smzdm.client.android
+
+# ========== 外卖/生活服务 ==========
+美团: com.sankuai.meituan
+淘宝闪购: me.ele
+盒马: com.wudaokou.hippo
+小象超市: com.meituan.retail.v.android
+大众点评: com.dianping.v1
+菜鸟: com.cainiao.wireless
+菜鸟裹裹: com.cainiao.wireless
+肯德基: com.yek.android.kfc.activitys
+麦当劳: com.mcdonalds.gma.cn
+瑞幸咖啡: com.lucky.luckyclient
+山姆会员商店: cn.samsclub.app
+
+# ========== 出行旅游 ==========
+高德地图: com.autonavi.minimap
+百度地图: com.baidu.BaiduMap
+腾讯地图: com.tencent.map
+滴滴出行: com.sdu.didi.psnger
+哈啰: com.jingyao.easybike
+携程旅行: ctrip.android.view
+飞猪旅行: com.taobao.trip
+去哪儿旅行: com.Qunar
+同程旅行: com.tongcheng.android
+铁路12306: com.MobileTicket
+航旅纵横: com.umetrip.android.msky.app
+
+# ========== 新闻资讯 ==========
+今日头条: com.ss.android.article.news
+今日头条极速版: com.ss.android.article.lite
+腾讯新闻: com.tencent.news
+网易新闻: com.netease.newsreader.activity
+百度: com.baidu.searchbox
+百度极速版: com.baidu.searchbox.lite
+懂车帝: com.ss.android.auto
+
+# ========== 阅读学习 ==========
+微信读书: com.tencent.weread
+掌阅: com.chaozh.iReaderFree
+起点读书: com.qidian.QDReader
+番茄免费小说: com.dragon.read
+七猫免费小说: com.kmxs.reader
+红袖读书: com.hongxiu.app
+红果免费短剧: com.phoenix.read
+豆瓣阅读: com.douban.book.reader
+百度文库: com.baidu.wenku
+网易有道词典: com.youdao.dict
+百词斩: com.jiongji.andriod.card
+作业帮: com.baidu.homework
+快对: com.kuaiduizuoye.scan
+小猿搜题: com.fenbi.android.solar
+小猿口算: com.fenbi.android.leo
+学习强国: cn.xuexi.android
+
+# ========== 支付/金融 ==========
+支付宝: com.eg.android.AlipayGphone
+云闪付: com.unionpay
+招商银行: cmbchina.ccd.pluto.cmbActivity
+买单吧: com.bankcomm.maidanba
+掌上生活: com.cmbchina.ccd.pluto.cmbActivity
+
+# ========== 通讯运营商 ==========
+中国移动: com.greenpoint.android.mc10086.activity
+中国联通: com.sinovatech.unicom.ui
+中国电信: com.ct.client
+
+# ========== 工具效率 ==========
+百度网盘: com.baidu.netdisk
+夸克: com.quark.browser
+UC浏览器: com.UCMobile
+QQ浏览器: com.tencent.mtt
+搜狗输入法: com.sohu.inputmethod.sogou
+百度输入法: com.baidu.input
+wpsoffice: cn.wps.moffice_eng
+腾讯文档: com.tencent.docs
+扫描全能王: com.intsig.camscanner
+Keep: com.gotokeep.keep
+邮件: com.android.email
+网易邮箱大师: com.netease.mail
+QQ邮箱: com.tencent.androidqqmail
+腾讯会议: com.tencent.wemeet.app
+随手记: com.mymoney
+墨迹天气: com.moji.mjweather
+
+# ========== 娱乐游戏 ==========
+王者荣耀: com.tencent.tmgp.sgame
+三角洲行动: com.tencent.tmgp.dfm
+钢琴块二: com.cmplay.tiles2_cn
+
+# ========== 图片视频编辑 ==========
+美图秀秀: com.mt.mtxx.mtxx
+剪映: com.lemon.lv
+醒图: com.ss.android.picshow
+
+# ========== 房产租房 ==========
+自如: com.ziroom.ziroomcustomer
+58同城: com.wuba
+
+# ========== 票务娱乐 ==========
+猫眼: com.sankuai.movie
+淘票票: com.taobao.movie.android
+大麦: cn.damai
+
+# ========== 社区/兴趣 ==========
+知乎: com.zhihu.android
+豆瓣: com.douban.frodo
+酷安: com.coolapk.market
+百度贴吧: com.baidu.tieba
+腾讯漫画: com.qq.ac.android
+
+# ========== AI 应用 ==========
+豆包: com.larus.nova
+kimi智能助手: com.moonshot.kimichat
+
+# ========== 品牌商城 ==========
+小米商城: com.xiaomi.shop
+OPPO商城: com.oppo.store
+
+# ========== 其他常用 ==========
+阿里巴巴: com.alibaba.wireless
+妙兜: com.agent.miaodou
+给到: com.guanaitong
+平安好车主: com.pingan.carowner
+网上国网: com.sgcc.wsgw.cn
+银联可信服务安全组件: com.unionpay.tsmservice
+YY: com.duowan.mobile
+
+# ========== 系统/通用应用 ==========
+设置: com.android.settings
+浏览器: com.android.browser
+日历: com.coloros.calendar
+计算器: com.coloros.calculator
+时钟: com.coloros.alarmclock
+天气: com.coloros.weather2
+指南针: com.coloros.compass2
+录音: com.coloros.soundrecorder
+便签: com.coloros.note
+文件管理: com.coloros.filemanager
diff --git a/examples/run_single_task.py b/examples/run_single_task.py
index c3720c2..ca6c716 100644
--- a/examples/run_single_task.py
+++ b/examples/run_single_task.py
@@ -63,49 +63,211 @@ def timed_automate_step(payload):
server_instance.automate_step = timed_automate_step
if __name__ == "__main__":
+ import argparse
- # task = "打开微信,给柏茗,发helloworld"
- # task = "打开 给到 app,在主页,下滑寻找,员工权益-奋斗食代,帮我领劵。如果不能领取就退出。"
- # task = "open wechat to send a message 'helloworld' to 'TKJ'"
- #task = "去淘宝帮我买本书"
- if len(sys.argv) < 2:
+ parser = argparse.ArgumentParser(description="Run a single task solely.")
+ parser.add_argument("task", type=str, nargs='?', help="The task description.")
+ parser.add_argument("--device-id", type=str, help="The device ID to use.")
+ parser.add_argument("--model", type=str, default="gelab-zero-4b-preview", help="Model name.")
+ parser.add_argument("--base-url", type=str, help="Base URL for the model API.")
+ parser.add_argument("--api-key", type=str, help="API Key for the model.")
+ parser.add_argument("--continue-session", type=str, help="Continue an existing session by session ID.")
+ parser.add_argument("--injection", type=str, help="User injection command to modify task direction.")
+
+ args = parser.parse_args()
+
+ # 检查是否是继续模式
+ is_continue_mode = args.continue_session is not None
+
+ if not args.task and not is_continue_mode:
print("❌ 错误:未传入任务参数!")
print("📝 使用方法:")
- print(f" python {sys.argv[0]} \"你的任务描述\"")
+ print(f" python {sys.argv[0]} \"你的任务描述\" [options]")
print(" 示例1:python script.py \"去淘宝帮我买本书\"")
- print(" 示例2:python script.py \"打开微信,给柏茗发helloworld\"")
- sys.exit(1)
-
- task = ' '.join(sys.argv[1:])
+ print(" 示例2:python script.py \"打开微信,给柏茗发helloworld\" --device-id 123456")
+ print(f" 示例3:python script.py --continue-session --injection \"修正指令\"")
+ sys.exit(1)
+
+ task = args.task # May be None in continue mode
+
+ # Use provided device_id or find the first available one
+ if args.device_id:
+ device_id = args.device_id
+ # Verify device is connected
+ available_devices = list_devices()
+ if device_id not in available_devices:
+ print(f"Warning: Device {device_id} not found in connected devices: {available_devices}")
+ else:
+ devices = list_devices()
+ if not devices:
+ print("❌ Error: No devices connected.")
+ sys.exit(1)
+ device_id = devices[0]
+ print(f"Auto-selected device: {device_id}")
- # The device ID you want to use
- device_id = list_devices()[0]
device_wm_size = get_device_wm_size(device_id)
device_info = {
"device_id": device_id,
"device_wm_size": device_wm_size
}
-
+ # Update model configuration based on arguments
+ tmp_rollout_config = local_model_config.copy()
+ if args.model:
+ tmp_rollout_config["model_config"]["model_name"] = args.model
+
+ if args.base_url or args.api_key:
+ # Switch provider to openai if URL/Key provided, or keep local if just overriding local params?
+ # Assuming if URL is provided, we might want to treat it as an OpenAI-compatible endpoint
+ # BUT for now, let's just inject these into args or model_config if the backend supports it.
+ # Looking at local_server.py might be needed to see how it handles base_url/api_key.
+ # For 'local' provider, it might not use them. Let's assume user knows what they are doing.
+ # If it is 'custom' or 'openai', provider might need to change.
+ # FOR NOW: We just update the 'args' or specific keys if the server class supports it.
+
+ # NOTE: The current LocalServer implementation details are not fully visible here.
+ # But commonly these are passed in model_config.
+ if args.base_url:
+ tmp_rollout_config["model_config"]["base_url"] = args.base_url
+ if args.api_key:
+ tmp_rollout_config["model_config"]["api_key"] = args.api_key
+
+ # If external URL is used, we might need to change provider from 'local' to 'openai' or similar if logic dictates
+ if args.base_url and "local" in tmp_rollout_config["model_config"]["model_provider"]:
+ # Heuristic: if base_url is set, it's likely not just 'local' weights but an invalidference server
+ pass
+
+ # Ensure log directories exist
+ if "log_dir" in tmp_server_config and not os.path.exists(tmp_server_config["log_dir"]):
+ os.makedirs(tmp_server_config["log_dir"], exist_ok=True)
+ if "image_dir" in tmp_server_config and not os.path.exists(tmp_server_config["image_dir"]):
+ os.makedirs(tmp_server_config["image_dir"], exist_ok=True)
- tmp_rollout_config = local_model_config
+ # Use tmp_server_config for LocalServer initialization as it expects log_dir etc.
l2_server = LocalServer(tmp_server_config)
# 注入计时逻辑
wrap_automate_step_with_timing(l2_server)
+
# 执行任务并计总时间
total_start = time.time()
- # Disable auto reply
- evaluate_task_on_device(l2_server, device_info, task, tmp_rollout_config, reflush_app=True)
+
+ # 使用 gui_agent_loop 支持暂停/继续
+ from copilot_agent_client.mcp_agent_loop import gui_agent_loop, clear_pause_signal
+
+ # 清除可能存在的旧暂停信号
+ clear_pause_signal()
+
+ if is_continue_mode:
+ # 继续已有 session
+ continue_session_id = args.continue_session
+ injection_text = args.injection or ""
+
+ print(f"[CONTINUE] 继续 Session: {continue_session_id}")
+ if injection_text:
+ print(f"[INJECTION] 用户注入指令: {injection_text}")
+ print(f"Device: {device_id}")
+ print(f"Model: {tmp_rollout_config['model_config']['model_name']}")
+
+ result = gui_agent_loop(
+ agent_server=l2_server,
+ agent_loop_config=tmp_rollout_config,
+ device_id=device_id,
+ max_steps=tmp_rollout_config.get('max_steps', 400),
+ reply_mode="pass_to_client",
+ session_id=continue_session_id,
+ reply_from_client=injection_text if injection_text else None,
+ )
+ else:
+ # 新任务
+ print(f"Starting task: {task}")
+ print(f"Device: {device_id}")
+ print(f"Model: {tmp_rollout_config['model_config']['model_name']}")
+
+ result = gui_agent_loop(
+ agent_server=l2_server,
+ agent_loop_config=tmp_rollout_config,
+ device_id=device_id,
+ max_steps=tmp_rollout_config.get('max_steps', 400),
+ reply_mode="pass_to_client",
+ task=task,
+ )
+
+ # 暂停/继续循环
+ total_steps = result.get('global_step_idx', 0)
+ while True:
+ stop_reason = result.get('stop_reason')
+
+ # 情况1: 用户手动暂停
+ if stop_reason == 'USER_PAUSED':
+ print("\n[PAUSED] 任务已暂停。请在 Web UI 输入补充信息并点击 [执行/回复] 继续...")
+ # 关键:这里阻塞等待 Web UI 发送输入
+ # 输入格式约定: "__PAUSE_INPUT__:用户实际输入的文本"
+ # Web UI 需要发送这个前缀,或者我们直接接受任何输入
+ user_input = input("WAITING_FOR_INPUT")
+
+ print(f"[RESUME] 收到补充信息: {user_input}")
+
+ remaining_steps = tmp_rollout_config.get('max_steps', 400) - total_steps
+ if remaining_steps <= 0:
+ print("[WARNING] 已达到最大步数限制")
+ break
+
+ result = gui_agent_loop(
+ agent_server=l2_server,
+ agent_loop_config=tmp_rollout_config,
+ device_id=device_id,
+ max_steps=remaining_steps,
+ reply_mode="pass_to_client",
+ session_id=result['session_id'], # 继续会话
+ reply_from_client=user_input, # 注入补充信息
+ )
+ total_steps = result.get('global_step_idx', total_steps)
+ continue
+
+ # 情况2: 之前的逻辑 (USER_PAUSED_WITH_NEW_PROMPT 已弃用)
+ elif stop_reason == 'USER_PAUSED_WITH_NEW_PROMPT':
+ # 兼容旧逻辑,但不应该再走到这里
+ pass
+
+ # 其他情况:INFO需要回复,或者任务结束
+ break
+
+ # original loop for INFO action handling
+ while result.get('stop_reason') == 'INFO_ACTION_NEEDS_REPLY':
+ info_action = result.get('final_action', {}).get('agent_action', {})
+ print(f"\n[INFO] Agent 询问: {info_action.get('value', '未知问题')}")
+ print("请在 Web UI 中回复或输入回复内容:")
+
+ # 确保 WAITING_FOR_INPUT 被刷新输出,让 Web UI 能检测到
+ print("WAITING_FOR_INPUT", flush=True)
+ import sys
+ sys.stdout.flush()
+ reply_info = input("")
+
+ remaining_steps = tmp_rollout_config.get('max_steps', 400) - total_steps
+ if remaining_steps <= 0:
+ print("[WARNING] 已达到最大步数限制")
+ break
+
+ result = gui_agent_loop(
+ agent_server=l2_server,
+ agent_loop_config=tmp_rollout_config,
+ device_id=device_id,
+ max_steps=remaining_steps,
+ reply_mode="pass_to_client",
+ session_id=result['session_id'],
+ reply_from_client=reply_info,
+ )
+ total_steps = result.get('global_step_idx', total_steps)
+
+ # 检查是否又被暂停了
+ if result.get('stop_reason') == 'USER_PAUSED_WITH_NEW_PROMPT':
+ continue # 继续外层的暂停/继续循环
+
total_time = time.time() - total_start
# 在最后加一行总时间
print(f"总计执行时间为 {total_time} 秒")
-
- pass
- # Enable auto reply
- # evaluate_task_on_device(l2_server, device_info, task, tmp_rollout_config, reflush_app=True, auto_reply=True)
-
-
+ print(f"最终状态: {result.get('stop_reason', 'UNKNOWN')}")
- pass
diff --git a/model_config.yaml b/model_config.yaml
index 97f066f..96a1e67 100644
--- a/model_config.yaml
+++ b/model_config.yaml
@@ -1,7 +1,11 @@
-local:
+local:
+ display_name: "本地模型 (Ollama)"
api_base: "http://localhost:11434/v1"
api_key: "EMPTY"
+ default_model: "gelab-zero-4b-preview"
stepfun:
+ display_name: "阶跃星辰 (StepFun)"
api_base: "https://api.stepfun.com/v1"
- api_key: "EMPTY"
\ No newline at end of file
+ api_key: ""
+ default_model: "step-gui"
diff --git a/requirements.txt b/requirements.txt
index bdb38e0..ae9505c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,3 +17,4 @@ tqdm
requests
fastmcp
+gradio
\ No newline at end of file
diff --git a/scrcpy-win64-v3.3.3/AdbWinApi.dll b/scrcpy-win64-v3.3.3/AdbWinApi.dll
new file mode 100644
index 0000000..1da794e
Binary files /dev/null and b/scrcpy-win64-v3.3.3/AdbWinApi.dll differ
diff --git a/scrcpy-win64-v3.3.3/AdbWinUsbApi.dll b/scrcpy-win64-v3.3.3/AdbWinUsbApi.dll
new file mode 100644
index 0000000..7f75aec
Binary files /dev/null and b/scrcpy-win64-v3.3.3/AdbWinUsbApi.dll differ
diff --git a/scrcpy-win64-v3.3.3/SDL2.dll b/scrcpy-win64-v3.3.3/SDL2.dll
new file mode 100644
index 0000000..82d2b1a
Binary files /dev/null and b/scrcpy-win64-v3.3.3/SDL2.dll differ
diff --git a/scrcpy-win64-v3.3.3/adb.exe b/scrcpy-win64-v3.3.3/adb.exe
new file mode 100644
index 0000000..34a0fd2
Binary files /dev/null and b/scrcpy-win64-v3.3.3/adb.exe differ
diff --git a/scrcpy-win64-v3.3.3/avcodec-61.dll b/scrcpy-win64-v3.3.3/avcodec-61.dll
new file mode 100644
index 0000000..a44b6c6
Binary files /dev/null and b/scrcpy-win64-v3.3.3/avcodec-61.dll differ
diff --git a/scrcpy-win64-v3.3.3/avformat-61.dll b/scrcpy-win64-v3.3.3/avformat-61.dll
new file mode 100644
index 0000000..347e657
Binary files /dev/null and b/scrcpy-win64-v3.3.3/avformat-61.dll differ
diff --git a/scrcpy-win64-v3.3.3/avutil-59.dll b/scrcpy-win64-v3.3.3/avutil-59.dll
new file mode 100644
index 0000000..9b847e3
Binary files /dev/null and b/scrcpy-win64-v3.3.3/avutil-59.dll differ
diff --git a/scrcpy-win64-v3.3.3/icon.png b/scrcpy-win64-v3.3.3/icon.png
new file mode 100644
index 0000000..b96a1af
Binary files /dev/null and b/scrcpy-win64-v3.3.3/icon.png differ
diff --git a/scrcpy-win64-v3.3.3/libusb-1.0.dll b/scrcpy-win64-v3.3.3/libusb-1.0.dll
new file mode 100644
index 0000000..b36c945
Binary files /dev/null and b/scrcpy-win64-v3.3.3/libusb-1.0.dll differ
diff --git a/scrcpy-win64-v3.3.3/open_a_terminal_here.bat b/scrcpy-win64-v3.3.3/open_a_terminal_here.bat
new file mode 100644
index 0000000..24d557f
--- /dev/null
+++ b/scrcpy-win64-v3.3.3/open_a_terminal_here.bat
@@ -0,0 +1 @@
+@cmd
diff --git a/scrcpy-win64-v3.3.3/scrcpy-console.bat b/scrcpy-win64-v3.3.3/scrcpy-console.bat
new file mode 100644
index 0000000..877c7f3
--- /dev/null
+++ b/scrcpy-win64-v3.3.3/scrcpy-console.bat
@@ -0,0 +1,2 @@
+@echo off
+scrcpy.exe --pause-on-exit=if-error %*
diff --git a/scrcpy-win64-v3.3.3/scrcpy-noconsole.vbs b/scrcpy-win64-v3.3.3/scrcpy-noconsole.vbs
new file mode 100644
index 0000000..7a1c579
--- /dev/null
+++ b/scrcpy-win64-v3.3.3/scrcpy-noconsole.vbs
@@ -0,0 +1,7 @@
+strCommand = "cmd /c scrcpy.exe"
+
+For Each Arg In WScript.Arguments
+ strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """"
+Next
+
+CreateObject("Wscript.Shell").Run strCommand, 0, false
diff --git a/scrcpy-win64-v3.3.3/scrcpy-server b/scrcpy-win64-v3.3.3/scrcpy-server
new file mode 100644
index 0000000..b36f14d
Binary files /dev/null and b/scrcpy-win64-v3.3.3/scrcpy-server differ
diff --git a/scrcpy-win64-v3.3.3/scrcpy.exe b/scrcpy-win64-v3.3.3/scrcpy.exe
new file mode 100644
index 0000000..496ec24
Binary files /dev/null and b/scrcpy-win64-v3.3.3/scrcpy.exe differ
diff --git a/scrcpy-win64-v3.3.3/swresample-5.dll b/scrcpy-win64-v3.3.3/swresample-5.dll
new file mode 100644
index 0000000..49d9cb0
Binary files /dev/null and b/scrcpy-win64-v3.3.3/swresample-5.dll differ
diff --git a/start_web_ui.py b/start_web_ui.py
new file mode 100644
index 0000000..2288425
--- /dev/null
+++ b/start_web_ui.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""
+启动AutoGLM Web UI
+"""
+
+import os
+import sys
+import subprocess
+
+def check_dependencies():
+ """检查依赖是否安装"""
+ try:
+ import gradio as gr
+ print("OK - Gradio已安装")
+ except ImportError:
+ print("ERROR - Gradio未安装,请运行: pip install -r requirements.txt")
+ return False
+
+ try:
+ import PIL
+ print("OK - Pillow已安装")
+ except ImportError:
+ print("ERROR - Pillow未安装,请运行: pip install -r requirements.txt")
+ return False
+
+ return True
+
+def main():
+ print("启动AutoGLM Web UI...")
+
+ # 检查依赖
+ if not check_dependencies():
+ sys.exit(1)
+
+ # 检查ADB
+ print("检查ADB连接...")
+ try:
+ result = subprocess.run(["adb", "version"], capture_output=True, text=True, timeout=5)
+ if result.returncode == 0:
+ print("OK - ADB已安装")
+ else:
+ print("WARNING - ADB未正确安装,请确保ADB已添加到系统PATH")
+ except:
+ print("WARNING - ADB未找到,请确保ADB已安装并添加到系统PATH")
+
+ # 检查web_ui目录是否存在
+ web_ui_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web_ui")
+ if not os.path.exists(web_ui_dir):
+ print("ERROR - web_ui目录不存在")
+ sys.exit(1)
+
+ # 将web_ui目录添加到Python路径
+ sys.path.insert(0, web_ui_dir)
+
+ # 启动Gradio应用
+ print("\n正在启动Web界面...")
+ print("正在尝试可用端口...")
+
+ # 强制尝试清理端口 8866
+ target_port = 8866
+ print(f"\n检查端口 {target_port} 占用情况...")
+ try:
+ # Windows下查找占用端口的进程
+ cmd_find = f"netstat -ano | findstr :{target_port}"
+ result = subprocess.run(cmd_find, shell=True, capture_output=True, text=True)
+
+ if result.stdout:
+ lines = result.stdout.strip().split('\n')
+ for line in lines:
+ parts = line.split()
+ if len(parts) >= 5:
+ pid = parts[-1]
+ try:
+ # 排除自身进程
+ if int(pid) != os.getpid():
+ print(f"发现占用端口 {target_port} 的进程 PID: {pid},正在尝试终止...")
+ subprocess.run(f"taskkill /F /PID {pid}", shell=True, capture_output=True)
+ print(f"进程 {pid} 已终止")
+ except ValueError:
+ pass
+ except Exception as e:
+ print(f"清理端口时出错: {e}")
+
+ try:
+ from app import create_ui
+
+ demo, css, head = create_ui()
+
+ print(f"访问地址: http://localhost:{target_port}")
+ demo.launch(
+ server_name="0.0.0.0",
+ server_port=target_port,
+ share=False,
+ inbrowser=True,
+ show_error=True,
+ quiet=False,
+ css=css,
+ head=head
+ )
+ except Exception as e:
+ print(f"ERROR - 启动失败: {e}")
+ print("请确保已安装所有依赖:pip install -r requirements.txt")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/tools/__pycache__/ask_llm_v2.cpython-312.pyc b/tools/__pycache__/ask_llm_v2.cpython-312.pyc
new file mode 100644
index 0000000..d58f586
Binary files /dev/null and b/tools/__pycache__/ask_llm_v2.cpython-312.pyc differ
diff --git a/tools/__pycache__/image_tools.cpython-312.pyc b/tools/__pycache__/image_tools.cpython-312.pyc
new file mode 100644
index 0000000..01fc26e
Binary files /dev/null and b/tools/__pycache__/image_tools.cpython-312.pyc differ
diff --git a/tools/ask_llm_v2.py b/tools/ask_llm_v2.py
index 3dcc35a..91ee654 100644
--- a/tools/ask_llm_v2.py
+++ b/tools/ask_llm_v2.py
@@ -18,17 +18,25 @@ def ask_llm_anything(model_provider, model_name, messages, args= {
"frequency_penalty": 0.0,
}, resize_config=None):
- with smart_open("model_config.yaml", "r") as f:
- model_config = yaml.safe_load(f)
+ # 优先使用 args 中传入的 api_base 和 api_key
+ override_api_base = args.get("api_base") or args.get("base_url")
+ override_api_key = args.get("api_key")
-
- if model_provider in model_config:
- openai.api_base = model_config[model_provider]["api_base"]
- openai.api_key = model_config[model_provider]["api_key"]
-
-
+ if override_api_base and override_api_key:
+ # 使用传入的配置
+ openai.api_base = override_api_base
+ openai.api_key = override_api_key
+ print(f"[LLM] Using override config: {override_api_base}")
else:
- raise ValueError(f"Unknown model provider: {model_provider}")
+ # 从配置文件读取
+ with smart_open("model_config.yaml", "r") as f:
+ model_config = yaml.safe_load(f)
+
+ if model_provider in model_config:
+ openai.api_base = model_config[model_provider]["api_base"]
+ openai.api_key = model_config[model_provider]["api_key"]
+ else:
+ raise ValueError(f"Unknown model provider: {model_provider}")
# preprocess
def preprocess_messages(messages):
diff --git a/user_package_map.yaml.example b/user_package_map.yaml.example
new file mode 100644
index 0000000..24e73c5
--- /dev/null
+++ b/user_package_map.yaml.example
@@ -0,0 +1,17 @@
+# 用户自定义应用名称 -> 包名映射
+#
+# 此文件用于保存:
+# 1. 扫描功能深度解析获取的应用名
+# 2. 用户手动添加的自定义映射
+#
+# 优先级高于 default_package_map.yaml
+#
+# 使用方法:
+# 1. 复制此文件为 user_package_map.yaml
+# 2. 在 Web UI 使用「扫描应用映射」功能自动填充
+# 3. 或手动添加映射
+#
+# 格式: 应用中文名: 包名
+# 例如:
+# 纳米AI: com.qihoo.namiso
+# 虎嗅: com.huxiu
diff --git a/web_ui/README.md b/web_ui/README.md
new file mode 100644
index 0000000..8b6b9ff
--- /dev/null
+++ b/web_ui/README.md
@@ -0,0 +1,117 @@
+# AutoGLM Web UI
+
+基于 Gradio 构建的现代化 Web 界面,提供友好的用户体验来使用 AutoGLM 进行 Android 设备自动化操作。
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+# 在项目根目录下
+pip install -r requirements.txt
+```
+
+### 2. 启动 Web UI
+
+```bash
+# 方法1:使用启动脚本(推荐)
+python start_web_ui.py
+
+# 方法2:直接运行
+cd web_ui
+python app.py
+```
+
+### 3. 访问界面
+
+打开浏览器访问:http://localhost:7860
+
+## 界面功能
+
+### 左侧面板
+
+- **设备状态**:检查 ADB 连接和 Android 设备状态
+- **模型配置**:
+ - 选项A:选择预设的模型服务配置
+ - 选项B:自定义本地模型配置
+- **应用列表**:查看所有支持的应用
+
+### 右侧面板
+
+- **命令输入**:输入自然语言命令
+- **命令示例**:查看常用命令示例
+- **执行结果**:显示 AI 的执行过程和结果
+
+## 预设配置
+
+Web UI 提供了以下预设配置:
+
+1. **智谱AI (推荐)**:官方提供的 AutoGLM 服务
+2. **本地Ollama**:本地部署的 Ollama 服务
+3. **本地vLLM**:本地 vLLM 部署的模型服务
+
+## 常用命令示例
+
+- "打开美团搜索附近的火锅店"
+- "发送微信消息给张三"
+- "打开抖音并搜索美食视频"
+- "设置明天早上8点的闹钟"
+- "拍照并发送给联系人"
+
+## 注意事项
+
+1. 确保 ADB 已正确安装并配置环境变量
+2. 确保 Android 设备已开启 USB 调试并连接电脑
+3. 确保已安装并启用 ADB Keyboard
+4. 确保模型服务正在运行(如果使用本地部署)
+
+## 故障排除
+
+### 设备未连接
+- 检查 USB 数据线是否支持数据传输
+- 确认手机已开启 USB 调试
+- 点击"检查状态"按钮查看详细错误信息
+
+### 模型连接失败
+- 确认模型服务正在运行
+- 检查网络连接和 URL 配置
+- 验证 API Key 是否正确(如果需要)
+
+### 命令执行失败
+- 查看输出区域的错误信息
+- 确认目标应用已安装在手机上
+- 检查手机屏幕是否处于可操作状态
+
+## 高级配置
+
+### 修改端口
+
+编辑 `web_ui/app.py` 文件,修改 `demo.launch()` 的 `server_port` 参数:
+
+```python
+demo.launch(
+ server_name="0.0.0.0",
+ server_port=8080, # 修改为您想要的端口
+ share=False,
+ inbrowser=True,
+ show_error=True
+)
+```
+
+### 启用公网访问
+
+如果您想让其他设备也能访问 Web UI,可以启用 `share` 参数:
+
+```python
+demo.launch(share=True) # 会生成公网链接
+```
+
+### 自定义主题
+
+编辑 `web_ui/app.py` 中的 `theme` 参数来更改界面主题:
+
+```python
+from gradio.themes import Soft, Base, Default
+
+theme = Soft() # 可选:Soft, Base, Default, Monochrome
+```
\ No newline at end of file
diff --git a/web_ui/__pycache__/app.cpython-312.pyc b/web_ui/__pycache__/app.cpython-312.pyc
new file mode 100644
index 0000000..74fe613
Binary files /dev/null and b/web_ui/__pycache__/app.cpython-312.pyc differ
diff --git a/web_ui/__pycache__/app_with_scrcpy.cpython-312.pyc b/web_ui/__pycache__/app_with_scrcpy.cpython-312.pyc
new file mode 100644
index 0000000..224903a
Binary files /dev/null and b/web_ui/__pycache__/app_with_scrcpy.cpython-312.pyc differ
diff --git a/web_ui/__pycache__/package_map_ui.cpython-312.pyc b/web_ui/__pycache__/package_map_ui.cpython-312.pyc
new file mode 100644
index 0000000..d504382
Binary files /dev/null and b/web_ui/__pycache__/package_map_ui.cpython-312.pyc differ
diff --git a/web_ui/__pycache__/scrcpy_integration.cpython-312.pyc b/web_ui/__pycache__/scrcpy_integration.cpython-312.pyc
new file mode 100644
index 0000000..5d8b784
Binary files /dev/null and b/web_ui/__pycache__/scrcpy_integration.cpython-312.pyc differ
diff --git a/web_ui/app.py b/web_ui/app.py
new file mode 100644
index 0000000..87dfc4e
--- /dev/null
+++ b/web_ui/app.py
@@ -0,0 +1,1665 @@
+"""
+Gradio Web UI for AutoGLM
+提供用户友好的Web界面来使用AutoGLM进行Android设备自动化操作
+集成轨迹可视化功能
+"""
+
+import gradio as gr
+import subprocess
+import threading
+import queue
+import time
+import os
+import sys
+import datetime
+import json
+import re
+import glob
+import yaml
+
+from PIL import Image
+from io import BytesIO
+import base64
+
+# 确保能找到项目模块
+if "." not in sys.path:
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+try:
+ import jsonlines
+ from megfile import smart_open, smart_exists
+ HAS_MEGFILE = True
+except ImportError:
+ HAS_MEGFILE = False
+ print("[WARNING] megfile/jsonlines not installed, visualization may be limited")
+
+# --- 轨迹可视化工具函数 ---
+
+def long_side_resize(image, long_side=600):
+ """将图片长边限制到指定尺寸"""
+ image = image.convert("RGB")
+ width, height = image.size
+ if max(width, height) > long_side:
+ if width >= height:
+ new_width = long_side
+ new_height = int(height * long_side / width)
+ else:
+ new_height = long_side
+ new_width = int(width * long_side / height)
+ image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
+ return image
+
+def image_to_base64(image):
+ """将PIL图片转换为base64 URL"""
+ buffered = BytesIO()
+ image.save(buffered, format="JPEG", quality=85)
+ img_str = base64.b64encode(buffered.getvalue()).decode()
+ return f"data:image/jpeg;base64,{img_str}"
+
+def load_session_logs(session_id):
+ """加载指定session的日志"""
+ if not HAS_MEGFILE or not session_id:
+ return []
+
+ log_file = f"running_log/server_log/os-copilot-local-eval-logs/traces/{session_id}.jsonl"
+
+ if not smart_exists(log_file):
+ return []
+
+ try:
+ with smart_open(log_file, "r", encoding='utf-8') as f:
+ reader = jsonlines.Reader(f)
+ logs = [log for log in reader]
+ return logs
+ except Exception as e:
+ print(f"[ERROR] 加载日志失败: {e}")
+ return []
+
+def logs_to_chatbot_messages(logs):
+ """将日志转换为Gradio Chatbot格式的消息列表 (Gradio 6.x messages格式)"""
+ if not logs:
+ return []
+
+ messages = []
+
+ # 第一条是配置信息
+ config_log = logs[0]
+ task = config_log.get('message', {}).get('task', '未知任务')
+ model_name = config_log.get('message', {}).get('model_config', {}).get('model_name', '未知模型')
+
+ # Gradio 6.x 使用 {"role": "user"|"assistant", "content": "..."} 格式
+ messages.append({"role": "assistant", "content": f"### 📋 任务: {task}\n\n**模型**: {model_name}"})
+
+ # 后续是环境-动作对
+ env_act_logs = logs[1:]
+ for idx, log in enumerate(env_act_logs):
+ try:
+ env = log.get('message', {}).get('environment', {})
+ act = log.get('message', {}).get('action', {})
+
+ image_url = env.get('image', '')
+ thought = act.get('cot', '')
+ action_type = act.get('action_type', '')
+
+ # 尝试加载截图
+ img_content = None
+ if image_url and HAS_MEGFILE:
+ try:
+ # 优先尝试处理过的图片
+ processed_url = image_url.replace(".jpeg", "_processed.jpeg")
+ target_url = processed_url if smart_exists(processed_url) else image_url
+
+ with smart_open(target_url, "rb") as f:
+ image = Image.open(f)
+ image = long_side_resize(image, long_side=800) # 保留较大尺寸以便放大查看
+ img_content = image_to_base64(image)
+ except Exception as e:
+ print(f"[WARNING] 加载图片失败: {e}")
+
+ # 用户消息显示步骤编号 + 截图
+ if img_content:
+ # Gradio 6.x 支持 gr.Image 或 HTML 格式显示图片
+ messages.append({"role": "user", "content": f"📱 Step {idx + 1}\n\n"})
+ else:
+ messages.append({"role": "user", "content": f"📱 Step {idx + 1}"})
+
+ # 构建动作描述
+ action_desc = f"**Step {idx + 1}**\n\n"
+ if thought:
+ action_desc += f"💭 **思考**: {thought}\n\n"
+ action_desc += f"🎯 **动作**: `{action_type}`\n\n"
+
+ # 添加动作详情
+ action_copy = {k: v for k, v in act.items() if k not in ['cot']}
+ action_desc += f"```json\n{json.dumps(action_copy, indent=2, ensure_ascii=False)}\n```"
+
+ # 助手回复动作详情
+ messages.append({"role": "assistant", "content": action_desc})
+
+ except Exception as e:
+ print(f"[WARNING] 处理日志条目失败: {e}")
+ continue
+
+ return messages
+
+def get_available_sessions():
+ """获取所有可用的session ID列表"""
+ traces_dir = "running_log/server_log/os-copilot-local-eval-logs/traces"
+ if not os.path.exists(traces_dir):
+ return []
+
+ sessions = []
+ for f in glob.glob(os.path.join(traces_dir, "*.jsonl")):
+ session_id = os.path.basename(f).replace(".jsonl", "")
+ # 获取文件修改时间
+ mtime = os.path.getmtime(f)
+ sessions.append((session_id, mtime))
+
+ # 按时间倒序排列(最新的在前)
+ sessions.sort(key=lambda x: x[1], reverse=True)
+ return [s[0] for s in sessions[:20]] # 只返回最近20个
+
+
+def export_trajectory_to_pdf(session_id, output_path=None):
+ """
+ 将轨迹导出为 PDF 文件
+
+ Args:
+ session_id: Session ID
+ output_path: 输出文件路径,默认为 traces 目录下
+
+ Returns:
+ 生成的 PDF 文件路径,失败返回 None
+ """
+ try:
+ from reportlab.lib.pagesizes import A4
+ from reportlab.lib.units import mm
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+ from reportlab.pdfbase import pdfmetrics
+ from reportlab.pdfbase.ttfonts import TTFont
+ except ImportError:
+ print("[ERROR] 需要安装 reportlab: pip install reportlab")
+ return None
+
+ # 加载日志
+ logs = load_session_logs(session_id)
+ if not logs:
+ print(f"[ERROR] 没有找到日志: {session_id}")
+ return None
+
+ # 输出路径
+ traces_dir = "running_log/server_log/os-copilot-local-eval-logs/traces"
+ if output_path is None:
+ output_path = os.path.join(traces_dir, f"{session_id}.pdf")
+
+ # 确保目录存在
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+
+ # 尝试注册中文字体
+ try:
+ font_paths = [
+ "C:/Windows/Fonts/msyh.ttc",
+ "C:/Windows/Fonts/simsun.ttc",
+ "C:/Windows/Fonts/simhei.ttf",
+ ]
+ font_registered = False
+ for fp in font_paths:
+ if os.path.exists(fp):
+ pdfmetrics.registerFont(TTFont('ChineseFont', fp))
+ font_registered = True
+ break
+ except Exception as e:
+ print(f"[WARNING] 注册中文字体失败: {e}")
+ font_registered = False
+
+ # 创建 PDF
+ doc = SimpleDocTemplate(
+ output_path,
+ pagesize=A4,
+ rightMargin=20*mm,
+ leftMargin=20*mm,
+ topMargin=20*mm,
+ bottomMargin=20*mm
+ )
+
+ # 样式
+ styles = getSampleStyleSheet()
+ if font_registered:
+ title_style = ParagraphStyle(
+ 'ChineseTitle',
+ parent=styles['Heading1'],
+ fontName='ChineseFont',
+ fontSize=18,
+ spaceAfter=12
+ )
+ body_style = ParagraphStyle(
+ 'ChineseBody',
+ parent=styles['Normal'],
+ fontName='ChineseFont',
+ fontSize=10,
+ leading=14
+ )
+ else:
+ title_style = styles['Heading1']
+ body_style = styles['Normal']
+
+ # 构建内容
+ story = []
+
+ # 标题
+ config_log = logs[0]
+ task = config_log.get('message', {}).get('task', session_id)
+ story.append(Paragraph(f"任务轨迹: {task[:50]}", title_style))
+ story.append(Spacer(1, 10*mm))
+
+ # 后续是环境-动作对
+ env_act_logs = logs[1:]
+ for idx, log in enumerate(env_act_logs):
+ try:
+ env = log.get('message', {}).get('environment', {})
+ act = log.get('message', {}).get('action', {})
+
+ thought = act.get('cot', '')
+ action_type = act.get('action_type', '')
+ image_url = env.get('image', '')
+
+ # 步骤标题
+ story.append(Paragraph(f"Step {idx + 1}", body_style))
+ story.append(Spacer(1, 2*mm))
+
+ # 思考
+ if thought:
+ thought_short = thought[:200] + "..." if len(thought) > 200 else thought
+ story.append(Paragraph(f"思考: {thought_short}", body_style))
+
+ # 动作
+ story.append(Paragraph(f"动作: {action_type}", body_style))
+
+ # 截图
+ if image_url and HAS_MEGFILE:
+ try:
+ processed_url = image_url.replace(".jpeg", "_processed.jpeg")
+ target_url = processed_url if smart_exists(processed_url) else image_url
+
+ with smart_open(target_url, "rb") as f:
+ pil_img = Image.open(f)
+ img_w, img_h = pil_img.size
+
+ # 保存临时文件
+ temp_img_path = os.path.join(traces_dir, f"temp_{session_id}_{idx}.jpg")
+ pil_img = long_side_resize(pil_img, 600)
+ pil_img.save(temp_img_path, "JPEG", quality=80)
+
+ # 添加到 PDF
+ img_w, img_h = pil_img.size
+ max_width = 160 * mm
+ max_height = 200 * mm
+ scale = min(max_width / img_w, max_height / img_h, 1.0)
+
+ rl_img = RLImage(temp_img_path, width=img_w * scale, height=img_h * scale)
+ story.append(Spacer(1, 3*mm))
+ story.append(rl_img)
+ except Exception as e:
+ print(f"[WARNING] 加载图片失败: {e}")
+
+ story.append(Spacer(1, 8*mm))
+ story.append(Paragraph("
", body_style))
+ story.append(Spacer(1, 5*mm))
+
+ except Exception as e:
+ print(f"[WARNING] 处理步骤 {idx+1} 失败: {e}")
+ continue
+
+ # 生成 PDF
+ try:
+ doc.build(story)
+ print(f"[PDF] 导出成功: {output_path}")
+ return output_path
+ except Exception as e:
+ print(f"[ERROR] PDF 生成失败: {e}")
+ return None
+
+# --- 全局命令执行管理器 ---
+class CommandRunner:
+ def __init__(self):
+ self.process = None
+ self.logs = ""
+ self.is_running = False
+ self.log_lock = threading.Lock()
+ self.current_session_id = None # 追踪当前session ID
+ self.waiting_for_input = False # 是否等待用户输入
+ self.paused_session_id = None # 暂停时的session ID
+ self.is_paused = False # 是否处于暂停状态
+
+ def start(self, cmd_args, cwd=None, env=None):
+ """启动新命令"""
+ if self.is_running:
+ return False, "当前已有任务在运行,请先停止"
+
+ self.stop()
+
+ with self.log_lock:
+ self.logs = f"--- 任务开始: {' '.join(cmd_args)} ---\n"
+ self.current_session_id = None # 重置session ID
+ print(f"\n[WebUI] 启动任务: {' '.join(cmd_args)}")
+
+ self.is_running = True
+
+ thread = threading.Thread(target=self._run_thread, args=(cmd_args, cwd, env), daemon=True)
+ thread.start()
+ return True, "任务已启动"
+
+ def stop(self, is_pause=False):
+ """停止当前任务
+ Args:
+ is_pause: 如果是暂停操作,保存session_id供后续继续
+ """
+ if self.process and self.process.poll() is None:
+ try:
+ self.process.terminate()
+ time.sleep(0.5)
+ if self.process.poll() is None:
+ self.process.kill()
+ except Exception as e:
+ self._append_log(f"\n[系统] 停止进程失败: {e}\n")
+
+ if is_pause:
+ # 暂停模式: 保存session_id,标记暂停状态
+ with self.log_lock:
+ self.paused_session_id = self.current_session_id
+ self.is_paused = True
+
+ self.is_running = False
+ return True, "任务停止指令已发送"
+
+ def _run_thread(self, cmd_args, cwd, env):
+ try:
+ self.process = subprocess.Popen(
+ cmd_args,
+ cwd=cwd,
+ env=env,
+ stdin=subprocess.PIPE, # 添加stdin支持
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding='utf-8',
+ errors='replace',
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ for line in iter(self.process.stdout.readline, ''):
+ if line:
+ self._append_log(line)
+ print(line, end="", flush=True)
+
+ # 解析 Session ID
+ match = re.search(r'Session ID:\s*([a-f0-9\-]+)', line)
+ if match:
+ with self.log_lock:
+ self.current_session_id = match.group(1)
+ print(f"[WebUI] 捕获到 Session ID: {self.current_session_id}")
+
+ # 检测是否需要用户输入
+ if any(k in line for k in ['Please Reply:', '回复一下', '[PAUSED]', 'WAITING_FOR_INPUT']):
+ with self.log_lock:
+ self.waiting_for_input = True
+
+ self.process.wait()
+ end_msg = f"\n--- 任务结束 (代码: {self.process.returncode}) ---\n"
+ self._append_log(end_msg)
+ print(end_msg)
+
+ except Exception as e:
+ err_msg = f"\n[系统错误] 执行异常: {str(e)}\n"
+ self._append_log(err_msg)
+ print(err_msg)
+ finally:
+ self.is_running = False
+ self.waiting_for_input = False
+ self.process = None
+
+ def send_input(self, text):
+ """发送输入到进程的stdin"""
+ if self.process and self.process.poll() is None and self.process.stdin:
+ try:
+ self.process.stdin.write(text + "\n")
+ self.process.stdin.flush()
+ self._append_log(f"\n[用户回复] {text}\n")
+ with self.log_lock:
+ self.waiting_for_input = False
+ return True, "已发送回复"
+ except Exception as e:
+ return False, f"发送失败: {e}"
+ return False, "没有正在运行的任务"
+
+ def _append_log(self, text):
+ with self.log_lock:
+ if len(self.logs) > 500000:
+ self.logs = self.logs[-400000:]
+ self.logs += text
+
+ def get_logs(self):
+ with self.log_lock:
+ return self.logs
+
+ def get_status(self):
+ if self.is_paused:
+ return "⏸ 已暂停 - 输入修正指令后点击 [执行/回复] 继续"
+ if self.waiting_for_input:
+ return "🟡 等待输入"
+ return "🟢 运行中" if self.is_running else "⚪ 就绪"
+
+ def get_current_session_id(self):
+ with self.log_lock:
+ return self.current_session_id
+
+ def get_paused_session_id(self):
+ with self.log_lock:
+ return self.paused_session_id
+
+ def is_waiting_for_input(self):
+ with self.log_lock:
+ return self.waiting_for_input
+
+ def clear_pause_state(self):
+ """清除暂停状态"""
+ with self.log_lock:
+ self.is_paused = False
+ self.paused_session_id = None
+
+# 全局单例
+runner = CommandRunner()
+
+# --- 辅助函数 ---
+
+def get_adb_devices():
+ """获取所有已连接的设备"""
+ try:
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding='utf-8', errors='ignore')
+ devices = []
+ device_details = []
+
+ if result.returncode == 0:
+ lines = result.stdout.split('\n')[1:]
+ for line in lines:
+ if '\tdevice' in line:
+ device_id = line.split('\t')[0]
+ devices.append(device_id)
+ device_type = "📶 无线" if ':' in device_id else "🔌 USB"
+ device_details.append(f"{device_type}: {device_id}")
+
+ if not device_details:
+ return ["未找到设备"], ""
+
+ device_list = "\n".join(device_details)
+ return devices, f"已连接设备 ({len(devices)}个):\n\n{device_list}\n\n默认设备: {devices[0]}"
+ except Exception as e:
+ return [f"错误: {str(e)}"], f"获取设备列表失败: {str(e)}"
+
+def connect_wireless_device(ip_address, port="5555"):
+ """连接无线设备"""
+ try:
+ parts = ip_address.strip().split('.')
+ if len(parts) != 4:
+ return False, "无效的IP地址格式"
+
+ connect_addr = f"{ip_address}:{port}"
+ result = subprocess.run(
+ ["adb", "connect", connect_addr],
+ capture_output=True, text=True, encoding='utf-8', errors='ignore', timeout=10
+ )
+
+ if result.returncode == 0:
+ devices_result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding='utf-8')
+ if connect_addr in devices_result.stdout and "device" in devices_result.stdout:
+ return True, f"成功连接到无线设备: {connect_addr}"
+ else:
+ return False, "连接失败,请检查设备设置"
+ else:
+ return False, f"连接失败: {result.stderr.strip() if result.stderr else result.stdout.strip()}"
+
+ except subprocess.TimeoutExpired:
+ return False, "连接超时"
+ except Exception as e:
+ return False, f"连接出错: {str(e)}"
+
+def disconnect_wireless_device(device_id):
+ """断开无线设备"""
+ try:
+ result = subprocess.run(
+ ["adb", "disconnect"] if not device_id else ["adb", "disconnect", device_id],
+ capture_output=True, text=True, encoding='utf-8'
+ )
+ return True, "已断开无线设备连接"
+ except Exception as e:
+ return False, f"断开连接出错: {str(e)}"
+
+def enable_tcpip(device_id, port="5555"):
+ """启用TCP/IP模式"""
+ try:
+ result = subprocess.run(
+ ["adb", "-s", device_id, "tcpip", str(port)],
+ capture_output=True, text=True, encoding='utf-8', timeout=10
+ )
+ if result.returncode == 0:
+ ip_result = subprocess.run(
+ ["adb", "-s", device_id, "shell", "ip", "route", "get", "8.8.8.8"],
+ capture_output=True, text=True, encoding='utf-8'
+ )
+ device_ip = "未知"
+ if ip_result.returncode == 0 and "src" in ip_result.stdout:
+ parts = ip_result.stdout.split()
+ for i, part in enumerate(parts):
+ if part == "src" and i + 1 < len(parts):
+ device_ip = parts[i + 1]
+ break
+ return True, f"TCP/IP已启用\n设备IP: {device_ip}"
+ return False, f"启用失败: {result.stderr}"
+ except Exception as e:
+ return False, f"启用TCP/IP出错: {str(e)}"
+
+def get_available_apps():
+ try:
+ result = subprocess.run(
+ ["adb", "shell", "pm", "list", "packages", "-3"],
+ capture_output=True, text=True, encoding='utf-8', errors='ignore'
+ )
+ if result.returncode != 0:
+ return "获取失败"
+ apps = [line.replace('package:', '').strip() for line in result.stdout.splitlines() if line.strip()]
+ apps.sort()
+ return "\n".join(apps)
+ except Exception as e:
+ return str(e)
+
+def start_scrcpy():
+ """启动 scrcpy 屏幕镜像"""
+ try:
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ project_dir = os.path.dirname(current_dir)
+ scrcpy_path = os.path.join(project_dir, "scrcpy-win64-v3.3.3", "scrcpy.exe")
+
+ if not os.path.exists(scrcpy_path):
+ return f"未找到 scrcpy.exe: {scrcpy_path}"
+
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding='utf-8')
+ devices = [line.split('\t')[0] for line in result.stdout.split('\n')[1:] if '\tdevice' in line]
+
+ if not devices:
+ return "没有检测到已连接的设备"
+
+ scrcpy_cmd = [scrcpy_path, '--no-audio']
+ if len(devices) > 1:
+ scrcpy_cmd.extend(['-s', devices[0]])
+
+ def run_scrcpy():
+ try:
+ if os.name == 'nt':
+ subprocess.Popen(scrcpy_cmd, creationflags=subprocess.CREATE_NEW_CONSOLE)
+ else:
+ subprocess.Popen(scrcpy_cmd)
+ except Exception as e:
+ print(f"[ERROR] 启动 scrcpy 失败: {e}")
+
+ threading.Thread(target=run_scrcpy, daemon=True).start()
+ time.sleep(0.5)
+ return f"✅ scrcpy 已启动 (设备: {devices[0]})"
+
+ except Exception as e:
+ return f"启动失败: {str(e)}"
+
+def check_adb_connection():
+ """检查ADB连接状态"""
+ try:
+ subprocess.run(["adb", "start-server"], capture_output=True, text=True, timeout=5)
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, timeout=5)
+
+ if result.returncode == 0:
+ lines = result.stdout.strip().split('\n')
+ devices = []
+ for line in lines[1:]:
+ if line.strip():
+ parts = line.split('\t')
+ if len(parts) >= 2:
+ devices.append(f"📱 {parts[0]} - {parts[1]}")
+
+ if devices:
+ return True, f"✅ ADB服务正常\n已连接设备:\n" + "\n".join(devices)
+ else:
+ return False, "⚠️ ADB服务正常但无设备连接"
+ return False, f"❌ ADB命令执行失败"
+
+ except FileNotFoundError:
+ return False, "❌ ADB未安装或未添加到PATH"
+ except subprocess.TimeoutExpired:
+ return False, "❌ ADB命令超时"
+ except Exception as e:
+ return False, f"❌ 检查ADB连接时出错: {str(e)}"
+
+def restart_adb():
+ """重启ADB服务"""
+ try:
+ subprocess.run(["adb", "kill-server"], capture_output=True, text=True, timeout=10)
+ time.sleep(1)
+ subprocess.run(["adb", "start-server"], capture_output=True, text=True, timeout=10)
+
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, timeout=5)
+ if result.returncode == 0:
+ lines = result.stdout.strip().split('\n')
+ devices = [f"📱 {line.split()[0]}" for line in lines[1:] if '\tdevice' in line]
+ if devices:
+ return True, f"✅ ADB重启成功\n当前设备:\n" + "\n".join(devices)
+ return True, "✅ ADB重启成功\n当前无设备连接"
+ return False, "❌ ADB重启失败"
+ except Exception as e:
+ return False, f"❌ 重启出错: {str(e)}"
+
+# --- Gradio 界面 ---
+
+def create_ui():
+ # 自定义CSS:简洁样式
+ custom_css = """
+ /* 轨迹图片样式 */
+ .trajectory-chatbot img {
+ max-width: 280px !important;
+ max-height: 500px !important;
+ width: auto !important;
+ height: auto !important;
+ object-fit: contain !important;
+ cursor: pointer;
+ transition: opacity 0.2s;
+ border-radius: 8px;
+ }
+ .trajectory-chatbot img:hover {
+ opacity: 0.85;
+ }
+ .trajectory-chatbot .message {
+ max-width: 100% !important;
+ }
+
+ /* 命令/回复文本框滚动条样式 */
+ #user-input-box textarea {
+ overflow-y: auto !important;
+ max-height: 120px !important;
+ }
+ """
+
+ # 灯箱脚本 - 使用head参数注入 (使用MutationObserver确保动态内容可点击)
+ lightbox_head = """
+
+
+ """
+
+ with gr.Blocks(title="Stepfun-ai/gelab-zero") as demo:
+
+ gr.Markdown("## 🤖 Stepfun-ai/gelab-zero 控制台")
+
+ with gr.Row():
+ # --- 左列:设备管理、配置、任务监控 ---
+ with gr.Column(scale=1, min_width=400):
+
+ # 1. 设备管理
+ with gr.Group():
+ gr.Markdown("### 📱 设备管理")
+
+ device_status = gr.Textbox(
+ label="设备状态",
+ value="❓ 未检查",
+ interactive=False,
+ lines=3
+ )
+ with gr.Row():
+ check_status_btn = gr.Button("检查", size="sm", min_width=1, scale=1)
+ adb_devices_btn = gr.Button("列表", size="sm", min_width=1, scale=1)
+ restart_adb_btn = gr.Button("重启ADB", size="sm", min_width=1, scale=1)
+
+ with gr.Accordion("📶 无线调试", open=False):
+ with gr.Row():
+ wireless_ip = gr.Textbox(label="IP", placeholder="192.168.1.x", scale=3)
+ wireless_port = gr.Textbox(label="端口", value="5555", scale=1)
+
+ with gr.Row():
+ connect_wireless_btn = gr.Button("🔗 连接", variant="primary", size="sm")
+ disconnect_wireless_btn = gr.Button("✂️ 断开", size="sm")
+
+ enable_tcpip_btn = gr.Button("📡 启用TCP/IP模式", size="sm")
+ wireless_status = gr.Textbox(label="状态", interactive=False, lines=1)
+
+ # 2. 任务监控(放在设备管理下面)
+ with gr.Group():
+ gr.Markdown("### 📊 任务监控")
+ with gr.Row():
+ session_dropdown = gr.Dropdown(
+ label="Session",
+ choices=[],
+ value=None,
+ scale=20,
+ allow_custom_value=True,
+ min_width=200
+ )
+ with gr.Column(scale=1, min_width=60):
+ gr.HTML("") # 占位符对其下拉框
+ refresh_sessions_btn = gr.Button("🔄", size="sm")
+
+ task_status = gr.Textbox(
+ label="任务状态",
+ value="⚪ 就绪",
+ interactive=False,
+ lines=1
+ )
+ user_input = gr.Textbox(
+ label="命令/回复",
+ placeholder="输入任务指令 或 回复Agent询问... (Ctrl+Enter 提交)",
+ lines=3,
+ max_lines=5,
+ elem_id="user-input-box"
+ )
+ with gr.Row():
+ submit_btn = gr.Button("▶ 执行/回复", variant="primary", scale=2, elem_id="submit-btn")
+ pause_btn = gr.Button("⏸ 暂停", variant="secondary", scale=1)
+ stop_btn = gr.Button("⏹ 停止", variant="stop", scale=1)
+
+ # 3. 参数配置
+ with gr.Accordion("⚙️ 参数配置", open=False):
+ try:
+ config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "model_config.yaml")
+ with open(config_path, "r", encoding="utf-8") as f:
+ full_config = yaml.safe_load(f)
+ except Exception as e:
+ print(f"Error loading config: {e}")
+ full_config = {}
+
+ # 准备 Provider 选项: (Display Name, Key)
+ provider_choices = []
+ for key, val in full_config.items():
+ display = val.get("display_name", key)
+ provider_choices.append((display, key))
+ provider_choices.append(("自定义", "custom"))
+
+ # default selection
+ default_prov = provider_choices[0][1] if provider_choices else "custom"
+ default_cfg = full_config.get(default_prov, {})
+
+ # 模型提供商选择
+ provider_dd = gr.Dropdown(
+ label="模型提供商",
+ choices=provider_choices,
+ value=default_prov
+ )
+
+ # Base URL 单独一行
+ base_url_input = gr.Textbox(
+ label="Base URL",
+ value=default_cfg.get("api_base", ""),
+ interactive=True,
+ placeholder="例如: http://localhost:11434/v1"
+ )
+
+ # API Key 单独一行
+ api_key_input = gr.Textbox(
+ label="API Key",
+ type="password",
+ value=default_cfg.get("api_key", ""),
+ interactive=True,
+ placeholder="留空使用配置文件中的默认值"
+ )
+
+ # 模型名称 单独一行
+ model_name_input = gr.Textbox(
+ label="模型名称",
+ value=default_cfg.get("default_model", ""),
+ interactive=True,
+ placeholder="例如: gelab-zero-4b-preview"
+ )
+
+ # 检查连接按钮和状态
+ with gr.Row():
+ check_model_btn = gr.Button("🔍 检查模型连接", size="sm")
+ model_status = gr.Textbox(
+ label="连接状态",
+ value="❓ 未检查",
+ interactive=False,
+ lines=2
+ )
+
+ # Event: Provider Change
+ def on_provider_change(provider):
+ if provider == "custom":
+ return (
+ gr.update(value="", interactive=True),
+ gr.update(value="", interactive=True),
+ gr.update(value="", interactive=True)
+ )
+
+ cfg = full_config.get(provider, {})
+ new_base = cfg.get("api_base", "")
+ new_key = cfg.get("api_key", "")
+ new_model = cfg.get("default_model", "")
+
+ return (
+ gr.update(value=new_base, interactive=True),
+ gr.update(value=new_key, interactive=True),
+ gr.update(value=new_model, interactive=True)
+ )
+
+ provider_dd.change(
+ fn=on_provider_change,
+ inputs=[provider_dd],
+ outputs=[base_url_input, api_key_input, model_name_input]
+ )
+
+ # 检查模型连接
+ def check_model_connection(base_url, model_name, api_key):
+ """检查模型连接状态"""
+ if not base_url:
+ return "⚠️ 请先填写 Base URL"
+ if not model_name:
+ return "⚠️ 请先填写模型名称"
+
+ import requests
+ base = base_url.rstrip('/')
+ headers = {"Content-Type": "application/json"}
+ if api_key:
+ headers["Authorization"] = f"Bearer {api_key}"
+
+ # 判断是本地还是在线
+ is_local = "localhost" in base or "127.0.0.1" in base or "0.0.0.0" in base
+ api_type = "本地" if is_local else "在线"
+
+ # 直接测试 /chat/completions 接口
+ try:
+ url = base + '/chat/completions'
+ test_payload = {
+ "model": model_name,
+ "messages": [{"role": "user", "content": "test"}],
+ "max_tokens": 1
+ }
+ response = requests.post(url, json=test_payload, headers=headers, timeout=15)
+
+ if response.status_code == 200:
+ return f"✅ 连接成功 ({api_type})\n📍 {base}\n🤖 {model_name}"
+ elif response.status_code == 404:
+ return f"❌ 模型 {model_name} 不存在"
+ else:
+ try:
+ err_msg = response.json().get('error', {}).get('message', response.text[:80])
+ except:
+ err_msg = response.text[:80]
+ return f"❌ 请求失败 ({response.status_code})\n{err_msg}"
+ except requests.exceptions.ConnectionError:
+ return f"❌ 无法连接 {base}"
+ except requests.exceptions.Timeout:
+ return f"❌ 连接超时"
+ except Exception as e:
+ return f"❌ {str(e)[:60]}"
+
+ check_model_btn.click(
+ fn=check_model_connection,
+ inputs=[base_url_input, model_name_input, api_key_input],
+ outputs=[model_status]
+ )
+
+ with gr.Row():
+ device_dd = gr.Dropdown(label="当前设备", choices=[], value=None, scale=3)
+ refresh_dev_btn = gr.Button("🔄", scale=1)
+
+ # 4. 实用工具
+ with gr.Accordion("🛠 实用工具", open=False):
+ scrcpy_btn = gr.Button("🖥️ 启动屏幕镜像", variant="secondary")
+ scrcpy_status = gr.Textbox(label="状态", interactive=False, lines=1)
+
+ list_apps_btn = gr.Button("📲 获取应用列表", size="sm")
+ app_list_output = gr.Textbox(label="应用列表", lines=3, interactive=False)
+
+ # 应用映射扫描功能 (增强版)
+ gr.Markdown("---")
+ gr.Markdown("#### 📦 应用映射管理")
+
+ with gr.Row():
+ scan_apps_btn = gr.Button("🔍 扫描应用", variant="primary", size="sm")
+ deep_scan_chk = gr.Checkbox(label="深度扫描", value=True, scale=0)
+ scan_status = gr.Textbox(label="扫描状态", interactive=False, lines=2)
+
+ # 增强版映射编辑器
+ with gr.Accordion("📝 应用映射编辑器", open=False) as mapping_editor:
+ with gr.Tabs():
+ # Tab 1: 映射表预览
+ with gr.TabItem("📋 列表"):
+ with gr.Row():
+ refresh_df_btn = gr.Button("🔄 刷新", size="sm")
+ stats_btn = gr.Button("📊 统计", size="sm")
+ mapping_df = gr.Dataframe(
+ label="应用名称 -> 包名映射",
+ headers=["应用名", "包名"],
+ datatype=["str", "str"],
+ interactive=False,
+ row_count=(10, "dynamic")
+ )
+ mapping_stats_txt = gr.Textbox(
+ label="统计信息",
+ value="",
+ interactive=False,
+ lines=2
+ )
+
+ # Tab 2: 包名搜索
+ with gr.TabItem("🔎 搜索"):
+ search_input = gr.Textbox(
+ label="应用名称",
+ placeholder="输入应用名 (如: 微信)",
+ lines=1
+ )
+ search_btn = gr.Button("🔎 查找包名", size="sm")
+ search_result = gr.Textbox(
+ label="查找结果",
+ lines=4,
+ interactive=False
+ )
+
+ # Tab 3: YAML 编辑器
+ with gr.TabItem("✏️ 编辑"):
+ mapping_textbox = gr.Textbox(
+ label="应用名称 -> 包名映射 (YAML格式)",
+ lines=10,
+ placeholder="微信: com.tencent.mm\n抖音: com.ss.android.ugc.aweme\n# 注释行以 # 开头",
+ interactive=True
+ )
+ with gr.Row():
+ load_mapping_btn = gr.Button("📥 加载", size="sm")
+ save_mapping_btn = gr.Button("💾 保存", size="sm", variant="primary")
+ refresh_mapping_btn = gr.Button("🔄 刷新", size="sm")
+ mapping_status = gr.Textbox(label="操作状态", lines=1, interactive=False)
+
+ # Tab 4: 批量导入
+ with gr.TabItem("📥 导入"):
+ gr.Markdown("**格式**: 应用名:包名 (一行一个)")
+ import_text = gr.Textbox(
+ label="批量导入",
+ placeholder="微信:com.tencent.mm\n抖音:com.ss.android.ugc.aweme",
+ lines=6
+ )
+ import_btn = gr.Button("📥 导入", size="sm", variant="primary")
+ import_result = gr.Textbox(
+ label="导入结果",
+ lines=3,
+ interactive=False
+ )
+
+
+ # --- 右列:日志与轨迹并排(更大空间) ---
+ with gr.Column(scale=3, min_width=700):
+ with gr.Row():
+ # 左边:任务轨迹
+ with gr.Column(scale=1):
+ gr.Markdown("### 📱 任务轨迹")
+ trajectory_output = gr.Chatbot(
+ label="轨迹回放",
+ height=660,
+ show_label=False,
+ elem_classes=["trajectory-chatbot"]
+ )
+ with gr.Row():
+ export_pdf_btn = gr.Button("📄 导出 PDF", size="sm")
+ export_file = gr.File(label="下载", visible=False)
+
+ # 右边:实时日志
+ with gr.Column(scale=1):
+ gr.Markdown("### 📋 实时日志")
+ log_output = gr.Textbox(
+ label="终端输出",
+ value="",
+ lines=25,
+ max_lines=30,
+ interactive=False,
+ elem_id="log-window"
+ )
+ with gr.Row():
+ clear_log_btn = gr.Button("🗑 清空", size="sm")
+ copy_log_btn = gr.Button("📋 复制", size="sm")
+
+ # --- 逻辑绑定 ---
+
+ # 刷新设备
+ def refresh_devices():
+ devices, _ = get_adb_devices()
+ valid_devices = [d for d in devices if not d.startswith("错误") and d != "未找到设备"]
+ return gr.Dropdown(choices=valid_devices, value=valid_devices[0] if valid_devices else None)
+
+ refresh_dev_btn.click(refresh_devices, outputs=device_dd)
+ demo.load(refresh_devices, outputs=device_dd)
+
+ # 刷新session列表
+ def refresh_sessions():
+ sessions = get_available_sessions()
+ current = runner.get_current_session_id()
+ # 如果有当前session且不在列表中,添加到最前面
+ if current and current not in sessions:
+ sessions = [current] + sessions
+ return gr.Dropdown(choices=sessions, value=current if current else (sessions[0] if sessions else None))
+
+ refresh_sessions_btn.click(refresh_sessions, outputs=session_dropdown)
+ demo.load(refresh_sessions, outputs=session_dropdown)
+
+ # 加载轨迹
+ def load_trajectory(session_id):
+ if not session_id:
+ return []
+ logs = load_session_logs(session_id)
+ messages = logs_to_chatbot_messages(logs)
+ return messages
+
+ # session_dropdown.change 事件在下方统一处理,避免重复绑定
+
+ # PDF 导出
+ def export_pdf_handler(session_id):
+ if not session_id:
+ return gr.update(value=None, visible=False)
+ pdf_path = export_trajectory_to_pdf(session_id)
+ if pdf_path:
+ return gr.update(value=pdf_path, visible=True)
+ else:
+ return gr.update(value=None, visible=False)
+
+ export_pdf_btn.click(export_pdf_handler, inputs=[session_dropdown], outputs=[export_file])
+
+ # 列出应用
+ list_apps_btn.click(get_available_apps, outputs=app_list_output)
+
+ # 启动 scrcpy
+ scrcpy_btn.click(fn=start_scrcpy, outputs=[scrcpy_status])
+
+ # === 应用映射扫描功能 (增强版) ===
+ # 导入增强的 package_map_ui 功能
+ from web_ui.package_map_ui import (
+ scan_apps_with_progress,
+ get_package_mapping_dataframe,
+ search_package_by_name,
+ batch_import_mappings,
+ get_mapping_statistics,
+ load_user_mapping_yaml,
+ save_user_mapping_yaml
+ )
+
+ def scan_apps_to_mapping(deep_scan):
+ """扫描应用并更新映射"""
+ try:
+ # 获取已连接设备
+ result = subprocess.run(["adb", "devices"], capture_output=True, text=True, timeout=5)
+ device_lines = [l for l in result.stdout.split('\n')[1:] if '\tdevice' in l]
+ if not device_lines:
+ return "❌ 没有检测到已连接的设备"
+
+ device_id = device_lines[0].split('\t')[0]
+
+ # 使用增强版扫描
+ logs, status, count = scan_apps_with_progress(
+ device_id=device_id,
+ deep_scan=deep_scan
+ )
+ return status
+ except Exception as e:
+ return f"❌ 扫描失败: {str(e)[:100]}"
+
+ scan_apps_btn.click(fn=scan_apps_to_mapping, inputs=[deep_scan_chk], outputs=[scan_status])
+
+ # DataFrame 刷新
+ refresh_df_btn.click(
+ fn=lambda: get_package_mapping_dataframe(),
+ outputs=[mapping_df]
+ )
+
+ # 统计信息
+ def get_stats_text():
+ stats = get_mapping_statistics()
+ return (
+ f"默认映射: {stats['default_count']} 条\n"
+ f"用户映射: {stats['user_count']} 条 (独有: {stats['user_only_count']})"
+ )
+
+ stats_btn.click(fn=get_stats_text, outputs=[mapping_stats_txt])
+
+ # 包名搜索
+ search_btn.click(
+ fn=search_package_by_name,
+ inputs=[search_input],
+ outputs=[search_result]
+ )
+
+ # YAML 编辑器事件
+ def load_mapping_yaml():
+ """加载 YAML 映射到编辑器"""
+ try:
+ from copilot_front_end.package_scanner import load_user_package_map, get_user_package_map_path
+ mapping = load_user_package_map()
+ if not mapping:
+ return "# 映射表为空,请先扫描或手动添加\n# 格式: 应用名称: 包名", f"ℹ️ 映射文件: {get_user_package_map_path()}"
+
+ # 转为 YAML 格式字符串
+ lines = ["# 用户自定义应用映射(可编辑)", ""]
+ for name, pkg in sorted(mapping.items()):
+ lines.append(f"{name}: {pkg}")
+ return "\n".join(lines), f"✅ 已加载 {len(mapping)} 条映射"
+ except Exception as e:
+ return f"# 加载失败: {e}", f"❌ {str(e)[:50]}"
+
+ load_mapping_btn.click(fn=load_mapping_yaml, outputs=[mapping_textbox, mapping_status])
+ refresh_mapping_btn.click(fn=load_mapping_yaml, outputs=[mapping_textbox, mapping_status])
+
+ def save_mapping_yaml(yaml_content):
+ """保存编辑器内容到 YAML"""
+ try:
+ from copilot_front_end.package_scanner import save_user_package_map
+
+ # 解析 YAML 内容
+ mapping = {}
+ for line in yaml_content.strip().split('\n'):
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ if ':' in line:
+ parts = line.split(':', 1)
+ key = parts[0].strip().strip('"').strip("'")
+ value = parts[1].strip().strip('"').strip("'")
+ if key and value:
+ mapping[key] = value
+
+ if not mapping:
+ return "⚠️ 没有有效的映射条目"
+
+ success = save_user_package_map(mapping)
+ if success:
+ return f"✅ 已保存 {len(mapping)} 条映射"
+ else:
+ return "❌ 保存失败"
+ except Exception as e:
+ return f"❌ 保存失败: {str(e)[:50]}"
+
+ save_mapping_btn.click(fn=save_mapping_yaml, inputs=[mapping_textbox], outputs=[mapping_status])
+
+ # 批量导入
+ import_btn.click(
+ fn=batch_import_mappings,
+ inputs=[import_text],
+ outputs=[import_result]
+ )
+
+ # 核心:智能提交(命令 或 回复 或 暂停后继续)
+ def smart_submit(prompt, provider, base_url, api_key, model_name, device):
+ # 情况1: 处于暂停状态 → 作为注入指令继续
+ if runner.is_paused:
+ paused_session = runner.get_paused_session_id()
+ if not paused_session:
+ runner.clear_pause_state()
+ return "⚠️ 没有可继续的会话", prompt
+
+ script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "examples", "run_single_task.py")
+ cmd_list = [sys.executable, script_path, "--continue-session", paused_session]
+
+ # 如果有注入指令
+ if prompt.strip():
+ cmd_list.extend(["--injection", prompt.strip()])
+
+ # 添加模型参数
+ if base_url: cmd_list.extend(["--base-url", base_url])
+ if model_name: cmd_list.extend(["--model", model_name])
+ if api_key: cmd_list.extend(["--api-key", api_key])
+ if device and device != "未找到设备":
+ cmd_list.extend(["--device-id", device])
+
+ env = os.environ.copy()
+ env["PYTHONIOENCODING"] = "utf-8"
+ env["PYTHONUNBUFFERED"] = "1"
+
+ runner.clear_pause_state()
+ success, msg = runner.start(cmd_list, cwd=os.getcwd(), env=env)
+ return ("🟢 继续运行中" if success else f"🔴 {msg}"), ""
+
+ # 情况2: 无输入时仅返回当前状态
+ if not prompt.strip():
+ return runner.get_status(), ""
+
+ # 情况3: 任务正在运行且等待输入,作为回复发送
+ if runner.is_running and runner.is_waiting_for_input():
+ success, msg = runner.send_input(prompt.strip())
+ return runner.get_status(), "" # 清空输入框
+
+ # 情况4: 任务运行中 → 提示先停止
+ if runner.is_running:
+ return "⚠️ 任务运行中,请先暂停或停止", prompt
+
+ # 情况5: 空闲 → 启动新任务
+ final_url = base_url
+ final_model = model_name
+ final_key = api_key
+
+ script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "examples", "run_single_task.py")
+ cmd_list = [sys.executable, script_path, prompt]
+
+ if final_url: cmd_list.extend(["--base-url", final_url])
+ if final_model: cmd_list.extend(["--model", final_model])
+ if final_key: cmd_list.extend(["--api-key", final_key])
+ if device and device != "未找到设备":
+ cmd_list.extend(["--device-id", device])
+
+ env = os.environ.copy()
+ env["PYTHONIOENCODING"] = "utf-8"
+ env["PYTHONUNBUFFERED"] = "1"
+
+ success, msg = runner.start(cmd_list, cwd=os.getcwd(), env=env)
+ return ("🟢 运行中" if success else f"🔴 {msg}"), "" # 清空输入框
+
+ submit_btn.click(
+ smart_submit,
+ inputs=[user_input, provider_dd, base_url_input, api_key_input, model_name_input, device_dd],
+ outputs=[task_status, user_input]
+ )
+
+ user_input.submit(
+ smart_submit,
+ inputs=[user_input, provider_dd, base_url_input, api_key_input, model_name_input, device_dd],
+ outputs=[task_status, user_input]
+ )
+
+ # 停止任务(完全停止,清除暂停状态)
+ def stop_command():
+ runner.stop()
+ runner.clear_pause_state() # 确保清除暂停状态
+ return "⚪ 已停止"
+
+ stop_btn.click(stop_command, outputs=[task_status])
+
+ # 暂停任务:立即终止进程,保存session供后续继续
+ def pause_and_inject(prompt_text):
+ """暂停当前任务:立即终止进程,保存 session_id 供后续继续"""
+ if not runner.is_running:
+ return "⚠️ 没有正在运行的任务"
+
+ current_session = runner.get_current_session_id()
+ if not current_session:
+ return "⚠️ 无法获取当前会话ID,请稍后重试"
+
+ # 直接终止进程,并保存 session_id
+ runner.stop(is_pause=True)
+
+ print(f"[PAUSED] 任务已暂停,Session: {current_session}")
+ return f"⏸ 已暂停 (Session: {current_session[:8]}...) - 输入修正指令后点击 [执行/回复] 继续"
+
+ pause_btn.click(pause_and_inject, inputs=[user_input], outputs=[task_status])
+
+ # 检查状态
+ def check_status_handler():
+ devices, device_info = get_adb_devices()
+ return device_info if device_info else "❌ 未发现设备"
+
+ check_status_btn.click(check_status_handler, outputs=device_status)
+
+ # 无线调试
+ def handle_connect_wireless(ip, port):
+ success, message = connect_wireless_device(ip, port)
+ if success:
+ devices, device_info = get_adb_devices()
+ return device_info, f"✅ {message}"
+ return "", f"❌ {message}"
+
+ connect_wireless_btn.click(handle_connect_wireless, inputs=[wireless_ip, wireless_port], outputs=[device_status, wireless_status])
+
+ def handle_disconnect_wireless():
+ devices, _ = get_adb_devices()
+ wireless_devices = [d for d in devices if ':' in d]
+ if wireless_devices:
+ disconnect_wireless_device("")
+ devices, device_info = get_adb_devices()
+ return device_info, "✅ 已断开"
+ return "", "ℹ️ 没有无线设备"
+
+ disconnect_wireless_btn.click(handle_disconnect_wireless, outputs=[device_status, wireless_status])
+
+ def handle_enable_tcpip():
+ devices, _ = get_adb_devices()
+ usb_devices = [d for d in devices if ':' not in d and d != "未找到设备" and not d.startswith("错误")]
+ if not usb_devices:
+ return "", "❌ 没有USB设备"
+ success, message = enable_tcpip(usb_devices[0])
+ return (f"✅ {message}", "✅ TCP/IP已启用") if success else ("", f"❌ {message}")
+
+ enable_tcpip_btn.click(handle_enable_tcpip, outputs=[device_status, wireless_status])
+
+ def handle_adb_devices():
+ success, message = check_adb_connection()
+ return message, message
+
+ adb_devices_btn.click(handle_adb_devices, outputs=[device_status, wireless_status])
+
+ def handle_restart_adb():
+ success, message = restart_adb()
+ return message, message
+
+ restart_adb_btn.click(handle_restart_adb, outputs=[device_status, wireless_status])
+
+ # 清除日志
+ def clear_logs():
+ with runner.log_lock:
+ runner.logs = ""
+ return ""
+
+ clear_log_btn.click(clear_logs, outputs=log_output)
+
+ # 复制日志
+ copy_log_btn.click(
+ fn=None, inputs=[], outputs=[],
+ js="""() => {
+ let el = document.querySelector('#log-window textarea');
+ if (el && el.value) {
+ navigator.clipboard.writeText(el.value).then(() => alert('已复制')).catch(() => alert('复制失败'));
+ }
+ }"""
+ )
+
+ # 实时轮询
+ timer = gr.Timer(1.0) # 1秒刷新一次
+
+ # 追踪上一次的运行状态,用于判断任务是否从停止变为运行
+ was_running_state = gr.State(value=False)
+ # 追踪上一次检测到的运行中session
+ last_running_session = gr.State(value=None)
+
+ def poll_updates(dropdown_value, was_running, last_session, user_switched):
+ """轮询更新日志、状态和轨迹"""
+ logs = runner.get_logs()
+
+ # 检查是否有暂停信号待处理 - 使用绝对路径
+ pause_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tmp_screenshot", "pause_signal.txt")
+ if runner.is_waiting_for_input():
+ status = "⏸ 已暂停 (等待输入...)"
+ elif os.path.exists(pause_file) and runner.is_running:
+ status = "⏸ 暂停信号已发送,等待任务暂停..."
+ else:
+ status = runner.get_status()
+
+ is_running = runner.is_running
+ current_running_session = runner.get_current_session_id()
+
+ # 获取可用sessions列表
+ sessions = get_available_sessions()
+ if current_running_session and current_running_session not in sessions:
+ sessions = [current_running_session] + sessions
+
+ # 检测是否有新的session(基于session ID变化)
+ has_new_session = (
+ current_running_session and
+ current_running_session != last_session
+ )
+
+ # 更新状态
+ new_was_running = is_running
+ new_last_session = current_running_session if current_running_session else last_session
+ new_user_switched = user_switched
+
+ # Dropdown 和 Trajectory 更新策略
+ if has_new_session:
+ # 检测到新session:切换到新session并加载轨迹
+ # 同时重置 user_switched 标志
+ print(f"[DEBUG] 新session检测到: {current_running_session}")
+ dropdown_update = gr.update(choices=sessions, value=current_running_session)
+ traj_logs = load_session_logs(current_running_session)
+ print(f"[DEBUG] 加载轨迹日志: {len(traj_logs)} 条")
+ trajectory_update = logs_to_chatbot_messages(traj_logs)
+ new_user_switched = False # 新任务开始,重置用户切换标志
+ elif is_running and current_running_session and not user_switched:
+ # 任务运行中且用户没有手动切换:实时刷新当前运行session的轨迹
+ print(f"[DEBUG] 刷新运行中任务: {current_running_session}, is_running={is_running}, user_switched={user_switched}")
+ dropdown_update = gr.update(choices=sessions, value=current_running_session)
+ traj_logs = load_session_logs(current_running_session)
+ print(f"[DEBUG] 加载轨迹日志: {len(traj_logs)} 条")
+ trajectory_update = logs_to_chatbot_messages(traj_logs)
+ else:
+ # 用户手动切换了session 或 任务未运行:只更新choices,保持轨迹不变
+ print(f"[DEBUG] 保持轨迹不变: is_running={is_running}, current_session={current_running_session}, user_switched={user_switched}")
+ dropdown_update = gr.update(choices=sessions)
+ trajectory_update = gr.update()
+
+ return (
+ logs,
+ status,
+ dropdown_update,
+ trajectory_update,
+ new_was_running,
+ new_last_session,
+ new_user_switched
+ )
+
+ # 追踪用户是否手动切换了session
+ user_switched_session = gr.State(value=False)
+
+ timer.tick(
+ fn=poll_updates,
+ inputs=[session_dropdown, was_running_state, last_running_session, user_switched_session],
+ outputs=[log_output, task_status, session_dropdown, trajectory_output, was_running_state, last_running_session, user_switched_session],
+ js="""() => {
+ setTimeout(() => {
+ // 检测日志内容是否包含"任务结束"
+ let logEl = document.querySelector('#log-window textarea');
+ let taskEnded = false;
+ if (logEl && logEl.value) {
+ taskEnded = logEl.value.includes('任务结束');
+ }
+
+ // 只在任务未结束时自动滚动日志窗口
+ if (logEl && !taskEnded) {
+ logEl.scrollTop = logEl.scrollHeight;
+ }
+
+ // 轨迹窗口:只在任务未结束时自动滚动
+ let trajEl = document.querySelector('.trajectory-chatbot');
+ if (trajEl && !taskEnded) {
+ let scrollContainer = trajEl.querySelector('[class*="chatbot"]') || trajEl;
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ }
+ }, 100);
+ }"""
+ )
+
+ # 当用户手动选择session时,加载对应的轨迹并标记用户已切换
+ def on_session_select(session_id):
+ """用户手动选择session"""
+ messages = load_trajectory(session_id)
+ # 只有当选择的session与当前运行的session不同时,才标记为用户切换
+ # 这样当程序自动切换到新session时,不会被误标记
+ current_running = runner.get_current_session_id()
+ user_switched = (session_id != current_running) if current_running else False
+ print(f"[DEBUG] on_session_select: session_id={session_id}, current_running={current_running}, user_switched={user_switched}")
+ return messages, user_switched
+
+ session_dropdown.change(
+ on_session_select,
+ inputs=[session_dropdown],
+ outputs=[trajectory_output, user_switched_session]
+ )
+
+ return demo, custom_css, lightbox_head
+
+if __name__ == "__main__":
+ ui, css, head = create_ui()
+ ui.launch(
+ server_name="0.0.0.0",
+ server_port=8870,
+ show_error=True,
+ css=css,
+ head=head
+ )
\ No newline at end of file
diff --git a/web_ui/package_map_ui.py b/web_ui/package_map_ui.py
new file mode 100644
index 0000000..43972f1
--- /dev/null
+++ b/web_ui/package_map_ui.py
@@ -0,0 +1,296 @@
+"""
+Package Map Web UI 模块
+提供应用包名映射的扫描、查看、编辑、搜索和批量导入功能
+
+移植自 MAI-UI 项目并针对 gelab-zero 进行优化
+"""
+
+import os
+import pandas as pd
+from typing import Optional, Tuple, List, Dict, Any
+
+# 导入 package_map 和 package_scanner (gelab-zero 路径)
+from copilot_front_end.package_map import get_package_name_map, find_package_name, get_list_of_package_names
+from copilot_front_end.package_scanner import (
+ scan_device_apps,
+ merge_scan_result,
+ load_user_package_map,
+ save_user_package_map,
+ get_user_package_map_path
+)
+
+# 项目根目录
+PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+def scan_apps_with_progress(
+ device_id: Optional[str] = None,
+ progress_callback=None,
+ deep_scan: bool = True
+) -> Tuple[str, str, int]:
+ """
+ 扫描设备应用并返回结果
+
+ Args:
+ device_id: 设备 ID
+ progress_callback: 进度回调函数
+ deep_scan: 是否深度扫描(使用 aapt2)
+
+ Returns:
+ Tuple of (log_text, status_message, app_count)
+ """
+ log_lines = []
+ log_lines.append("🔍 开始扫描设备应用...\n")
+
+ def progress_adapter(current, total, pkg, status):
+ """适配进度回调"""
+ if progress_callback:
+ progress_val = current / total
+ label = f"[{current}/{total}] {pkg}"
+ if status == 'mapping':
+ label += " ✅ (映射匹配)"
+ elif status == 'parsed':
+ label += " 🔍 (深度解析)"
+ else:
+ label += " ⚠️ (fallback)"
+ progress_callback(progress_val, desc=label)
+
+ log_lines.append(f"{current}/{total} - {pkg} - {status}\n")
+
+ try:
+ apps = scan_device_apps(
+ device_id=device_id,
+ progress_callback=progress_adapter,
+ deep_scan=deep_scan
+ )
+
+ # 合并到用户映射
+ merge_scan_result(apps)
+
+ log_lines.append(f"\n✅ 扫描完成!共发现 {len(apps)} 个应用")
+ log_lines.append(f"📝 映射已保存到: {get_user_package_map_path()}")
+
+ stats = f"✅ 扫描完成,共发现 {len(apps)} 个应用"
+ return "".join(log_lines), stats, len(apps)
+
+ except Exception as e:
+ error_msg = f"❌ 扫描失败: {str(e)}"
+ log_lines.append(error_msg)
+ return "".join(log_lines), error_msg, 0
+
+
+def get_package_mapping_dataframe() -> pd.DataFrame:
+ """
+ 获取映射表的 DataFrame 格式
+
+ Returns:
+ pandas DataFrame with columns ['应用名', '包名']
+ """
+ mapping_list = get_list_of_package_names()
+ df = pd.DataFrame(mapping_list)
+
+ # 确保列名正确
+ if len(df.columns) >= 2:
+ df.columns = ['应用名', '包名'] + list(df.columns[2:])
+ elif len(df.columns) == 2:
+ df.columns = ['应用名', '包名']
+ elif len(df) == 0:
+ df = pd.DataFrame(columns=['应用名', '包名'])
+
+ return df
+
+
+def search_package_by_name(app_name: str) -> str:
+ """
+ 智能查找包名
+
+ Args:
+ app_name: 应用名称(支持中文)
+
+ Returns:
+ 查找结果字符串
+ """
+ if not app_name or not app_name.strip():
+ return "⚠️ 请输入应用名称"
+
+ app_name = app_name.strip()
+
+ try:
+ package_name = find_package_name(app_name)
+ result = f"✅ 找到映射:\n📱 应用名: {app_name}\n📦 包名: {package_name}"
+
+ # 检查是否是默认映射还是用户映射
+ user_map = load_user_package_map()
+ if app_name in user_map:
+ result += "\n📌 来源: 用户自定义映射"
+ else:
+ result += "\n📌 来源: 默认映射"
+
+ return result
+
+ except AssertionError:
+ # 尝试模糊搜索
+ current_map = get_package_name_map()
+ app_name_lower = app_name.lower()
+
+ matches = []
+ for key, value in current_map.items():
+ if app_name_lower in key.lower() or key.lower() in app_name_lower:
+ matches.append((key, value))
+
+ if matches:
+ result = f"⚠️ 未找到精确匹配,但有 {len(matches)} 个相似结果:\n\n"
+ for i, (app, pkg) in enumerate(matches[:10], 1):
+ result += f"{i}. {app} -> {pkg}\n"
+ return result
+ else:
+ return f"❌ 未找到应用: {app_name}\n\n💡 提示: 您可以扫描设备应用来添加映射"
+
+ except Exception as e:
+ return f"❌ 查找出错: {str(e)}"
+
+
+def load_user_mapping_yaml() -> str:
+ """
+ 加载用户映射 YAML 内容
+
+ Returns:
+ YAML 文件内容
+ """
+ user_map_file = get_user_package_map_path()
+
+ if not os.path.exists(user_map_file):
+ return f"# 用户自定义映射文件 (user_package_map.yaml)\n# 文件不存在,将自动创建\n\n"
+
+ try:
+ with open(user_map_file, 'r', encoding='utf-8') as f:
+ content = f.read()
+ return content if content.strip() else "# 空文件\n"
+ except Exception as e:
+ return f"# 读取文件失败: {str(e)}\n"
+
+
+def save_user_mapping_yaml(yaml_content: str) -> str:
+ """
+ 保存用户映射 YAML 内容
+
+ Args:
+ yaml_content: YAML 内容
+
+ Returns:
+ 状态消息
+ """
+ user_map_file = get_user_package_map_path()
+
+ try:
+ # 验证 YAML 格式
+ import yaml
+ parsed = yaml.safe_load(yaml_content)
+ if parsed is None:
+ parsed = {}
+ if not isinstance(parsed, dict):
+ return "❌ YAML 格式错误: 根元素必须是字典"
+
+ # 保存
+ with open(user_map_file, 'w', encoding='utf-8') as f:
+ f.write(yaml_content)
+
+ return f"✅ 映射表已保存 ({len(parsed)} 条记录)\n📁 文件: {user_map_file}"
+
+ except yaml.YAMLError as e:
+ return f"❌ YAML 格式错误: {str(e)}\n\n💡 提示: 请确保格式为 '应用名: 包名'"
+ except Exception as e:
+ return f"❌ 保存失败: {str(e)}"
+
+
+def batch_import_mappings(mappings_text: str) -> str:
+ """
+ 从文本批量导入映射
+
+ Args:
+ mappings_text: 映射文本,格式为 "应用名:包名" (一行一个)
+
+ Returns:
+ 状态消息
+ """
+ if not mappings_text or not mappings_text.strip():
+ return "⚠️ 请输入要导入的映射"
+
+ lines = mappings_text.strip().split('\n')
+ new_mappings = {}
+ errors = []
+
+ for line_num, line in enumerate(lines, 1):
+ line = line.strip()
+
+ # 跳过注释和空行
+ if not line or line.startswith('#'):
+ continue
+
+ # 解析
+ if ':' in line:
+ parts = line.split(':', 1)
+ if len(parts) == 2:
+ app_name = parts[0].strip()
+ package_name = parts[1].strip()
+
+ if app_name and package_name:
+ new_mappings[app_name] = package_name
+ else:
+ errors.append(f"第 {line_num} 行: 应用名或包名为空")
+ else:
+ errors.append(f"第 {line_num} 行: 格式错误")
+ else:
+ errors.append(f"第 {line_num} 行: 缺少冒号分隔符")
+
+ if not new_mappings:
+ return "❌ 没有有效的映射可导入\n\n" + "\n".join(errors)
+
+ # 合并到现有映射
+ existing = load_user_package_map()
+ original_count = len(existing)
+ updated = 0
+
+ for app_name, package_name in new_mappings.items():
+ if app_name not in existing:
+ existing[app_name] = package_name
+ updated += 1
+ else:
+ errors.append(f"⚠️ {app_name}: 已存在,已跳过")
+
+ # 保存
+ save_user_package_map(existing)
+
+ result = f"✅ 导入完成:\n"
+ result += f"• 新增: {updated} 条\n"
+ result += f"• 已存在: {len(new_mappings) - updated} 条\n"
+ result += f"• 总计: {len(existing)} 条映射\n"
+
+ if errors:
+ result += f"\n⚠️ 警告:\n" + "\n".join(errors[:10])
+ if len(errors) > 10:
+ result += f"\n... 还有 {len(errors) - 10} 条警告"
+
+ return result
+
+
+def get_mapping_statistics() -> Dict[str, Any]:
+ """
+ 获取映射表统计信息
+
+ Returns:
+ 统计信息字典
+ """
+ default_map = get_package_name_map()
+ user_map = load_user_package_map()
+
+ # 计算用户独有映射
+ user_only = {k: v for k, v in user_map.items() if k not in default_map}
+
+ return {
+ "default_count": len(default_map),
+ "user_count": len(user_map),
+ "user_only_count": len(user_only),
+ "total_count": len(default_map), # 用户映射会覆盖默认映射
+ "user_map_path": get_user_package_map_path()
+ }
diff --git a/web_ui/run.py b/web_ui/run.py
new file mode 100644
index 0000000..a38f36f
--- /dev/null
+++ b/web_ui/run.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+"""
+直接运行Web UI的脚本
+"""
+
+if __name__ == "__main__":
+ import sys
+ import os
+
+ # 将当前目录添加到Python路径
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ sys.path.insert(0, current_dir)
+
+ try:
+ from app import create_ui
+ demo = create_ui()
+ print("启动AutoGLM Web UI...")
+ demo.launch(
+ server_name="0.0.0.0",
+ server_port=7861, # 使用不同的端口
+ share=False,
+ inbrowser=True,
+ show_error=True
+ )
+ except Exception as e:
+ print(f"启动失败: {e}")
+ print("请确保已安装所有依赖:pip install -r requirements.txt")
\ No newline at end of file