Files
NianToB/docs/PRD.md

75 KiB
Raw Permalink Blame History

智念桌面端 PRDYINIAN Hotel Desktop

版本: v0.3(完整草案)
状态: 第一至九章与附录已补齐,进入评审与拆票阶段
上次更新: 2026-04-26
代号: YINIAN · 内部用 yinian-desktop


目录与推进议程

章节 主题 状态
第一章 产品概述 已成稿
第二章 系统架构 已成稿
第三章 领域模型与端口契约(kernel-core 已成稿
第四章 YINIAN Skill Spec v0.1 已成稿
第五章 客户端模块设计Main / Renderer 各模块) 草案已补齐
第六章 服务端 API 契约 草案已补齐
第七章 Skills v1 详细设计 草案已补齐
第八章 非功能性要求(安全 / 性能 / 端口检测 / 离线 / 可观测性) 草案已补齐
第九章 里程碑、交付计划与风险登记册 草案已补齐
附录 A 术语表 草案已补齐
附录 B OpenClaw 上游同步策略 草案已补齐
附录 C CI 强制规则清单 草案已补齐

第一章 · 产品概述

1.1 产品定位

智念桌面端YINIAN Hotel Desktop是面向中国酒店运营场景的 AI Agent 桌面客户端。它把 AI Agent 的能力封装成"装在前台 / 财务 / 运营那台电脑里的工具"——零配置、零学习曲线、登录即用。

技术上采用 壳-核-能力包 三段式架构:

  • Shell = 桌面 UI + 登录态 + 本地存储 + 通知
  • Kernel = Agent runtime当前 OpenClaw可替换
  • 能力包Skills = 酒店行业 know-how 编码而成的可执行单元

三段之间通过明确的端口契约解耦,确保 核可以替换、能力包可以跨平台复用、UI 可以独立演进

1.2 目标用户

角色 核心特征 与本产品的关系
酒店前台 / 运营 普通办公能力、不懂技术、工作高频重复 日常使用者,主要交互对象
酒店店长 / 老板 看数据做决策、不会每天打开 价值确认者,看每日 / 每周推送
NIANXX 实施 / 客成 需要远程开通、远程诊断 间接用户,运维入口

1.3 核心价值主张

对酒店: 把"原本需要 1 名运营每天 2-3 小时的 OTA 巡检 / 早报整理 / 客评回复"压缩到 5 分钟(看推送 + 处理异常)。

对 NIANXX 每个酒店客户的运营 know-how 沉淀为 可复用、可升级、可跨内核移植 的 skill 资产构成长期护城河。Skills 是产品的真正 IP桌面壳只是当下的最佳载体。

1.4 与原 ClawX 的本质区别

智念桌面端基于 ClawX fork但产品形态是另一个东西

维度 ClawX(上游) 智念桌面端
用户画像 开发者 / 极客 酒店运营 / 老板
配置心智 自填 API key + 装 skill 登录即用、零配置
Skill 来源 用户自由浏览安装 服务端按客户等级远程下发
Provider / Key 用户自管 NIANXX 服务端代理,前端无 key
数据归属 全部本地 本地优先 + 云端选择性同步
内核耦合 UI 直接调 OpenClaw RPC 通过 Adapter 隔离,可替换
视觉调性 通用 dev tool 高端 SaaS白底 + 单一品牌蓝 #1A56DB
通知通道 50+ 全开放 继承 OpenClaw 全部通道 + NIANXX 自建渠道

1.5 不做什么(防 scope creep

  • 不做酒店 PMS —— 不与西软、绿云正面冲突,专注 AI Agent 工具层
  • 不做客户私有部署 —— v1 阶段统一 SaaS 模式
  • 不做开发者 Marketplace —— skill 由 NIANXX 集中维护与下发
  • 不做手机端 —— 单点突破桌面办公场景,移动端通过企微 / 钉钉接收推送
  • 不做通用对话助手 —— 所有能力围绕"酒店运营任务"组织
  • 不暴露 API Key 配置 —— 所有 LLM 调用走服务端代理,客户端永不持有 key

第二章 · 系统架构

2.1 四层洋葱模型

┌──────────────────────────────────────────────┐
│  Vertical Layer · 业务垂直层                  │
│  酒店 skill / 报表 / 数据回流                  │
├──────────────────────────────────────────────┤
│  Shell Layer · 桌面壳 / UI                    │
│  React 渲染层,零内核依赖                      │
├──────────────────────────────────────────────┤
│  Adapter Layer · 适配器(关键解耦点)           │
│  把领域指令翻译成内核 RPC                      │
├──────────────────────────────────────────────┤
│  Kernel Layer · Agent 内核                    │
│  当前 OpenClaw可替换                         │
└──────────────────────────────────────────────┘

核心原则:每一层只能依赖更深的层,不能反向依赖。

Adapter 是这套架构最关键的一层——它的存在意味着UI 写成什么样跟内核换不换没关系内核换了UI 不用动。

2.2 四个核心端口Port

智念桌面端的所有能力围绕四个端口组织。Shell 只与端口对话,端口由 Adapter 提供具体实现。

Port 1 · ConversationPort

对话与消息流。Shell 操作:发消息、订阅消息流、订阅工具调用、订阅 artifact 输出。

Port 2 · SkillPort

技能注册与执行。Shell 操作:列出已开通技能、触发技能、订阅技能执行进度、查看历史执行记录。

Port 3 · SchedulerPort

定时与触发。Shell 操作:增删改查 cron 任务、查看执行历史、临时禁用 / 启用。

Port 4 · NotificationPort

对外通知派发。Shell 操作:发现可用通道、发送通知、订阅送达状态。采用"通道发现 + 派发"模式——通道列表由内核动态提供,kind 字段为开放字符串Shell 不写死任何通道枚举。

详细的 TypeScript 接口签名见 第三章

2.3 进程模型

┌──────────────────────────────────────────────────────────────┐
│                  智念桌面端 (Electron App)                    │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │           Electron Main Process                         │ │
│  │  • 窗口与生命周期                                         │ │
│  │  • Auth 模块token 管理 / keychain 存储)                │ │
│  │  • Config Sync 模块(与 NIANXX 服务端通信)               │ │
│  │  • Skill Manager拉取 / 写入 skill bundle              │ │
│  │  • Kernel LifecycleOpenClaw Gateway 子进程管理)        │ │
│  │  • Port Detection端口冲突检测与处理                    │ │
│  │  • Notification Bridge系统托盘 + OS 通知)              │ │
│  └─────────────────────┬──────────────────────────────────┘ │
│                        │ IPC (contextBridge)                 │
│  ┌─────────────────────▼──────────────────────────────────┐ │
│  │           Renderer Process (React 19)                  │ │
│  │  • 全部 UI                                              │ │
│  │  • 通过 kernel-context 注入的 Port 访问能力              │ │
│  │  • 不直接 import OpenClaw 任何 API 或类型                │ │
│  └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────┬──────────────────────────┘
                                   │ WebSocket (JSON-RPC)
                                   ▼
┌──────────────────────────────────────────────────────────────┐
│  OpenClaw Gateway(子进程) · 监听 127.0.0.1:18928             │
│  Agent runtime / Skill 执行 / Cron / Sandbox                  │
└──────────────────────────────────┬──────────────────────────┘
                                   │ HTTPS
                                   ▼
┌──────────────────────────────────────────────────────────────┐
│  NIANXX 服务端                                                 │
│  • Auth(登录 / token 刷新)                                    │
│  • Config Sync(用户 / 酒店 / agent 配置)                      │
│  • Skill Manifest(已开通技能清单 + bundle 下载链接)           │
│  • LLM Proxy(代理 OpenAI / Anthropic 调用 + 计费 + 脱敏)      │
│  • Data Sink(数据回流:执行结果、报表)                         │
│  • Custom Notification Channels(自建企微 / 钉钉 / 自定义)      │
└──────────────────────────────────────────────────────────────┘

关键设计点

  1. Gateway 由 Main 拉起子进程,生命周期由 Main 管理。Main 退出时确保 Gateway 优雅关闭。
  2. Gateway 端口固定 127.0.0.1:18928,与上游 ClawX 默认端口18789刻意错开避免与原版 ClawX 共存冲突。Main 启动前执行端口检测协议(详见 §2.7)。
  3. Renderer 通过 WebSocket 连接 Gateway(同机回环,性能 OK连接地址与短期会话凭证由 Main 通过 contextBridge 下发,所有调用必须经过 Adapter 包装层。
  4. 所有外部网络通信原则上由 Main 发起NIANXX 服务端、第三方 APIRenderer 不直接发出公网请求。
  5. LLM 调用全部走 NIANXX 服务端代理Gateway 配置中的 OPENAI_BASE_URL 指向 NIANXX 代理网关,密钥永远不下发到客户端。这一条是硬约束,不留客户自带 key 的口子。

2.4 Monorepo 包结构

使用 pnpm workspace

yinian-desktop/
├── apps/
│   └── desktop/                       # 应用本体(原 ClawX 改造而来)
│       ├── electron/                  # Main 进程代码
│       └── src/                       # Renderer 代码
├── packages/
│   ├── kernel-core/                   # 领域类型 + Port 接口(零内核依赖)
│   ├── kernel-adapter-openclaw/       # OpenClaw 适配器
│   ├── kernel-context/                # 适配器注入与切换的统一入口
│   ├── skill-spec/                    # YINIAN Skill Spec 类型 + 编译器
│   ├── skills-hotel-core/             # 通用酒店 skill 基类与共享逻辑
│   ├── skills/
│   │   ├── ctrip-price-monitor/
│   │   ├── meituan-price-monitor/
│   │   ├── daily-report/
│   │   └── review-reply-helper/
│   └── ui-kit/                        # 设计 tokens + 公共组件
├── vendor/
│   └── openclaw/                      # OpenClaw submodule
├── docs/
│   └── PRD.md                         # 本文档
└── tools/
    └── eslint-rules/                  # 强制依赖方向的自定义规则

2.5 严格的依赖方向CI 强制)

