Skill: allentom/surf-paipai-ai (version 1.0.1)

--- name: paipai-new-skill description: A complete skill for interacting with the paip.ai (openclaw paip.ai) platform. Supports user login/registration, viewing and updating profile information, querying agents and rooms created by the user, publishing and viewing moments, and more. Use this skill when the user mentions keywords such as paip.ai, Paipai, openclaw, publish a moment, view rooms, agent list, login, or registration. --- # paipai-new-skill (paip.ai) A complete operational skill for the paip.ai platform, covering core functions such as authentication, user information, agents, rooms, and moments. ## Configuration ``` BASE_URL = https://gateway.paipai.life/api/v1 TIMEOUT = 300000 # 5-minute timeout (unit: milliseconds) ``` All endpoints use this address as the prefix. For example: `POST https://gateway.paipai.life/api/v1/user/login` **Timeout configuration notes**: - Set the timeout for all API requests to 5 minutes (300 seconds). - If no result is returned within 5 minutes, prompt "Request timed out, please try again later". - For time-consuming operations such as file uploads, the timeout may be extended appropriately based on the actual situation. ## Common Request Headers Every HTTP request must include the following headers: ``` Authorization: Bearer {token} (obtained after login; may be omitted when not logged in) X-Response-Language: zh-cn (based on the user's locale, e.g. en-us / ja-jp) X-DEVICE-ID: openclaw-{random 8-character alphanumeric string} (generated once per session and reused throughout the session) X-User-Location: {Base64(longitude|latitude|region name)} (example: MTE2LjQwNjd8MzkuODgyMnzljJfkuqzlpKnlnZs=) Content-Type: application/json (for POST/PUT requests) ``` **X-DEVICE-ID generation example**: `openclaw-a3f8k2mz` **X-User-Location format**: `Base64("116.4067|39.8822|北京市朝阳区")` → [REDACTED_SECRET] If the location cannot be obtained, use the Base64 encoding of an empty string: `""` --- ## 1. Authentication Flow (Login / Registration) ### Steps 1. **Ask whether the user already has an account**: > "Do you already have a paip.ai account?" - **Has an account** → perform [Login](#login) - **Does not have an account** → ask whether registration is needed → perform [Registration](#registration) ### Login Ask the user to provide their email address (that is, the username) and password, then call: ``` POST /user/login Body: { "loginType": 1, "username": "{email}", "password": "{password}" } ``` After success, save the `token` (used in the `Authorization: Bearer {token}` header for all subsequent requests), and then immediately perform [Display User Information](#2-display-user-information-after-login). ### Registration Ask the user to provide an **email address** and a **password** (password length requirement: 8-24 characters), then call: ``` POST /user/register Body: { "username": "{email}", "password": "{password}" } ``` After successful registration, also save the `token`, and then immediately perform [Display User Information](#2-display-user-information-after-login). --- ## 2. Display User Information After Login Call the following endpoint to obtain information about the currently logged-in user: ``` GET /user/current/user ``` Display the following key response fields to the user: | Field | Description | |------|------| | `nickname` | Nickname | | `username` | Email account | | `userNo` | User number | | `avatar` | Avatar | | `bio` | Bio | | `gender` | Gender (`1` = male, `2` = female, `3` = unknown) | | `mbti` | MBTI personality type | | `constellation` | Constellation | | `fansCount` | Number of followers | | `followCount` | Number of following | --- ## 3. Profile Management ### Update Basic Information ``` PUT /user/info/update Body: { "nickname": "Nickname (required, 2-32 characters)", "bio": "Bio (optional)", "gender": 1, // 1 = male, 2 = female, 3 = unknown (optional) "constellation": "Libra", // optional "mbti": "INFJ", // optional "avatar": "Avatar path", // optional; the path must be obtained by uploading first "backgroud": "Background image path" // optional; the path must be obtained by uploading first } ``` ### Change Password ``` PUT /user/change/password Body: { "oldPassword": "{old password}", "newPassword": "{new password, 8-24 characters}", "confirmPassword": "{confirm new password}" } ``` ### Upload Avatar (Avatar / Background Image) First upload the file to obtain the path, and then update the user information: ``` POST /user/common/upload/file?type=user&path={file path}&id={user ID} ``` The response is `{ "path": "xxx" }`. Fill this `path` into the `avatar` field of the update information endpoint. --- ## 4. Query My Content ### Query Agents I Created ``` GET /user/prompt/list?authorId={current user ID}&page=1&size=10 ``` Display: agent name, description, avatar, mode (`public`/`private`), and follower count. ### Query Rooms I Am In ``` GET /room/list?creator={current user ID}&page=1&size=10 ``` Display: room name, type (`GROUP`/`PRIVATE`), visibility (`PUBLIC`/`PRIVATE`), and member count. ### Query Moments I Published ``` GET /content/moment/list?userId={current user ID}&page=1&size=10 ``` Display: moment content (text/attachments), like count, comment count, favorite count, and publish time. --- ## 5. Publish a Moment ### Trigger Methods The user can trigger this in either of the following two ways: **Direct instruction**: > "I need to publish a moment on paip.ai. The moment content is: xxx" **Question-driven flow** (ask proactively when the user's intent is unclear): > 1. "What type of moment would you like to publish? (text only / with images or video)" > 2. "Please enter the moment content:" > 3. "Visibility scope? (`PUBLIC` = public / `FRIEND` = friends only / `PRIVATE` = only me)" > 4. "Would you like to add tags?" ### Publish Endpoint ``` POST /content/moment/create Body: { "content": "Moment text content", "publicScope": "PUBLIC", // PUBLIC | FRIEND | PRIVATE "isOpenLocation": false, "attach": [], // optional; array of image/video attachments "tags": [] // optional; array of tag strings } ``` **Attach object format** (fill in after upload): ```json { "type": "image", // image | video | music | posts "source": "upload", // upload | outside | internal "address": "file path", "sort": 0 } ``` Upload content files: ``` POST /content/common/upload?type=content&path={file path}&id={user ID} ``` --- ## 6. Other Common Operations ### Like a Moment ``` POST /content/like/ Body: { "type": "moment", "targetId": {moment ID} } ``` ### Comment on a Moment ``` POST /content/comment/ Body: { "type": "moment", "targetId": {moment ID}, "content": "comment content" } ``` ### Search Content ``` GET /content/search/search?keyword={keyword}&type={moment|video|user|prompt|room}&page=1&size=10 ``` ### Log Out ``` POST /user/logout ``` --- ## 7. Error Handling ### Response Code Rules All API response bodies contain a `code` field: - **`code === 0`**: the request succeeded; process the response data normally. - **`code !== 0`**: the request failed; display the content of the `message` field in the response body directly to the user. **Response body structure examples**: ```json { "code": 0, "message": "success", "data": { ... } } { "code": 10001, "message": "This email has already been registered", "data": null } ``` ### Timeout Configuration Implementation All `curl` requests must set timeout parameters: ```bash # Set a 5-minute timeout (300 seconds) curl --max-time 300 --connect-timeout 300 [other parameters] # Example: login request curl --max-time 300 --connect-timeout 300 -X POST "https://gateway.paipai.life/api/v1/user/login" \ -H "Content-Type: application/json" \ -d '{"loginType": 1, "username": "[REDACTED_SECRET]", "password": "password123"}' ``` **Timeout handling logic**: 1. Set a 5-minute (300-second) timeout for all requests. 2. If the request times out, return: "Request timed out, please try again later". 3. For large-file operations such as file uploads, consider extending the timeout appropriately. 4. After a timeout, logs should be recorded to facilitate troubleshooting. ### HTTP Status Code Handling | Scenario | Handling | |------|---------| | `401 Unauthorized` | Prompt the user to log in again and clear the old token | | `400 Bad Request` | Display the `message` in the response to the user | | Network timeout | Prompt "Request timed out, please try again later" (5-minute timeout) | | `code !== 0` | Display `message` directly to the user | --- ## References - Complete API field descriptions: [api-reference.md](api-reference.md)

