更新 README.md

This commit is contained in:
2026-03-11 08:10:11 +00:00
parent 9e94d47920
commit c65cb8caef

760
README.md
View File

@ -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<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 变更,请以最新的官方文档为准,并相应调整本方案中的实现。