依赖图是单向的,由 ESLint no-restricted-imports + 自定义规则在 CI 强制:

包 / 路径 允许 import
apps/desktop/src/renderer @yinian/kernel-core@yinian/kernel-context@yinian/ui-kit@yinian/skill-spec;仅 bootstrap.tsx 可 import @yinian/kernel-adapter-openclaw
apps/desktop/electron/main 上述 + @yinian/kernel-adapter-openclaw
@yinian/kernel-core 仅纯工具库zod、date-fns 等),禁止 任何内核相关依赖
@yinian/kernel-adapter-openclaw @yinian/kernel-core + OpenClaw SDK
@yinian/kernel-context @yinian/kernel-core + 由调用方注入的具体 adapter
@yinian/skills/* @yinian/skill-spec + @yinian/skills-hotel-core

违规视为 PR 红灯,不允许合并。 这条规则比文档约定有效得多——它把"层级纪律"从一个口号变成了 CI 红灯。

2.6 Renderer 如何拿到 Port 实现:唯一的依赖注入点

整个系统中,只有一个文件 把"具体内核"和"应用"绑在一起:

// apps/desktop/src/bootstrap.tsx
import { createKernelContext } from '@yinian/kernel-context';
import { createOpenClawAdapter } from '@yinian/kernel-adapter-openclaw';

const endpoint = await window.yinian.app.getKernelEndpoint();

const kernel = createKernelContext({
  adapter: createOpenClawAdapter({
    wsUrl: endpoint.wsUrl,
    token: endpoint.token,
  }),
});

// 之后整个 React 树通过 useKernel() Hook 拿所有 Port
// 例如const { conversation } = useKernel(); conversation.send(...)

未来更换内核只需要改这一个文件——把 createOpenClawAdapter 换成 createXxxAdapter,整个 UI 树不用动。

这是端口与适配器模式落地最关键的实操细节。所有"换内核"的承诺,最终都要兑现在这一个文件能不能干净地切换。

2.7 启动时序与端口检测

1.  用户打开应用
2.  Electron Main 启动
3.  Main 检查 keychain 中 token
       ├─ 无 / 过期 → 渲染 LoginPageGateway 暂不启动)
       └─ 有效     → 继续步骤 4
4.  Main 调用 NIANXX `/auth/me` 验证 token
5.  Main 拉取 `/config/sync`:用户 + 酒店 + agent 配置
6.  Main 拉取 `/skills/manifest`,比对本地版本
7.  Main 下载 / 更新需要的 skill bundle写入 OpenClaw skills 目录
8.  Main 执行端口检测协议127.0.0.1:18928
       ├─ 未占用       → 启动 Gateway
       ├─ 占用且握手通过 → 是上次崩溃残留的我们的 Gateway复用或 kill+重启
       └─ 占用但握手失败 → 进入"端口冲突"失败态 UI引导用户处理
9.  Main 启动 OpenClaw Gateway 子进程(注入 NIANXX LLM proxy endpoint
10. Main 健康检查 Gateway轮询直到 ready
11. Renderer 通过 kernel-context 建立连接,进入 MainAppPage

端口检测协议

为区分"占用 18928 的进程是不是我们自己",定义两个本地端点:

  • GET http://127.0.0.1:18928/__yinian_probe__:无需 token只返回最小服务身份用于端口占用识别
  • GET http://127.0.0.1:18928/__yinian_health__:需要 Main 注入的 healthToken,返回详细健康状态

probe 响应:

{ "service": "yinian-kernel-gateway", "version": "x.y.z", "started_at": <unix_ms> }
  • Main 启动时先 GET probe 端点3 秒超时
    • 200 + 正确 service 字符串 → 是我们的 Gateway直接复用或 kill 重启,按用户配置)
    • 任意其他响应 / 超时 / 连接拒绝 → 视为"被陌生进程占用"
  • Main 启动或复用 Gateway 后,再用 healthToken 访问 health 端点确认 ready
  • 失败态 UI 提供:识别占用进程(用 lsof / netstat 风格的系统命令)、一键终止、或选择临时使用 18929 备用端口

这个协议成本极低(两个 HTTP 端点 + 几十行检测逻辑),但能解决"用户机器上同时装了原版 ClawX"或"上次没干净退出"两个最常见的体验雷点。

2.8 此架构需要诚实承认的泄漏点

完美的内核无关性不存在。以下两处会有泄漏,提前画好红线:

泄漏点 1Sandbox / 安全策略

OpenClaw 的 Docker sandbox 模式如果换到无 sandbox 的内核就没了。短期对策sandbox 配置作为 Adapter 内部细节,不暴露给 Shell长期看入风险登记册第九章

泄漏点 2流式输出语义差异

不同内核的 token 流式输出语义会有细微差异中断恢复点、tool call 内嵌位置、reasoning block 格式等。Adapter 必须把所有内核的输出归一到统一的 ConversationStreamEvent 类型,由 Adapter 吃掉差异。这是 Adapter 实现复杂度最高的部分,也是 Adapter 真正的价值所在。

关于通知通道v0.1 设计阶段曾考虑把通道收口为四类抽象,最终采用"通道发现 + 派发"模式(详见 §3.6)——继承内核全部通道 + 允许 NIANXX 服务端动态注册自建通道。这不算泄漏点,是有意设计。


第三章 · 领域模型与端口契约kernel-core

3.1 设计原则

kernel-core 是整个系统中唯一不知道 OpenClaw 存在的代码包。任何 PR 试图在 kernel-core 里 import OpenClaw、使用 OpenClaw 类型、或硬编码 OpenClaw 的 RPC 方法名,都直接 CI 红灯。

这一章定义的所有类型与接口,未来无论换到哪个内核,都不会改变。换内核 = 写一个新的 Adapter 包实现这些接口,仅此而已。

设计准则:

  1. 领域语言优先 —— 用"对话"、"技能"、"任务"、"通知",不用 agent / channel / session 等内核术语
  2. 流式优先 —— 所有可能耗时的操作返回 AsyncIterable<Event>,不返回单个 Promise
  3. 不可变快照 —— 跨进程边界的数据全部是 plain object可被 JSON 序列化),不带类方法
  4. 错误是值 —— 错误以 KernelError 类型显式建模,不依赖 throw / catch 跨进程
  5. 零运行时依赖 —— 除 zodschema 验证)、date-fns(日期处理)外不允许其他依赖

3.2 共享核心类型

// === 标识与身份Branded Types 防误用) ===

export type UserId         = string & { readonly __brand: 'UserId' };
export type HotelId        = string & { readonly __brand: 'HotelId' };
export type ConversationId = string & { readonly __brand: 'ConversationId' };
export type MessageId      = string & { readonly __brand: 'MessageId' };
export type SkillId        = string & { readonly __brand: 'SkillId' };
export type TaskId         = string & { readonly __brand: 'TaskId' };
export type ExecutionId    = string & { readonly __brand: 'ExecutionId' };

// === 用户与酒店 ===

export interface User {
  id: UserId;
  name: string;
  avatar?: string;
  email?: string;
  phone?: string;
  hotels: HotelMembership[];
}

export interface HotelMembership {
  hotelId: HotelId;
  role: 'owner' | 'manager' | 'staff' | 'viewer';
}

export interface Hotel {
  id: HotelId;
  name: string;
  brand?: string;
  city: string;
  ota: HotelOTABinding[];
}

export interface HotelOTABinding {
  ota: 'ctrip' | 'meituan' | 'booking' | 'agoda' | 'qunar' | string;
  externalId: string;        // 平台侧的酒店 ID
  enabled: boolean;
}

// === 消息与 Artifact ===

export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';

export interface Message {
  id: MessageId;
  conversationId: ConversationId;
  role: MessageRole;
  blocks: ContentBlock[];
  createdAt: number;          // unix ms
}

export type ContentBlock =
  | TextBlock
  | ToolCallBlock
  | ToolResultBlock
  | ArtifactBlock
  | ReasoningBlock;

export interface TextBlock      { type: 'text'; text: string }
export interface ReasoningBlock { type: 'reasoning'; text: string }

export interface ToolCallBlock {
  type: 'tool_call';
  toolCallId: string;
  name: string;
  input: Record<string, unknown>;
}

export interface ToolResultBlock {
  type: 'tool_result';
  toolCallId: string;
  status: 'success' | 'failed';
  output: unknown;
  error?: KernelError;
}

export interface ArtifactBlock { type: 'artifact'; artifact: Artifact }

export type Artifact =
  | { kind: 'markdown'; id: string; title?: string; content: string }
  | { kind: 'table';    id: string; title?: string; columns: string[]; rows: unknown[][] }
  | { kind: 'chart';    id: string; title?: string; spec: unknown }     // vega-lite spec
  | { kind: 'image';    id: string; title?: string; url: string }
  | { kind: 'file';     id: string; title?: string; url: string; mime: string };

// === 错误模型 ===

export interface KernelError {
  code: KernelErrorCode;
  message: string;
  retryable: boolean;
  cause?: unknown;
}

export type KernelErrorCode =
  | 'kernel_unavailable'
  | 'kernel_timeout'
  | 'kernel_aborted'
  | 'auth_required'
  | 'permission_denied'
  | 'skill_not_found'
  | 'skill_execution_failed'
  | 'invalid_input'
  | 'rate_limited'
  | 'unknown';

3.3 ConversationPort

export interface ConversationPort {
  list(query?: { hotelId?: HotelId; limit?: number }): Promise<Conversation[]>;

  get(id: ConversationId): Promise<Conversation>;

  create(input: CreateConversationInput): Promise<Conversation>;

  delete(id: ConversationId): Promise<void>;

  /** 发送消息并订阅响应流 */
  send(input: SendMessageInput): AsyncIterable<ConversationStreamEvent>;

  /** 中止正在进行的消息流(流会以 error 事件结束code = kernel_aborted */
  abort(conversationId: ConversationId): Promise<void>;

  history(
    conversationId: ConversationId,
    opts?: { before?: MessageId; limit?: number }
  ): Promise<Message[]>;
}