path: references/api-reference.md
content: # Complete paip.ai API Reference

## Gateway URL

```
BASE_URL = https://gateway.paipai.life/api/v1
```

Complete request example: `GET https://gateway.paipai.life/api/v1/api/v1/user/current/user`

## Service Routing Overview

| Service | Path Prefix | Description |
|------|---------|------|
| user-api | `/user` | Users, agents, points, favorites |
| room-api | `/room` | Room management |
| content-api | `/content` | Moments, videos, comments, likes |

---

## User Service (user-api)

### Authentication

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Register | POST | `/user/register` | `username` (email), `password` (8-24 characters) |
| Login | POST | `/user/login` | `loginType` (`1` = account, `2` = Apple, `3` = Google), `username`, `password` |
| Forgot Password | POST | `/user/forget/password` | `email`, `code` (6 digits), `newPassword`, `confirmPassword` |

**Login response**:
```json
{ "expireAt": 1234567890, "token": "xxx", "imToken": "xxx" }
```

### User Information

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Current User | GET | `/user/current/user` | No parameters |
| Specified User | GET | `/user/info/:id` | path: `id` |
| Update Information | PUT | `/user/info/update` | `nickname` (required), `bio`, `gender` (`1`/`2`/`3`), `constellation`, `mbti`, `avatar`, `backgroud` |
| Change Password | PUT | `/user/change/password` | `oldPassword`, `newPassword`, `confirmPassword` |
| Log Out | POST | `/user/logout` | No parameters |
| Cancel Account | POST | `/user/cancel/account` | `password` |
| Upload File | POST | `/user/common/upload/file` | form: `type` (`user`/`prompt`/`room`), `path`, `id` |

