# OpenClaw 鸿蒙客户端项目设计规格及实施要求说明书 版本: 2.0 目标平台: **HarmonyOS 6.0.2 (API 22)** 开发语言: ArkTS(严格模式) 最后更新: 2026-03-11 --- ## 1. 项目概述 ### 1.1 项目目标 开发一款运行于鸿蒙手机的原生应用,通过手机热点与树莓派5上部署的 **OpenClaw AI 服务** 通信,提供类微信的 AI 对话界面,并完整支持 Markdown 格式的内容渲染(包括流式输出效果)。应用需具备自动发现局域网内 OpenClaw 服务器的能力,并能在无法自动发现时通过手动配置连接。 ### 1.2 核心特性 - **原生 ArkUI 实现**:基于 ArkTS + Stage 模型,完全适配 API 22。 - **高性能 Markdown 渲染**:采用支付宝开源的 **FluidMarkdown 鸿蒙版**,直接调用 ArkUI 渲染引擎,支持流式增量输出,告别 WebView 性能瓶颈。 - **自动服务发现**:优先使用 mDNS/DNS-SD 协议自动发现局域网内的 OpenClaw 网关,兼容手动配置。 - **简洁对话界面**:消息气泡区分用户与 AI,支持本地历史记录存储。 --- ## 2. 技术栈与依赖 | 类别 | 名称/库 | 版本/要求 | 说明 | | ---------------- | -------------------------------- | ---------------------------------- | ------------------------------------------------------------ | | 操作系统 | HarmonyOS | 6.0.2 (API 22) | 编译 SDK 版本必须为 22,兼容运行版本 ≥12 | | 开发语言 | ArkTS | 严格模式 | 使用 `@ohos.*` 系统模块,无额外 JS 依赖 | | UI 框架 | ArkUI | API 22 内置 | 声明式 UI 开发 | | Markdown 渲染 | **@antgroup/fluidmarkdown** | ≥1.0.0 (2025.12 开源) | [GitHub 仓库](https://github.com/antgroup/FluidMarkdown) | | 网络通信 | @ohos.net.http | API 22 内置 | HTTP 客户端 | | 服务发现 | @ohos.net.mdns (待确认) | API 22 可能支持 | 需查阅 API 22 官方文档确认 mDNS 模块是否存在 | | 状态管理 | AppStorage / LocalStorage | API 22 内置 | 应用级状态持久化 | | 资源回收 | AutoFinalizer (Util) | API 22 新特性 | 用于 WebView 等资源的自动回收(本项目已无 WebView,但保留说明) | --- ## 3. 系统架构 ### 3.1 部署架构 ``` ┌─────────────────┐ WiFi Hotspot ┌─────────────────┐ │ 华为手机 │ ═══════════════════► │ 树莓派5 │ │ (HarmonyOS) │ (192.168.43.x) │ (OpenClaw) │ │ │ │ Port: 18789 │ │ ┌───────────┐ │ │ mDNS Broadcast │ │ │ OpenClaw │ │◄───────────────────────│ _openclaw._tcp │ │ │ Client │ │ HTTP API │ local. │ │ └───────────┘ │ └──────────────────┘ └─────────────────┘ ``` ### 3.2 应用架构(文件结构) ``` OpenClaw_Client/ ├── AppScope/ # (无需修改) ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ │ ├── entryability/ │ │ │ │ └── EntryAbility.ets # 应用入口 │ │ │ ├── pages/ │ │ │ │ ├── Index.ets # 主聊天界面 │ │ │ │ └── Settings.ets # 服务器配置界面 │ │ │ ├── components/ │ │ │ │ ├── ChatBubble.ets # 消息气泡组件 │ │ │ │ └── ServerDiscovery.ets # (可选)服务发现列表组件 │ │ │ ├── model/ │ │ │ │ └── ChatMessage.ets # 消息数据模型 │ │ │ ├── service/ │ │ │ │ ├── OpenClawApi.ets # OpenClaw HTTP API 封装 │ │ │ │ └── DiscoveryService.ets # (可选)mDNS 服务发现封装 │ │ │ └── utils/ │ │ │ └── StorageUtil.ets # (可选)本地存储工具 │ │ └── resources/ │ │ └── base/ │ │ └── element/ │ │ └── string.json # 国际化字符串 │ └── module.json5 # 模块配置 ├── build-profile.json5 # 项目级构建配置 └── oh-package.json5 # 项目级依赖配置 ``` --- ## 4. 功能需求 | 功能模块 | 需求描述 | 优先级 | | ---------------- | ------------------------------------------------------------ | ------ | | **服务器连接** | 支持手动输入服务器地址(IP:Port)和令牌;支持自动发现局域网内 OpenClaw 服务并列出,点击即连接。 | P0 | | **对话界面** | 类微信聊天界面,底部输入框,消息列表滚动。用户消息右对齐(纯文本),AI 消息左对齐(Markdown 渲染)。 | P0 | | **Markdown 渲染**| AI 返回内容必须完整支持 Markdown 语法(标题、列表、代码块、表格、LaTeX 公式等)。采用 **FluidMarkdown** 原生渲染,支持流式追加输出(模拟逐字生成)。 | P0 | | **消息历史** | 自动保存最近 50 条对话到本地存储,重启应用后恢复。 | P1 | | **连接状态指示** | 主界面顶部显示当前连接状态(绿点/红点),点击红点可快速跳转到设置页。 | P1 | | **错误处理** | 网络请求失败、服务端返回错误时,在对话中以气泡形式给出明确提示。 | P1 | | **自动重连** | 当网络切换或服务重启时,尝试自动重新连接(可选)。 | P2 | --- ## 5. 技术规范与约束 ### 5.1 API 22 合规要求 - 编译 SDK 版本必须为 **22** (`compileSdkVersion = 22`),兼容运行版本 ≥12 (`compatibleSdkVersion = 22` 表示仅支持 API 22 及以上设备)。 - 必须使用 **ArkTS 严格模式**,所有变量需显式类型声明。 - 网络请求必须使用 `@ohos.net.http`,并正确调用 `destroy()` 释放资源。 - 禁止直接操作 DOM 或使用 WebView 进行核心渲染(已替换为原生组件)。 ### 5.2 FluidMarkdown 集成规范 - 通过 `ohpm` 安装依赖:`ohpm install @antgroup/fluidmarkdown`。 - 在代码中导入:`import { Markdown, EMarkdownMode, MarkdownController } from '@antgroup/fluidmarkdown';` - **流式输出**:若需模拟逐字输出,可将内容按字符拆分后逐步设置 `content` 属性,FluidMarkdown 内部会增量更新。 - **交互处理**:通过 `onMarkdownNodeClick` 回调处理链接/图片点击事件,可使用 `router.pushUrl` 或 `@ohos.web.webview` 打开链接。 ### 5.3 自动服务发现(mDNS)实现说明 - **首选方案**:使用系统 mDNS 模块 `@ohos.net.mdns`(API 22 中需确认是否存在)。若存在,通过 `addLocalServiceDiscovery` 监听 `_openclaw._tcp` 服务。 - **备选方案**:若系统 mDNS 不可用,可尝试局域网广播扫描(UDP 广播 + 特定端口探测),但可靠性较低。 - **实现位置**:建议在 `Settings.ets` 页面中集成发现列表,并在 `Index.ets` 的 `aboutToAppear` 中尝试自动连接上次成功的主机。 - **权限**:若使用 mDNS,需在 `module.json5` 中添加 `ohos.permission.DISTRIBUTED_DEVICE_DISCOVERY`(需实际验证)。 --- ## 6. 文件替换与修改指南 ### 6.1 项目初始化 使用 DevEco Studio 创建一个新的 **Empty Ability** 工程,选择 **Stage 模型**,语言选择 **ArkTS**,目标 SDK 版本选择 **6.0.2(22)**。生成的标准工程目录结构如上所示。 ### 6.2 文件替换与新增清单 | 文件路径 | 操作 | 说明 | | -------------------------------------------------------- | ------ | ------------------------------------------------------------ | | `build-profile.json5` (项目根目录) | 替换 | 设置 `compileSdkVersion` 和 `compatibleSdkVersion` 为 22 | | `oh-package.json5` (项目根目录) | 修改 | 添加 `@antgroup/fluidmarkdown` 依赖 | | `entry/oh-package.json5` | 新增 | 可选,若模块级依赖独立,可在此添加依赖 | | `entry/module.json5` | 替换 | 配置权限(INTERNET、GET_NETWORK_INFO)和 Ability 信息 | | `entry/src/main/ets/entryability/EntryAbility.ets` | 替换 | 初始化全局状态 `serverBaseUrl`, `accessToken`, `isServerConnected` | | `entry/src/main/ets/model/ChatMessage.ets` | 新增 | 定义消息接口 | | `entry/src/main/ets/service/OpenClawApi.ets` | 新增 | 封装 HTTP 请求方法 `chat` 和 `healthCheck` | | `entry/src/main/ets/components/ChatBubble.ets` | 新增 | 实现消息气泡,AI 消息集成 FluidMarkdown 组件 | | `entry/src/main/ets/pages/Index.ets` | 替换 | 主聊天界面,包含消息列表、输入框、发送逻辑 | | `entry/src/main/ets/pages/Settings.ets` | 新增 | 服务器配置界面(手动输入 + 自动发现列表占位) | | `entry/src/main/resources/base/element/string.json` | 修改 | 添加必要的字符串资源(可选) | **重要**:原 WebView 相关文件(`MarkdownView.ets`, `markdown-it.min.js`)**不再使用**,请删除以避免编译冲突。 ### 6.3 配置文件详细内容 #### 6.3.1 `build-profile.json5`(项目根目录) ```json5 { "app": { "signingConfigs": [], "compileSdkVersion": 22, "compatibleSdkVersion": 22, "products": [ { "name": "default", "signingConfig": "default", "compileSdkVersion": 22, "compatibleSdkVersion": 22, "runtimeOS": "HarmonyOS" } ] }, "modules": [ { "name": "entry", "srcPath": "./entry", "targets": [ { "name": "default", "applyToProducts": ["default"] } ] } ] } ``` #### 6.3.2 `oh-package.json5`(项目根目录) ```json5 { "name": "openclaw-client", "version": "1.0.0", "description": "OpenClaw HarmonyOS Client", "dependencies": { "@antgroup/fluidmarkdown": "^1.0.0" // 请以仓库最新版本号为准 } } ``` **执行命令**:在项目根目录执行 `ohpm install` 下载依赖。 #### 6.3.3 `entry/module.json5` ```json5 { "module": { "name": "entry", "type": "entry", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": ["phone"], "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:layered_image", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ], "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.GET_NETWORK_INFO" } // 若使用 mDNS,取消下一行注释 // { // "name": "ohos.permission.DISTRIBUTED_DEVICE_DISCOVERY" // } ] } } ``` ### 6.4 源代码文件详细内容 #### 6.4.1 `entry/src/main/ets/entryability/EntryAbility.ets` ```typescript import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { AppStorage.setOrCreate('serverBaseUrl', ''); AppStorage.setOrCreate('accessToken', 'mobile-access-token-2026'); AppStorage.setOrCreate('isServerConnected', false); } onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('pages/Index', (err) => { if (err.code) { console.error(`Failed to load main page: ${JSON.stringify(err)}`); } }); } } ``` #### 6.4.2 `entry/src/main/ets/model/ChatMessage.ets` ```typescript export interface ChatMessage { id: string; role: 'user' | 'assistant' | 'system'; content: string; timestamp: Date; } ``` #### 6.4.3 `entry/src/main/ets/service/OpenClawApi.ets` ```typescript import { http } from '@ohos.net.http'; export class OpenClawApi { async chat(baseUrl: string, token: string, agentId: string = 'main', message: string): Promise { const httpRequest = http.createHttp(); const url = `${baseUrl}/v1/responses`; const payload = { model: `openclaw:${agentId}`, messages: [{ role: 'user', content: message }] }; try { const response = await httpRequest.request(url, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, extraData: JSON.stringify(payload), expectDataType: http.HttpDataType.STRING, connectTimeout: 10000, readTimeout: 30000 }); if (response.responseCode === 200) { const data = JSON.parse(response.result as string); return data.choices?.[0]?.message?.content || '[无响应内容]'; } else { throw new Error(`HTTP Error ${response.responseCode}: ${response.result}`); } } catch (error) { console.error(`OpenClawApi.chat failed: ${error.message}`); throw new Error(`请求失败: ${error.message}`); } finally { httpRequest.destroy(); } } async healthCheck(baseUrl: string, token: string): Promise { const httpRequest = http.createHttp(); try { const response = await httpRequest.request(`${baseUrl}/health`, { method: http.RequestMethod.GET, header: { 'Authorization': `Bearer ${token}` }, connectTimeout: 5000 }); return response.responseCode === 200; } catch { return false; } finally { httpRequest.destroy(); } } } ``` #### 6.4.4 `entry/src/main/ets/components/ChatBubble.ets` ```typescript import { ChatMessage } from '../model/ChatMessage'; import { Markdown, EMarkdownMode, MarkdownController } from '@antgroup/fluidmarkdown'; @Component export struct ChatBubble { @Prop message: ChatMessage; private markdownController: MarkdownController = new MarkdownController(); build() { Row() { if (this.message.role === 'user') { Blank() Column() { Text(this.message.content) .fontSize(16) .fontColor(Color.White) .padding(12) .backgroundColor('#007AFF') .borderRadius(18) .maxLines(100) .wordBreak(WordBreak.BREAK_ALL) } .padding({ right: 8 }) .alignItems(HorizontalAlign.End) } else { Column() { Markdown({ content: this.message.content, controller: this.markdownController, mode: EMarkdownMode.Normal, onMarkdownNodeClick: (data) => { if (data.type === 'link' && data.href) { console.info('Link clicked: ' + data.href); // 可调用系统浏览器或 WebView 打开链接 } } }) .backgroundColor('#E9E9EB') .borderRadius(18) .padding(8) } .padding({ left: 8 }) .alignItems(HorizontalAlign.Start) Blank() } } .width('100%') .padding({ top: 4, bottom: 4 }) } } ``` #### 6.4.5 `entry/src/main/ets/pages/Index.ets` ```typescript import { OpenClawApi } from '../service/OpenClawApi'; import { ChatMessage } from '../model/ChatMessage'; import { ChatBubble } from '../components/ChatBubble'; import { router } from '@kit.ArkUI'; @Entry @Component struct Index { @State messageList: ChatMessage[] = []; @State inputText: string = ''; @State isLoading: boolean = false; @StorageLink('serverBaseUrl') serverBaseUrl: string = ''; @StorageLink('accessToken') accessToken: string = ''; @StorageLink('isServerConnected') isServerConnected: boolean = false; private listScroller: ListScroller = new ListScroller(); private api: OpenClawApi = new OpenClawApi(); aboutToAppear() { this.loadHistory(); if (this.serverBaseUrl) { this.checkServerConnection(); } } loadHistory() { const history = AppStorage.get('chat_history'); if (history) { this.messageList = history; } } saveHistory() { const historyToSave = this.messageList.slice(-50); AppStorage.setOrCreate('chat_history', historyToSave); } async checkServerConnection() { if (!this.serverBaseUrl) return; const connected = await this.api.healthCheck(this.serverBaseUrl, this.accessToken); this.isServerConnected = connected; } async sendMessage() { if (!this.inputText.trim() || !this.serverBaseUrl) { // 提示用户配置服务器 return; } const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: this.inputText, timestamp: new Date() }; this.messageList.push(userMsg); this.inputText = ''; this.scrollToBottom(); this.isLoading = true; try { const aiResponse = await this.api.chat(this.serverBaseUrl, this.accessToken, 'main', userMsg.content); const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: aiResponse, timestamp: new Date() }; this.messageList.push(aiMsg); this.saveHistory(); } catch (error) { const errorMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: `**连接错误**\n\`\`\`\n${error.message}\n\`\`\``, timestamp: new Date() }; this.messageList.push(errorMsg); } finally { this.isLoading = false; this.scrollToBottom(); } } scrollToBottom() { setTimeout(() => { this.listScroller.scrollEdge(Edge.Bottom); }, 150); } build() { Column() { // 标题栏 Row() { Text('OpenClaw') .fontSize(20) .fontWeight(FontWeight.Bold) Blank() Circle() .width(12) .height(12) .fill(this.isServerConnected ? Color.Green : Color.Red) .margin({ right: 8 }) Button('设置') .fontSize(14) .onClick(() => { router.pushUrl({ url: 'pages/Settings' }); }) } .width('100%') .padding(16) .backgroundColor('#F5F5F5') // 消息列表 List({ scroller: this.listScroller }) { ForEach(this.messageList, (msg: ChatMessage) => { ListItem() { ChatBubble({ message: msg }) } }, (msg: ChatMessage) => msg.id) } .width('100%') .layoutWeight(1) .padding(8) // 输入区域 Row() { TextInput({ placeholder: '输入消息...', text: $$this.inputText }) .width('80%') .height(48) .fontSize(16) .enabled(this.isServerConnected) Button(this.isLoading ? '发送中' : '发送') .width('18%') .height(48) .enabled(!this.isLoading && this.inputText.length > 0 && this.isServerConnected) .onClick(() => this.sendMessage()) } .width('100%') .padding(16) .backgroundColor(Color.White) } .width('100%') .height('100%') .backgroundColor('#F0F0F0') } } ``` #### 6.4.6 `entry/src/main/ets/pages/Settings.ets` ```typescript import { router } from '@kit.ArkUI'; import { OpenClawApi } from '../service/OpenClawApi'; @Entry @Component struct Settings { @StorageLink('serverBaseUrl') serverBaseUrl: string = ''; @StorageLink('accessToken') accessToken: string = ''; @State inputUrl: string = ''; @State inputToken: string = ''; @State discoveredServers: Array<{ name: string, url: string }> = []; @State isScanning: boolean = false; @State statusMessage: string = ''; private api: OpenClawApi = new OpenClawApi(); aboutToAppear() { this.inputUrl = this.serverBaseUrl; this.inputToken = this.accessToken; this.startDiscovery(); } startDiscovery() { // TODO: 使用 mDNS 实现自动发现 // 此处为占位示例,模拟发现过程 this.isScanning = true; setTimeout(() => { this.discoveredServers = [ { name: '树莓派5 (192.168.43.101)', url: 'http://192.168.43.101:18789' } ]; this.isScanning = false; }, 2000); } selectServer(url: string) { this.inputUrl = url; } async testAndSave() { if (!this.inputUrl) { this.statusMessage = '请输入服务器地址'; return; } this.isScanning = true; // 复用为测试中状态 this.statusMessage = '正在测试连接...'; const isValid = await this.api.healthCheck(this.inputUrl, this.inputToken); if (isValid) { this.serverBaseUrl = this.inputUrl; this.accessToken = this.inputToken; this.statusMessage = '连接成功!'; setTimeout(() => router.back(), 1000); } else { this.statusMessage = '连接失败,请检查地址和令牌'; } this.isScanning = false; } build() { Column() { // 标题栏 Row() { Button('返回') .onClick(() => router.back()) Blank() Text('服务器设置') .fontSize(20) .fontWeight(FontWeight.Bold) Blank() } .width('100%') .padding(16) Column() { // 手动输入区域 TextInput({ placeholder: '服务器地址', text: $$this.inputUrl }) .height(48) .margin({ top: 20 }) .placeholderColor('#999999') Text('示例: http://192.168.43.101:18789') .fontSize(12) .fontColor('#999999') .width('100%') .textAlign(TextAlign.Start) TextInput({ placeholder: '访问令牌', text: $$this.inputToken }) .height(48) .margin({ top: 10 }) Button(this.isScanning ? '测试中...' : '测试并保存') .width('100%') .height(48) .margin({ top: 20 }) .enabled(!this.isScanning) .onClick(() => this.testAndSave()) if (this.statusMessage) { Text(this.statusMessage) .fontColor(this.statusMessage.includes('成功') ? Color.Green : Color.Red) .margin({ top: 10 }) } // 自动发现列表 Text('自动发现设备') .fontSize(18) .fontWeight(FontWeight.Bold) .margin({ top: 30 }) if (this.isScanning) { Text('正在扫描...') } else { List() { ForEach(this.discoveredServers, (item) => { ListItem() { Row() { Text(item.name) Blank() Button('连接') .onClick(() => this.selectServer(item.url)) } .width('100%') .padding(10) } }, item => item.url) } .height(200) } } .width('90%') .padding(16) } .width('100%') .height('100%') .backgroundColor('#F0F0F0') } } ``` ### 6.5 资源文件修改 #### `entry/src/main/resources/base/element/string.json` ```json { "string": [ { "name": "module_desc", "value": "OpenClaw 鸿蒙客户端" }, { "name": "EntryAbility_desc", "value": "AI 对话界面" }, { "name": "EntryAbility_label", "value": "OpenClaw" } ] } ``` --- ## 7. 自动服务发现详细设计(可选,P2) ### 7.1 方案选择 - **mDNS(推荐)**:使用 `@ohos.net.mdns` 模块。需在 `module.json5` 中添加权限,并查询 API 22 文档确认接口。 - **UDP 广播扫描**:若不支持 mDNS,可向局域网广播地址发送探测包,监听特定端口响应。但可靠性较低,且需处理多线程。 ### 7.2 mDNS 接口设计示例 ```typescript // DiscoveryService.ets import { mdns } from '@ohos.net.mdns'; export class DiscoveryService { private discovery: mdns.DiscoveryService | null = null; startDiscovery(callback: (services: Array<{ name: string, host: string, port: number }>) => void) { mdns.createDiscoveryService('_openclaw._tcp', (err, discovery) => { if (err) return; this.discovery = discovery; discovery.on('discoveryResult', (result) => { callback([{ name: result.serviceName, host: result.host, port: result.port }]); }); discovery.startDiscovering(); }); } stopDiscovery() { this.discovery?.stopDiscovering(); } } ``` **注意**:以上代码仅为示例,实际需根据 API 22 的 `@ohos.net.mdns` 模块文档调整。 --- ## 8. 构建与验证清单 ### 8.1 构建步骤 1. 在 DevEco Studio 中创建工程,选择 SDK 6.0.2(22)。 2. 按照 6.2 节清单替换/新增所有文件。 3. 在项目根目录执行 `ohpm install` 下载 FluidMarkdown 依赖。 4. 连接真机(API 22 设备)或模拟器,点击运行。 ### 8.2 功能验证 - [ ] **编译通过**:无报错,成功生成 HAP 并安装。 - [ ] **手动配置连接**:在设置页输入正确的树莓派 IP:Port 和令牌,点击测试并保存,返回主界面后连接状态指示变绿。 - [ ] **消息发送与渲染**:发送消息后,AI 响应内容以 Markdown 格式正确显示(如 `# 标题`、`- 列表`、\`代码\` 等)。 - [ ] **流式输出测试**:可通过模拟分片追加内容验证 FluidMarkdown 的增量渲染(例如使用定时器逐字增加消息内容)。 - [ ] **历史记录**:重启应用后,之前对话应保留。 - [ ] **自动发现(若实现)**:在树莓派启动 OpenClaw 并广播服务后,设置页应显示发现的设备,点击可自动填充地址。 --- ## 9. 注意事项 - **FluidMarkdown 版本**:请始终参考 [GitHub 仓库](https://github.com/antgroup/FluidMarkdown) 的最新文档,API 可能迭代。 - **mDNS 可用性**:在编写自动发现代码前,务必在 API 22 真机上测试 `@ohos.net.mdns` 模块是否存在及可用。若不可用,及时回退至手动配置。 - **资源回收**:虽然本项目无 WebView,但若使用定时器或网络请求,需确保在组件销毁时取消,避免内存泄漏。 - **错误处理**:网络请求务必包含 `try-catch`,并在 UI 层给出用户友好的提示。 --- ## 10. 文档结束 本说明书提供了 OpenClaw 鸿蒙客户端在 **API 22** 平台上的完整设计规格与实施细节。请开发人员严格按照上述文件替换与代码编写要求进行开发,确保应用的稳定性与性能。如有任何因平台更新导致的 API 变更,请以最新的官方文档为准,并相应调整本方案中的实现。