export interface Conversation {
  id: ConversationId;
  title: string;
  hotelId: HotelId;
  createdAt: number;
  updatedAt: number;
  messageCount: number;
}

export interface CreateConversationInput {
  hotelId: HotelId;
  title?: string;
}

export interface SendMessageInput {
  conversationId: ConversationId;
  content:
    | TextBlock
    | { type: 'invoke_skill'; skillId: SkillId; input: Record<string, unknown> };
}

export type ConversationStreamEvent =
  | { type: 'message_start';     message: Pick<Message, 'id' | 'role' | 'createdAt'> }
  | { type: 'text_delta';        messageId: MessageId; delta: string }
  | { type: 'reasoning_delta';   messageId: MessageId; delta: string }
  | { type: 'tool_call';         messageId: MessageId; block: ToolCallBlock }
  | { type: 'tool_result';       messageId: MessageId; block: ToolResultBlock }
  | { type: 'artifact';          messageId: MessageId; artifact: Artifact }
  | { type: 'message_complete';  messageId: MessageId; message: Message }
  | { type: 'error';             error: KernelError };

3.4 SkillPort

export interface SkillPort {
  list(hotelId: HotelId): Promise<InstalledSkill[]>;

  get(id: SkillId): Promise<InstalledSkill>;

  invoke(input: InvokeSkillInput): AsyncIterable<SkillExecutionEvent>;

  abort(executionId: ExecutionId): Promise<void>;

  history(skillId: SkillId, opts?: { hotelId?: HotelId; limit?: number }): Promise<SkillExecution[]>;

  getExecution(executionId: ExecutionId): Promise<SkillExecution>;
}

export interface InstalledSkill {
  id: SkillId;
  spec: SkillSpec;            // 见第四章
  enabled: boolean;
  installedAt: number;
  lastInvokedAt?: number;
}

export interface InvokeSkillInput {
  skillId: SkillId;
  hotelId: HotelId;
  input: Record<string, unknown>;
  triggeredBy: 'user' | 'schedule' | 'webhook' | 'reply';
}

export type SkillExecutionEvent =
  | { type: 'started';   executionId: ExecutionId; startedAt: number }
  | { type: 'progress';  executionId: ExecutionId; phase: string; ratio?: number; message?: string }
  | { type: 'log';       executionId: ExecutionId; level: 'info' | 'warn' | 'error'; message: string }
  | { type: 'artifact';  executionId: ExecutionId; artifact: Artifact }
  | { type: 'completed'; executionId: ExecutionId; output: Record<string, unknown>; finishedAt: number }
  | { type: 'failed';    executionId: ExecutionId; error: KernelError; finishedAt: number };

export interface SkillExecution {
  id: ExecutionId;
  skillId: SkillId;
  hotelId: HotelId;
  input: Record<string, unknown>;
  output?: Record<string, unknown>;
  artifacts: Artifact[];
  status: 'running' | 'success' | 'failed' | 'aborted';
  triggeredBy: InvokeSkillInput['triggeredBy'];
  startedAt: number;
  finishedAt?: number;
  error?: KernelError;
}

3.5 SchedulerPort

export interface SchedulerPort {
  list(hotelId?: HotelId): Promise<ScheduledTask[]>;
  get(id: TaskId): Promise<ScheduledTask>;
  create(input: CreateTaskInput): Promise<ScheduledTask>;
  update(id: TaskId, patch: UpdateTaskPatch): Promise<ScheduledTask>;
  delete(id: TaskId): Promise<void>;
  pause(id: TaskId): Promise<void>;
  resume(id: TaskId): Promise<void>;
  history(id: TaskId, opts?: { limit?: number }): Promise<TaskExecutionRecord[]>;
}

export interface ScheduledTask {
  id: TaskId;
  hotelId: HotelId;
  name: string;
  cron: string;               // 标准 5 字段 cron 表达式
  timezone: string;           // IANA 时区,如 'Asia/Shanghai'
  target: TaskTarget;
  enabled: boolean;
  createdAt: number;
  updatedAt: number;
  lastRunAt?: number;
  nextRunAt: number;
}

export type TaskTarget =
  | { kind: 'skill';  skillId: SkillId; input: Record<string, unknown> }
  | { kind: 'prompt'; conversationId?: ConversationId; prompt: string };

export interface CreateTaskInput {
  hotelId: HotelId;
  name: string;
  cron: string;
  timezone: string;
  target: TaskTarget;
}

export type UpdateTaskPatch = Partial<Pick<
  ScheduledTask,
  'name' | 'cron' | 'timezone' | 'target' | 'enabled'
>>;

export interface TaskExecutionRecord {
  id: ExecutionId;
  taskId: TaskId;
  startedAt: number;
  finishedAt?: number;
  status: 'running' | 'success' | 'failed' | 'skipped';
  error?: KernelError;
  /** 关联的 SkillExecution 或 Conversation前端可点击跳转 */
  link?:
    | { kind: 'skill_execution'; id: ExecutionId }
    | { kind: 'conversation';    id: ConversationId };
}

3.6 NotificationPort通道发现 + 派发模式)

export interface NotificationPort {
  /** 列出当前可用的通知通道(来自内核 + NIANXX 服务端注册) */
  channels(hotelId: HotelId): Promise<NotificationChannel[]>;

  /** 派发通知到一个或多个通道 */
  send(input: SendNotificationInput): Promise<NotificationDispatch>;

  /** 查询发送状态 */
  status(dispatchId: string): Promise<NotificationDispatch>;
}

export interface NotificationChannel {
  id: string;
  /** 开放字符串而非枚举:'email' | 'sms' | 'wecom' | 'dingtalk' |
   *  'whatsapp' | 'telegram' | 'slack' | <NIANXX 自建 kind> | ... */
  kind: string;
  label: string;              // 用户可见名称,如"前台微信群"
  recipient: string;          // 形态依 kind 而定(邮箱、群 ID、手机号、URL 等)
  enabled: boolean;
  source: 'kernel' | 'nianxx';   // 区分内核自带还是 NIANXX 服务端注册
  iconUrl?: string;
}

export interface SendNotificationInput {
  hotelId: HotelId;
  channelIds: string[];
  content: NotificationContent;
}

export type NotificationContent =
  | { kind: 'text';     text: string }
  | { kind: 'markdown'; markdown: string }
  | { kind: 'card';     title: string; body: string; actions?: NotificationAction[] };

export interface NotificationAction {
  label: string;
  url?: string;
  reply?: string;             // 用户点击后回写到对应 conversation
}

export interface NotificationDispatch {
  id: string;
  perChannel: { channelId: string; status: 'pending' | 'sent' | 'failed'; error?: KernelError }[];
  createdAt: number;
}

关于通道kind 是开放字符串而非枚举。Shell 拿到 channel 列表后,根据 kind 决定渲染方式(图标、是否需要二次确认等),但不需要为每种 kind 写专属代码。OpenClaw 内核自带 50+ 种通道全部继承可用NIANXX 后续在服务端实现自建通道(如企微群机器人、钉钉自定义机器人),通过 source: 'nianxx' 注入到 channels() 返回结果中。

3.7 Adapter 契约

每个内核适配器导出一个工厂函数,返回 Adapter 实例:

export interface Adapter {
  readonly info: { name: string; kernelVersion: string };

  /** 启动连接(建立 WebSocket 等) */
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  health(): Promise<{ ready: boolean; details?: Record<string, unknown> }>;

  /** 四个 Port 的实现 */
  readonly conversation: ConversationPort;
  readonly skill: SkillPort;
  readonly scheduler: SchedulerPort;
  readonly notification: NotificationPort;
}

export type AdapterFactory<Config> = (config: Config) => Adapter;

OpenClaw 适配器实现:

// packages/kernel-adapter-openclaw/src/index.ts
export interface OpenClawAdapterConfig {
  wsUrl: string;
  token: string;
  reconnect?: { maxAttempts: number; backoffMs: number };
}

export const createOpenClawAdapter: AdapterFactory<OpenClawAdapterConfig> = (config) => {
  // 内部建立 WebSocket订阅事件把 OpenClaw RPC 包装为 Port 实现
  // 流式事件做 OpenClaw → ConversationStreamEvent 的归一化映射
  return {
    info: { name: 'openclaw', kernelVersion: '...' },
    connect:    /* ... */,
    disconnect: /* ... */,
    health:     /* ... */,
    conversation: createOpenClawConversationPort(config),
    skill:        createOpenClawSkillPort(config),
    scheduler:    createOpenClawSchedulerPort(config),
    notification: createOpenClawNotificationPort(config),
  };
};