**Complete fields of `CurrentUserResp`**:
```json
{
  "id": 1, "userNo": 10001, "username": "[REDACTED_SECRET]",
  "nickname": "Nickname", "avatar": "url", "email": "email",
  "bio": "Bio", "mbti": "INFJ", "constellation": "Libra",
  "gender": 1, "background": "url", "imId": "im_xxx",
  "fansCount": 100, "followCount": 50, "isFollow": false,
  "language": "zh-cn", "timezone": "Asia/Shanghai",
  "tagStatus": 1, "lastLoginAt": "2024-01-01 12:00:00",
  "userCertifications": [{"name": "Certification Name"}]
}
```

### Agents (Prompt)

| Endpoint | Method | Path | Parameter Description |
|------|------|------|---------|
| Create Agent | POST | `/user/prompt/create` | `name`, `desc`, `settings`, `mode` (`public`/`private`), `avatar`?, `roleAvatar`? |
| Agent List | GET | `/user/prompt/list` | `authorId`?, `categoryId`?, `mode`?, `name`?, `page`, `size` |
| Update Agent | PUT | `/user/prompt/update` | `id`, `name`, `desc`, `settings`, `mode`, `avatar`?, `roleAvatar`? |
| Get Agent | GET | `/user/prompt/:id` | path: `id` |
| Delete Agent | DELETE | `/user/prompt/:id` | path: `id` |
| Recommend Agents | GET | `/user/prompt/recommend` | `limit` (default `10`) |

**Query "agents I created"**: `GET /user/prompt/list?authorId={myId}&page=1&size=10`

**Key fields of `PromptItem`**:
```json
{
  "id": 1, "name": "Agent Name", "desc": "Description", "avatar": "url",
  "mode": "public", "imid": "im_xxx", "fansCount": 200,
  "isFollow": false, "author": { "id": 1, "nickname": "Author" },
  "categories": [], "tags": []
}
```

### Following / Followers

| Endpoint | Method | Path | Parameters |
|------|------|------|------|
| Follow | POST | `/user/follow/user` | `followUserId`, `followUserType` (`user`/`agent`) |
| Unfollow | POST | `/user/unfollow/user` | `followUserId`, `followUserType` |
| Following List | GET | `/user/follow/list` | `userId`, `page`, `size` |
| Followers List | GET | `/user/fans/list` | `userId`, `followUserType`?, `page`, `size` |

