75 KiB
智念桌面端 PRD(YINIAN 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 Lifecycle(OpenClaw 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(自建企微 / 钉钉 / 自定义) │
└──────────────────────────────────────────────────────────────┘
关键设计点
- Gateway 由 Main 拉起子进程,生命周期由 Main 管理。Main 退出时确保 Gateway 优雅关闭。
- Gateway 端口固定 127.0.0.1:18928,与上游 ClawX 默认端口(18789)刻意错开,避免与原版 ClawX 共存冲突。Main 启动前执行端口检测协议(详见 §2.7)。
- Renderer 通过 WebSocket 连接 Gateway(同机回环,性能 OK),连接地址与短期会话凭证由 Main 通过
contextBridge下发,所有调用必须经过 Adapter 包装层。 - 所有外部网络通信原则上由 Main 发起(NIANXX 服务端、第三方 API),Renderer 不直接发出公网请求。
- 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
├─ 无 / 过期 → 渲染 LoginPage(Gateway 暂不启动)
└─ 有效 → 继续步骤 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 此架构需要诚实承认的泄漏点
完美的内核无关性不存在。以下两处会有泄漏,提前画好红线:
泄漏点 1:Sandbox / 安全策略
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 包实现这些接口,仅此而已。
设计准则:
- 领域语言优先 —— 用"对话"、"技能"、"任务"、"通知",不用 agent / channel / session 等内核术语
- 流式优先 —— 所有可能耗时的操作返回
AsyncIterable<Event>,不返回单个 Promise - 不可变快照 —— 跨进程边界的数据全部是 plain object(可被 JSON 序列化),不带类方法
- 错误是值 —— 错误以
KernelError类型显式建模,不依赖 throw / catch 跨进程 - 零运行时依赖 —— 除
zod(schema 验证)、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。约定:
- 每个 assistant 消息以
message_start开始,以message_complete结束 text_delta是增量文本,前端做拼接tool_call一次性给出完整工具调用(不切片下发);tool_result同理artifact独立事件,不嵌套在 text 中- 任何错误都用
error事件结束流(不抛异常) abort()后内核必须发出error事件,code =kernel_aborted
这套语义是 Adapter 的核心工作,也是它复杂度最高的部分。把这一层做好,整个 Shell 层就再也不需要关心"我在跟哪个内核说话"。
第四章 · YINIAN Skill Spec v0.1
4.1 设计目标
YINIAN Skill 是产品的核心 IP——它把酒店行业的 know-how(OTA 巡检逻辑、价格异常判定规则、客评回复话术等)编码为可执行单元。Skill Spec 决定了这些 IP 资产的形态。
设计目标按优先级:
- 跨内核可移植 —— Skill 描述层不耦合任何内核的具体 API
- 声明式优先 —— 元信息、输入输出、权限都用静态描述,能被工具静态校验、自动生成 UI
- 可组合 —— skill 可以调用 skill,构成工作流
- 可版本化 —— 单个 skill 自身有版本,Skill Spec 自身也有版本(v0.1 → v0.2 …)
- 可远程下发 —— 每个 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.2:composite 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 同时支持当前和上一版 spec(N - 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。工程策略分两步:
- 先保留 ClawX 可运行主体:窗口、OpenClaw Gateway 生命周期、原有 IPC、构建发布链路先不大拆。
- 再建立 YINIAN 边界层:新增 Auth、Config Sync、Skill Manager、Kernel Adapter、UI Kit 等模块,把酒店产品逻辑逐步迁出原 ClawX 页面与配置模型。
- 最后做目录迁移:等登录、服务端配置、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 是远程下发能力包的安全边界。启动流程:
- 请求
/skills/manifest?hotel_id=... - 对比本地
skill_registry.json - 下载新增或升级的 bundle
- 校验服务端签名、公钥指纹、bundle hash、manifest hash
- 解包到 staging 目录
- 调用 skill-spec 校验 schema 与权限
- 原子替换到 active 目录
- 更新本地 registry
- 通知 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 支持:
- 系统桌面通知
- 企业微信群机器人
- 钉钉群机器人
- 邮箱
通道来源分两类:
kernel:OpenClaw 内核继承通道nianxx:NIANXX 服务端托管通道
普通用户只能启停和测试通道;实施人员可以配置 webhook、群机器人 token 等敏感参数。敏感参数写服务端,不写客户端。
5.4 视觉与交互原则
设计目标:高端、克制、清楚、低学习成本。
原则:
- 主色使用品牌蓝
#1A56DB,但界面不做单一蓝色铺满 - 页面以白底和浅灰分区为主,少用大面积渐变
- 卡片只用于任务、skill、报告等重复实体
- 侧栏固定,主工作区稳定,避免页面跳动
- 状态颜色统一:成功绿、警告橙、失败红、处理中蓝灰
- 所有长耗时任务必须有进度、可取消、可查看详情
- 技术术语默认隐藏,例如 provider、API key、RPC、sandbox
- 失败文案给出可执行下一步,例如“重新同步 skill”或“联系实施人员”
关键组件:
| 组件 | 用途 |
|---|---|
HotelSwitcher |
当前租户上下文 |
TaskStatusBadge |
任务状态 |
SkillCard |
skill 概览与入口 |
ArtifactViewer |
表格、图表、Markdown、文件预览 |
ExecutionTimeline |
skill 执行步骤 |
NotificationChannelRow |
通知通道管理 |
DiagnosticsPanel |
健康检查与日志导出 |
5.5 首次启动与失败态
首次启动流程:
- 打开应用
- 显示登录页
- 登录成功后选择酒店
- 同步配置与 skills
- 启动 Gateway
- 进入今日页
必须设计的失败态:
| 失败态 | 处理 |
|---|---|
| 无网络 | 允许登录页重试;已登录用户进入离线只读 |
| 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:
- 价格巡检:直接节省运营时间
- 早间日报:每天固定触达店长和老板
- 客评回复:体现 AI 生成质量
- 通知闭环:把结果送到用户已经在用的企微/钉钉
数据获取原则:
- 优先使用客户授权的官方 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 |
解析置信度过低,需要人工确认 |
执行步骤:
- 读取酒店 OTA 绑定
- 打开携程酒店页面或调用服务端数据接口
- 抓取未来 N 天房型价格
- 标准化房型名称
- 执行规则判定
- 必要时调用 LLM 对异常描述做自然语言解释
- 生成 table artifact 和 markdown 摘要
- 有异常则通知指定通道
验收标准:
- 能稳定输出未来 7 天价格快照
- 同一酒店同一 skill 不并发执行
- 页面结构变化时返回
parse_suspect,不输出误导性结论 - 失败日志能定位到抓取、解析、规则、通知哪个阶段
7.3 meituan-price-monitor
目标与携程巡检一致,但适配美团/大众点评酒店页面或服务端数据源。
复用策略:
- 复用
skills-hotel-core中的价格标准化、异常规则、报告模板 - 独立维护
meituanparser - 与携程输出结构保持一致,方便日报 skill 聚合
新增关注点:
- 美团房型命名与携程不完全一致,需要房型映射表
- 同一房型可能出现多个套餐,需要选择代表价格或列出套餐差异
- 可售状态、取消政策、早餐信息可作为 v1.1 扩展字段
验收标准:
- 输出 schema 与
ctrip-price-monitor兼容 - 可与携程结果在
daily-report中合并 - 支持房型映射配置
7.4 daily-report
目标:每天早上生成酒店运营日报,汇总 OTA 价格巡检、异常、昨日任务执行和建议动作。
输入:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
hotelId |
hotel_id |
是 | 当前酒店 |
date |
date |
否 | 默认今天 |
includeOta |
string[] |
否 | 默认已启用 OTA |
sendToChannels |
string[] |
否 | 默认服务端配置 |
执行步骤:
- 调用或读取携程巡检结果
- 调用或读取美团巡检结果
- 汇总异常与趋势
- 生成店长可读的 Markdown 日报
- 输出 report artifact
- 按配置发送企微/钉钉/邮箱
日报结构:
# 今日运营早报
## 重点结论
## 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存系统 keychainaccess_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_startedlogin_successconfig_sync_successskill_sync_failedkernel_readyskill_execution_startedskill_execution_completednotification_dispatch_failedllm_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/* |
客户发布分支 |
同步流程:
- 定期 fetch ClawX upstream
- 更新
upstream-main - 创建
sync/upstream-YYYYMMDD - merge 或 cherry-pick 到
yinian-main - 跑完整 CI
- 对冲突文件写同步记录
B.2 改造边界
尽量少改上游核心文件,把 YINIAN 逻辑放入独立模块:
src/yinian/*electron/yinian/*packages/kernel-corepackages/kernel-contextpackages/kernel-adapter-openclawpackages/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之外 importkernel-adapter-openclaw - Skills 禁止 import App UI 代码
- Skill logic 只能依赖
skill-spec和skills-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 改造。