# 智念桌面端 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": } ``` - 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`,不返回单个 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; } 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; get(id: ConversationId): Promise; create(input: CreateConversationInput): Promise; delete(id: ConversationId): Promise; /** 发送消息并订阅响应流 */ send(input: SendMessageInput): AsyncIterable; /** 中止正在进行的消息流(流会以 error 事件结束,code = kernel_aborted) */ abort(conversationId: ConversationId): Promise; history( conversationId: ConversationId, opts?: { before?: MessageId; limit?: number } ): Promise; } 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 }; } export type ConversationStreamEvent = | { type: 'message_start'; message: Pick } | { 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; get(id: SkillId): Promise; invoke(input: InvokeSkillInput): AsyncIterable; abort(executionId: ExecutionId): Promise; history(skillId: SkillId, opts?: { hotelId?: HotelId; limit?: number }): Promise; getExecution(executionId: ExecutionId): Promise; } export interface InstalledSkill { id: SkillId; spec: SkillSpec; // 见第四章 enabled: boolean; installedAt: number; lastInvokedAt?: number; } export interface InvokeSkillInput { skillId: SkillId; hotelId: HotelId; input: Record; 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; finishedAt: number } | { type: 'failed'; executionId: ExecutionId; error: KernelError; finishedAt: number }; export interface SkillExecution { id: ExecutionId; skillId: SkillId; hotelId: HotelId; input: Record; output?: Record; 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; get(id: TaskId): Promise; create(input: CreateTaskInput): Promise; update(id: TaskId, patch: UpdateTaskPatch): Promise; delete(id: TaskId): Promise; pause(id: TaskId): Promise; resume(id: TaskId): Promise; history(id: TaskId, opts?: { limit?: number }): Promise; } 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 } | { kind: 'prompt'; conversationId?: ConversationId; prompt: string }; export interface CreateTaskInput { hotelId: HotelId; name: string; cron: string; timezone: string; target: TaskTarget; } export type UpdateTaskPatch = Partial>; 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; /** 派发通知到一个或多个通道 */ send(input: SendNotificationInput): Promise; /** 查询发送状态 */ status(dispatchId: string): Promise; } export interface NotificationChannel { id: string; /** 开放字符串而非枚举:'email' | 'sms' | 'wecom' | 'dingtalk' | * 'whatsapp' | 'telegram' | 'slack' | | ... */ 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; disconnect(): Promise; health(): Promise<{ ready: boolean; details?: Record }>; /** 四个 Port 的实现 */ readonly conversation: ConversationPort; readonly skill: SkillPort; readonly scheduler: SchedulerPort; readonly notification: NotificationPort; } export type AdapterFactory = (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 = (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 { 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; click(selector: string): Promise; // ... } export interface LlmCapability { complete(input: { prompt: string; system?: string; maxTokens?: number }): Promise; classify(input: { prompt: string; input: unknown; schema: ZodSchema }): Promise; // ... } ``` 每种 capability 由 Adapter 实现并注入。Skill 代码不知道 browser 是 Playwright 还是 Puppeteer 还是 OpenClaw 自带 browser tool。换内核时 capability 的实现换,但 skill 代码不动——这是整个 Skill IP 跨平台变现的基础。 ## 4.5 Adapter 编译流程 每个 skill 的 `adapters/.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/` 目录,OpenClaw Gateway 加载即用。未来支持新内核时,bundle 包里多一个 `for-/` 子目录即可。 ## 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; 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; loginWithSms(input: LoginWithSmsInput): Promise; loginWithPassword(input: LoginWithPasswordInput): Promise; logout(): Promise; }; app: { getConfig(): Promise; switchHotel(hotelId: HotelId): Promise; getKernelEndpoint(): Promise<{ wsUrl: string; token: string }>; openDiagnostics(): Promise; }; skills: { sync(): Promise; listLocal(): Promise; }; diagnostics: { getHealth(): Promise; 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 X-YINIAN-App-Version: 0.1.0 X-YINIAN-Device-Id: X-YINIAN-Request-Id: X-YINIAN-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 改造。