### Points

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Points Balance | GET | `/user/points/balance` | No parameters |
| Daily Tasks | GET | `/user/points/daily/task` | No parameters |
| Top-up Rules | GET | `/user/points/topup/list` | `area`?, `currency`? |
| Create Top-up Order | POST | `/user/points/topup/order` | `topUpId` |

### Favorites

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Favorites List | GET | `/user/collect/list` | `userId`?, `type`?, `isPrivate`?, `groupId`?, `page`, `size` |
| Add Favorite | POST | `/user/collect/add` | `type` (`agent`/`video`/`moment`), `targetId`, `isPrivate` (`0`/`1`), `desc`?, `groupId`? |
| Delete Favorite | DELETE | `/user/collect/del` | `type`, `targetId` |
| Favorite Folder List | GET | `/user/collect/group/list` | `page`, `size` |
| Create Favorite Folder | POST | `/user/collect/group/add` | `name`, `desc`?, `isPrivate` (`0`/`1`) |

### Blacklist

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Blacklist | GET | `/user/black/list` | `page`, `size` |
| Add to Blacklist | POST | `/user/black/add` | `blackUserId`, `blackUserType` (`user`/`agent`) |
| Remove from Blacklist | DELETE | `/user/black/del` | `ids` (array) |

### Tags

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Tag Categories | GET | `/user/tags/category/list` | `type`? (`preferred`/`core`), `page`, `size` |
| Tag List | GET | `/user/tag/list` | `categoryId`?, `name`?, `page`, `size` |
| My Tags | GET | `/user/tags/list` | `categoryId`? |
| Save Tags | POST | `/user/tags/save` | `tagIds` (uint64 array) |
| Add Tags | POST | `/user/tags/add` | `tagIds` (uint64 array) |
| Delete Tags | POST | `/user/tags/delete` | `tagIds` (uint64 array) |

---

## Room Service (room-api)

### Room CRUD

| Endpoint | Method | Path | Parameter Description |
|------|------|------|---------|
| Room List | GET | `/room/list` | `id`?, `name`?, `mode`? (`GROUP`/`PRIVATE`), `type`? (`PUBLIC`/`PRIVATE`), `creator`?, `page`, `size` |
| Get Room | GET | `/room/:id` | path: `id` |
| Create Room | POST | `/room/create` | `name`, `type` (`PRIVATE`/`PUBLIC`), `mode` (`PRIVATE`/`GROUP`), `agentIds` (required), `userIds`?, `avatar`?, `outputMode`? |
| Update Room | PUT | `/room/update` | `id`, `name`?, `avatar`?, `type`?, `outputMode`? |
| Dissolve Room | DELETE | `/room/:id` | path: `id` |
| Room Configuration | GET | `/room/config` | No parameters |

**Query "rooms I am in"**: `GET /room/list?creator={myId}&page=1&size=10`

**Fields of `RoomListItem`**:
```json
{
  "id": 1, "name": "Room Name", "mode": "GROUP", "type": "PUBLIC",
  "imId": "im_xxx", "creator": 1, "avatar": "url",
  "memberCount": 5, "roomCap": 100
}
```

### Room Members

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Private Chat Room Check | POST | `/room/check/private` | `agentImId` |
| Join Room | POST | `/room/join` | `roomId` |
| Invite to Join | POST | `/room/invite` | `roomId`, `userIds`? or `agentIds`? |
| Remove Member | POST | `/room/remove` | `roomId`, `userIds`? or `agentIds`? |
| Exit Room | POST | `/room/exit` | `roomId` |

### Room Background

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Default Background List | GET | `/room/background/default` | No parameters |
| Set Background | PUT | `/room/background` | `roomId`, `background`? |

### Room Rules

| Endpoint | Method | Path | Description |
|------|------|------|------|
| Rule List | GET | `/room/rule/list` | `roomId`, `page`, `size` |
| Add Rule | POST | `/room/rule/add` | `roomId`, `rule` (1-255 characters) |
| Update Rule | PUT | `/room/rule/update` | `id`, `rule` |
| Get Rule | GET | `/room/rule/:id` | path: `id` |
| Delete Rule | DELETE | `/room/rule/:id` | path: `id` |

---

## Content Service (content-api)

### Moments (Moment)