3.8 流式语义统一Adapter 的核心职责)

不同内核的流式输出OpenClaw / Anthropic / 自研在底层格式上有差异Adapter 必须把它们归一化到 ConversationStreamEvent。约定:

  1. 每个 assistant 消息以 message_start 开始,以 message_complete 结束
  2. text_delta 是增量文本,前端做拼接
  3. tool_call 一次性给出完整工具调用(不切片下发);tool_result 同理
  4. artifact 独立事件,不嵌套在 text 中
  5. 任何错误都用 error 事件结束流(不抛异常)
  6. abort() 后内核必须发出 error 事件code = kernel_aborted

这套语义是 Adapter 的核心工作,也是它复杂度最高的部分。把这一层做好,整个 Shell 层就再也不需要关心"我在跟哪个内核说话"。


第四章 · YINIAN Skill Spec v0.1

4.1 设计目标

YINIAN Skill 是产品的核心 IP——它把酒店行业的 know-howOTA 巡检逻辑、价格异常判定规则、客评回复话术等编码为可执行单元。Skill Spec 决定了这些 IP 资产的形态。

设计目标按优先级:

  1. 跨内核可移植 —— Skill 描述层不耦合任何内核的具体 API
  2. 声明式优先 —— 元信息、输入输出、权限都用静态描述,能被工具静态校验、自动生成 UI
  3. 可组合 —— skill 可以调用 skill构成工作流
  4. 可版本化 —— 单个 skill 自身有版本Skill Spec 自身也有版本v0.1 → v0.2 …)
  5. 可远程下发 —— 每个 skill 是一个独立目录,能打包、签名、远程拉取

4.2 Skill 包目录结构

yinian-skill-ctrip-price-monitor/
├── manifest.yaml              # 必须:声明性元数据
├── README.md                  # 推荐:人读说明
├── prompts/                   # 推荐:提示词文件(与执行环境无关)
│   ├── system.md
│   └── analysis.md
├── logic/                     # 必须业务逻辑TS / Python
│   ├── index.ts               # 入口
│   ├── parsers/
│   └── rules/
├── templates/                 # 推荐:通知 / 报告模板Handlebars
│   ├── daily-report.md.hbs
│   └── anomaly-alert.md.hbs
├── fixtures/                  # 推荐:测试用例
│   └── sample-response.json
├── adapters/                  # 必须:适配各内核的编译产物源
│   ├── openclaw.ts
│   └── (mcp.ts、claude-skill.ts 后续按需添加)
└── tests/                     # 推荐:单元 / 集成测试

4.3 manifest.yaml 完整 schema

spec_version: "0.1"               # YINIAN Skill Spec 版本

# === 身份 ===
id: "ctrip-price-monitor"          # 全局唯一 slug
name: "携程酒店价格巡检"
version: "1.0.0"                   # skill 自身版本semver
author: "NIANXX"
category: "ota-monitoring"         # ota-monitoring | reporting | guest-comm | ops-automation
icon: "ctrip"                      # ui-kit 已注册的 icon 名,或 url

# === 描述 ===
description: |
  每日巡检指定酒店在携程的房型与报价;
  对异常价格(如低于成本价、相邻日期价差过大)触发告警。

# === 输入契约 ===
inputs:
  - name: hotelId
    type: hotel_id                 # YINIAN 自定义类型UI 自动渲染酒店选择器
    required: true
    label: "酒店"

  - name: dateRange
    type: date_range
    required: false
    label: "巡检日期范围"
    default: { kind: "next_n_days", n: 7 }

  - name: priceFloor
    type: number
    required: false
    label: "成本价下限(用于异常判定)"
    unit: "CNY"

# === 输出契约 ===
outputs:
  - name: snapshot
    type: object
    schema:                        # zod-style 子集
      properties:
        rooms:
          type: array
          item:
            properties:
              roomType:  { type: string }
              date:      { type: string, format: date }
              price:     { type: number }
              available: { type: boolean }

  - name: anomalies
    type: array
    item:
      properties:
        roomType: { type: string }
        date:     { type: string, format: date }
        kind:     { type: string, enum: [below_floor, large_gap, unavailable] }
        details:  { type: object }

# === 触发器 ===
triggers:
  - kind: manual                   # 用户手动点击
  - kind: scheduled                # 可被 cron 触发
  - kind: webhook                  # 服务端可远程触发
  - kind: reply                    # 用户回复某条通知触发

# === 运行时能力需求(Adapter 据此分配工具) ===
required_capabilities:
  - browser                        # 浏览器自动化
  - http                           # HTTP 请求
  - llm                            # LLM 推理(用于异常的语义判定)

# === 权限 / 沙箱声明 ===
permissions:
  network:
    - "*.ctrip.com"
    - "*.ctrip.com.cn"
  filesystem: none
  shell: false

# === 通知模板 ===
notifications:
  on_success:
    template: "templates/daily-report.md.hbs"
    suggested_channels: ["wecom", "email"]    # 渲染建议,用户可改
  on_anomaly:
    template: "templates/anomaly-alert.md.hbs"
    suggested_channels: ["wecom"]

# === 执行入口 ===
entrypoint:
  type: "logic"                    # logic | prompt-only | composite
  path: "./logic/index.ts"
  function: "run"

# === 资源限制 ===
limits:
  timeout_seconds: 300
  max_llm_tokens: 50000
  max_concurrent: 1                # 同酒店同 skill 不允许并发执行

4.4 入口函数签名

skill 的执行入口是一个标准化函数,签名固定:

// logic/index.ts
import type { SkillRunContext, SkillRunResult } from '@yinian/skill-spec';

export async function run(
  input: { hotelId: string; dateRange?: DateRange; priceFloor?: number },
  ctx: SkillRunContext
): Promise<SkillRunResult> {
  const html = await ctx.browser.fetch('https://hotels.ctrip.com/...');
  const rooms = parseRooms(html);

  const anomalies = await ctx.llm.classify({
    prompt: ctx.prompts.load('analysis.md'),
    input: { rooms, priceFloor: input.priceFloor },
    schema: AnomalySchema,
  });

  ctx.emit.artifact({
    kind: 'table',
    columns: ['房型', '日期', '价格'],
    rows: rooms.map(/* ... */),
  });
  ctx.emit.progress({ phase: 'done', ratio: 1 });

  return {
    output: { snapshot: { rooms }, anomalies },
    notifications: anomalies.length
      ? [{ template: 'on_anomaly', vars: { anomalies } }]
      : [{ template: 'on_success', vars: { rooms } }],
  };
}

SkillRunContext 是 skill 与 Adapter 之间的接口——这是整个 Skill 系统能跨内核移植的关键。Skill 代码只 import SkillRunContext,不关心具体内核:

export interface SkillRunContext {
  hotel: Hotel;
  user: User;
  browser: BrowserCapability;
  http: HttpCapability;
  llm: LlmCapability;
  prompts: { load(path: string): string };
  emit: {
    progress(e: { phase: string; ratio?: number; message?: string }): void;
    log(level: 'info' | 'warn' | 'error', message: string): void;
    artifact(a: Artifact): void;
  };
  abortSignal: AbortSignal;
}

export interface BrowserCapability {
  fetch(url: string, opts?: { waitFor?: string; userAgent?: string }): Promise<string>;
  click(selector: string): Promise<void>;
  // ...
}

export interface LlmCapability {
  complete(input: { prompt: string; system?: string; maxTokens?: number }): Promise<string>;
  classify<T>(input: { prompt: string; input: unknown; schema: ZodSchema<T> }): Promise<T>;
  // ...
}

每种 capability 由 Adapter 实现并注入。Skill 代码不知道 browser 是 Playwright 还是 Puppeteer 还是 OpenClaw 自带 browser tool。换内核时 capability 的实现换,但 skill 代码不动——这是整个 Skill IP 跨平台变现的基础。

4.5 Adapter 编译流程

每个 skill 的 adapters/<kernel>.ts 文件负责把 Skill 包翻译成目标内核能消费的格式:

// adapters/openclaw.ts
import { defineSkillAdapter } from '@yinian/skill-spec/adapters/openclaw';

export default defineSkillAdapter({
  // manifest 字段映射到 OpenClaw skill 配置
  toOpenClawConfig(manifest) { /* ... */ },

  // 把 entrypoint logic 包装成 OpenClaw skill 可执行形态
  // (注入 ctx → 提供 browser/http/llm 实现)
  buildRuntime(manifest, logic) { /* ... */ },

  // 把 manifest.permissions 翻译成 OpenClaw sandbox 配置
  toSandboxPolicy(permissions) { /* ... */ },
});

构建时(@yinian/skill-spec/cli)扫描所有 skill 目录,产出可下发的 bundle

ctrip-price-monitor-1.0.0.tgz
├── manifest.yaml
├── manifest.lock                  # 校验和
├── logic-bundled.js               # 已 bundle 的 JS
├── prompts/
├── templates/
└── for-openclaw/                  # OpenClaw 适配产物
    └── skill.yaml                 # OpenClaw 原生格式

服务端只下发 bundle 包;客户端 Skill Manager 把它解包到 vendor/openclaw/skills/<id> 目录OpenClaw Gateway 加载即用。未来支持新内核时bundle 包里多一个 for-<kernel>/ 子目录即可。

4.6 Skill 与四个 Port 的关系

