Files
openclaw_client-_on_harmonyos/README.md
2026-03-11 08:10:11 +00:00

760 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string> {
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<boolean> {
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<ChatMessage[]>('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 变更,请以最新的官方文档为准,并相应调整本方案中的实现。