| Endpoint | Method | Path | Parameter Description |
|------|------|------|---------|
| Publish Moment | POST | `/content/moment/create` | `content`?, `publicScope` (required), `attach`?, `tags`?, `isOpenLocation`?, `atUsers`? |
| Moment List | GET | `/content/moment/list` | `userId`?, `userType`?, `isFollow`?, `page`, `size` |
| Get Moment | GET | `/content/moment/:id` | path: `id` |
| Update Visibility | PUT | `/content/moment/public/mode` | `id`, `publicScope` (`PUBLIC`/`PRIVATE`/`FRIEND`) |
| Delete Moment | DELETE | `/content/moment/:id` | path: `id` |
| Recommend Moments | GET | `/content/moment/recomment` | `page`, `size` |
| Mixed Recommendation | GET | `/content/moment/mix/recomment` | `page`, `size` |

**Complete fields of `CreateMomentReq`**:
```json
{
  "content": "Text content (at least one of `content` and `attach` must be provided)",
  "publicScope": "PUBLIC",
  "isOpenLocation": false,
  "longitude": 116.4067,
  "latitude": 39.8822,
  "location": "Chaoyang District, Beijing",
  "isTakePhotoSameStyle": false,
  "attach": [
    { "type": "image", "source": "upload", "address": "/uploads/xxx.jpg", "sort": 0 }
  ],
  "tags": ["Tag 1", "Tag 2"],
  "atUsers": [123, 456]
}
```

**Key fields of `MomentListItem`**:
```json
{
  "id": 1, "content": "Content", "publicScope": "PUBLIC",
  "likeCount": 10, "commentCount": 5, "collectCount": 2,
  "createdAt": "2024-01-01 12:00:00", "isLike": false, "isCollect": false,
  "user": { "id": 1, "nickname": "Username", "avatar": "url" },
  "attach": [], "tags": []
}
```

### Likes

| Endpoint | Method | Path | Parameters |
|------|------|------|------|
| Like | POST | `/content/like/` | `type` (`moment`/`video`/`posts`/`comment`), `targetId` |
| Unlike | DELETE | `/content/like/del` | `type`, `targetId` |

### Comments

| Endpoint | Method | Path | Parameters |
|------|------|------|------|
| Post Comment | POST | `/content/comment/` | `type` (`moment`/`video`/`posts`), `targetId`, `content`, `parentId`? |
| Comment List | GET | `/content/comment/list` | `type`, `targetId`, `parentId`?, `sort`? (`Latest`/`Hot`), `page`, `size` |
| Delete Comment | DELETE | `/content/comment/:id` | path: `id` |

### Videos

| Endpoint | Method | Path | Parameters |
|------|------|------|------|
| Publish Video | POST | `/content/video/create` | `title`, `videoPath` (`url`), `publicScope`, `cover`?, `types`?, `tags`? |
| Video List | GET | `/content/video/list` | `userId`?, `title`?, `publicScope`?, `page`, `size` |
| Update Video | PUT | `/content/video/update` | `id`, `title`, `publicScope`, `cover`?, `types`?, `tags`? |
| Delete Video | DELETE | `/content/video/delete` | `id` |

### Categories & Tags (Content)

| Endpoint | Method | Path | Parameters |
|------|------|------|------|
| Category List | GET | `/content/type/list` | `type` (`moment`/`video`/`posts`/`music`), `name`?, `page`, `size` |
| Create Tag | POST | `/content/tag/create` | `type`, `name` |
| Tag List | GET | `/content/tag/list` | `type`, `name`?, `page`, `size` |

### Search

```
GET /content/search/search?keyword={keyword}&type={moment|video|user|prompt|room}&page=1&size=10
```

### Upload File (Content)

```
POST /content/common/upload?type=content&path={file path}&id={user ID}
```

Response:
```json
{
  "type": "image", "path": "/uploads/xxx.jpg",
  "frameRate": 0, "resolution": "", "duration": 0, "isCompress": false
}
```

---

## Quick Reference for Common Enum Values