Port 与 Skill 的关系
ConversationPort 用户在对话里 @skill 触发;返回流中包含 tool_call 表示正在调技能
SkillPort 直接调用入口;提供独立于对话的执行视图(卡片式 UI
SchedulerPort target.kind = 'skill' 即"定时跑技能"
NotificationPort skill 输出的 notifications 字段最终由 NotificationPort 投递

四个 Port 都和 Skill 有交集,但 Skill 自身只依赖 SkillRunContext,不直接 import 任何 Port——这是关键的解耦点。Skill 通过 ctx.emit.* 上报状态,由内核+Adapter 把这些事件映射到对应 Port 的事件流。

4.7 Skill 组合v0.1 仅声明,不实现)

v0.1 不要求实现 skill 调 skill但 manifest 字段结构预留:

# 未来 v0.2composite skill
entrypoint:
  type: "composite"
  steps:
    - call: ctrip-price-monitor
      input: { hotelId: "${input.hotelId}" }
      bind: ctripResult
    - call: meituan-price-monitor
      input: { hotelId: "${input.hotelId}" }
      bind: meituanResult
    - call: daily-report-writer
      input:
        ctrip: "${ctripResult.snapshot}"
        meituan: "${meituanResult.snapshot}"

v0.1 阶段 composite 可以在 logic 里手写——下个 spec 版本再做声明式编排。

4.8 版本演进策略

  • spec_version 遵循 semver向后兼容字段加 minor破坏性改动加 major
  • @yinian/skill-spec 包提供 migrate(manifest, fromVersion, toVersion),老 skill 自动迁移到新 spec
  • Adapter 同时支持当前和上一版 specN - 1 兼容)
  • 客户端运行时校验 spec_version 与 Adapter 能力匹配,不匹配则拒绝加载并报错

4.9 v1 必做的 4 个 Skill详细设计放第七章

Skill ID 名称 角色
ctrip-price-monitor 携程价格巡检 标杆 skill作为模板复制到其他 OTA
meituan-price-monitor 美团价格巡检 复用同套 logic 框架,仅 parser 不同
daily-report 早间日报生成 组合型 skill调用上述巡检 + LLM 写日报
review-reply-helper 客评回复助手 不带 cron用户对话触发

第五章 · 客户端模块设计Main / Renderer

5.1 改造原则

智念桌面端基于 ClawX fork但 v1 阶段不追求一次性把上游完全重构为理想 monorepo。工程策略分两步

  1. 先保留 ClawX 可运行主体窗口、OpenClaw Gateway 生命周期、原有 IPC、构建发布链路先不大拆。
  2. 再建立 YINIAN 边界层:新增 Auth、Config Sync、Skill Manager、Kernel Adapter、UI Kit 等模块,把酒店产品逻辑逐步迁出原 ClawX 页面与配置模型。
  3. 最后做目录迁移等登录、服务端配置、skill 下发、核心 UI 跑通后,再把工程结构迁移到第二章定义的 workspace 形态。

这样做的目标是降低 fork 初期风险:第一版产品先跑起来,后续再把架构原则逐步固化为工程纪律。

5.2 Main 进程模块

Main 进程负责所有系统级能力、外部网络通信和敏感状态。Renderer 只拿到受限 API不直接读取 token、不直接访问服务端、不直接管理 Gateway 子进程。

模块 职责 关键输入 关键输出
AppLifecycle 应用启动、退出、单实例锁、窗口恢复 Electron lifecycle 主窗口、托盘、退出钩子
AuthManager 登录、刷新 token、退出、keychain 存储 手机号/验证码、账号密码、refresh token AuthSession
DeviceManager 设备注册、设备 ID、设备撤销 本机信息、登录态 device_id、设备绑定状态
TenantContext 当前用户、酒店、角色、权限 /auth/me/config/sync 当前酒店上下文
ConfigSync 拉取服务端配置、合并本地配置 token、hotelId、appVersion ClientConfigSnapshot
SkillManager 拉取 manifest、下载 bundle、验签、解包、回滚 /skills/manifest 本地 skill registry
KernelLifecycle 启动、健康检查、停止 OpenClaw Gateway config、skill 目录、端口 Gateway ready 状态
PortDetection 检测 18928 端口占用与握手 端口、health endpoint 可用端口或失败态
NotificationBridge 系统通知、托盘提醒、点击回调 skill 执行结果、服务端推送 OS 通知、应用内事件
UpdateManager 客户端更新检查与灰度 appVersion、channel 更新提示或静默更新
Diagnostics 日志打包、健康检查、远程诊断 日志、配置快照、Gateway 状态 诊断报告
Telemetry 事件上报、错误上报、性能采样 匿名事件、错误 /events/ingest

5.2.1 AuthManager

AuthManager 是唯一可以读写 token 的模块。token 存储规则:

  • access_token 只保存在内存,应用重启后通过 refresh_token 换取
  • refresh_token 存入系统 keychain不落普通文件
  • Renderer 永远拿不到 token 字符串,只能调用 window.yinian.auth.*
  • 退出登录时清理 token、本地租户缓存、Gateway 会话凭证

登录方式 v1 支持两种:

方式 用途
手机号 + 验证码 酒店员工主路径
账号 + 密码 NIANXX 内部实施、客成和管理员

后续可扩展企业微信扫码、钉钉扫码,但 v1 不做。

5.2.2 ConfigSync

ConfigSync 拉取服务端控制面配置,并生成客户端可消费的快照:

export interface ClientConfigSnapshot {
  user: User;
  hotels: Hotel[];
  currentHotelId: HotelId;
  entitlements: SkillEntitlement[];
  llmProxy: {
    baseUrl: string;
    modelPolicyId: string;
  };
  notificationChannels: NotificationChannel[];
  featureFlags: Record<string, boolean>;
  updatedAt: number;
}

合并规则:

  • 服务端配置优先级高于本地默认配置
  • 用户本地 UI 偏好可以覆盖服务端默认值,例如侧栏折叠、主题密度
  • skill entitlement、LLM proxy、通知通道、权限必须以服务端为准
  • 配置快照写入本地加密存储,离线时可只读使用

5.2.3 SkillManager

SkillManager 是远程下发能力包的安全边界。启动流程:

  1. 请求 /skills/manifest?hotel_id=...
  2. 对比本地 skill_registry.json
  3. 下载新增或升级的 bundle
  4. 校验服务端签名、公钥指纹、bundle hash、manifest hash
  5. 解包到 staging 目录
  6. 调用 skill-spec 校验 schema 与权限
  7. 原子替换到 active 目录
  8. 更新本地 registry
  9. 通知 KernelLifecycle 重载 Gateway 或延迟到下次启动

本地目录建议:

~/Library/Application Support/YINIAN/
├── config/
├── skills/
│   ├── active/
│   ├── staging/
│   └── rollback/
├── kernel/
├── logs/
└── diagnostics/

安全规则:

  • 未签名 bundle 拒绝加载
  • 签名通过但权限超出 manifest 声明,拒绝加载
  • 同一 hotel 同一 skill 只保留当前版本和上一版本
  • 服务端可下发 disabled: true 作为 kill switch
  • bundle 不允许覆盖客户端任意路径,只能解包到 skill sandbox 目录

5.2.4 KernelLifecycle

KernelLifecycle 负责把 OpenClaw Gateway 作为受控子进程启动。启动参数由 Main 生成:

export interface KernelLaunchConfig {
  port: number;
  healthToken: string;
  wsSessionToken: string;
  skillDir: string;
  llmProxyBaseUrl: string;
  modelPolicyId: string;
  logDir: string;
}

关键要求:

  • Gateway 只监听 127.0.0.1
  • health endpoint 校验 healthToken
  • WebSocket 握手必须携带 wsSessionToken
  • OPENAI_BASE_URL 指向 NIANXX LLM Proxy
  • 客户端不注入任何第三方模型 API key
  • Main 退出时发送优雅关闭信号,超时后再强制结束进程

5.2.5 IPC 与 preload 契约

Renderer 通过 contextBridge 使用受限 API

declare global {
  interface Window {
    yinian: {
      auth: {
        getSessionState(): Promise<AuthSessionState>;
        loginWithSms(input: LoginWithSmsInput): Promise<LoginResult>;
        loginWithPassword(input: LoginWithPasswordInput): Promise<LoginResult>;
        logout(): Promise<void>;
      };
      app: {
        getConfig(): Promise<ClientConfigSnapshot>;
        switchHotel(hotelId: HotelId): Promise<void>;
        getKernelEndpoint(): Promise<{ wsUrl: string; token: string }>;
        openDiagnostics(): Promise<void>;
      };
      skills: {
        sync(): Promise<SkillSyncResult>;
        listLocal(): Promise<InstalledSkill[]>;
      };
      diagnostics: {
        getHealth(): Promise<DiagnosticsHealth>;
        exportBundle(): Promise<{ path: string }>;
      };
    };
  }
}

禁止事项:

  • 禁止 Renderer 读取 keychain
  • 禁止 Renderer 直接调用 NIANXX 服务端
  • 禁止 Renderer 直接 spawn 子进程
  • 禁止 Renderer 直接读写 skill bundle 目录

5.3 Renderer 信息架构

智念不是通用聊天工具,第一屏应是酒店运营工作台。推荐主导航:

