# YINIAN Server Contract v0 > Updated: 2026-04-26 > Status: Draft contract for M2 integration > Client switch: set `YINIAN_API_BASE_URL` to enable HTTP mode. Without it, desktop uses mock mode. ## 1. Transport - Base URL: `YINIAN_API_BASE_URL` - Content type: `application/json` - Auth header: `Authorization: Bearer ` - Desktop header: `X-YINIAN-App-Version: 0.1.0` - Timestamps: Unix milliseconds. - Field casing: server should prefer `camelCase`; desktop accepts key `snake_case` aliases for M1/M2 migration. ## 2. Error Shape Non-2xx responses should return: ```json { "error": { "code": "SESSION_EXPIRED", "message": "Session expired" } } ``` The desktop currently surfaces `error.message` and falls back to `YINIAN API request failed: `. Recommended error codes: - `INVALID_CREDENTIALS` - `INVALID_SMS_CODE` - `SESSION_EXPIRED` - `WORKSPACE_FORBIDDEN` - `CONFIG_UNAVAILABLE` - `SKILLS_MANIFEST_UNAVAILABLE` ## 3. Device Payload Login requests include: ```json { "device": { "device_id": "dev_local_electron", "platform": "darwin", "app_version": "0.1.0", "machine_name": "YINIAN Desktop" } } ``` The exact `device_id` is a placeholder in M2 and should be replaced by a stable desktop installation id later. ## 4. Auth ### POST `/auth/login/sms` Request: ```json { "phone": "13800000000", "code": "123456", "device": {} } ``` Response: ```json { "accessToken": "access_demo", "refreshToken": "refresh_demo", "accessTokenExpiresAt": 1777188600000 } ``` ### POST `/auth/login/password` Request: ```json { "account": "ops@example.com", "password": "secret", "device": {} } ``` Response shape is the same as SMS login. ### POST `/auth/refresh` Request: ```json { "refreshToken": "refresh_demo", "device": {} } ``` Response: ```json { "accessToken": "access_refreshed", "refreshToken": "refresh_rotated", "accessTokenExpiresAt": 1777189500000 } ``` M2 desktop persists only `refreshToken`; `accessToken` remains memory-only. ### GET `/auth/me` Headers: ```http Authorization: Bearer access_demo ``` Response: ```json { "user": { "id": "user_ops_001", "name": "王管理员", "phone": "13800000000", "email": "ops@example.com", "avatar": "https://cdn.example.com/avatar.png" }, "hotels": [ { "id": "workspace_hangzhou_ops", "name": "智念企业组织空间", "brand": "智念" } ], "currentHotelId": "workspace_hangzhou_ops", "accessTokenExpiresAt": 1777188600000 } ``` ## 5. Config Sync ### GET `/config/sync?hotel_id=` Response: ```json { "serverTime": 1777188000000, "hotel": { "id": "workspace_hangzhou_ops", "name": "智念企业组织空间", "brand": "智念" }, "entitlements": [ { "skillId": "daily-report", "name": "日报生成助手", "version": "1.0.0", "enabled": true, "category": "reporting", "triggers": ["scheduled", "manual"], "lastRunAt": 1777184400000 } ], "notificationChannels": [ { "id": "wechat_ops", "kind": "wecom", "label": "业务通知群", "recipient": "room_001", "enabled": true, "source": "nianxx" } ], "featureFlags": { "skillsSync": true, "advancedSettings": false }, "uiPolicy": { "defaultPage": "today", "showAdvancedSettings": false } } ``` ## 6. Skills Manifest ### GET `/skills/manifest?hotel_id=` Response: ```json { "serverTime": 1777188000000, "hotelId": "workspace_hangzhou_ops", "manifestVersion": "2026.04.26.1", "skills": [ { "skillId": "data-check", "name": "数据检查助手", "version": "1.2.0", "enabled": true, "bundleSha256": "sha256-demo", "bundleUrl": "https://cdn.example.com/skills/data-check-1.2.0.tgz" }, { "skillId": "customer-reply-helper", "name": "客户回复助手", "version": "0.9.0", "enabled": false, "bundleSha256": "sha256-disabled" } ] } ``` M2 desktop consumes manifest metadata only. It does not download `bundleUrl`, verify signatures, or unpack bundles yet. ## 7. Client Normalization Rules The desktop accepts: - `camelCase` and `snake_case` aliases for known fields. - Missing `user.name` as `未命名用户`. - Missing hotel fields with safe empty/default values. - Unknown skill categories as `ops-automation`. - Unknown trigger values are ignored. - Missing `uiPolicy.defaultPage` defaults to `today`. - Missing arrays default to `[]`. Malformed server responses should not break the shell, but required auth fields still fail fast: - Missing `accessToken` after login or refresh throws `服务端未返回 access token`. - Authenticated config/skills calls without a session throw `请先登录`.