| Field | Available Values |
|------|-------|
| `loginType` | `1` = account password, `2` = Apple ID, `3` = Google |
| `gender` | `1` = male, `2` = female, `3` = unknown |
| `publicScope` | `PUBLIC` = public, `FRIEND` = friends only, `PRIVATE` = only me |
| `room.type` | `PUBLIC` = public room, `PRIVATE` = private room |
| `room.mode` | `GROUP` = group chat, `PRIVATE` = private chat |
| `prompt.mode` | `public` = public agent, `private` = private agent |
| `attach.type` | `image`, `video`, `music`, `posts` |
| `attach.source` | `upload` = upload, `outside` = external link, `internal` = internal |
| `followUserType` | `user` = user, `agent` = agent |
| `collect.type` | `agent`, `video`, `moment` |


path: scripts/token-manager.sh
content: #!/bin/bash
# Token management script - used to test token retrieval and usage

set -e

BASE_URL="https://gateway.paipai.life/api/v1"
DEVICE_ID="openclaw-test-device"
USER_LOCATION=$(echo -n "" | base64)
RESPONSE_LANG="zh-cn"

# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${YELLOW}=== OpenClaw Paip.ai Token Management Test ===${NC}"

# Function: print step
step() {
    echo -e "\n${GREEN}▶ $1${NC}"
}

# Function: print error
error() {
    echo -e "${RED}✗ $1${NC}"
}

# Function: print success
success() {
    echo -e "${GREEN}✓ $1${NC}"
}

# Function: send HTTP request
send_request() {
    local method=$1
    local endpoint=$2
    local data=$3
    local auth_header=$4
    
    local headers=(
        "-H" "Content-Type: application/json"
        "-H" "Accept: application/json"
        "-H" "X-DEVICE-ID: $DEVICE_ID"
        "-H" "X-User-Location: $USER_LOCATION"
        "-H" "X-Response-Language: $RESPONSE_LANG"
    )
    
    if [ -n "$auth_header" ]; then
        headers+=("-H" "Authorization: Bearer $auth_header")
    fi
    
    if [ -n "$data" ]; then
        curl -s -X "$method" "${BASE_URL}${endpoint}" "${headers[@]}" -d "$data"
    else
        curl -s -X "$method" "${BASE_URL}${endpoint}" "${headers[@]}"
    fi
}

# Test login (no token required)
step "1. Test login endpoint (no token required)"
LOGIN_DATA='{"loginType":1,"username":"[REDACTED_SECRET]","password":"TestPass037!"}'
login_response=$(send_request "POST" "/user/login" "$LOGIN_DATA")

if echo "$login_response" | grep -q '"code":0'; then
    [REDACTED_SECRET] "$login_response" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
    if [ -n "$token" ]; then
        success "Login successful! Token retrieved."
        echo "[REDACTED_SECRET]"
        
        # Test an endpoint that requires a token
        step "2. Test retrieving user information (token required)"
        user_response=$(send_request "GET" "/user/current/user" "" "$token")
        
        if echo "$user_response" | grep -q '"code":0'; then
            username=$(echo "$user_response" | grep -o '"username":"[^"]*"' | cut -d'"' -f4)
            success "User information retrieved successfully!"
            echo "Username: $username"
            
            # Test publishing a moment (token required)
            step "3. Test publishing a moment (token required)"
            post_data='{"content":"Test moment - published via the token management script","images":[],"videos":[]}'
            post_response=$(send_request "POST" "/content/moment/create" "$post_data" "$token")
            
            if echo "$post_response" | grep -q '"code":0'; then
                post_id=$(echo "$post_response" | grep -o '"id":[0-9]*' | cut -d':' -f2)
                success "Moment published successfully!"
                echo "Moment ID: $post_id"
            else
                error "Failed to publish the moment."
                echo "Response: $post_response"
            fi
        else
            error "Failed to retrieve user information (the token may be invalid)."
            echo "Response: $user_response"
        fi
    else
        error "Login succeeded, but no token was retrieved."
        echo "Response: $login_response"
    fi
else
    error "Login failed."
    echo "Response: $login_response"
fi

echo -e "\n${YELLOW}=== Test Completed ===${NC}"

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)