页面 目标用户 主要任务
今日 前台、运营、店长 看异常、看日报、处理待确认事项
对话 运营、实施 临时询问、追问结果、自然语言触发任务
Skills 运营、实施 查看已开通能力、手动运行、查看说明
自动任务 运营、店长 管理早报、巡检、定时提醒
报告 店长、老板 查看日报、周报、异常归档
通知 运营、实施 管理企微、钉钉、邮箱等通道
设置 实施、管理员 酒店信息、账号、诊断、版本

5.3.1 今日页

今日页是默认首页,解决“打开软件后我该看什么”的问题。

核心模块:

  • 顶部酒店切换器:显示当前酒店、城市、账号角色
  • 今日摘要:已完成任务、异常数量、待确认数量
  • 待处理队列:价格异常、任务失败、通知未送达、客评待回复
  • 早报卡片:展示当天日报摘要,支持展开完整报告
  • 快捷运行:携程巡检、美团巡检、生成早报、客评回复
  • 最近执行:显示任务状态、耗时、是否有 artifact

状态设计:

状态 UI 行为
无任务 显示空态和推荐开通的首个自动任务
正在同步 顶部轻量进度,不阻塞查看旧数据
有异常 待处理队列置顶,使用明确优先级
离线 显示本地缓存结果,禁用需要联网的操作

5.3.2 对话页

对话页保留 Agent 的灵活性,但交互要比 ClawX 更业务化:

  • 左侧是会话列表,默认按酒店分组
  • 中间是消息流
  • 右侧是上下文面板,展示当前酒店、可用 skills、最近 artifacts
  • 输入框支持 @skill,但普通用户不需要理解技术名称
  • 工具调用以业务步骤展示,例如“正在读取携程价格”而不是裸 tool name
  • artifact 可固定到报告页或导出

5.3.3 Skills 页

Skills 页只展示服务端已开通能力,不做开放 Marketplace。

每个 skill 卡片包含:

  • 名称、图标、简介
  • 开通状态、版本、最近运行时间
  • 支持触发方式:手动、定时、通知回复
  • 需要的权限摘要
  • 手动运行按钮
  • 历史执行入口

实施人员可以看到更多诊断信息:

  • bundle 版本
  • manifest hash
  • 最近失败原因
  • 本地路径
  • 重新同步按钮

5.3.4 自动任务页

自动任务页围绕 SchedulerPort 构建。

功能:

  • 创建定时任务
  • 暂停/恢复任务
  • 修改 cron、人类可读时间、时区
  • 查看上次运行与下次运行
  • 查看执行历史
  • 失败重试策略配置

酒店用户看到的是“每天 08:30 生成早报”,不是 cron 表达式。cron 只在高级模式展示。

5.3.5 通知页

通知页管理 NotificationPort 返回的通道。

v1 支持:

  • 系统桌面通知
  • 企业微信群机器人
  • 钉钉群机器人
  • 邮箱

通道来源分两类:

  • kernelOpenClaw 内核继承通道
  • nianxxNIANXX 服务端托管通道

普通用户只能启停和测试通道;实施人员可以配置 webhook、群机器人 token 等敏感参数。敏感参数写服务端,不写客户端。

5.4 视觉与交互原则

设计目标:高端、克制、清楚、低学习成本。

原则:

  • 主色使用品牌蓝 #1A56DB,但界面不做单一蓝色铺满
  • 页面以白底和浅灰分区为主,少用大面积渐变
  • 卡片只用于任务、skill、报告等重复实体
  • 侧栏固定,主工作区稳定,避免页面跳动
  • 状态颜色统一:成功绿、警告橙、失败红、处理中蓝灰
  • 所有长耗时任务必须有进度、可取消、可查看详情
  • 技术术语默认隐藏,例如 provider、API key、RPC、sandbox
  • 失败文案给出可执行下一步,例如“重新同步 skill”或“联系实施人员”

关键组件:

组件 用途
HotelSwitcher 当前租户上下文
TaskStatusBadge 任务状态
SkillCard skill 概览与入口
ArtifactViewer 表格、图表、Markdown、文件预览
ExecutionTimeline skill 执行步骤
NotificationChannelRow 通知通道管理
DiagnosticsPanel 健康检查与日志导出

5.5 首次启动与失败态

首次启动流程:

  1. 打开应用
  2. 显示登录页
  3. 登录成功后选择酒店
  4. 同步配置与 skills
  5. 启动 Gateway
  6. 进入今日页

必须设计的失败态:

失败态 处理
无网络 允许登录页重试;已登录用户进入离线只读
token 过期 静默刷新,失败后回登录
未绑定酒店 显示“联系管理员开通”
skill 同步失败 进入主界面,但禁用相关 skill 并显示重试
Gateway 启动失败 显示诊断页和导出日志
端口冲突 显示占用进程、切换备用端口、重试
LLM Proxy 不可用 对话和需要 LLM 的 skill 禁用,其他本地功能可用

第六章 · 服务端 API 契约

6.1 设计原则

NIANXX 服务端是智念桌面端的控制面。客户端不持有模型密钥,不自行决定 skill entitlement不直接保存第三方敏感凭证。

服务端职责:

  • 认证与设备管理
  • 用户、酒店、角色、权限
  • 客户套餐和 skill 开通
  • skill manifest 与 bundle 下载
  • LLM Proxy、计费、限流、脱敏
  • 通知通道托管
  • 执行结果、日志、报表回流
  • 灰度、功能开关、版本策略

6.2 通用约定

基础路径:

https://api.nianxx.com/yinian/v1

请求头:

Authorization: Bearer <access_token>
X-YINIAN-App-Version: 0.1.0
X-YINIAN-Device-Id: <device_id>
X-YINIAN-Request-Id: <uuid>
X-YINIAN-Hotel-Id: <hotel_id>

错误响应:

{
  "error": {
    "code": "permission_denied",
    "message": "当前账号无权操作该酒店",
    "retryable": false,
    "request_id": "req_xxx"
  }
}

错误码与 KernelErrorCode 尽量对齐,但服务端可扩展:

code 含义
auth_required 需要登录
token_expired token 过期
permission_denied 权限不足
hotel_not_found 酒店不存在或未绑定
skill_not_entitled 未开通 skill
skill_bundle_invalid skill 包无效
rate_limited 触发限流
llm_proxy_unavailable 模型代理不可用
invalid_input 请求参数错误
server_error 服务端异常

6.3 Auth API

6.3.1 发送短信验证码

POST /auth/sms/send
{
  "phone": "13800000000",
  "purpose": "login"
}

6.3.2 手机号登录

POST /auth/login/sms
{
  "phone": "13800000000",
  "code": "123456",
  "device": {
    "device_id": "dev_xxx",
    "platform": "darwin",
    "app_version": "0.1.0",
    "machine_name": "FrontDesk-Mac"
  }
}

响应:

{
  "access_token": "eyJ...",
  "refresh_token": "rt_xxx",
  "expires_in": 3600,
  "user": {
    "id": "user_001",
    "name": "王店长",
    "phone": "13800000000"
  }
}

6.3.3 账号密码登录

POST /auth/login/password

用于 NIANXX 内部账号和部分管理员账号。

6.3.4 刷新 token

POST /auth/refresh
{
  "refresh_token": "rt_xxx",
  "device_id": "dev_xxx"
}

6.3.5 当前用户

GET /auth/me

返回用户、可访问酒店、角色、权限:

{
  "user": { "id": "user_001", "name": "王店长", "phone": "13800000000" },
  "hotels": [
    {
      "id": "hotel_001",
      "name": "智念杭州西湖店",
      "city": "杭州",
      "role": "manager",
      "permissions": ["skill:run", "task:manage", "report:view"]
    }
  ]
}

6.4 Config API

6.4.1 配置同步

GET /config/sync?hotel_id=hotel_001

响应:

{
  "server_time": 1777190400000,
  "hotel": {
    "id": "hotel_001",
    "name": "智念杭州西湖店",
    "brand": "智念",
    "city": "杭州",
    "ota": [
      { "ota": "ctrip", "externalId": "ctrip_123", "enabled": true },
      { "ota": "meituan", "externalId": "mt_456", "enabled": true }
    ]
  },
  "feature_flags": {
    "review_reply_helper": true,
    "auto_update_skills": true
  },
  "model_policy": {
    "id": "policy_standard_cn",
    "allowed_models": ["default"],
    "max_tokens_per_day": 1000000
  },
  "ui_policy": {
    "default_page": "today",
    "show_advanced_settings": false
  }
}

6.5 Skill API

6.5.1 Skill manifest

GET /skills/manifest?hotel_id=hotel_001&client_version=0.1.0

响应:

{
  "manifest_version": "2026-04-26.1",
  "public_key_id": "yinian-skill-signing-2026",
  "skills": [
    {
      "id": "ctrip-price-monitor",
      "version": "1.0.0",
      "enabled": true,
      "entitlement": "standard",
      "bundle_url": "https://cdn.nianxx.com/skills/ctrip-price-monitor-1.0.0.tgz",
      "bundle_sha256": "abc123...",
      "manifest_sha256": "def456...",
      "signature": "base64...",
      "required_client_version": ">=0.1.0",
      "kill_switch": false
    }
  ]
}

6.5.2 Bundle 下载

GET /skills/bundles/{skill_id}/{version}

实际生产可返回短期 CDN 签名 URL。客户端必须校验 hash 和签名,不信任 CDN 本身。

6.5.3 Skill 执行结果回流

