2312 lines
75 KiB
Markdown
2312 lines
75 KiB
Markdown
# 智念桌面端 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(自建企微 / 钉钉 / 自定义) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 关键设计点
|
||
|
||
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 服务端、第三方 API),Renderer 不直接发出公网请求。
|
||
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 实现:唯一的依赖注入点
|
||
|
||
整个系统中,**只有一个文件** 把"具体内核"和"应用"绑在一起:
|
||
|
||
```ts
|
||
// 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 响应:
|
||
|
||
```json
|
||
{ "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 包实现这些接口,仅此而已。
|
||
|
||
设计准则:
|
||
|
||
1. **领域语言优先** —— 用"对话"、"技能"、"任务"、"通知",不用 agent / channel / session 等内核术语
|
||
2. **流式优先** —— 所有可能耗时的操作返回 `AsyncIterable<Event>`,不返回单个 Promise
|
||
3. **不可变快照** —— 跨进程边界的数据全部是 plain object(可被 JSON 序列化),不带类方法
|
||
4. **错误是值** —— 错误以 `KernelError` 类型显式建模,不依赖 throw / catch 跨进程
|
||
5. **零运行时依赖** —— 除 `zod`(schema 验证)、`date-fns`(日期处理)外不允许其他依赖
|
||
|
||
## 3.2 共享核心类型
|
||
|
||
```typescript
|
||
// === 标识与身份(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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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(通道发现 + 派发模式)
|
||
|
||
```typescript
|
||
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` 实例:
|
||
|
||
```typescript
|
||
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 适配器实现:
|
||
|
||
```typescript
|
||
// 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-how(OTA 巡检逻辑、价格异常判定规则、客评回复话术等)编码为可执行单元。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
|
||
|
||
```yaml
|
||
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 的执行入口是一个标准化函数,签名固定:
|
||
|
||
```typescript
|
||
// 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`,不关心具体内核:
|
||
|
||
```typescript
|
||
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 包翻译成目标内核能消费的格式:
|
||
|
||
```typescript
|
||
// 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 字段结构预留:
|
||
|
||
```yaml
|
||
# 未来 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。工程策略分两步:
|
||
|
||
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 拉取服务端控制面配置,并生成客户端可消费的快照:
|
||
|
||
```typescript
|
||
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 生成:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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 首次启动与失败态
|
||
|
||
首次启动流程:
|
||
|
||
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 通用约定
|
||
|
||
基础路径:
|
||
|
||
```text
|
||
https://api.nianxx.com/yinian/v1
|
||
```
|
||
|
||
请求头:
|
||
|
||
```http
|
||
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>
|
||
```
|
||
|
||
错误响应:
|
||
|
||
```json
|
||
{
|
||
"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 发送短信验证码
|
||
|
||
```http
|
||
POST /auth/sms/send
|
||
```
|
||
|
||
```json
|
||
{
|
||
"phone": "13800000000",
|
||
"purpose": "login"
|
||
}
|
||
```
|
||
|
||
### 6.3.2 手机号登录
|
||
|
||
```http
|
||
POST /auth/login/sms
|
||
```
|
||
|
||
```json
|
||
{
|
||
"phone": "13800000000",
|
||
"code": "123456",
|
||
"device": {
|
||
"device_id": "dev_xxx",
|
||
"platform": "darwin",
|
||
"app_version": "0.1.0",
|
||
"machine_name": "FrontDesk-Mac"
|
||
}
|
||
}
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"access_token": "eyJ...",
|
||
"refresh_token": "rt_xxx",
|
||
"expires_in": 3600,
|
||
"user": {
|
||
"id": "user_001",
|
||
"name": "王店长",
|
||
"phone": "13800000000"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.3.3 账号密码登录
|
||
|
||
```http
|
||
POST /auth/login/password
|
||
```
|
||
|
||
用于 NIANXX 内部账号和部分管理员账号。
|
||
|
||
### 6.3.4 刷新 token
|
||
|
||
```http
|
||
POST /auth/refresh
|
||
```
|
||
|
||
```json
|
||
{
|
||
"refresh_token": "rt_xxx",
|
||
"device_id": "dev_xxx"
|
||
}
|
||
```
|
||
|
||
### 6.3.5 当前用户
|
||
|
||
```http
|
||
GET /auth/me
|
||
```
|
||
|
||
返回用户、可访问酒店、角色、权限:
|
||
|
||
```json
|
||
{
|
||
"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 配置同步
|
||
|
||
```http
|
||
GET /config/sync?hotel_id=hotel_001
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```http
|
||
GET /skills/manifest?hotel_id=hotel_001&client_version=0.1.0
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"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 下载
|
||
|
||
```http
|
||
GET /skills/bundles/{skill_id}/{version}
|
||
```
|
||
|
||
实际生产可返回短期 CDN 签名 URL。客户端必须校验 hash 和签名,不信任 CDN 本身。
|
||
|
||
### 6.5.3 Skill 执行结果回流
|
||
|
||
```http
|
||
POST /skills/executions
|
||
```
|
||
|
||
```json
|
||
{
|
||
"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。
|
||
|
||
```http
|
||
POST /llm/v1/chat/completions
|
||
```
|
||
|
||
约束:
|
||
|
||
- 请求必须携带客户端 access token 或由 Gateway 换取的短期 proxy token
|
||
- 服务端根据 hotel、user、skill、model_policy 做鉴权
|
||
- 服务端负责选择真实模型供应商
|
||
- 服务端负责限流、计费、日志脱敏
|
||
- 客户端不出现 OpenAI、Anthropic 等供应商 API key
|
||
|
||
建议增加 Gateway 专用 token:
|
||
|
||
```http
|
||
POST /llm/proxy-token
|
||
```
|
||
|
||
```json
|
||
{
|
||
"hotel_id": "hotel_001",
|
||
"device_id": "dev_xxx",
|
||
"kernel_session_id": "ks_xxx"
|
||
}
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"proxy_token": "kpt_xxx",
|
||
"expires_in": 1800,
|
||
"base_url": "https://api.nianxx.com/yinian/v1/llm/v1"
|
||
}
|
||
```
|
||
|
||
## 6.7 Notification API
|
||
|
||
### 6.7.1 查询通道
|
||
|
||
```http
|
||
GET /notifications/channels?hotel_id=hotel_001
|
||
```
|
||
|
||
```json
|
||
{
|
||
"channels": [
|
||
{
|
||
"id": "ch_wecom_001",
|
||
"kind": "wecom",
|
||
"label": "前台运营群",
|
||
"recipient": "杭州西湖店运营群",
|
||
"enabled": true,
|
||
"source": "nianxx"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 6.7.2 测试通道
|
||
|
||
```http
|
||
POST /notifications/channels/{channel_id}/test
|
||
```
|
||
|
||
### 6.7.3 派发通知
|
||
|
||
```http
|
||
POST /notifications/dispatch
|
||
```
|
||
|
||
```json
|
||
{
|
||
"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` | 可能含敏感信息,上传前脱敏 |
|
||
|
||
行为事件示例:
|
||
|
||
```json
|
||
{
|
||
"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 |
|
||
|
||
权限粒度:
|
||
|
||
```text
|
||
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. 按配置发送企微/钉钉/邮箱
|
||
|
||
日报结构:
|
||
|
||
```markdown
|
||
# 今日运营早报
|
||
|
||
## 重点结论
|
||
|
||
## 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-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 改造。
|