diff --git a/README.md b/README.md index 9acaa47..3694d79 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,760 @@ -# openclaw_client-_on_harmonyos +# 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 变更,请以最新的官方文档为准,并相应调整本方案中的实现。 \ No newline at end of file