POST /skills/executions
{
  "execution_id": "exec_001",
  "hotel_id": "hotel_001",
  "skill_id": "ctrip-price-monitor",
  "version": "1.0.0",
  "status": "success",
  "started_at": 1777190400000,
  "finished_at": 1777190460000,
  "duration_ms": 60000,
  "summary": {
    "rooms_checked": 32,
    "anomalies": 2
  },
  "artifacts": [
    { "kind": "table", "id": "artifact_001", "title": "价格快照" }
  ]
}

6.6 LLM Proxy API

Gateway 的模型请求统一走 NIANXX LLM Proxy。

POST /llm/v1/chat/completions

约束:

  • 请求必须携带客户端 access token 或由 Gateway 换取的短期 proxy token
  • 服务端根据 hotel、user、skill、model_policy 做鉴权
  • 服务端负责选择真实模型供应商
  • 服务端负责限流、计费、日志脱敏
  • 客户端不出现 OpenAI、Anthropic 等供应商 API key

建议增加 Gateway 专用 token

POST /llm/proxy-token
{
  "hotel_id": "hotel_001",
  "device_id": "dev_xxx",
  "kernel_session_id": "ks_xxx"
}

响应:

{
  "proxy_token": "kpt_xxx",
  "expires_in": 1800,
  "base_url": "https://api.nianxx.com/yinian/v1/llm/v1"
}

6.7 Notification API

6.7.1 查询通道

GET /notifications/channels?hotel_id=hotel_001
{
  "channels": [
    {
      "id": "ch_wecom_001",
      "kind": "wecom",
      "label": "前台运营群",
      "recipient": "杭州西湖店运营群",
      "enabled": true,
      "source": "nianxx"
    }
  ]
}

6.7.2 测试通道

POST /notifications/channels/{channel_id}/test

6.7.3 派发通知

POST /notifications/dispatch
{
  "hotel_id": "hotel_001",
  "channel_ids": ["ch_wecom_001"],
  "content": {
    "kind": "markdown",
    "markdown": "今日价格巡检发现 2 个异常"
  },
  "source": {
    "kind": "skill_execution",
    "id": "exec_001"
  }
}

6.8 Data Sink 与事件上报

客户端上报分三类:

类型 Endpoint 是否含业务数据
产品行为事件 POST /events/ingest 否,默认匿名或弱标识
skill 执行结果 POST /skills/executions 是,需按租户隔离
诊断日志 POST /diagnostics/upload 可能含敏感信息,上传前脱敏

行为事件示例:

{
  "events": [
    {
      "name": "skill_run_clicked",
      "time": 1777190400000,
      "hotel_id": "hotel_001",
      "properties": {
        "skill_id": "ctrip-price-monitor",
        "entry": "today_page"
      }
    }
  ]
}

6.9 权限模型

角色初版:

角色 权限
owner 查看全部报告、管理通知、管理自动任务、运行 skills
manager 查看报告、管理自动任务、运行 skills
staff 查看今日页、运行部分 skills、处理待办
viewer 只读查看报告和历史
nianxx_admin 跨酒店实施、诊断、配置 entitlement

权限粒度:

hotel:view
report:view
skill:view
skill:run
skill:manage
task:view
task:manage
notification:view
notification:manage
diagnostics:export

第七章 · Skills v1 详细设计

7.1 v1 Skill 设计原则

v1 不追求覆盖所有酒店场景,只做能快速证明价值的四个 skill

  1. 价格巡检:直接节省运营时间
  2. 早间日报:每天固定触达店长和老板
  3. 客评回复:体现 AI 生成质量
  4. 通知闭环:把结果送到用户已经在用的企微/钉钉

数据获取原则:

  • 优先使用客户授权的官方 API 或服务端集成
  • 无官方 API 时,浏览器自动化作为 v1 可用路径
  • 涉及 OTA 账号密码时,优先服务端托管,不写入客户端
  • skill 输出必须可解释,不能只给一句模型结论

7.2 ctrip-price-monitor

目标:每日巡检指定酒店在携程的房型、日期、价格、可售状态,识别异常并通知运营人员。

输入:

字段 类型 必填 说明
hotelId hotel_id 当前酒店
dateRange date_range 默认未来 7 天
priceFloor number 成本价或底价
roomTypes string[] 指定房型

输出:

  • snapshot:价格快照
  • anomalies:异常列表
  • summary:房型数、日期数、异常数

异常规则:

异常 规则
below_floor 价格低于成本价或配置底价
large_gap 相邻日期价差超过配置阈值
unavailable 应售房型不可售
missing_room 页面未找到已配置房型
parse_suspect 解析置信度过低,需要人工确认

执行步骤:

  1. 读取酒店 OTA 绑定
  2. 打开携程酒店页面或调用服务端数据接口
  3. 抓取未来 N 天房型价格
  4. 标准化房型名称
  5. 执行规则判定
  6. 必要时调用 LLM 对异常描述做自然语言解释
  7. 生成 table artifact 和 markdown 摘要
  8. 有异常则通知指定通道

验收标准:

  • 能稳定输出未来 7 天价格快照
  • 同一酒店同一 skill 不并发执行
  • 页面结构变化时返回 parse_suspect,不输出误导性结论
  • 失败日志能定位到抓取、解析、规则、通知哪个阶段

7.3 meituan-price-monitor

目标与携程巡检一致,但适配美团/大众点评酒店页面或服务端数据源。

复用策略:

  • 复用 skills-hotel-core 中的价格标准化、异常规则、报告模板
  • 独立维护 meituan parser
  • 与携程输出结构保持一致,方便日报 skill 聚合

新增关注点:

  • 美团房型命名与携程不完全一致,需要房型映射表
  • 同一房型可能出现多个套餐,需要选择代表价格或列出套餐差异
  • 可售状态、取消政策、早餐信息可作为 v1.1 扩展字段

验收标准:

  • 输出 schema 与 ctrip-price-monitor 兼容
  • 可与携程结果在 daily-report 中合并
  • 支持房型映射配置

7.4 daily-report

目标:每天早上生成酒店运营日报,汇总 OTA 价格巡检、异常、昨日任务执行和建议动作。

输入:

字段 类型 必填 说明
hotelId hotel_id 当前酒店
date date 默认今天
includeOta string[] 默认已启用 OTA
sendToChannels string[] 默认服务端配置

执行步骤:

  1. 调用或读取携程巡检结果
  2. 调用或读取美团巡检结果
  3. 汇总异常与趋势
  4. 生成店长可读的 Markdown 日报
  5. 输出 report artifact
  6. 按配置发送企微/钉钉/邮箱

日报结构:

# 今日运营早报

## 重点结论

## OTA 价格概览

## 异常与建议

## 今日建议动作

## 明细表

验收标准:

  • 生成内容不超过 2 屏手机阅读长度
  • 明细可在桌面端展开查看
  • LLM 生成内容必须基于结构化输入,不允许凭空编造
  • 没有异常时也要生成“已完成巡检”的正向反馈

7.5 review-reply-helper

目标:根据客人评价内容、酒店品牌语气、问题类型,生成可编辑的回复建议。

输入:

字段 类型 必填 说明
hotelId hotel_id 当前酒店
reviewText string 客评原文
rating number 评分
platform string 携程/美团/大众点评等
tone string 默认酒店品牌语气

输出:

  • reply:建议回复
  • issueTags:问题标签
  • riskLevel:是否需要人工升级
  • alternatives:可选回复版本

规则:

  • 差评默认不自动发送,只生成建议
  • 涉及安全、卫生、赔偿、隐私、投诉升级,标记 high
  • 回复必须避免承诺未确认的补偿
  • 回复中不得泄露内部操作信息

验收标准:

  • 生成回复可直接复制
  • 高风险客评必须提示人工处理
  • 同一评价可生成“正式/温和/简短”三种版本

7.6 共享包 skills-hotel-core

共享能力:

  • OTA 房型标准化
  • 价格异常规则
  • 日期与时区处理
  • 酒店品牌语气配置
  • 通知模板渲染
  • artifact 生成工具
  • 执行日志辅助函数

建议目录:

packages/skills-hotel-core/
├── src/
│   ├── ota/
│   ├── pricing/
│   ├── report/
│   ├── review/
│   └── templates/
└── tests/

7.7 Skill 测试要求

每个 v1 skill 至少包含:

  • manifest schema 测试
  • 输入校验测试
  • parser fixture 测试
  • 异常规则单元测试
  • artifact snapshot 测试
  • 失败态测试
  • bundle 构建测试

价格巡检类 skill 额外要求:

  • 页面结构字段缺失时不崩溃
  • 空房型、空价格、重复房型可处理
  • 时区固定为酒店所在地

第八章 · 非功能性要求

8.1 安全

安全是 v1 硬门槛,不是上线后的增强项。

8.1.1 Token 与密钥

  • 客户端永不保存第三方模型 API key
  • refresh_token 存系统 keychain
  • access_token 只在内存保存
  • Gateway 使用短期 proxy_token 调用 LLM Proxy
  • token 刷新失败立即降级到登录态

8.1.2 Gateway 本地访问控制

  • Gateway 只监听 127.0.0.1
  • health endpoint 需要 Main 注入的 healthToken
  • WebSocket 连接需要短期 wsSessionToken
  • token 随应用启动生成,退出即失效
  • Renderer 只能通过 Main 获取 endpoint 和 token

8.1.3 Skill Bundle 安全

  • 所有 bundle 必须签名
  • 客户端内置 NIANXX skill 公钥指纹
  • 下载后校验 sha256 和签名
  • manifest 权限与实际 bundle 行为不一致时拒绝加载
  • 支持服务端 kill switch
  • 支持回滚到上一版本

