乐于分享
好东西不私藏

openclaw源码移植到Android平台补充(Memory和Skills生态优化实践)

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 协作,各司其职

二、三种执行模式详解

模式
触发条件
执行路径
延迟
适用场景
KOTLIN_NATIVE
本地 Android 能力
直接调用 Android API
<100ms
闹钟、设备信息、短信
REMOTE_PROXY
PC Gateway plugin/core tools
HTTP POST /tools/invoke
200ms~1s
百度搜索、HTTP 请求
AGENT_SESSION
PC workspace skills
WebSocket → 服务器 AI
5s~60s
天气、发微信公众号、做 PPT

三、完整架构流程图

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:&#x27;上海&#x27;}"    ├─ 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. 1. 优先取 role=assistant 的最后一条消息
  2. 2. fallback 取 role=toolResult 的最后一条消息(工具直接返回结果时)

坑3:huashu-slides 不出现在同步列表

现象:服务器有 huashu-slides,手机端同步后看不到

排查过程

  1. 1. 加日志打印所有 workspace skill 的 eligible 值
  2. 2. 发现 huashu-slides 完全不在 skills.status 返回列表中
  3. 3. 检查目录结构:备份解压后有两层目录 huashu-slides/huashu-slides/SKILL.md,OpenClaw 找不到 SKILL.md
  4. 4. 修正目录结构后仍不出现 → 检查依赖:Playwright 未安装
  5. 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?

REMOTE_PROXY
AGENT_SESSION
接口
标准化 JSON Schema
自然语言 SKILL.md
AI 参与
有(服务器 AI)
延迟
低(<1s)
高(5~60s)
适用
plugin/core tools
workspace skills

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的能力:

Skill
依赖
huashu-slides
uv

nodeplaywright(npm install),GEMINI_API_KEY
weather
无额外依赖(curl wttr.in)
baoyu-post-to-wechat
微信公众号 AppID/Secret 配置
github
GITHUB_TOKEN

 环境变量
feishu-*
飞书 App credentials 配置

Memory 架构设计 V2(定稿)

基于实际代码实现整理,反映当前系统真实状态。


三层记忆结构

Session Memory   → 当前会话上下文(内存,不持久化到文件)Daily Memory     → 每日记忆日志(文件 + Room DB)Curated Memory   → 长期记忆(MEMORY.md 文件 + Room DB 向量化)

记忆生成

1. 自动 Flush(Session → Daily)

触发条件: 会话消息数 >= 50 条时自动触发

触发位置:SessionManager.shouldTriggerFlush() → MainActivity.triggerMemoryFlush()

执行流程:

  1. 1. AI 分析当前会话历史(tool-use loop,最多 3 轮)
  2. 2. 调用 memory-store skill,priority 范围 1-7
  3. 3. 写入当天 daily 文件:/sdcard/ZTEPhoneClaw/workspace/memory/YYYY-MM-DD.md
  4. 4. priority >= 8 时同时写入 MEMORY.md(flush 提示词限制 1-7,不会触发)
  5. 5. Flush 完成后自动清空会话,开启新对话

手动触发: 输入 /new 命令,无论消息数多少立即触发,逻辑相同。


2. 手动整理长期记忆(Daily → Curated)

触发方式: Memory Dashboard → “整理长期记忆” 按钮

触发位置:MainActivity.triggerCuratedMemoryOrganize()

执行流程:

  1. 1. 读取最近 30 天的 daily 文件(每个文件最多 3000 字符)
  2. 2. AI 提炼高价值信息(tool-use loop,最多 5 轮)
  3. 3. 调用 memory-store skill,priority 必须 >= 9
  4. 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 规则

priority
含义
写入目标
1-4
低价值,临时信息
仅 daily 文件
5-7
日常记录,有一定价值
仅 daily 文件
>= 8
重要信息(自动 flush 不会产生)
daily 文件 + MEMORY.md
>= 9
长期记忆(手动整理专用)
daily 文件 + MEMORY.md

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 关键词搜索

维度
关键词搜索(Daily)
向量搜索(Curated)
匹配方式
精确关键词 LIKE
语义相似度余弦计算
召回能力
只找包含关键词的记忆
找语义相近的记忆
举例
搜”运动” → 只找含”运动”的
搜”运动” → 找到”篮球、乒乓球”
性能
快,纯 SQL
需要 embedding API + 全量遍历

关键参数汇总

参数
位置
自动 Flush 阈值
50 条 SessionManager.shouldTriggerFlush()
Session 检索轮数
15 轮 LayeredMemoryManager.searchAcrossLayers()
Daily 检索天数
7 天 LayeredMemoryManager.searchAcrossLayers()
Daily 检索最大条数
3 条 LayeredMemoryManager.searchAcrossLayers()
Curated 检索最大条数
3 条 LayeredMemoryManager.searchAcrossLayers()
向量相似度阈值
0.35 LayeredMemoryManager.curatedMemorySearch()
MMR lambda
0.7 LayeredMemoryManager.mmrRerank()
手动整理读取天数
30 天 MainActivity.triggerCuratedMemoryOrganize()
手动整理 priority 阈值
>= 9
flush prompt 中约束 AI
自动写入长期记忆阈值
>= 8 KotlinSkillExecutor.executeMemoryStore()

效果视频

已关注

关注

重播 分享