openclaw源码移植到Android平台补充(Memory和Skills生态优化实践)
当AI助手从PC走向手机,我们需要重新构建什么?
背景与目标
OpenClaw 是一个运行在 PC/服务器上的 AI Gateway,拥有丰富的 skills生态,在手机端怎么复用这些生态是近期一直思考的问题。
目标:让 Android 手机端的 AI 助手能够调用这些 PC 端 skills,实现”手机发起 → 服务器执行 → 结果返回手机”的完整链路。
ZTEPhoneCraw Skills 移植实战指南
本文档记录将 OpenClaw PC 端 workspace skills 移植到 Android 手机端的完整方案、踩坑经验与架构设计。
一、PC端 vs 手机端:skills架构对比
OpenClaw 原生流程(PC/飞书 Channel)
用户(飞书/终端) │ ▼OpenClaw Gateway(PC/服务器) │ ├─ 内置 AI Agent(Claude/GPT) │ └─ 直接读取 workspace/skills/*/SKILL.md │ └─ 理解 skill 描述,自主决定调用哪个工具 │ ├─ plugin/core tools(read, write, exec, fetch...) │ └─ 直接执行,无需额外转发 │ └─ workspace skills(weather, baoyu-post-to-wechat...) └─ AI 读 SKILL.md → 调用 exec/fetch 等工具执行
特点:
-
• AI 与工具在同一进程,延迟极低 -
• skill 选择由服务器 AI 完成,上下文完整 -
• 用户直接与服务器 AI 对话
手机端移植方案
用户(手机 voice-assistant) │ ▼手机端 AI(Qwen/本地模型) │ 根据 skill description 选择工具 ▼SkillsService(Android Service) │ ├─ KOTLIN_NATIVE(本地执行) │ └─ alarm, device-info, web-search... │ ├─ REMOTE_PROXY(直接调用) │ └─ HTTP POST /tools/invoke → PC Gateway │ └─ plugin/core tools 直接执行 │ └─ AGENT_SESSION(委托服务器 AI) └─ WebSocket chat.send → agent:main:main └─ 服务器 AI 执行 workspace skill └─ chat.history 拉取结果 → 返回手机
特点:
-
• 手机端 AI 负责意图理解和 skill 选择 -
• 服务器 AI 负责 workspace skill 的具体执行 -
• 两层 AI 协作,各司其职
二、三种执行模式详解
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三、完整架构流程图
3.1 泛化任务分配流程
用户输入:"帮我查一下上海天气" │ ▼MainActivity.buildToolDefinitions() │ 从 SkillsService 获取全部 152 个 skill │ 每个 skill 包含 name + description ▼Qwen 大模型 chatWithTools() │ 根据 description 匹配用户意图 │ 输出 tool_calls: [{name:"weather", params:{location:"上海"}}] ▼MainActivity tool-use loop │ ▼SkillsService.execute("weather", params) │ 查找 skill 注册表 │ weather → executionMode = AGENT_SESSION ▼AgentSessionExecutor.execute() │ ├─ 1. 注册 chat 事件监听(先注册避免竞态) ├─ 2. chat.send → agent:main:main │ message: "请立即调用 weather 工具,参数:{location:'上海'}" ├─ 3. 等待 chat event state=final(最多 120s) ├─ 4. chat.history 拉取最后一条消息 │ 优先 role=assistant,fallback role=toolResult └─ 5. 返回结果文本 │ ▼手机端 AI 收到结果 → 生成最终回复 → 显示给用户
3.2 Skills 同步流程
voice-assistant 启动 │ ▼SkillsService.configurePcGateway(url) │ ▼PcSkillSyncManager.syncWithRetry() │ ▼WsGatewayClient.connect() → WebSocket 握手 │ ├─ tools.catalog RPC │ └─ 返回 plugin/core tools(39个) │ └─ 注册为 REMOTE_PROXY │ └─ skills.status RPC └─ 返回全部 workspace skills(含 eligible=false) └─ 注册为 AGENT_SESSION │ ▼合并去重(workspace 优先覆盖 catalog 同名 skill) │ ▼registry 注册完成,共 ~150 个 skills
四、踩坑记录与修复方案
坑1:weather 被错误注册为 REMOTE_PROXY
现象:调用 weather 返回”天气服务暂时无法获取数据”
根因:skill 合并逻辑 catalog 优先,weather 同时存在于 catalog 和 workspace skills 中,catalog 版本覆盖了 workspace 版本,导致 isWorkspaceSkill=false,注册为 REMOTE_PROXY,走 /tools/invoke 接口失败。
修复:
// 修复前(catalog 优先)catalogSkills.forEach { merged[it.name] = it }workspaceSkills.forEach { if (!merged.containsKey(it.name)) merged[it.name] = it }// 修复后(workspace 优先)catalogSkills.forEach { merged[it.name] = it }workspaceSkills.forEach { merged[it.name] = it } // workspace 覆盖 catalog
坑2:AGENT_SESSION 结果为空
现象:weather 执行显示”✓ 完成”,但返回空字符串,AI 回复”无法获取数据”
根因:chat 事件的 state=final payload 里没有 message 字段,只是执行完毕的信号。实际结果在 chat.history 里。
修复:收到 final 后主动调用 chat.history RPC 拉取最后一条消息:
"final" -> { wsClient.removeEventListener("chat") CoroutineScope(Dispatchers.IO).launch { val text = fetchLastAssistantMessage(serverSessionKey) deferred.complete(text) }}
结果提取优先级:
-
1. 优先取 role=assistant的最后一条消息 -
2. fallback 取 role=toolResult的最后一条消息(工具直接返回结果时)
坑3:huashu-slides 不出现在同步列表
现象:服务器有 huashu-slides,手机端同步后看不到
排查过程:
-
1. 加日志打印所有 workspace skill 的 eligible 值 -
2. 发现 huashu-slides 完全不在 skills.status返回列表中 -
3. 检查目录结构:备份解压后有两层目录 huashu-slides/huashu-slides/SKILL.md,OpenClaw 找不到 SKILL.md -
4. 修正目录结构后仍不出现 → 检查依赖:Playwright 未安装 -
5. 安装 Playwright 后出现但 eligible=false→ GEMINI_API_KEY 未配置
最终方案:去掉 eligible 过滤,同步所有 workspace skills(含 eligible=false),让服务器 AI 自行判断能否执行:
// 修复前if (!eligible) continue// 修复后(去掉过滤)// 所有 workspace skills 均注册为 AGENT_SESSION
坑4:AGENT_SESSION 服务器 AI 不调用 skill,直接编造结果
现象:发送任务后服务器 AI 直接回复文字,没有真正调用 skill
根因:buildTaskMessage 只发了自然语言描述,服务器 AI 可能选择直接回复而不调用工具
修复:强化指令,明确要求必须调用工具:
// 修复前"请执行 ${skill.name}。${skill.description}\n参数:$paramsJson"// 修复后"请立即调用 ${skill.name} 工具完成以下任务,不要用文字描述代替实际工具调用。\n参���:$paramsJson"
五、关键设计决策
为什么区分 REMOTE_PROXY 和 AGENT_SESSION?
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plugin/core tools 有标准化参数接口,可直接调用;workspace skills 本质是给 AI 看的 prompt 文件,必须经过服务器 AI 解读执行。
为什么用 agent:main:main 而不是新建 session?
sessions.create 在腾讯云部署版本不可用,直接复用 agent:main:main 是最稳定的方案。代价是多个 AGENT_SESSION skills 并发执行时可能互相干扰(已知问题,待解决)。
eligible=false 的 skill 要不要同步?
同步。eligible 只代表服务器判断依赖是否完整,但服务器 AI 仍可能部分执行该 skill。过滤掉会导致用户无法使用这些 skill,体验更差。
六、远端Gateway扩展
手机端扩展的skills取决于远端Gateway的能力:
|
|
|
|---|---|
|
|
uv
node, playwright(npm install),GEMINI_API_KEY |
|
|
|
|
|
|
|
|
GITHUB_TOKEN
|
|
|
|
Memory 架构设计 V2(定稿)
基于实际代码实现整理,反映当前系统真实状态。
三层记忆结构
Session Memory → 当前会话上下文(内存,不持久化到文件)Daily Memory → 每日记忆日志(文件 + Room DB)Curated Memory → 长期记忆(MEMORY.md 文件 + Room DB 向量化)
记忆生成
1. 自动 Flush(Session → Daily)
触发条件: 会话消息数 >= 50 条时自动触发
触发位置:SessionManager.shouldTriggerFlush() → MainActivity.triggerMemoryFlush()
执行流程:
-
1. AI 分析当前会话历史(tool-use loop,最多 3 轮) -
2. 调用 memory-storeskill,priority 范围 1-7 -
3. 写入当天 daily 文件: /sdcard/ZTEPhoneClaw/workspace/memory/YYYY-MM-DD.md -
4. priority >= 8 时同时写入 MEMORY.md(flush 提示词限制 1-7,不会触发) -
5. Flush 完成后自动清空会话,开启新对话
手动触发: 输入 /new 命令,无论消息数多少立即触发,逻辑相同。
2. 手动整理长期记忆(Daily → Curated)
触发方式: Memory Dashboard → “整理长期记忆” 按钮
触发位置:MainActivity.triggerCuratedMemoryOrganize()
执行流程:
-
1. 读取最近 30 天的 daily 文件(每个文件最多 3000 字符) -
2. AI 提炼高价值信息(tool-use loop,最多 5 轮) -
3. 调用 memory-storeskill,priority 必须 >= 9 -
4. 写入 daily 文件 + MEMORY.md
提炼标准(严格筛选,宁缺毋滥):
-
• 用户稳定偏好、习惯 -
• 重要个人信息(姓名、职业、项目背景等) -
• 长期有效的决策或约定 -
• 需要跨会话记住的关键事实 -
• 日常对话、临时任务、一次性信息不保存
3. 自动向量化(MEMORY.md → Room DB)
触发方式: 无需手动,memory-service 启动后自动监听文件系统
实现机制: Android FileObserver 监听 CLOSE_WRITE 事件
执行流程:
memory-store skill 写入 MEMORY.md(appendText) ↓ CLOSE_WRITE 事件触发workspaceFileObserver(监听 /sdcard/ZTEPhoneClaw/workspace/) ↓ 重新解析整个文件所有 ## sectioncuratedMemoryAdd(text, category, priority) ↓ SHA256 去重(已存在的 section 直接跳过) ↓ 只对新 section 调用 embedding API ↓ 写入 Room DB(curated_memory 表)
去重机制: 按文本 SHA256 hash 判断,已存在则跳过,不会重复插入。
embedding 失败处理: 写入 Room DB 时 embeddingJson 为空数组 [],检索时自动降级为关键词搜索。
priority 规则
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
category 触发长期记忆的特殊规则:category 为 curated / permanent / preference / user_preference / long_term 时,无论 priority 多少,都同时写入 MEMORY.md。
记忆检索
调用入口
AI 调用 memory-search tool→ MemoryService.searchHybrid()→ LayeredMemoryManager.searchAcrossLayers()
分层检索流程
第一层:Session Memory
-
• 取最近 15 轮对话,直接返回,score = 1.0 -
• 不做向量搜索,保证当前上下文完整性
第二层:Daily Memory
-
• 搜索范围:最近 7 天 -
• 搜索方式:关键词 LIKE 匹配(Room DB) -
• 最多返回 3 条
第三层:Curated Memory
-
• 搜索范围:Room DB 中所有长期记忆(来自 MEMORY.md 向量化) -
• 搜索方式:余弦相似度向量搜索,minScore = 0.35 -
• embedding 失败时自动降级为关键词 LIKE 搜索 -
• 最多返回 3 条
后处理
Temporal Decay(时间衰减)越旧的记忆 score 乘以衰减系数,避免旧记忆因向量相似度高而压过新记忆。
MMR Re-ranking(最大边际相关性去重)
-
• lambda = 0.7(相关度权重 70%,多样性权重 30%) -
• 每次选下一条时同时考虑与 query 的相关度和与已选结果的差异度 -
• 避免返回内容高度重复的记忆
向量搜索 vs 关键词搜索
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
关键参数汇总
|
|
|
|
|---|---|---|
|
|
50 条 | SessionManager.shouldTriggerFlush() |
|
|
15 轮 | LayeredMemoryManager.searchAcrossLayers() |
|
|
7 天 | LayeredMemoryManager.searchAcrossLayers() |
|
|
3 条 | LayeredMemoryManager.searchAcrossLayers() |
|
|
3 条 | LayeredMemoryManager.searchAcrossLayers() |
|
|
0.35 | LayeredMemoryManager.curatedMemorySearch() |
|
|
0.7 | LayeredMemoryManager.mmrRerank() |
|
|
30 天 | MainActivity.triggerCuratedMemoryOrganize() |
|
|
>= 9 |
|
|
|
>= 8 | KotlinSkillExecutor.executeMemoryStore() |
效果视频
夜雨聆风