8.1.4 权限与沙箱

  • skill 权限最小化
  • 默认禁止 shell
  • 默认禁止任意文件系统访问
  • 网络权限按域名白名单声明
  • 浏览器能力必须受限于目标域
  • 高风险 capability 需要服务端 entitlement 开启

8.1.5 数据脱敏

日志中默认脱敏:

  • 手机号
  • 邮箱
  • token
  • webhook URL
  • OTA 账号
  • 客评中的手机号、身份证号等个人信息

诊断包上传前必须二次脱敏,并显示将上传的内容摘要。

8.2 性能

启动目标:

指标 目标
冷启动到登录页 < 3 秒
已登录启动到今日页可见 < 6 秒
Gateway ready < 15 秒
skill manifest 同步 < 5 秒,网络正常时
今日页首次渲染 < 2 秒,使用本地缓存

运行目标:

  • 对话 token 流不卡 UI
  • 大 artifact 分页或虚拟滚动
  • 单个 skill 执行不阻塞其他 UI 操作
  • 日志写入异步化
  • CPU 长时间占用超过阈值时上报诊断事件

8.3 离线能力

离线状态下:

功能 行为
今日页 显示最近一次缓存
报告页 可查看历史报告
对话 禁用发送,显示离线提示
Skills 可查看说明和历史,默认禁止运行需要网络的 skill
自动任务 本地可显示,但执行前检查网络
通知 本地系统通知可用,外部通道排队或失败

离线恢复后:

  • 自动刷新 token
  • 同步 config
  • 同步 skill manifest
  • 上报积压事件
  • 检查失败任务是否可重试

8.4 可观测性

日志分层:

日志 内容
app.log 应用生命周期、登录、配置同步
kernel.log Gateway 启停、健康检查
skill.log skill 执行、阶段、错误
network.log 服务端请求摘要,不记录敏感 body
renderer.log UI 错误、页面崩溃

关键事件:

  • app_started
  • login_success
  • config_sync_success
  • skill_sync_failed
  • kernel_ready
  • skill_execution_started
  • skill_execution_completed
  • notification_dispatch_failed
  • llm_proxy_rate_limited

错误必须带:

  • request id
  • hotel id
  • user id hash
  • device id
  • app version
  • skill id/version
  • kernel version

8.5 更新策略

客户端更新:

  • v1 使用手动确认更新
  • 关键安全更新可强提醒
  • 更新前检查 Gateway 是否有任务运行
  • 更新失败保留旧版本可启动

Skill 更新:

  • 默认静默更新
  • 正在执行的 skill 不热替换
  • 新版本失败可回滚上一版本
  • 服务端可按酒店灰度发布

8.6 兼容性

v1 支持:

平台 要求
macOS 13 及以上
Windows Windows 10 及以上

暂不承诺 Linux 正式支持。内部开发可运行,但不作为 v1 客户交付范围。

8.7 隐私与合规

原则:

  • 明确区分本地数据、服务端数据、第三方平台数据
  • 默认最小化回传
  • 报告与执行结果按酒店租户隔离
  • 客评内容属于敏感业务数据,回传和训练用途需单独授权
  • 不使用客户数据训练通用模型,除非合同另行约定

第九章 · 里程碑、交付计划与风险登记册

9.1 里程碑

以 2026-04-26 为 v0 启动日,建议第一阶段按 10 周推进。

里程碑 日期 目标 交付物
M0 项目启动 2026-04-26 至 2026-05-03 完成 fork、技术验证、PRD 评审 ClawX fork、架构 ADR、任务拆分
M1 登录与控制面 2026-05-04 至 2026-05-17 登录、酒店上下文、配置同步跑通 AuthManager、ConfigSync、基础 API
M2 Kernel 适配 2026-05-18 至 2026-05-31 Gateway 生命周期、Port Adapter、端口检测 KernelLifecycle、ConversationPort MVP
M3 Skill 下发 2026-06-01 至 2026-06-14 manifest、bundle、验签、首个 skill SkillManager、ctrip-price-monitor
M4 产品 UI Alpha 2026-06-15 至 2026-06-28 今日页、Skills、自动任务、报告页 Alpha 客户端
M5 试点 Beta 2026-06-29 至 2026-07-12 2 到 3 家酒店试点,闭环反馈 Beta 包、试点报告、风险修复

9.2 MVP 范围

必须包含:

  • 登录与酒店选择
  • 今日页
  • 对话页基础能力
  • Skill 列表与手动运行
  • 携程价格巡检
  • 日报生成
  • 企业微信或钉钉通知一种
  • Gateway 生命周期管理
  • skill bundle 签名校验
  • 基础诊断包导出

暂缓:

  • 开放 Marketplace
  • 私有部署
  • 手机端
  • 多内核切换 UI
  • 复杂工作流编排
  • 全量通知通道管理

9.3 评审关口

关口 必须通过
架构评审 端口契约、Main/Renderer 边界、安全边界
安全评审 token、Gateway、本地文件、skill bundle
UI 评审 今日页、对话页、失败态、诊断页
Skill 评审 manifest、权限、测试、回滚
试点评审 真实酒店任务完成率、失败率、用户反馈

9.4 风险登记册

风险 概率 影响 应对
ClawX 上游结构变化快fork 同步困难 保持上游同步分支,减少早期大重构
OpenClaw Gateway API 不稳定 Adapter 隔离,关键 RPC 写契约测试
OTA 页面结构变化导致 parser 失效 fixture 测试、parse_suspect、快速热更新 skill
skill 远程下发带来安全风险 极高 签名、hash、权限、沙箱、kill switch
酒店用户不理解 Agent 对话 今日页优先,任务卡片优先,对话作为补充
LLM Proxy 成本失控 model policy、配额、缓存、按 skill 计量
通知送达不稳定 状态回执、重试、失败提示、备用通道
Windows 环境差异 早期纳入 Windows 测试机和自动化打包
客户数据合规边界不清 合同、授权、脱敏、租户隔离、审计

附录 A · 术语表

术语 说明
智念桌面端 面向酒店运营的 AI Agent 桌面客户端
Shell 桌面壳,包括 UI、登录态、本地存储、通知
Kernel Agent runtime当前为 OpenClaw
Adapter 把领域 Port 翻译为具体内核调用的适配层
Port Shell 使用的领域接口,包括 Conversation、Skill、Scheduler、Notification
Skill 可下发、可版本化、可执行的行业能力包
Skill Bundle skill 打包后的发布产物
Manifest skill 的声明式元数据
Entitlement 某酒店或客户已开通的能力权益
LLM Proxy NIANXX 服务端模型代理层
Gateway 本地 OpenClaw 服务进程
Artifact skill 或对话产生的结构化结果,如表格、报告、图片、文件
Kill Switch 服务端远程禁用某 skill 或某版本的能力

附录 B · OpenClaw 上游同步策略

B.1 分支模型

建议保留三个长期分支:

分支 用途
upstream-main 镜像 ClawX 上游 main
yinian-main 智念主开发分支
release/* 客户发布分支

同步流程:

  1. 定期 fetch ClawX upstream
  2. 更新 upstream-main
  3. 创建 sync/upstream-YYYYMMDD
  4. merge 或 cherry-pick 到 yinian-main
  5. 跑完整 CI
  6. 对冲突文件写同步记录

B.2 改造边界

尽量少改上游核心文件,把 YINIAN 逻辑放入独立模块:

  • src/yinian/*
  • electron/yinian/*
  • packages/kernel-core
  • packages/kernel-context
  • packages/kernel-adapter-openclaw
  • packages/ui-kit

如果必须改上游文件,要求:

  • 添加注释说明 YINIAN 修改原因
  • 在同步记录中登记
  • 对应测试覆盖

B.3 上游能力取舍

保留:

  • Gateway 生命周期基础能力
  • 原有 channel 能力
  • Cron / scheduler 能力
  • Skill 加载机制
  • Provider 抽象中可复用部分

替换或隐藏:

  • 用户自填 API key
  • 开发者 Marketplace
  • 过度技术化设置页
  • 不适合酒店用户的 debug 信息

附录 C · CI 强制规则清单

C.1 依赖方向

必须强制:

  • kernel-core 禁止 import OpenClaw
  • Renderer 禁止 import electron
  • Renderer 禁止 import Node 内置模块
  • Renderer 禁止在 bootstrap.tsx 之外 import kernel-adapter-openclaw
  • Skills 禁止 import App UI 代码
  • Skill logic 只能依赖 skill-specskills-hotel-core

C.2 安全检查

CI 必须检查:

  • 无硬编码 API key
  • 无硬编码生产 token
  • skill manifest 包含 permissions
  • skill bundle 构建产物可复现
  • bundle hash 与 manifest.lock 一致
  • preload 暴露 API 白名单

C.3 测试

最低要求:

范围 要求
kernel-core 类型和 schema 测试
Adapter RPC 映射契约测试
Main Auth、Config、SkillManager 单元测试
Renderer 关键页面组件测试
Skills fixture、parser、规则、bundle 测试
E2E 登录、同步、运行 skill、查看报告

C.4 发布检查

发布前必须通过:

  • TypeScript typecheck
  • ESLint
  • 单元测试
  • skill bundle 验签测试
  • macOS 打包
  • Windows 打包
  • smoke test
  • 诊断包导出测试

v0.3 后续建议:下一轮评审重点放在第五章 UI 信息架构、第六章 API 权限模型、第八章 skill 安全边界。评审通过后即可拆分工程 tickets进入 ClawX fork 改造。